diff --git a/.cargo/config.toml b/.cargo/config.toml index c2cf0e821..e5c67dcfb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,6 @@ [env] -STRINGS_JSON = { value = "out/rslib/i18n/strings.json", relative = true } +# STRINGS_JSON = { value = "out/rslib/i18n/strings.json", relative = true } +STRINGS_PY = { value = "out/pylib/anki/_fluent.py", relative = true } STRINGS_JS = { value = "out/ts/lib/ftl.js", relative = true } STRINGS_DTS = { value = "out/ts/lib/ftl.d.ts", relative = true } DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true } diff --git a/Cargo.toml b/Cargo.toml index 1fbf91f14..226da6d51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,6 +147,10 @@ debug = 0 opt-level = 1 debug = 0 +[profile.dev.package.anki_proto] +opt-level = 1 +debug = 0 + # Debug info off by default, which speeds up incremental builds and produces a considerably # smaller library. [profile.dev.package.anki] diff --git a/build/configure/src/pylib.rs b/build/configure/src/pylib.rs index 2e01d1a23..7d269cbd2 100644 --- a/build/configure/src/pylib.rs +++ b/build/configure/src/pylib.rs @@ -27,22 +27,8 @@ pub fn build_pylib(build: &mut Build) -> Result<()> { }, )?; build.add_dependency("pylib:anki:proto", ":rslib:proto:py".into()); + build.add_dependency("pylib:anki:i18n", ":rslib:i18n:py".into()); - build.add_action( - "pylib:anki:_fluent.py", - RunCommand { - command: ":pyenv:bin", - args: "$script $strings $out", - inputs: hashmap! { - "script" => inputs!["pylib/tools/genfluent.py"], - "strings" => inputs![":rslib:i18n:strings.json"], - "" => inputs!["pylib/anki/_vendor/stringcase.py"] - }, - outputs: hashmap! { - "out" => vec!["pylib/anki/_fluent.py"] - }, - }, - )?; build.add_action( "pylib:anki:hooks_gen.py", RunCommand { diff --git a/build/configure/src/rust.rs b/build/configure/src/rust.rs index 32d9795a7..c4592eab4 100644 --- a/build/configure/src/rust.rs +++ b/build/configure/src/rust.rs @@ -48,10 +48,11 @@ fn prepare_translations(build: &mut Build) -> Result<()> { glob!["ftl/{core,core-repo,qt,qt-repo}/**"], ":ftl:repo", ], - outputs: &[RustOutput::Data( - "strings.json", - "$builddir/rslib/i18n/strings.json", - )], + outputs: &[ + RustOutput::Data("py", "pylib/anki/_fluent.py"), + RustOutput::Data("ts", "ts/lib/ftl.d.ts"), + RustOutput::Data("ts", "ts/lib/ftl.js"), + ], target: None, extra_args: "-p anki_i18n", release_override: None, diff --git a/build/configure/src/web.rs b/build/configure/src/web.rs index 1800bcc3d..b56014e82 100644 --- a/build/configure/src/web.rs +++ b/build/configure/src/web.rs @@ -114,7 +114,7 @@ fn setup_node(build: &mut Build) -> Result<()> { } fn build_and_check_tslib(build: &mut Build) -> Result<()> { - build.add_dependency("ts:lib:i18n", ":rslib:i18n".into()); + build.add_dependency("ts:lib:i18n", ":rslib:i18n:ts".into()); build.add_action( "ts:lib:proto", GenTypescriptProto { diff --git a/pylib/tools/genfluent.py b/pylib/tools/genfluent.py deleted file mode 100644 index cff247cf7..000000000 --- a/pylib/tools/genfluent.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# pylint: disable=unbalanced-tuple-unpacking - -import json -import sys -from typing import Literal, TypedDict - -sys.path.append("pylib/anki/_vendor") - -import stringcase - -strings_json, outfile = sys.argv[1:] -with open(strings_json, encoding="utf8") as f: - modules = json.load(f) - - -class Variable(TypedDict): - name: str - kind: Literal["Any", "Int", "String", "Float"] - - -def legacy_enum() -> str: - out = ["class LegacyTranslationEnum:"] - for module in modules: - for translation in module["translations"]: - key = stringcase.constcase(translation["key"]) - value = f'({module["index"]}, {translation["index"]})' - out.append(f" {key} = {value}") - - return "\n".join(out) + "\n" - - -def methods() -> str: - out = [ - "class GeneratedTranslations:", - " def _translate(self, module: int, translation: int, args: Dict) -> str:", - " raise Exception('not implemented')", - ] - for module in modules: - for translation in module["translations"]: - key = translation["key"].replace("-", "_") - arg_types = get_arg_types(translation["variables"]) - args = get_args(translation["variables"]) - doc = translation["text"] - out.append( - f""" - def {key}(self, {arg_types}) -> str: - r''' {doc} ''' - return self._translate({module["index"]}, {translation["index"]}, {{{args}}}) -""" - ) - - return "\n".join(out) + "\n" - - -def get_arg_types(args: list[Variable]) -> str: - return ", ".join( - [f"{stringcase.snakecase(arg['name'])}: {arg_kind(arg)}" for arg in args] - ) - - -def arg_kind(arg: Variable) -> str: - if arg["kind"] == "Int": - return "int" - elif arg["kind"] == "Any": - return "FluentVariable" - elif arg["kind"] == "Float": - return "float" - else: - return "str" - - -def get_args(args: list[Variable]) -> str: - return ", ".join( - [f'"{arg["name"]}": {stringcase.snakecase(arg["name"])}' for arg in args] - ) - - -out = "" - -out += legacy_enum() -out += methods() - - -with open(outfile, "w", encoding="utf8") as f: - f.write( - '''# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -# pylint: skip-file - -from __future__ import annotations - -""" -This file is automatically generated from the *.ftl files. -""" - -import enum -from typing import Dict, Union - -FluentVariable = Union[str, int, float] - -''' - + out - ) diff --git a/rslib/i18n/Cargo.toml b/rslib/i18n/Cargo.toml index e74efc6b2..899680cc3 100644 --- a/rslib/i18n/Cargo.toml +++ b/rslib/i18n/Cargo.toml @@ -2,7 +2,6 @@ name = "anki_i18n" version.workspace = true authors.workspace = true -build = "build/main.rs" edition.workspace = true license.workspace = true publish = false diff --git a/rslib/i18n/build/main.rs b/rslib/i18n/build.rs similarity index 95% rename from rslib/i18n/build/main.rs rename to rslib/i18n/build.rs index 7fae2a7f9..0f2478158 100644 --- a/rslib/i18n/build/main.rs +++ b/rslib/i18n/build.rs @@ -4,6 +4,7 @@ mod check; mod extract; mod gather; +mod python; mod typescript; mod write_strings; @@ -27,6 +28,7 @@ fn main() -> Result<()> { write_strings(&map, &modules); typescript::write_ts_interface(&modules)?; + python::write_py_interface(&modules)?; // write strings.json file to requested path println!("cargo:rerun-if-env-changed=STRINGS_JSON"); diff --git a/rslib/i18n/build/check.rs b/rslib/i18n/check.rs similarity index 100% rename from rslib/i18n/build/check.rs rename to rslib/i18n/check.rs diff --git a/rslib/i18n/build/extract.rs b/rslib/i18n/extract.rs similarity index 100% rename from rslib/i18n/build/extract.rs rename to rslib/i18n/extract.rs diff --git a/rslib/i18n/build/gather.rs b/rslib/i18n/gather.rs similarity index 100% rename from rslib/i18n/build/gather.rs rename to rslib/i18n/gather.rs diff --git a/rslib/i18n/python.rs b/rslib/i18n/python.rs new file mode 100644 index 000000000..ca780c041 --- /dev/null +++ b/rslib/i18n/python.rs @@ -0,0 +1,109 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::env; +use std::fmt::Write; +use std::path::PathBuf; + +use anki_io::create_dir_all; +use anki_io::write_file_if_changed; +use anyhow::Result; +use inflections::Inflect; +use itertools::Itertools; + +use crate::extract::Module; +use crate::extract::Variable; +use crate::extract::VariableKind; + +pub fn write_py_interface(modules: &[Module]) -> Result<()> { + let mut out = header(); + + render_methods(modules, &mut out); + render_legacy_enum(modules, &mut out); + + if let Ok(path) = env::var("STRINGS_PY") { + let path = PathBuf::from(path); + create_dir_all(path.parent().unwrap())?; + write_file_if_changed(path, out)?; + } + + Ok(()) +} + +fn render_legacy_enum(modules: &[Module], out: &mut String) { + out.push_str("class LegacyTranslationEnum:\n"); + for (mod_idx, module) in modules.iter().enumerate() { + for (str_idx, translation) in module.translations.iter().enumerate() { + let upper = translation.key.replace('-', "_").to_upper_case(); + writeln!(out, r#" {upper} = ({mod_idx}, {str_idx})"#).unwrap(); + } + } +} + +fn render_methods(modules: &[Module], out: &mut String) { + for (mod_idx, module) in modules.iter().enumerate() { + for (str_idx, translation) in module.translations.iter().enumerate() { + let text = &translation.text; + let key = &translation.key; + let func_name = key.replace('-', "_").to_snake_case(); + let arg_types = get_arg_types(&translation.variables); + let args = get_args(&translation.variables); + writeln!( + out, + r#" + def {func_name}(self, {arg_types}) -> str: + r''' {text} ''' + return self._translate({mod_idx}, {str_idx}, {{{args}}}) +"#, + ) + .unwrap(); + } + } +} + +fn header() -> String { + "# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +# This file is automatically generated from the *.ftl files. + +from __future__ import annotations + +from typing import Union + +FluentVariable = Union[str, int, float] + +class GeneratedTranslations: + def _translate(self, module: int, translation: int, args: dict) -> str: + raise Exception('not implemented') + +" + .to_string() +} + +fn get_arg_types(args: &[Variable]) -> String { + let args = args + .iter() + .map(|arg| format!("{}: {}", arg.name.to_snake_case(), arg_kind(&arg.kind))) + .join(", "); + if args.is_empty() { + "".into() + } else { + args + } +} + +fn get_args(args: &[Variable]) -> String { + args.iter() + .map(|arg| format!("\"{}\": {}", arg.name, arg.name.to_snake_case())) + .join(", ") +} + +fn arg_kind(kind: &VariableKind) -> &str { + match kind { + VariableKind::Int => "int", + VariableKind::Float => "float", + VariableKind::String => "str", + VariableKind::Any => "FluentVariable", + } +} diff --git a/rslib/i18n/build/typescript.rs b/rslib/i18n/typescript.rs similarity index 100% rename from rslib/i18n/build/typescript.rs rename to rslib/i18n/typescript.rs diff --git a/rslib/i18n/build/write_strings.rs b/rslib/i18n/write_strings.rs similarity index 100% rename from rslib/i18n/build/write_strings.rs rename to rslib/i18n/write_strings.rs diff --git a/rslib/proto/build.rs b/rslib/proto/build.rs index e75f3367b..feb84d410 100644 --- a/rslib/proto/build.rs +++ b/rslib/proto/build.rs @@ -3,7 +3,7 @@ pub mod python; pub mod rust; -pub mod ts; +pub mod typescript; use anki_proto_gen::descriptors_path; use anki_proto_gen::get_services; @@ -15,7 +15,7 @@ fn main() -> Result<()> { let pool = rust::write_rust_protos(descriptors_path)?; let (_, services) = get_services(&pool); python::write_python_interface(&services)?; - ts::write_ts_interface(&services)?; + typescript::write_ts_interface(&services)?; Ok(()) } diff --git a/rslib/proto/ts.rs b/rslib/proto/typescript.rs similarity index 100% rename from rslib/proto/ts.rs rename to rslib/proto/typescript.rs