mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git
synced 2024-11-27 22:41:28 +00:00
feat(l10n): Add option for localizing the output of strings in Service classes
This commit is contained in:
parent
3d4d05ff11
commit
9376fe151f
|
@ -13,6 +13,6 @@ from selfprivacy_api.services import get_all_services
|
|||
@strawberry.type
|
||||
class Services:
|
||||
@strawberry.field
|
||||
def all_services(self) -> typing.List[Service]:
|
||||
def all_services(self, locale: str = "en") -> typing.List[Service]:
|
||||
services = get_all_services()
|
||||
return [service_to_graphql_service(service) for service in services]
|
||||
return [service_to_graphql_service(service, locale) for service in services]
|
||||
|
|
52
selfprivacy_api/locales/en/services.json
Normal file
52
selfprivacy_api/locales/en/services.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"services": {
|
||||
"bitwarden": {
|
||||
"display_name": "Bitwarden",
|
||||
"description": "Bitwarden is an open source password management solution you can run on your own server.",
|
||||
"move_job": {
|
||||
"name": "Move Bitwarden",
|
||||
"description": "Moving Bitwarden data to {volume}"
|
||||
}
|
||||
},
|
||||
"gitea": {
|
||||
"display_name": "Gitea",
|
||||
"description": "Gitea is a lightweight code hosting solution written in Go.",
|
||||
"move_job": {
|
||||
"name": "Move Gitea",
|
||||
"description": "Moving Gitea data to {volume}"
|
||||
}
|
||||
},
|
||||
"jitsi": {
|
||||
"display_name": "Jitsi",
|
||||
"description": "Jitsi is a free and open source video conferencing solution."
|
||||
},
|
||||
"mailserver": {
|
||||
"display_name": "Email",
|
||||
"description": "E-Mail for company and family.",
|
||||
"move_job": {
|
||||
"name": "Move Mail Server",
|
||||
"description": "Moving mailserver data to {volume}"
|
||||
}
|
||||
},
|
||||
"nextcloud": {
|
||||
"display_name": "Nextcloud",
|
||||
"description": "Nextcloud is a cloud storage service that offers a web interface and a desktop client.",
|
||||
"move_job": {
|
||||
"name": "Move Nextcloud",
|
||||
"description": "Moving Nextcloud data to {volume}"
|
||||
}
|
||||
},
|
||||
"ocserv": {
|
||||
"display_name": "OpenConnect VPN",
|
||||
"description": "OpenConnect VPN to connect your devices and access the internet."
|
||||
},
|
||||
"pleroma": {
|
||||
"display_name": "Pleroma",
|
||||
"description": "Pleroma is a free and open source microblogging server.",
|
||||
"move_job": {
|
||||
"name": "Move Pleroma",
|
||||
"description": "Moving Pleroma data to {volume}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
selfprivacy_api/locales/ru/services.json
Normal file
12
selfprivacy_api/locales/ru/services.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"services": {
|
||||
"bitwarden": {
|
||||
"display_name": "Bitwarden",
|
||||
"description": "Bitwarden это менеджер паролей с открытым исходным кодом, который может работать на вашем сервере.",
|
||||
"move_job": {
|
||||
"name": "Переместить Bitwarden",
|
||||
"description": "Перемещение данных Bitwarden на {volume}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceS
|
|||
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.huey import huey
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.services.bitwarden.icon import BITWARDEN_ICON
|
||||
|
||||
|
@ -24,14 +25,14 @@ class Bitwarden(Service):
|
|||
return "bitwarden"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
"""Return service display name."""
|
||||
return "Bitwarden"
|
||||
return L10n().get("services.bitwarden.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
def get_description(locale: str = "en") -> str:
|
||||
"""Return service description."""
|
||||
return "Bitwarden is a password manager."
|
||||
return L10n().get("services.bitwarden.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -143,11 +144,13 @@ class Bitwarden(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
job = Jobs.add(
|
||||
type_id="services.bitwarden.move",
|
||||
name="Move Bitwarden",
|
||||
description=f"Moving Bitwarden data to {volume.name}",
|
||||
name=L10n().get("services.bitwarden.move_job.name", locale),
|
||||
description=L10n()
|
||||
.get("services.bitwarden.move_job.description")
|
||||
.format(volume=volume.name),
|
||||
)
|
||||
|
||||
move_service(
|
||||
|
|
|
@ -11,6 +11,7 @@ from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceS
|
|||
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.huey import huey
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.services.gitea.icon import GITEA_ICON
|
||||
|
||||
|
@ -24,14 +25,14 @@ class Gitea(Service):
|
|||
return "gitea"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
"""Return service display name."""
|
||||
return "Gitea"
|
||||
return L10n().get("services.gitea.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
def get_description(locale: str = "en") -> str:
|
||||
"""Return service description."""
|
||||
return "Gitea is a Git forge."
|
||||
return L10n().get("services.gitea.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -140,11 +141,13 @@ class Gitea(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
job = Jobs.add(
|
||||
type_id="services.gitea.move",
|
||||
name="Move Gitea",
|
||||
description=f"Moving Gitea data to {volume.name}",
|
||||
name=L10n().get("services.gitea.move_job.name", locale),
|
||||
description=L10n()
|
||||
.get("services.gitea.move_job.description", locale)
|
||||
.format(volume=volume.name),
|
||||
)
|
||||
|
||||
move_service(
|
||||
|
|
|
@ -3,19 +3,17 @@ import base64
|
|||
import subprocess
|
||||
import typing
|
||||
|
||||
from selfprivacy_api.jobs import Job, Jobs
|
||||
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.jobs import Job
|
||||
from selfprivacy_api.services.generic_size_counter import get_storage_usage
|
||||
from selfprivacy_api.services.generic_status_getter import (
|
||||
get_service_status,
|
||||
get_service_status_from_several_units,
|
||||
)
|
||||
from selfprivacy_api.services.jitsi.icon import JITSI_ICON
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.huey import huey
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.services.jitsi.icon import JITSI_ICON
|
||||
|
||||
|
||||
class Jitsi(Service):
|
||||
|
@ -27,14 +25,14 @@ class Jitsi(Service):
|
|||
return "jitsi"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
"""Return service display name."""
|
||||
return "Jitsi"
|
||||
return L10n().get("services.jitsi.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
def get_description(locale: str = "en") -> str:
|
||||
"""Return service description."""
|
||||
return "Jitsi is a free and open-source video conferencing solution."
|
||||
return L10n().get("services.jitsi.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -138,5 +136,5 @@ class Jitsi(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
raise NotImplementedError("jitsi service is not movable")
|
||||
|
|
|
@ -13,6 +13,7 @@ from selfprivacy_api.services.generic_status_getter import (
|
|||
)
|
||||
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||
import selfprivacy_api.utils as utils
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.huey import huey
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
|
@ -27,12 +28,12 @@ class MailServer(Service):
|
|||
return "mailserver"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
return "Mail Server"
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
return L10n().get("services.mailserver.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
return "E-Mail for company and family."
|
||||
def get_description(locale: str = "en") -> str:
|
||||
return L10n().get("services.mailserver.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -148,11 +149,13 @@ class MailServer(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
job = Jobs.add(
|
||||
type_id="services.mailserver.move",
|
||||
name="Move Mail Server",
|
||||
description=f"Moving mailserver data to {volume.name}",
|
||||
name=L10n().get("services.mailserver.move_job.name", locale),
|
||||
description=L10n()
|
||||
.get("services.mailserver.move_job.description", locale)
|
||||
.format(volume=volume.name),
|
||||
)
|
||||
|
||||
move_service(
|
||||
|
|
|
@ -9,6 +9,7 @@ from selfprivacy_api.services.generic_status_getter import get_service_status
|
|||
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.services.nextcloud.icon import NEXTCLOUD_ICON
|
||||
|
||||
|
@ -22,14 +23,14 @@ class Nextcloud(Service):
|
|||
return "nextcloud"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
"""Return service display name."""
|
||||
return "Nextcloud"
|
||||
return L10n().get("services.nextcloud.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
def get_description(locale: str = "en") -> str:
|
||||
"""Return service description."""
|
||||
return "Nextcloud is a cloud storage service that offers a web interface and a desktop client."
|
||||
return L10n().get("services.nextcloud.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -148,11 +149,13 @@ class Nextcloud(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
job = Jobs.add(
|
||||
type_id="services.nextcloud.move",
|
||||
name="Move Nextcloud",
|
||||
description=f"Moving Nextcloud to volume {volume.name}",
|
||||
name=L10n().get("services.nextcloud.move_job.name", locale),
|
||||
description=L10n()
|
||||
.get("services.nextcloud.move_job.description", locale)
|
||||
.format(volume=volume.name),
|
||||
)
|
||||
move_service(
|
||||
self,
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
import base64
|
||||
import subprocess
|
||||
import typing
|
||||
from selfprivacy_api.jobs import Job, Jobs
|
||||
from selfprivacy_api.services.generic_service_mover import FolderMoveNames, move_service
|
||||
from selfprivacy_api.services.generic_size_counter import get_storage_usage
|
||||
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.jobs import Job
|
||||
from selfprivacy_api.services.generic_status_getter import get_service_status
|
||||
from selfprivacy_api.services.ocserv.icon import OCSERV_ICON
|
||||
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||
from selfprivacy_api.utils import ReadUserData, WriteUserData
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.services.ocserv.icon import OCSERV_ICON
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
|
||||
|
||||
class Ocserv(Service):
|
||||
|
@ -21,12 +21,12 @@ class Ocserv(Service):
|
|||
return "ocserv"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
return "OpenConnect VPN"
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
return L10n().get("services.ocserv.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
return "OpenConnect VPN to connect your devices and access the internet."
|
||||
def get_description(locale: str = "en") -> str:
|
||||
return L10n().get("services.ocserv.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -117,5 +117,5 @@ class Ocserv(Service):
|
|||
def get_storage_usage() -> int:
|
||||
return 0
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
raise NotImplementedError("ocserv service is not movable")
|
||||
|
|
|
@ -9,6 +9,7 @@ from selfprivacy_api.services.generic_status_getter import get_service_status
|
|||
from selfprivacy_api.services.service import Service, ServiceDnsRecord, ServiceStatus
|
||||
from selfprivacy_api.utils import ReadUserData, WriteUserData, get_domain
|
||||
from selfprivacy_api.utils.block_devices import BlockDevice
|
||||
from selfprivacy_api.utils.localization import Localization as L10n
|
||||
import selfprivacy_api.utils.network as network_utils
|
||||
from selfprivacy_api.services.pleroma.icon import PLEROMA_ICON
|
||||
|
||||
|
@ -21,12 +22,12 @@ class Pleroma(Service):
|
|||
return "pleroma"
|
||||
|
||||
@staticmethod
|
||||
def get_display_name() -> str:
|
||||
return "Pleroma"
|
||||
def get_display_name(locale: str = "en") -> str:
|
||||
return L10n().get("services.pleroma.display_name", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_description() -> str:
|
||||
return "Pleroma is a microblogging service that offers a web interface and a desktop client."
|
||||
def get_description(locale: str = "en") -> str:
|
||||
return L10n().get("services.pleroma.description", locale)
|
||||
|
||||
@staticmethod
|
||||
def get_svg_icon() -> str:
|
||||
|
@ -128,11 +129,13 @@ class Pleroma(Service):
|
|||
),
|
||||
]
|
||||
|
||||
def move_to_volume(self, volume: BlockDevice) -> Job:
|
||||
def move_to_volume(self, volume: BlockDevice, locale: str = "en") -> Job:
|
||||
job = Jobs.add(
|
||||
type_id="services.pleroma.move",
|
||||
name="Move Pleroma",
|
||||
description=f"Moving Pleroma to volume {volume.name}",
|
||||
name=L10n().get("services.pleroma.move_job.name", locale),
|
||||
description=L10n()
|
||||
.get("services.pleroma.move_job.description", locale)
|
||||
.format(volume=volume.name),
|
||||
)
|
||||
move_service(
|
||||
self,
|
||||
|
|
59
selfprivacy_api/utils/localization.py
Normal file
59
selfprivacy_api/utils/localization.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
A localization module that loads strings from JSONs in the locale directory.
|
||||
It provides a function to get a localized string by its ID.
|
||||
If the string is not found in the current locale, it will try to find it in the default locale.
|
||||
If the string is not found in the default locale, it will return the ID.
|
||||
|
||||
The locales are loaded into the memory at the api startup and kept in a singleton.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
from selfprivacy_api.utils.singleton_metaclass import SingletonMetaclass
|
||||
|
||||
DEFAULT_LOCALE = "en"
|
||||
LOCALE_DIR: Path = Path(__file__).parent.parent / "locales"
|
||||
|
||||
|
||||
class Localization(metaclass=SingletonMetaclass):
|
||||
"""Localization class."""
|
||||
|
||||
def __init__(self):
|
||||
self.locales: typing.Dict[str, typing.Dict[str, str]] = {}
|
||||
self.load_locales()
|
||||
|
||||
def load_locales(self):
|
||||
"""Load locales from locale directory."""
|
||||
for locale in os.listdir(str(LOCALE_DIR)):
|
||||
locale_path = LOCALE_DIR / locale
|
||||
if not locale_path.is_dir():
|
||||
continue
|
||||
self.locales[locale] = {}
|
||||
for file in os.listdir(str(locale_path)):
|
||||
if file.endswith(".json"):
|
||||
with open(locale_path / file, "r") as locale_file:
|
||||
locale_data = self.flatten_dict(json.load(locale_file))
|
||||
self.locales[locale].update(locale_data)
|
||||
|
||||
def get(self, string_id: str, locale: str = DEFAULT_LOCALE) -> str:
|
||||
"""Get localized string by its ID."""
|
||||
if locale in self.locales and string_id in self.locales[locale]:
|
||||
return self.locales[locale][string_id]
|
||||
if DEFAULT_LOCALE in self.locales and string_id in self.locales[DEFAULT_LOCALE]:
|
||||
return self.locales[DEFAULT_LOCALE][string_id]
|
||||
return string_id
|
||||
|
||||
def flatten_dict(
|
||||
self, d: typing.Dict[str, typing.Any], parent_key: str = "", sep: str = "."
|
||||
) -> typing.Dict[str, str]:
|
||||
"""Flatten a dict."""
|
||||
items = []
|
||||
for k, v in d.items():
|
||||
new_key = parent_key + sep + k if parent_key else k
|
||||
if isinstance(v, dict):
|
||||
items.extend(self.flatten_dict(v, new_key, sep=sep).items())
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
return dict(items)
|
Loading…
Reference in a new issue