From e10b140e1407c9720c096119649ee4d1684125ae Mon Sep 17 00:00:00 2001 From: rf_tar_railt <3165388245@qq.com> Date: Sun, 19 May 2024 15:14:57 +0800 Subject: [PATCH] :sparkles: lang model generate --- src/tarina/i18n/.lang.schema.json | 17 ++--- src/tarina/i18n/.template.json | 6 +- src/tarina/i18n/__init__.py | 8 +++ src/tarina/i18n/en-US.json | 10 +-- src/tarina/i18n/model.py | 14 ++++ src/tarina/i18n/zh-CN.json | 10 +-- src/tarina/lang/__init__.py | 109 +++++++++++++++--------------- src/tarina/lang/__main__.py | 76 +++++++++++++++++++++ src/tarina/lang/model.py | 75 ++++++++++++++++++++ src/tarina/lang/schema.py | 5 +- tests/test_main.py | 20 +++--- 11 files changed, 262 insertions(+), 88 deletions(-) create mode 100644 src/tarina/i18n/__init__.py create mode 100644 src/tarina/i18n/model.py create mode 100644 src/tarina/lang/model.py diff --git a/src/tarina/i18n/.lang.schema.json b/src/tarina/i18n/.lang.schema.json index 6302418..ce0b5b6 100644 --- a/src/tarina/i18n/.lang.schema.json +++ b/src/tarina/i18n/.lang.schema.json @@ -9,7 +9,13 @@ "title": "Lang", "description": "Scope 'lang' of lang item", "type": "object", + "additionalProperties": false, "properties": { + "locale_error": { + "title": "locale_error", + "description": "value of lang item type 'locale_error'", + "type": "string" + }, "scope_error": { "title": "scope_error", "description": "value of lang item type 'scope_error'", @@ -20,20 +26,15 @@ "description": "value of lang item type 'type_error'", "type": "string" }, - "name_error": { - "title": "name_error", - "description": "value of lang item type 'name_error'", + "miss_require_scope": { + "title": "miss_require_scope", + "description": "value of lang item type 'miss_require_scope'", "type": "string" }, "miss_require_type": { "title": "miss_require_type", "description": "value of lang item type 'miss_require_type'", "type": "string" - }, - "miss_require_name": { - "title": "miss_require_name", - "description": "value of lang item type 'miss_require_name'", - "type": "string" } } } diff --git a/src/tarina/i18n/.template.json b/src/tarina/i18n/.template.json index 93d029e..e134661 100644 --- a/src/tarina/i18n/.template.json +++ b/src/tarina/i18n/.template.json @@ -4,11 +4,11 @@ { "scope": "lang", "types": [ + "locale_error", "scope_error", "type_error", - "name_error", - "miss_require_type", - "miss_require_name" + "miss_require_scope", + "miss_require_type" ] } ] diff --git a/src/tarina/i18n/__init__.py b/src/tarina/i18n/__init__.py new file mode 100644 index 0000000..5745d1b --- /dev/null +++ b/src/tarina/i18n/__init__.py @@ -0,0 +1,8 @@ +from pathlib import Path + +from tarina.lang import lang + + +lang.load(Path(__file__).parent) + +from .model import Lang as Lang diff --git a/src/tarina/i18n/en-US.json b/src/tarina/i18n/en-US.json index d33bbc3..020fdf8 100644 --- a/src/tarina/i18n/en-US.json +++ b/src/tarina/i18n/en-US.json @@ -1,10 +1,10 @@ { "$schema": "./.lang.schema.json", "lang": { - "scope_error": "'{target}' is not a valid language scope", - "type_error": "'{target}' is not a valid type in '{scope}'", - "name_error": "'{target}' is not a valid name in '{scope}:{type}'", - "miss_require_type": "lang file '{scope}' missed require type '{target}'", - "miss_require_name": "'{type}' missed require name {target} in '{scope}'" + "locale_error": "'{target}' is not a valid language locale", + "scope_error": "'{target}' is not a valid scope in '{locale}'", + "type_error": "'{target}' is not a valid type in '{locale}:{scope}'", + "miss_require_scope": "lang file '{locale}' missed require scope '{target}'", + "miss_require_type": "'{scope}' missed require type {target} in lang file '{locale}'" } } \ No newline at end of file diff --git a/src/tarina/i18n/model.py b/src/tarina/i18n/model.py new file mode 100644 index 0000000..796a367 --- /dev/null +++ b/src/tarina/i18n/model.py @@ -0,0 +1,14 @@ +from tarina.lang.model import LangModel, LangItem + + +class Lang_: + locale_error: LangItem = LangItem("lang", "locale_error") + scope_error: LangItem = LangItem("lang", "scope_error") + type_error: LangItem = LangItem("lang", "type_error") + miss_require_scope: LangItem = LangItem("lang", "miss_require_scope") + miss_require_type: LangItem = LangItem("lang", "miss_require_type") + + +class Lang(LangModel): + lang = Lang_ + diff --git a/src/tarina/i18n/zh-CN.json b/src/tarina/i18n/zh-CN.json index 185c25f..6c19210 100644 --- a/src/tarina/i18n/zh-CN.json +++ b/src/tarina/i18n/zh-CN.json @@ -1,10 +1,10 @@ { "$schema": "./.lang.schema.json", "lang": { - "scope_error": "'{target}' 不是合法的语种", - "type_error": "'{target}' 在 '{scope}' 中不是合法的类型", - "name_error": "'{target}' 在 '{scope}:{type}' 不是合法的名称", - "miss_require_type": "语言文件 '{scope}' 缺少需求的类型 '{target}'", - "miss_require_name": "'{scope}' 的 '{type}' 缺少需求的内容 {target}" + "locale_error": "'{target}' 不是合法的语种", + "scope_error": "'{target}' 在 '{locale}' 中不是合法的域名", + "type_error": "'{target}' 在 '{locale}:{scope}' 不是合法的类型", + "miss_require_scope": "语言文件 '{locale}' 缺少需求的域名 '{target}'", + "miss_require_type": "语言文件 '{locale}' 的 '{scope}' 缺少需求的内容 {target}" } } \ No newline at end of file diff --git a/src/tarina/lang/__init__.py b/src/tarina/lang/__init__.py index 6ff3894..59eff5a 100644 --- a/src/tarina/lang/__init__.py +++ b/src/tarina/lang/__init__.py @@ -61,12 +61,14 @@ def _get_config(root: Path) -> _LangDict: def _get_scopes(root: Path) -> list[str]: - return [i.stem for i in root.iterdir() if i.is_file() and not i.name.startswith(".")] + return [i.stem for i in root.iterdir() if i.is_file() and i.suffix == ".json" and not i.name.startswith(".")] def _get_lang(root: Path, _type: str) -> dict[str, dict[str, str]]: with (root / f"{_type}.json").open("r", encoding="utf-8") as f: - return json.load(f) + data: dict[str, dict[str, str]] = json.load(f) + data.pop("$schema", None) + return data def merge(source: dict, target: dict, ignore: list[str] | None = None) -> dict: @@ -87,105 +89,106 @@ def merge(source: dict, target: dict, ignore: list[str] | None = None) -> dict: class _LangConfig: def __init__(self): __config = _get_config(root_dir) - self.__scope: str = __config["default"] + self.__locale: str = __config["default"] self.__frozen: list[str] = __config["frozen"] self.__require: list[str] = __config["require"] self.__langs = {t.replace("_", "-"): _get_lang(root_dir, t) for t in _get_scopes(root_dir)} - self.__scopes = set(self.__langs.keys()) + self.__locales = set(self.__langs.keys()) self.select_local() @property - def scopes(self): - return self.__scopes + def locales(self): + return self.__locales @property def current(self): - return self.__scope + return self.__locale def select_local(self): """ 依据系统语言尝试自动选择语言 """ if (lc := get_locale()) and lc.replace("_", "-") in self.__langs: - self.__scope = lc.replace("_", "-") + self.__locale = lc.replace("_", "-") return self - def select(self, item: str) -> Self: - item = item.replace("_", "-") - if item not in self.__langs: - raise ValueError(self.require("lang", "scope_error").format(target=item)) - self.__scope = item + def select(self, locale: str) -> Self: + locale = locale.replace("_", "-") + if locale not in self.__langs: + raise ValueError(self.require("lang", "locale_error").format(target=locale)) + self.__locale = locale return self def save(self, root: Path | None = None): _root = root or root_dir config = _get_config(_root) - config["default"] = self.__scope + config["default"] = self.__locale with (_root / ".config.json").open("w+", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) - def load_data(self, scope: str, data: dict[str, dict[str, str]]): - if scope in self.__langs: - self.__langs[scope] = merge(data, self.__langs[scope], self.__frozen) + def load_data(self, locale: str, data: dict[str, dict[str, str]]): + if locale in self.__langs: + self.__langs[locale] = merge(data, self.__langs[locale], self.__frozen) else: - self.__scopes.add(scope) - self.__langs[scope] = data + self.__locales.add(locale) + self.__langs[locale] = data for key in self.__require: parts = key.split(".", 1) - t = parts[0] - n = parts[1] if len(parts) > 1 else None - if t not in self.__langs[scope]: - raise KeyError(self.require("lang", "miss_require_type", scope).format(scope=scope, target=t)) - if n and n not in self.__langs[scope][t]: - raise KeyError(self.require("lang", "miss_require_name", scope).format(scope=scope, type=t, target=n)) + s = parts[0] + t = parts[1] if len(parts) > 1 else None + if s not in self.__langs[locale]: + raise KeyError(self.require("lang", "miss_require_scope", locale).format(locale=locale, target=s)) + if t and t not in self.__langs[locale][s]: + raise KeyError(self.require("lang", "miss_require_type", locale).format(locale=locale, scope=s, target=t)) def load_file(self, filepath: Path): return self.load_data(filepath.stem, _get_lang(filepath.parent, filepath.stem)) def load_config(self, config: _LangDict): - self.__scope = config.get("default", self.__scope) + self.__locale = config.get("default", self.__locale) self.__frozen.extend(config.get("frozen", [])) self.__require.extend(config.get("require", [])) self.select_local() - def load(self, root: Path): + def load(self, root: Path) -> Self: self.load_config(_get_config(root)) for i in root.iterdir(): if not i.is_file() or i.name.startswith("."): continue self.load_file(i) + return self - def require(self, _type: str, _name: str, scope: str | None = None) -> str: - scope = scope or self.__scope - if scope not in self.__langs: - raise ValueError(self.__langs[self.__scope]["lang"]["scope_error"].format(target=scope)) - if _type in self.__langs[scope]: - _types = self.__langs[scope][_type] - elif _type in self.__langs[self.__scope]: - _types = self.__langs[self.__scope][_type] - elif _type in self.__langs[(default := _get_config(root_dir)["default"])]: - _types = self.__langs[default][_type] + def require(self, scope: str, type: str, locale: str | None = None) -> str: + locale = locale or self.__locale + if locale not in self.__langs: + raise ValueError(self.__langs[self.__locale]["lang"]["locale_error"].format(target=locale)) + if scope in self.__langs[locale]: + _types = self.__langs[locale][scope] + elif scope in self.__langs[self.__locale]: + _types = self.__langs[self.__locale][scope] + elif scope in self.__langs[(default := _get_config(root_dir)["default"])]: + _types = self.__langs[default][scope] else: - raise ValueError(self.__langs[scope]["lang"]["type_error"].format(target=_type, scope=scope)) - if _name in _types: - return _types[_name] - elif _name in self.__langs[self.__scope][_type]: - return self.__langs[self.__scope][_type][_name] - elif _name in self.__langs[(default := _get_config(root_dir)["default"])][_type]: - return self.__langs[default][_type][_name] + raise ValueError(self.__langs[locale]["lang"]["scope_error"].format(target=scope, locale=locale)) + if type in _types: + return _types[type] + elif type in self.__langs[self.__locale][scope]: + return self.__langs[self.__locale][scope][type] + elif type in self.__langs[(default := _get_config(root_dir)["default"])][scope]: + return self.__langs[default][scope][type] else: - raise ValueError(self.__langs[scope]["lang"]["name_error"].format(target=_name, scope=scope, type=_type)) + raise ValueError(self.__langs[locale]["lang"]["type_error"].format(target=type, locale=locale, scope=scope)) - def set(self, _type: str, _name: str, content: str, scope: str | None = None): - scope = scope or self.__scope - if scope not in self.__langs: - raise ValueError(self.__langs[self.__scope]["lang"]["scope_error"].format(target=scope)) - if _type in self.__frozen: - raise ValueError(self.__langs[scope]["lang"]["type_error"].format(target=_type, scope=scope)) - self.__langs[scope].setdefault(_type, {})[_name] = content + def set(self, scope: str, type: str, content: str, locale: str | None = None): + locale = locale or self.__locale + if locale not in self.__langs: + raise ValueError(self.__langs[self.__locale]["lang"]["locale_error"].format(target=locale)) + if scope in self.__frozen: + raise ValueError(self.__langs[locale]["lang"]["scope_error"].format(target=scope, locale=locale)) + self.__langs[locale].setdefault(scope, {})[type] = content def __repr__(self): - return f"" + return f"" lang: _LangConfig = _LangConfig() diff --git a/src/tarina/lang/__main__.py b/src/tarina/lang/__main__.py index 4ab8c2d..679deab 100644 --- a/src/tarina/lang/__main__.py +++ b/src/tarina/lang/__main__.py @@ -1,6 +1,8 @@ from argparse import ArgumentParser from tarina.lang.schema import write_lang_schema +from tarina.lang.model import write_model from pathlib import Path +import json CONFIG_TEMPLATE = """ { @@ -10,6 +12,19 @@ } """ +CONFIG_INIT = """ +# This file is @generated by tarina.lang CLI tool +# It is not intended for manual editing. + +from pathlib import Path + +from tarina.lang import lang + + +lang.load(Path(__file__).parent) +""" + + TEMPLATE_SCHEMA = """ { "title": "Template", @@ -59,16 +74,28 @@ } """ +def new(*_): + i18n_dir = Path.cwd() / "i18n" + if i18n_dir.exists(): + print("i18n directory already exists") + return + i18n_dir.mkdir() + print(f"i18n directory created: {i18n_dir}") + def init(*_): root = Path.cwd() config_file = root / ".config.json" + init_file = root / "__init__.py" template_file = root / ".template.json" template_schema = root / ".template.schema.json" with config_file.open("w+") as f: f.write(CONFIG_TEMPLATE) + with init_file.open("w+") as f: + f.write(CONFIG_INIT) + with template_file.open("w+") as f: f.write(TEMPLATE_TEMPLATE) @@ -85,6 +112,26 @@ def init(*_): """ ) + +def default(args): + root = Path.cwd() + config_file = root / ".config.json" + if not config_file.exists(): + print("config file not found") + return + + with config_file.open("r") as f: + config = json.load(f) + + if args.locale: + config["default"] = args.locale + with config_file.open("w") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + print(f"default lang scope set to: {args.locale}") + else: + print(f"default lang scope: {config['default']}") + + def schema(*_): root = Path.cwd() schema_file = root / ".lang.schema.json" @@ -97,6 +144,25 @@ def schema(*_): print(f"schema for lang file {'created' if created else 'updated'}. Now you can create or update your lang files.") +def model(*_): + root = Path.cwd() + model_file = root / "model.py" + init_file = root / "__init__.py" + created = not model_file.exists() + try: + write_model(Path.cwd()) + with init_file.open("r") as f: + lines = f.readlines() + if lines[-1] != "from .model import Lang as Lang\n": + with init_file.open("a") as f: + f.write("\nfrom .model import Lang as Lang\n") + except Exception as e: + print(repr(e)) + else: + print(f"model for lang file {'created' if created else 'updated'}. Now you can create or update your lang files.") + + + def create(args): root = Path.cwd() lang_file = root / f"{args.name}.json" @@ -124,12 +190,22 @@ def main(): subparsers = parser.add_subparsers(dest="command") + new_parser = subparsers.add_parser("new", help="create a new i18n directory") + new_parser.set_defaults(func=new) + init_parser = subparsers.add_parser("init", help="initialize a new lang configs") init_parser.set_defaults(func=init) + default_parser = subparsers.add_parser("default", help="show or set default lang locale") + default_parser.add_argument("locale", type=str, nargs="?", help="lang locale to set as default") + default_parser.set_defaults(func=default) + schema_parser = subparsers.add_parser("schema", help="generate or update lang schema") schema_parser.set_defaults(func=schema) + model_parser = subparsers.add_parser("model", help="generate or update lang model") + model_parser.set_defaults(func=model) + create_parser = subparsers.add_parser("create", help="create a new lang file") create_parser.add_argument("name", type=str, help="name of the lang file") create_parser.set_defaults(func=create) diff --git a/src/tarina/lang/model.py b/src/tarina/lang/model.py new file mode 100644 index 0000000..2e559a9 --- /dev/null +++ b/src/tarina/lang/model.py @@ -0,0 +1,75 @@ +import re +from pathlib import Path +from tarina.lang import lang +from tarina.lang.schema import _TemplateDict, get_template +import keyword + + +class LangItem: + def __init__(self, scope: str, type: str): + self.scope = scope + self.type = type + + def __call__(self, **format_kwargs): + return lang.require(self.scope, self.type).format(**format_kwargs) + + def __iter__(self): + return iter([self.scope, self.type]) + +class LangModel: + pass + + +MODEL_TEMPLATE = """\ +# This file is @generated by tarina.lang CLI tool +# It is not intended for manual editing. + +from tarina.lang.model import LangModel, LangItem + + +{scope_classes} +class Lang(LangModel): +{scopes} +""" + + +SCOPE_TEMPLATE = """\ +class {scope}: +{types} +""" + +TYPE_TEMPLATE = """\ + {name}: LangItem = LangItem("{scope}", "{type}") +""" + + + +def generate_model(root: Path): + template = get_template(root) + if "scopes" not in template: + raise KeyError("Template file must have a 'scopes' key") + scopes: list[_TemplateDict] = template["scopes"] + scopes_classes = "" + scopes_str = "" + for s in scopes: + scope = re.sub(r"[\W\s]+", "_", s["scope"]) + if scope in keyword.kwlist: + scope += "_" + scope_class = scope.capitalize() + if scope_class == "Lang": + scope_class += "_" + types_str = "" + for t in s["types"]: + name = re.sub(r"[\W\s]+", "_", t) + if name in keyword.kwlist: + name += "_" + types_str += TYPE_TEMPLATE.format(name=t, scope=s["scope"], type=t) + scopes_classes += SCOPE_TEMPLATE.format(scope=scope_class, types=types_str) + scopes_str += f" {scope} = {scope_class}\n" + return MODEL_TEMPLATE.format(scope_classes=scopes_classes, scopes=scopes_str) + + +def write_model(root: Path): + model = generate_model(root) + with (root / f"model.py").open("w", encoding="utf-8") as f: + f.write(model) diff --git a/src/tarina/lang/schema.py b/src/tarina/lang/schema.py index 016c485..c4bf4e2 100644 --- a/src/tarina/lang/schema.py +++ b/src/tarina/lang/schema.py @@ -20,6 +20,7 @@ def schema_scope(scope: str, types: list[str]): "title": scope.capitalize(), "description": f"Scope '{scope}' of lang item", "type": "object", + "additionalProperties": False, "properties": { i: { "title": i, @@ -52,7 +53,3 @@ def write_lang_schema(root: Path): schema = generate_lang_schema(root) with (root / f".lang.schema.json").open("w", encoding="utf-8") as f: json.dump(schema, f, ensure_ascii=False, indent=2) - - -if __name__ == "__main__": - write_lang_schema(Path(__file__).parent.parent / "i18n") \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 4a02ae8..1dc945e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -27,10 +27,10 @@ def test_lru(): cache["a"] = "a" cache["b"] = "b" cache["c"] = "c" - assert cache.peek_first_item()[1] == "c" + assert cache.peek_first_item()[1] == "c" # type: ignore _ = cache.get("a", Ellipsis) print(f"\n{cache.items()}") - assert cache.peek_first_item()[1] == "a" + assert cache.peek_first_item()[1] == "a" # type: ignore cache["d"] = "d" assert cache.get("b", Ellipsis) == Ellipsis @@ -77,25 +77,25 @@ def test_lang(): """测试 i18n""" from tarina import lang - assert lang.scopes == {"zh-CN", "en-US"} + assert lang.locales == {"zh-CN", "en-US"} lang.select("zh-CN") assert lang.current == "zh-CN" - assert lang.require("lang", "name_error") == "'{target}' 在 '{scope}:{type}' 不是合法的名称" - assert lang.require("lang", "name_error", "en-US") == "'{target}' is not a valid name in '{scope}:{type}'" + assert lang.require("lang", "type_error") == "'{target}' 在 '{locale}:{scope}' 不是合法的类型" + assert lang.require("lang", "type_error", "en-US") == "'{target}' is not a valid type in '{locale}:{scope}'" lang.select("en-US") assert lang.current == "en-US" try: lang.select("ru-RU") except ValueError as e: - assert str(e) == "'ru-RU' is not a valid language scope" + assert str(e) == "'ru-RU' is not a valid language locale" try: lang.load_data("test", {}) except KeyError as e: - assert str(e) == "\"lang file 'test' missed require type 'lang'\"" - lang.load_data("test", {"lang": {"name_error": "test"}}) + assert str(e) == "\"lang file 'test' missed require scope 'lang'\"" + lang.load_data("test", {"lang": {"type_error": "test"}}) lang.select("test") - assert lang.require("lang", "name_error") == "test" - assert lang.require("lang", "scope_error") == "'{target}' 不是合法的语种" + assert lang.require("lang", "type_error") == "test" + assert lang.require("lang", "locale_error") == "'{target}' 不是合法的语种" def test_init_spec():