I18n Tree & locale reload (#695)
This commit is contained in:
parent
fafe8e5d80
commit
4731ab00df
5 changed files with 76 additions and 36 deletions
|
@ -12,8 +12,40 @@ from .text import remove_suffix
|
|||
|
||||
# We might change this behavior in the future and read them on demand as
|
||||
# locale files get too large
|
||||
locale_cache = {}
|
||||
|
||||
class LocaleNode():
|
||||
'''本地化树节点'''
|
||||
value: str
|
||||
childen: dict
|
||||
def __init__(self,v:str = None):
|
||||
self.value = v
|
||||
self.childen = {}
|
||||
def qurey_node(self,path:str):
|
||||
'''查询本地化树节点'''
|
||||
return self._qurey_node(path.split('.'))
|
||||
def _qurey_node(self,path:list):
|
||||
'''通过路径队列查询本地化树节点'''
|
||||
if len(path) == 0:
|
||||
return self
|
||||
nxt_node = path[0]
|
||||
if nxt_node in self.childen.keys():
|
||||
return self.childen[nxt_node]._qurey_node(path[1:])
|
||||
else:
|
||||
return None
|
||||
def update_node(self,path:str,write_value:str):
|
||||
'''更新本地化树节点'''
|
||||
return self._update_node(path.split('.'),write_value)
|
||||
def _update_node(self,path:list,write_value:str):
|
||||
'''通过路径队列更新本地化树节点'''
|
||||
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()
|
||||
self.childen[nxt_node]._update_node(path[1:],write_value)
|
||||
|
||||
locale_root = LocaleNode()
|
||||
|
||||
# From https://stackoverflow.com/a/6027615
|
||||
def flatten(d: Dict[str, Any], parent_key='', sep='.'):
|
||||
|
@ -26,15 +58,15 @@ def flatten(d: Dict[str, Any], parent_key='', sep='.'):
|
|||
items.append((new_key, v))
|
||||
return dict(items)
|
||||
|
||||
|
||||
def load_locale_file():
|
||||
locale_dict = {}
|
||||
err_prompt = []
|
||||
locales_path = os.path.abspath('./locales')
|
||||
locales = os.listdir(locales_path)
|
||||
try:
|
||||
for l in locales:
|
||||
with open(f'{locales_path}/{l}', 'r', encoding='utf-8') as f:
|
||||
locale_cache[remove_suffix(l, '.json')] = flatten(json.load(f))
|
||||
locale_dict[remove_suffix(l, '.json')] = flatten(json.load(f))
|
||||
except Exception as e:
|
||||
err_prompt.append(str(e))
|
||||
modules_path = os.path.abspath('./modules')
|
||||
|
@ -45,29 +77,24 @@ def load_locale_file():
|
|||
ml = f'{modules_path}/{m}/locales/{lm}'
|
||||
with open(ml, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
if remove_suffix(lm, '.json') in locale_cache:
|
||||
locale_cache[remove_suffix(lm, '.json')].update(flatten(json.load(f)))
|
||||
if remove_suffix(lm, '.json') in locale_dict:
|
||||
locale_dict[remove_suffix(lm, '.json')].update(flatten(json.load(f)))
|
||||
else:
|
||||
locale_cache[remove_suffix(lm, '.json')] = flatten(json.load(f))
|
||||
locale_dict[remove_suffix(lm, '.json')] = flatten(json.load(f))
|
||||
except Exception as e:
|
||||
err_prompt.append(f'Failed to load {ml}: {e}')
|
||||
|
||||
for lang in locale_dict.keys():
|
||||
for k in locale_dict[lang].keys():
|
||||
locale_root.update_node(f'{lang}.{k}',locale_dict[lang][k])
|
||||
return err_prompt
|
||||
|
||||
|
||||
class LocaleFile(TypedDict):
|
||||
key: str
|
||||
string: str
|
||||
|
||||
|
||||
class Locale:
|
||||
def __init__(self, locale: str, fallback_lng=None):
|
||||
"""创建一个本地化对象"""
|
||||
|
||||
if fallback_lng is None:
|
||||
fallback_lng = ['zh_cn', 'zh_tw', 'en_us']
|
||||
self.locale = locale
|
||||
self.data: LocaleFile = locale_cache[locale]
|
||||
self.data: LocaleNode = locale_root.qurey_node(locale)
|
||||
self.fallback_lng = fallback_lng
|
||||
|
||||
def __getitem__(self, key: str):
|
||||
|
@ -80,16 +107,20 @@ class Locale:
|
|||
'''获取本地化字符串'''
|
||||
localized = self.get_string_with_fallback(key, fallback_failed_prompt)
|
||||
return Template(localized).safe_substitute(*args, **kwargs)
|
||||
|
||||
def get_locale_node(self, path: str):
|
||||
'''获取本地化节点'''
|
||||
return self.data.qurey_node(path)
|
||||
|
||||
def get_string_with_fallback(self, key: str, fallback_failed_prompt) -> str:
|
||||
value = self.data.get(key, None)
|
||||
if value is not None:
|
||||
return value # 1. 如果本地化字符串存在,直接返回
|
||||
node = self.data.qurey_node(key)
|
||||
if node is not None:
|
||||
return node.value # 1. 如果本地化字符串存在,直接返回
|
||||
fallback_lng = list(self.fallback_lng)
|
||||
fallback_lng.insert(0, self.locale)
|
||||
for lng in fallback_lng:
|
||||
if lng in locale_cache:
|
||||
string = locale_cache[lng].get(key, None)
|
||||
if lng in locale_root.childen:
|
||||
string = locale_root.qurey_node(lng).qurey_node(key).value
|
||||
if string is not None:
|
||||
return string # 2. 如果在 fallback 语言中本地化字符串存在,直接返回
|
||||
if fallback_failed_prompt:
|
||||
|
@ -99,9 +130,7 @@ class Locale:
|
|||
return key
|
||||
# 3. 如果在 fallback 语言中本地化字符串不存在,返回 key
|
||||
|
||||
|
||||
def get_available_locales():
|
||||
return list(locale_cache.keys())
|
||||
return list(locale_root.keys())
|
||||
|
||||
|
||||
__all__ = ['Locale', 'load_locale_file', 'get_available_locales']
|
||||
__all__ = ['Locale', 'load_locale_file', 'get_available_locales']
|
|
@ -1,4 +1,4 @@
|
|||
import secrets
|
||||
import secrets
|
||||
|
||||
from config import Config
|
||||
from core.builtins import Bot
|
||||
|
@ -36,7 +36,7 @@ async def flipCoins(count: int, msg):
|
|||
if FACE_UP_RATE + FACE_DOWN_RATE > 10000 or FACE_UP_RATE < 0 or FACE_DOWN_RATE < 0 or MAX_COIN_NUM <= 0:
|
||||
raise OverflowError(msg.locale.t("error.config"))
|
||||
if count > MAX_COIN_NUM:
|
||||
return msg.locale.t("coin.message.error.out_of_range", max=count_max)
|
||||
return msg.locale.t("coin.message.error.out_of_range", max=MAX_COIN_NUM)
|
||||
if count == 0:
|
||||
return msg.locale.t("coin.message.error.nocoin")
|
||||
if count < 0:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"coin.help": "抛任意枚硬币。",
|
||||
"coin.help.desc": "抛硬币。",
|
||||
"coin.help.regex.desc": "(丢|抛)[<count>(个|枚)]硬币",
|
||||
|
@ -14,7 +14,7 @@
|
|||
"coin.message.mix.stand": "……还有 ${stand} 枚立起来了!",
|
||||
"coin.message.mix.tail": ",${tail} 枚是反面",
|
||||
"coin.message.mix.tail2": "…有 ${tail} 枚是反面",
|
||||
"coin.message.prompt": "你抛了 ${count} 枚硬币",
|
||||
"coin.message.prompt": "你抛了 ${count} 枚硬币…",
|
||||
"coin.message.stand": "…\n…它立起来了!",
|
||||
"coin.message.tail": "…\n…是反面!"
|
||||
}
|
|
@ -16,7 +16,8 @@
|
|||
"core.help.alias.remove": "移除自定义命令别名。",
|
||||
"core.help.alias.reset": "重置自定义命令别名。",
|
||||
"core.help.leave": "使机器人离开群组。",
|
||||
"core.help.locale": "设置机器人运行语言。",
|
||||
"core.help.locale.reload": "重载机器人语言文件",
|
||||
"core.help.locale.set": "设置机器人运行语言。",
|
||||
"core.help.module.disable": "关闭一个/多个模块。",
|
||||
"core.help.module.disable_all": "关闭所有模块。",
|
||||
"core.help.module.enable": "开启一个/多个模块。",
|
||||
|
@ -66,7 +67,8 @@
|
|||
"core.message.confirm": "你确定吗?",
|
||||
"core.message.leave.confirm": "你确定吗?此操作不可逆。",
|
||||
"core.message.leave.success": "已执行,再见。",
|
||||
"core.message.locale.invalid": "语言格式错误,支持的语言有:${lang}。",
|
||||
"core.message.locale.set.invalid": "语言格式错误,支持的语言有:${lang}。",
|
||||
"core.message.locale.reload.failed": "以下字符串重载失败:${detail}。",
|
||||
"core.message.module.disable.already": "失败:“${module}”模块已关闭。",
|
||||
"core.message.module.disable.base": "失败:“${module}”为基础模块,无法关闭。",
|
||||
"core.message.module.disable.not_found": "失败:“${module}”模块不存在。",
|
||||
|
|
|
@ -6,7 +6,7 @@ import time
|
|||
from config import Config
|
||||
from core.builtins import Bot, PrivateAssets
|
||||
from core.component import module
|
||||
from core.utils.i18n import get_available_locales, Locale
|
||||
from core.utils.i18n import get_available_locales, Locale, load_locale_file
|
||||
from cpuinfo import get_cpu_info
|
||||
from database import BotDBUtil
|
||||
from datetime import datetime
|
||||
|
@ -128,20 +128,29 @@ async def config_ban(msg: Bot.MessageSession):
|
|||
locale = module('locale',
|
||||
base=True,
|
||||
required_admin=True,
|
||||
developers=['Dianliang233']
|
||||
developers=['Dianliang233','Light-Beacon']
|
||||
)
|
||||
|
||||
|
||||
@locale.handle(['<lang> {{core.help.locale}}'])
|
||||
@locale.handle(['set <lang> {{core.help.locale.set}}'])
|
||||
async def config_gu(msg: Bot.MessageSession):
|
||||
lang = msg.parsed_msg['<lang>']
|
||||
if lang in ['zh_cn', 'zh_tw', 'en_us']:
|
||||
if BotDBUtil.TargetInfo(msg.target.targetId).edit('locale', lang):
|
||||
await msg.finish(Locale(lang).t('success'))
|
||||
else:
|
||||
await msg.finish(msg.locale.t("core.message.locale.invalid", lang='、'.join(get_available_locales())))
|
||||
|
||||
|
||||
await msg.finish(msg.locale.t("core.message.locale.set.invalid", lang='、'.join(get_available_locales())))
|
||||
@locale.handle(['reload {{core.help.locale.reload}}'])
|
||||
async def reload_locale(msg: Bot.MessageSession):
|
||||
if msg.checkSuperUser():
|
||||
err = load_locale_file()
|
||||
if len(err) == 0:
|
||||
await msg.finish(msg.locale.t("success"))
|
||||
else:
|
||||
await msg.finish(msg.locale.t("core.message.locale.reload.failed",detail='\n'.join(err)))
|
||||
else:
|
||||
await msg.finish(msg.locale.t("parser.superuser.permission.denied"))
|
||||
|
||||
whoami = module('whoami', developers=['Dianliang233'], base=True)
|
||||
|
||||
|
||||
|
|
Reference in a new issue