Archived
1
0
Fork 0
This repository has been archived on 2024-04-26. You can view files and clone it, but cannot push or open issues or pull requests.
akari-bot/core/utils/i18n.py

170 lines
6 KiB
Python
Raw Normal View History

2022-08-18 01:16:48 +00:00
import os
2023-08-12 06:18:43 +00:00
import re
2022-08-18 05:46:18 +00:00
from collections.abc import MutableMapping
2022-08-30 12:49:01 +00:00
from string import Template
2023-08-07 05:04:13 +00:00
from typing import TypedDict, Dict, Any, Union
2022-08-30 12:49:01 +00:00
import ujson as json
2022-08-18 05:46:18 +00:00
2023-04-05 04:33:29 +00:00
from config import Config
2023-04-19 07:41:39 +00:00
from .text import remove_suffix
2023-08-07 03:43:23 +00:00
2023-02-22 08:29:03 +00:00
2022-08-18 01:16:48 +00:00
# Load all locale files into memory
# We might change this behavior in the future and read them on demand as
# locale files get too large
2023-07-17 17:42:29 +00:00
class LocaleNode():
'''本地化树节点'''
value: str
childen: dict
2023-08-07 03:43:23 +00:00
def __init__(self, v: str = None):
2023-07-17 17:42:29 +00:00
self.value = v
self.childen = {}
2023-08-07 03:43:23 +00:00
2023-08-08 16:57:43 +00:00
def query_node(self, path: str):
2023-07-17 17:42:29 +00:00
'''查询本地化树节点'''
2023-08-08 16:57:43 +00:00
return self._query_node(path.split('.'))
2023-08-07 03:43:23 +00:00
2023-08-08 16:57:43 +00:00
def _query_node(self, path: list):
2023-07-17 17:42:29 +00:00
'''通过路径队列查询本地化树节点'''
if len(path) == 0:
return self
nxt_node = path[0]
if nxt_node in self.childen.keys():
2023-08-08 16:57:43 +00:00
return self.childen[nxt_node]._query_node(path[1:])
2023-07-17 17:42:29 +00:00
else:
return None
2023-08-07 03:43:23 +00:00
def update_node(self, path: str, write_value: str):
2023-07-17 17:42:29 +00:00
'''更新本地化树节点'''
2023-08-07 03:43:23 +00:00
return self._update_node(path.split('.'), write_value)
def _update_node(self, path: list, write_value: str):
2023-07-17 17:42:29 +00:00
'''通过路径队列更新本地化树节点'''
if len(path) == 0:
self.value = write_value
return
nxt_node = path[0]
if nxt_node not in self.childen.keys():
self.childen[nxt_node] = LocaleNode()
2023-08-07 03:43:23 +00:00
self.childen[nxt_node]._update_node(path[1:], write_value)
2023-07-17 17:42:29 +00:00
locale_root = LocaleNode()
2022-08-18 15:26:26 +00:00
2023-08-07 03:43:23 +00:00
2022-08-18 05:46:18 +00:00
# From https://stackoverflow.com/a/6027615
2023-06-02 18:09:46 +00:00
def flatten(d: Dict[str, Any], parent_key='', sep='.'):
2022-08-18 05:46:18 +00:00
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, MutableMapping):
items.extend(flatten(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)
2023-08-07 03:43:23 +00:00
2022-08-18 01:16:48 +00:00
def load_locale_file():
2023-07-17 17:42:29 +00:00
locale_dict = {}
2023-04-08 17:42:48 +00:00
err_prompt = []
2023-03-01 09:10:45 +00:00
locales_path = os.path.abspath('./locales')
locales = os.listdir(locales_path)
2023-04-21 08:47:46 +00:00
try:
for l in locales:
with open(f'{locales_path}/{l}', 'r', encoding='utf-8') as f:
2023-07-17 17:42:29 +00:00
locale_dict[remove_suffix(l, '.json')] = flatten(json.load(f))
2023-04-21 08:47:46 +00:00
except Exception as e:
2023-04-21 08:53:03 +00:00
err_prompt.append(str(e))
2023-03-01 09:10:45 +00:00
modules_path = os.path.abspath('./modules')
for m in os.listdir(modules_path):
2023-06-02 18:09:46 +00:00
if os.path.isdir(f'{modules_path}/{m}/locales'):
locales_m = os.listdir(f'{modules_path}/{m}/locales')
for lang_file in locales_m:
lang_file_path = f'{modules_path}/{m}/locales/{lang_file}'
with open(lang_file_path, 'r', encoding='utf-8') as f:
2023-06-02 18:09:46 +00:00
try:
if remove_suffix(lang_file, '.json') in locale_dict:
locale_dict[remove_suffix(lang_file, '.json')].update(flatten(json.load(f)))
2023-06-02 18:09:46 +00:00
else:
locale_dict[remove_suffix(lang_file, '.json')] = flatten(json.load(f))
2023-06-02 18:09:46 +00:00
except Exception as e:
err_prompt.append(f'Failed to load {lang_file_path}: {e}')
2023-07-17 17:42:29 +00:00
for lang in locale_dict.keys():
for k in locale_dict[lang].keys():
2023-08-07 03:43:23 +00:00
locale_root.update_node(f'{lang}.{k}', locale_dict[lang][k])
2023-04-08 17:42:48 +00:00
return err_prompt
2022-08-18 15:26:26 +00:00
2023-08-07 03:43:23 +00:00
2022-08-18 01:16:48 +00:00
class Locale:
2022-08-18 15:26:26 +00:00
def __init__(self, locale: str, fallback_lng=None):
"""创建一个本地化对象"""
if fallback_lng is None:
2023-03-01 11:38:39 +00:00
fallback_lng = ['zh_cn', 'zh_tw', 'en_us']
2022-08-18 01:16:48 +00:00
self.locale = locale
2023-08-08 16:57:43 +00:00
self.data: LocaleNode = locale_root.query_node(locale)
2022-08-18 01:16:48 +00:00
self.fallback_lng = fallback_lng
def __getitem__(self, key: str):
return self.data[key]
def __contains__(self, key: str):
return key in self.data
2023-08-07 05:04:13 +00:00
def t(self, key: Union[str, dict], fallback_failed_prompt=True, *args, **kwargs) -> str:
2022-08-18 01:16:48 +00:00
'''获取本地化字符串'''
2023-08-07 05:04:13 +00:00
if isinstance(key, dict):
if ft := key.get(self.locale) is not None:
return ft
elif 'fallback' in key:
return key['fallback']
else:
return str(key) + self.t("i18n.prompt.fallback.failed", url=Config('bug_report_url'),
fallback=self.locale)
2023-03-14 10:14:05 +00:00
localized = self.get_string_with_fallback(key, fallback_failed_prompt)
2022-08-18 07:31:24 +00:00
return Template(localized).safe_substitute(*args, **kwargs)
2023-08-07 03:43:23 +00:00
2023-07-17 17:42:29 +00:00
def get_locale_node(self, path: str):
'''获取本地化节点'''
2023-08-08 16:57:43 +00:00
return self.data.query_node(path)
2022-08-18 01:16:48 +00:00
2023-03-14 10:14:05 +00:00
def get_string_with_fallback(self, key: str, fallback_failed_prompt) -> str:
2023-08-08 16:57:43 +00:00
node = self.data.query_node(key)
2023-07-17 17:42:29 +00:00
if node is not None:
return node.value # 1. 如果本地化字符串存在,直接返回
2022-08-18 01:16:48 +00:00
fallback_lng = list(self.fallback_lng)
fallback_lng.insert(0, self.locale)
for lng in fallback_lng:
2023-07-17 17:42:29 +00:00
if lng in locale_root.childen:
2023-08-08 16:57:43 +00:00
node = locale_root.query_node(lng).query_node(key)
2023-07-24 04:40:26 +00:00
if node is not None:
return node.value # 2. 如果在 fallback 语言中本地化字符串存在,直接返回
2023-03-14 10:14:05 +00:00
if fallback_failed_prompt:
2023-04-21 10:11:04 +00:00
return f'{{{key}}}' + self.t("i18n.prompt.fallback.failed", url=Config('bug_report_url'),
fallback_failed_prompt=False)
2023-03-14 10:14:05 +00:00
else:
return key
2023-03-13 17:50:54 +00:00
# 3. 如果在 fallback 语言中本地化字符串不存在,返回 key
2022-08-18 15:26:26 +00:00
2023-08-12 06:18:43 +00:00
tl = t
def tl_str(self, text: str, fallback_failed_prompt=False) -> str:
return tl_str(self, text, fallback_failed_prompt=fallback_failed_prompt)
2023-08-07 03:43:23 +00:00
2022-08-18 05:46:18 +00:00
def get_available_locales():
2023-07-25 09:03:12 +00:00
return list(locale_root.childen.keys())
2022-08-18 05:46:18 +00:00
2023-08-07 03:43:23 +00:00
2023-08-12 06:18:43 +00:00
def tl_str(locale: Locale, text: str, fallback_failed_prompt=False) -> str:
if locale_str := re.findall(r'\{(.*)}', text):
for l in locale_str:
text = text.replace(f'{{{l}}}', locale.t(l, fallback_failed_prompt=fallback_failed_prompt))
return text
__all__ = ['Locale', 'load_locale_file', 'get_available_locales', 'tl_str']