Move .py i18n method generation to Rust

This commit is contained in:
Damien Elmes 2023-07-03 15:58:39 +10:00
parent 4c76e3150b
commit cb8007ce30
16 changed files with 126 additions and 130 deletions

View file

@ -1,5 +1,6 @@
[env] [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_JS = { value = "out/ts/lib/ftl.js", relative = true }
STRINGS_DTS = { value = "out/ts/lib/ftl.d.ts", relative = true } STRINGS_DTS = { value = "out/ts/lib/ftl.d.ts", relative = true }
DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true } DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true }

View file

@ -147,6 +147,10 @@ debug = 0
opt-level = 1 opt-level = 1
debug = 0 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 # Debug info off by default, which speeds up incremental builds and produces a considerably
# smaller library. # smaller library.
[profile.dev.package.anki] [profile.dev.package.anki]

View file

@ -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: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( build.add_action(
"pylib:anki:hooks_gen.py", "pylib:anki:hooks_gen.py",
RunCommand { RunCommand {

View file

@ -48,10 +48,11 @@ fn prepare_translations(build: &mut Build) -> Result<()> {
glob!["ftl/{core,core-repo,qt,qt-repo}/**"], glob!["ftl/{core,core-repo,qt,qt-repo}/**"],
":ftl:repo", ":ftl:repo",
], ],
outputs: &[RustOutput::Data( outputs: &[
"strings.json", RustOutput::Data("py", "pylib/anki/_fluent.py"),
"$builddir/rslib/i18n/strings.json", RustOutput::Data("ts", "ts/lib/ftl.d.ts"),
)], RustOutput::Data("ts", "ts/lib/ftl.js"),
],
target: None, target: None,
extra_args: "-p anki_i18n", extra_args: "-p anki_i18n",
release_override: None, release_override: None,

View file

@ -114,7 +114,7 @@ fn setup_node(build: &mut Build) -> Result<()> {
} }
fn build_and_check_tslib(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( build.add_action(
"ts:lib:proto", "ts:lib:proto",
GenTypescriptProto { GenTypescriptProto {

View file

@ -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
)

View file

@ -2,7 +2,6 @@
name = "anki_i18n" name = "anki_i18n"
version.workspace = true version.workspace = true
authors.workspace = true authors.workspace = true
build = "build/main.rs"
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
publish = false publish = false

View file

@ -4,6 +4,7 @@
mod check; mod check;
mod extract; mod extract;
mod gather; mod gather;
mod python;
mod typescript; mod typescript;
mod write_strings; mod write_strings;
@ -27,6 +28,7 @@ fn main() -> Result<()> {
write_strings(&map, &modules); write_strings(&map, &modules);
typescript::write_ts_interface(&modules)?; typescript::write_ts_interface(&modules)?;
python::write_py_interface(&modules)?;
// write strings.json file to requested path // write strings.json file to requested path
println!("cargo:rerun-if-env-changed=STRINGS_JSON"); println!("cargo:rerun-if-env-changed=STRINGS_JSON");

109
rslib/i18n/python.rs Normal file
View file

@ -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",
}
}

View file

@ -3,7 +3,7 @@
pub mod python; pub mod python;
pub mod rust; pub mod rust;
pub mod ts; pub mod typescript;
use anki_proto_gen::descriptors_path; use anki_proto_gen::descriptors_path;
use anki_proto_gen::get_services; use anki_proto_gen::get_services;
@ -15,7 +15,7 @@ fn main() -> Result<()> {
let pool = rust::write_rust_protos(descriptors_path)?; let pool = rust::write_rust_protos(descriptors_path)?;
let (_, services) = get_services(&pool); let (_, services) = get_services(&pool);
python::write_python_interface(&services)?; python::write_python_interface(&services)?;
ts::write_ts_interface(&services)?; typescript::write_ts_interface(&services)?;
Ok(()) Ok(())
} }