feat: add runtime locale overrides
This commit is contained in:
@@ -41,6 +41,7 @@ from config.tariffs_config import TariffsConfig
|
||||
from db.dal import (
|
||||
ad_dal,
|
||||
app_settings_dal,
|
||||
locale_overrides_dal,
|
||||
message_log_dal,
|
||||
panel_sync_dal,
|
||||
payment_dal,
|
||||
|
||||
@@ -66,6 +66,8 @@ def setup_admin_routes(app: web.Application) -> None:
|
||||
|
||||
router.add_get("/api/admin/settings", admin_settings_get_route)
|
||||
router.add_patch("/api/admin/settings", admin_settings_patch_route)
|
||||
router.add_get("/api/admin/translations", admin_translations_get_route)
|
||||
router.add_patch("/api/admin/translations", admin_translations_patch_route)
|
||||
|
||||
router.add_get("/api/admin/tariffs", admin_tariffs_get_route)
|
||||
router.add_put("/api/admin/tariffs", admin_tariffs_save_route)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# ruff: noqa: F401,F403,F405,I001
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
|
||||
from bot.middlewares.i18n import JsonI18n, locale_language_options, resolve_locale_key
|
||||
from bot.services.locale_override_service import (
|
||||
LOCALE_OVERRIDES_PATH,
|
||||
audience_for_locale_key,
|
||||
group_id_for_locale_key,
|
||||
locale_group_catalog,
|
||||
load_locale_overrides,
|
||||
update_locale_overrides,
|
||||
)
|
||||
|
||||
def _locale_languages(
|
||||
i18n: JsonI18n,
|
||||
overrides: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
base_languages = set((i18n.base_locales_data or {}).keys())
|
||||
override_languages = {str(entry.get("lang") or "") for entry in overrides or []}
|
||||
override_languages.update((i18n.locale_overrides or {}).keys())
|
||||
return locale_language_options(
|
||||
base_languages | override_languages,
|
||||
base_languages=base_languages,
|
||||
)
|
||||
|
||||
|
||||
def _locale_override_meta_map(overrides: List[Dict[str, Any]]) -> Dict[Tuple[str, str], Dict]:
|
||||
result: Dict[Tuple[str, str], Dict] = {}
|
||||
for entry in overrides:
|
||||
lang = str(entry.get("lang") or "")
|
||||
raw_key = str(entry.get("key") or "")
|
||||
key = resolve_locale_key(raw_key)
|
||||
if lang and key:
|
||||
if raw_key != key and (lang, key) in result:
|
||||
continue
|
||||
result[(lang, key)] = entry
|
||||
return result
|
||||
|
||||
|
||||
def _admin_translations_payload(
|
||||
i18n: JsonI18n,
|
||||
overrides: List[Dict[str, Any]],
|
||||
) -> Dict[str, Any]:
|
||||
base_data = i18n.base_locales_data or i18n.locales_data or {}
|
||||
effective_data = i18n.locales_data or {}
|
||||
override_meta = _locale_override_meta_map(overrides)
|
||||
language_items = _locale_languages(i18n, overrides)
|
||||
languages = [item["code"] for item in language_items]
|
||||
all_keys = sorted(
|
||||
{key for messages in base_data.values() for key in messages.keys()}
|
||||
| {key for _, key in override_meta.keys()}
|
||||
)
|
||||
|
||||
groups_by_id = {
|
||||
group["id"]: {
|
||||
**group,
|
||||
"items": [],
|
||||
}
|
||||
for group in locale_group_catalog()
|
||||
}
|
||||
|
||||
for key in all_keys:
|
||||
values: Dict[str, Dict[str, Any]] = {}
|
||||
for lang in languages:
|
||||
meta = override_meta.get((lang, key))
|
||||
fallback_base = base_data.get(i18n.default_lang, {}).get(key, "")
|
||||
values[lang] = {
|
||||
"base": base_data.get(lang, {}).get(key, ""),
|
||||
"fallback": fallback_base,
|
||||
"effective": effective_data.get(lang, {}).get(key, ""),
|
||||
"override": meta.get("value") if meta else "",
|
||||
"overridden": bool(meta),
|
||||
"updated_at": meta.get("updated_at") if meta else None,
|
||||
"updated_by": meta.get("updated_by") if meta else None,
|
||||
}
|
||||
group_id = group_id_for_locale_key(key)
|
||||
groups_by_id.setdefault(
|
||||
group_id,
|
||||
{"id": group_id, "title": group_id, "description": "", "items": []},
|
||||
)
|
||||
groups_by_id[group_id]["items"].append(
|
||||
{
|
||||
"key": key,
|
||||
"audience": audience_for_locale_key(key),
|
||||
"values": values,
|
||||
}
|
||||
)
|
||||
|
||||
groups = [group for group in groups_by_id.values() if group["items"]]
|
||||
return {
|
||||
"languages": language_items,
|
||||
"groups": groups,
|
||||
"path": str(LOCALE_OVERRIDES_PATH),
|
||||
"override_count": len(overrides),
|
||||
}
|
||||
|
||||
|
||||
async def admin_translations_get_route(request: web.Request) -> web.Response:
|
||||
_require_admin_user_id(request)
|
||||
i18n: Optional[JsonI18n] = request.app.get("i18n")
|
||||
if i18n is None:
|
||||
return _error(503, "i18n_unavailable")
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
|
||||
await load_locale_overrides(i18n, async_session_factory)
|
||||
async with async_session_factory() as session:
|
||||
overrides = await locale_overrides_dal.get_overrides_with_meta(session)
|
||||
|
||||
return _ok(_admin_translations_payload(i18n, overrides))
|
||||
|
||||
|
||||
async def admin_translations_patch_route(request: web.Request) -> web.Response:
|
||||
actor_id = _require_admin_user_id(request)
|
||||
i18n: Optional[JsonI18n] = request.app.get("i18n")
|
||||
if i18n is None:
|
||||
return _error(503, "i18n_unavailable")
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
payload = await _read_json(request)
|
||||
updates = payload.get("updates") or {}
|
||||
deletes = payload.get("deletes") or []
|
||||
if not isinstance(updates, dict):
|
||||
return _error(400, "invalid_updates")
|
||||
if not isinstance(deletes, list):
|
||||
return _error(400, "invalid_deletes")
|
||||
|
||||
result = await update_locale_overrides(
|
||||
i18n,
|
||||
async_session_factory,
|
||||
updates=updates,
|
||||
deletes=deletes,
|
||||
actor_id=actor_id,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return web.json_response(
|
||||
{"ok": False, "error": "validation_failed", "errors": result.get("errors", {})},
|
||||
status=400,
|
||||
)
|
||||
|
||||
return _ok(
|
||||
{
|
||||
"applied": result.get("applied", 0),
|
||||
"reverted": result.get("reverted", 0),
|
||||
"file_written": result.get("file_written", False),
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user