feat(l10n): Add option for localizing the output of strings in Service classes

l10n
Inex Code 2023-04-12 14:55:34 +03:00
parent 3d4d05ff11
commit 9376fe151f
11 changed files with 194 additions and 58 deletions

View File

@ -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]

View 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}"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"services": {
"bitwarden": {
"display_name": "Bitwarden",
"description": "Bitwarden это менеджер паролей с открытым исходным кодом, который может работать на вашем сервере.",
"move_job": {
"name": "Переместить Bitwarden",
"description": "Перемещение данных Bitwarden на {volume}"
}
}
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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")

View File

@ -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(

View File

@ -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,

View File

@ -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")

View File

@ -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,

View 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)