From 8ae1a435814f2a0e2b56103cdd1aae6a4cb2587e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 13 Jan 2026 19:10:41 +0800 Subject: [PATCH] Update i18n language storage and loading (#1008) --- backend/common/i18n.py | 42 +++---------------------- backend/core/path_conf.py | 2 +- backend/locale/__init__.py | 0 backend/locale/{ => langs}/en-US.json | 0 backend/locale/{ => langs}/zh-CN.yml | 0 backend/locale/loader.py | 44 +++++++++++++++++++++++++++ backend/middleware/i18n_middleware.py | 3 +- 7 files changed, 52 insertions(+), 39 deletions(-) create mode 100644 backend/locale/__init__.py rename backend/locale/{ => langs}/en-US.json (100%) rename backend/locale/{ => langs}/zh-CN.yml (100%) create mode 100644 backend/locale/loader.py diff --git a/backend/common/i18n.py b/backend/common/i18n.py index 8f77f585..a50b7cda 100644 --- a/backend/common/i18n.py +++ b/backend/common/i18n.py @@ -1,25 +1,15 @@ -import glob -import json - -from pathlib import Path from typing import Any -import yaml - from starlette_context.errors import ContextDoesNotExistError from backend.common.context import ctx from backend.core.conf import settings -from backend.core.path_conf import LOCALE_DIR +from backend.locale.loader import locale_loader class I18n: """国际化管理器""" - def __init__(self) -> None: - self.locales: dict[str, dict[str, Any]] = {} - self.load_locales() - @property def current_language(self) -> str: """获取当前请求的语言""" @@ -33,29 +23,6 @@ class I18n: """设置当前请求的语言""" ctx.language = language - def load_locales(self) -> None: - """加载语言文本""" - patterns = [ - LOCALE_DIR / '*.json', - LOCALE_DIR / '*.yaml', - LOCALE_DIR / '*.yml', - ] - - lang_files = [] - - for pattern in patterns: - lang_files.extend(glob.glob(str(pattern))) - - for lang_file in lang_files: - with open(lang_file, encoding='utf-8') as f: - lang = Path(lang_file).stem - file_type = Path(lang_file).suffix[1:] - match file_type: - case 'json': - self.locales[lang] = json.loads(f.read()) - case 'yaml' | 'yml': - self.locales[lang] = yaml.full_load(f.read()) - def t(self, key: str, default: Any | None = None, **kwargs) -> str: """ 翻译函数 @@ -68,10 +35,10 @@ class I18n: keys = key.split('.') try: - translation = self.locales[self.current_language] + translation = locale_loader.locales[self.current_language] except KeyError: - keys = 'error.language_not_found' - translation = self.locales[settings.I18N_DEFAULT_LANGUAGE] + keys = 'error.language_not_found'.split('.') + translation = locale_loader.locales[settings.I18N_DEFAULT_LANGUAGE] for k in keys: if isinstance(translation, dict) and k in list(translation.keys()): @@ -79,6 +46,7 @@ class I18n: else: # Pydantic 兼容 translation = None if keys[0] == 'pydantic' else key + break if translation and kwargs: translation = translation.format(**kwargs) diff --git a/backend/core/path_conf.py b/backend/core/path_conf.py index 2c6fd18e..89c42205 100644 --- a/backend/core/path_conf.py +++ b/backend/core/path_conf.py @@ -25,7 +25,7 @@ UPLOAD_DIR = STATIC_DIR / 'upload' PLUGIN_DIR = BASE_PATH / 'plugin' # 国际化文件目录 -LOCALE_DIR = BASE_PATH / 'locale' +LOCALE_DIR = BASE_PATH / 'locale' / 'langs' # MySQL 脚本目录 MYSQL_SCRIPT_DIR = BASE_PATH / 'sql' / 'mysql' diff --git a/backend/locale/__init__.py b/backend/locale/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/locale/en-US.json b/backend/locale/langs/en-US.json similarity index 100% rename from backend/locale/en-US.json rename to backend/locale/langs/en-US.json diff --git a/backend/locale/zh-CN.yml b/backend/locale/langs/zh-CN.yml similarity index 100% rename from backend/locale/zh-CN.yml rename to backend/locale/langs/zh-CN.yml diff --git a/backend/locale/loader.py b/backend/locale/loader.py new file mode 100644 index 00000000..54784be2 --- /dev/null +++ b/backend/locale/loader.py @@ -0,0 +1,44 @@ +import glob +import json + +from pathlib import Path +from typing import Any + +import yaml + +from backend.core.path_conf import LOCALE_DIR + + +class LocaleLoader: + """语言文件加载器""" + + def __init__(self) -> None: + self.locales: dict[str, dict[str, Any]] = {} + self.load_locales() + + def load_locales(self) -> None: + """加载语言文本""" + patterns = [ + LOCALE_DIR / '*.json', + LOCALE_DIR / '*.yaml', + LOCALE_DIR / '*.yml', + ] + + lang_files = [] + + for pattern in patterns: + lang_files.extend(glob.glob(str(pattern))) + + for lang_file in lang_files: + with open(lang_file, encoding='utf-8') as f: + lang = Path(lang_file).stem + file_type = Path(lang_file).suffix[1:] + match file_type: + case 'json': + self.locales[lang] = json.loads(f.read()) + case 'yaml' | 'yml': + self.locales[lang] = yaml.full_load(f.read()) + + +# 创建语言加载器单例 +locale_loader = LocaleLoader() diff --git a/backend/middleware/i18n_middleware.py b/backend/middleware/i18n_middleware.py index 7c8981d6..48867441 100644 --- a/backend/middleware/i18n_middleware.py +++ b/backend/middleware/i18n_middleware.py @@ -4,6 +4,7 @@ from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware from backend.common.i18n import i18n +from backend.core.conf import settings def get_current_language(request: Request) -> str | None: @@ -15,7 +16,7 @@ def get_current_language(request: Request) -> str | None: """ accept_language = request.headers.get('Accept-Language', '') if not accept_language: - return None + return settings.I18N_DEFAULT_LANGUAGE languages = [lang.split(';')[0] for lang in accept_language.split(',')] lang = languages[0].lower().strip()