mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Move .ts i18n method generation to Rust
Based on a similar approach I used for AnkiDroid. The separate modules file has been integrated into ftl.js.
This commit is contained in:
parent
f3b6deefe9
commit
4c76e3150b
12 changed files with 136 additions and 168 deletions
|
@ -1,5 +1,7 @@
|
|||
[env]
|
||||
STRINGS_JSON = { value = "out/rslib/i18n/strings.json", 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 }
|
||||
# build script will append .exe if necessary
|
||||
PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true }
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -182,6 +182,7 @@ dependencies = [
|
|||
"fluent-syntax",
|
||||
"inflections",
|
||||
"intl-memoizer",
|
||||
"itertools 0.11.0",
|
||||
"num-format",
|
||||
"phf 0.11.2",
|
||||
"serde",
|
||||
|
|
|
@ -161,7 +161,6 @@ pub fn check_python(build: &mut Build) -> Result<()> {
|
|||
PythonTypecheck {
|
||||
folders: &[
|
||||
"pylib",
|
||||
"ts/lib",
|
||||
"qt/aqt",
|
||||
"qt/tools",
|
||||
"out/pylib/anki",
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use ninja_gen::action::BuildAction;
|
||||
use ninja_gen::command::RunCommand;
|
||||
use ninja_gen::glob;
|
||||
use ninja_gen::hashmap;
|
||||
use ninja_gen::input::BuildInput;
|
||||
|
@ -115,26 +114,7 @@ fn setup_node(build: &mut Build) -> Result<()> {
|
|||
}
|
||||
|
||||
fn build_and_check_tslib(build: &mut Build) -> Result<()> {
|
||||
build.add_action(
|
||||
"ts:lib:i18n",
|
||||
RunCommand {
|
||||
command: ":pyenv:bin",
|
||||
args: "$script $strings $out",
|
||||
inputs: hashmap! {
|
||||
"script" => inputs!["ts/lib/genfluent.py"],
|
||||
"strings" => inputs![":rslib:i18n:strings.json"],
|
||||
"" => inputs!["pylib/anki/_vendor/stringcase.py"]
|
||||
},
|
||||
outputs: hashmap! {
|
||||
"out" => vec![
|
||||
"ts/lib/ftl.js",
|
||||
"ts/lib/ftl.d.ts",
|
||||
"ts/lib/i18n/modules.js",
|
||||
"ts/lib/i18n/modules.d.ts"
|
||||
]
|
||||
},
|
||||
},
|
||||
)?;
|
||||
build.add_dependency("ts:lib:i18n", ":rslib:i18n".into());
|
||||
build.add_action(
|
||||
"ts:lib:proto",
|
||||
GenTypescriptProto {
|
||||
|
|
|
@ -22,6 +22,7 @@ serde_json.workspace = true
|
|||
inflections.workspace = true
|
||||
anki_io.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fluent.workspace = true
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
mod check;
|
||||
mod extract;
|
||||
mod gather;
|
||||
mod typescript;
|
||||
mod write_strings;
|
||||
|
||||
use std::path::Path;
|
||||
|
@ -25,6 +26,8 @@ fn main() -> Result<()> {
|
|||
let modules = get_modules(&map);
|
||||
write_strings(&map, &modules);
|
||||
|
||||
typescript::write_ts_interface(&modules)?;
|
||||
|
||||
// write strings.json file to requested path
|
||||
println!("cargo:rerun-if-env-changed=STRINGS_JSON");
|
||||
if let Some(path) = option_env!("STRINGS_JSON") {
|
||||
|
|
123
rslib/i18n/build/typescript.rs
Normal file
123
rslib/i18n/build/typescript.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
// 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_ts_interface(modules: &[Module]) -> Result<()> {
|
||||
let mut dts_out = header();
|
||||
let mut js_out = header();
|
||||
write_translate_method(&mut js_out);
|
||||
dts_out.push_str("export declare const funcs: any;\n");
|
||||
|
||||
render_module_map(modules, &mut dts_out, &mut js_out);
|
||||
render_methods(modules, &mut dts_out, &mut js_out);
|
||||
|
||||
if let Ok(path) = env::var("STRINGS_JS") {
|
||||
let path = PathBuf::from(path);
|
||||
create_dir_all(path.parent().unwrap())?;
|
||||
write_file_if_changed(path, js_out)?;
|
||||
}
|
||||
if let Ok(path) = env::var("STRINGS_DTS") {
|
||||
let path = PathBuf::from(path);
|
||||
create_dir_all(path.parent().unwrap())?;
|
||||
write_file_if_changed(path, dts_out)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_module_map(modules: &[Module], dts_out: &mut String, js_out: &mut String) {
|
||||
dts_out.push_str("export declare enum ModuleName {\n");
|
||||
js_out.push_str("export const ModuleName = {};\n");
|
||||
for module in modules {
|
||||
let name = &module.name;
|
||||
let upper = name.to_upper_case();
|
||||
writeln!(dts_out, r#" {upper} = "{name}","#).unwrap();
|
||||
writeln!(js_out, r#"ModuleName["{upper}"] = "{name}";"#).unwrap();
|
||||
}
|
||||
dts_out.push('}');
|
||||
}
|
||||
|
||||
fn render_methods(modules: &[Module], dts_out: &mut String, js_out: &mut String) {
|
||||
for module in modules {
|
||||
for translation in &module.translations {
|
||||
let text = &translation.text;
|
||||
let key = &translation.key;
|
||||
let func_name = key.replace('-', "_").to_camel_case();
|
||||
let arg_types = get_arg_types(&translation.variables);
|
||||
let maybe_args = if translation.variables.is_empty() {
|
||||
""
|
||||
} else {
|
||||
"args"
|
||||
};
|
||||
writeln!(
|
||||
dts_out,
|
||||
"
|
||||
/** {text} */
|
||||
export declare function {func_name}({arg_types}): string;",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
js_out,
|
||||
r#"
|
||||
export function {func_name}({maybe_args}) {{
|
||||
return translate("{key}", {maybe_args})
|
||||
}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_translate_method(buf: &mut String) {
|
||||
buf.push_str(
|
||||
"
|
||||
// tslib is responsible for injecting getMessage helper in
|
||||
export const funcs = {};
|
||||
|
||||
function translate(key, args = {}) {
|
||||
return funcs.getMessage(key, args) ?? `missing key: ${key}`;
|
||||
}
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
fn header() -> String {
|
||||
"// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_arg_types(args: &[Variable]) -> String {
|
||||
let args = args
|
||||
.iter()
|
||||
.map(|arg| format!("{}: {}", arg.name.to_camel_case(), arg_kind(&arg.kind)))
|
||||
.join(", ");
|
||||
if args.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!("args: {{{args}}}",)
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_kind(kind: &VariableKind) -> &str {
|
||||
match kind {
|
||||
VariableKind::Int | VariableKind::Float => "number",
|
||||
VariableKind::String => "string",
|
||||
VariableKind::Any => "number | string",
|
||||
}
|
||||
}
|
|
@ -97,11 +97,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const settings = {
|
||||
newGatherPriority: {
|
||||
title: tr.deckConfigNewGatherPriority(),
|
||||
help: tr.deckConfigNewGatherPriorityTooltip_2() + currentDeck,
|
||||
help: tr.deckConfigNewGatherPriorityTooltip2() + currentDeck,
|
||||
},
|
||||
newCardSortOrder: {
|
||||
title: tr.deckConfigNewCardSortOrder(),
|
||||
help: tr.deckConfigNewCardSortOrderTooltip_2() + currentDeck,
|
||||
help: tr.deckConfigNewCardSortOrderTooltip2() + currentDeck,
|
||||
},
|
||||
newReviewPriority: {
|
||||
title: tr.deckConfigNewReviewPriority(),
|
||||
|
|
|
@ -59,7 +59,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
$search = displayedSearch;
|
||||
}
|
||||
|
||||
const year = tr.statisticsRange_1YearHistory();
|
||||
const year = tr.statisticsRange1YearHistory();
|
||||
const deck = tr.statisticsRangeDeck();
|
||||
const collection = tr.statisticsRangeCollection();
|
||||
const searchLabel = tr.statisticsRangeSearch();
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import List, Literal, TypedDict
|
||||
|
||||
sys.path.append("pylib/anki/_vendor")
|
||||
|
||||
import stringcase
|
||||
|
||||
strings_json, ftl_js_path, ftl_dts_path, modules_js_path, modules_dts_path = 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 write_methods() -> None:
|
||||
js_out = [
|
||||
"""
|
||||
// tslib is responsible for injecting getMessage helper in
|
||||
export const funcs = {};
|
||||
|
||||
function translate(key, args = {}) {
|
||||
return funcs.getMessage(key, args) ?? `missing key: ${key}`;
|
||||
}
|
||||
"""
|
||||
]
|
||||
dts_out = ["export declare const funcs: any;"]
|
||||
for module in modules:
|
||||
for translation in module["translations"]:
|
||||
key = stringcase.camelcase(translation["key"].replace("-", "_"))
|
||||
arg_types = get_arg_name_and_types(translation["variables"])
|
||||
args = get_args(translation["variables"])
|
||||
doc = translation["text"]
|
||||
dts_out.append(
|
||||
f"""
|
||||
/** {doc} */
|
||||
export declare function {key}({arg_types}): string;
|
||||
"""
|
||||
)
|
||||
js_out.append(
|
||||
f"""
|
||||
export function {key}({"args" if arg_types else ""}) {{
|
||||
return translate("{translation["key"]}"{args})
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
write(ftl_dts_path, "\n".join(dts_out) + "\n")
|
||||
write(ftl_js_path, "\n".join(js_out) + "\n")
|
||||
|
||||
|
||||
def write_modules() -> None:
|
||||
dts_buf = "export declare enum ModuleName {\n"
|
||||
for module in modules:
|
||||
name = module["name"]
|
||||
upper = name.upper()
|
||||
dts_buf += f' {upper} = "{name}",\n'
|
||||
dts_buf += "}\n"
|
||||
js_buf = "export const ModuleName = {};\n"
|
||||
for module in modules:
|
||||
name = module["name"]
|
||||
upper = name.upper()
|
||||
js_buf += f'ModuleName["{upper}"] = "{name}";\n'
|
||||
write(modules_dts_path, dts_buf)
|
||||
write(modules_js_path, js_buf)
|
||||
|
||||
|
||||
def get_arg_name_and_types(args: List[Variable]) -> str:
|
||||
if not args:
|
||||
return ""
|
||||
else:
|
||||
return (
|
||||
"args: {"
|
||||
+ ", ".join(
|
||||
[f"{typescript_arg_name(arg)}: {arg_kind(arg)}" for arg in args]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
|
||||
|
||||
def arg_kind(arg: Variable) -> str:
|
||||
if arg["kind"] in ("Int", "Float"):
|
||||
return "number"
|
||||
elif arg["kind"] == "Any":
|
||||
return "number | string"
|
||||
else:
|
||||
return "string"
|
||||
|
||||
|
||||
def map_args_to_real_names(args: List[Variable]) -> str:
|
||||
return (
|
||||
"{"
|
||||
+ ", ".join(
|
||||
[f'"{arg["name"]}": args.{typescript_arg_name(arg)}' for arg in args]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
|
||||
|
||||
def get_args(args: List[Variable]) -> str:
|
||||
if not args:
|
||||
return ""
|
||||
else:
|
||||
for arg in args:
|
||||
if typescript_arg_name(arg) != arg["name"]:
|
||||
# we'll need to map variables to their fluent equiv
|
||||
return ", " + map_args_to_real_names(args)
|
||||
|
||||
# variable names match, reference object instead
|
||||
return ", args"
|
||||
|
||||
|
||||
def typescript_arg_name(arg: Variable) -> str:
|
||||
name = stringcase.camelcase(arg["name"].replace("-", "_"))
|
||||
if name == "new":
|
||||
return "new_"
|
||||
else:
|
||||
return name
|
||||
|
||||
|
||||
def write(outfile, out) -> None:
|
||||
with open(outfile, "w", encoding="utf8") as f:
|
||||
f.write(
|
||||
f"""// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"""
|
||||
+ out
|
||||
)
|
||||
|
||||
|
||||
write_methods()
|
||||
write_modules()
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export * from "../../../out/ts/lib/i18n/modules";
|
||||
export { ModuleName } from "../../../out/ts/lib/ftl";
|
||||
export * from "./bundles";
|
||||
export * from "./utils";
|
||||
// this is an ugly hack to inject code into a generated module
|
||||
|
|
|
@ -5,9 +5,9 @@ import "intl-pluralrules";
|
|||
|
||||
import { FluentBundle, FluentResource } from "@fluent/bundle";
|
||||
import { i18nResources } from "@tslib/backend";
|
||||
import type { ModuleName } from "@tslib/ftl";
|
||||
|
||||
import { firstLanguage, setBundles } from "./bundles";
|
||||
import type { ModuleName } from "./modules";
|
||||
|
||||
export function supportsVerticalText(): boolean {
|
||||
const firstLang = firstLanguage();
|
||||
|
|
Loading…
Reference in a new issue