mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12: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]
|
[env]
|
||||||
STRINGS_JSON = { value = "out/rslib/i18n/strings.json", relative = true }
|
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 }
|
DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true }
|
||||||
# build script will append .exe if necessary
|
# build script will append .exe if necessary
|
||||||
PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true }
|
PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true }
|
||||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -182,6 +182,7 @@ dependencies = [
|
||||||
"fluent-syntax",
|
"fluent-syntax",
|
||||||
"inflections",
|
"inflections",
|
||||||
"intl-memoizer",
|
"intl-memoizer",
|
||||||
|
"itertools 0.11.0",
|
||||||
"num-format",
|
"num-format",
|
||||||
"phf 0.11.2",
|
"phf 0.11.2",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -161,7 +161,6 @@ pub fn check_python(build: &mut Build) -> Result<()> {
|
||||||
PythonTypecheck {
|
PythonTypecheck {
|
||||||
folders: &[
|
folders: &[
|
||||||
"pylib",
|
"pylib",
|
||||||
"ts/lib",
|
|
||||||
"qt/aqt",
|
"qt/aqt",
|
||||||
"qt/tools",
|
"qt/tools",
|
||||||
"out/pylib/anki",
|
"out/pylib/anki",
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use ninja_gen::action::BuildAction;
|
use ninja_gen::action::BuildAction;
|
||||||
use ninja_gen::command::RunCommand;
|
|
||||||
use ninja_gen::glob;
|
use ninja_gen::glob;
|
||||||
use ninja_gen::hashmap;
|
use ninja_gen::hashmap;
|
||||||
use ninja_gen::input::BuildInput;
|
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<()> {
|
fn build_and_check_tslib(build: &mut Build) -> Result<()> {
|
||||||
build.add_action(
|
build.add_dependency("ts:lib:i18n", ":rslib:i18n".into());
|
||||||
"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_action(
|
build.add_action(
|
||||||
"ts:lib:proto",
|
"ts:lib:proto",
|
||||||
GenTypescriptProto {
|
GenTypescriptProto {
|
||||||
|
|
|
@ -22,6 +22,7 @@ serde_json.workspace = true
|
||||||
inflections.workspace = true
|
inflections.workspace = true
|
||||||
anki_io.workspace = true
|
anki_io.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fluent.workspace = true
|
fluent.workspace = true
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
mod check;
|
mod check;
|
||||||
mod extract;
|
mod extract;
|
||||||
mod gather;
|
mod gather;
|
||||||
|
mod typescript;
|
||||||
mod write_strings;
|
mod write_strings;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -25,6 +26,8 @@ fn main() -> Result<()> {
|
||||||
let modules = get_modules(&map);
|
let modules = get_modules(&map);
|
||||||
write_strings(&map, &modules);
|
write_strings(&map, &modules);
|
||||||
|
|
||||||
|
typescript::write_ts_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");
|
||||||
if let Some(path) = option_env!("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 = {
|
const settings = {
|
||||||
newGatherPriority: {
|
newGatherPriority: {
|
||||||
title: tr.deckConfigNewGatherPriority(),
|
title: tr.deckConfigNewGatherPriority(),
|
||||||
help: tr.deckConfigNewGatherPriorityTooltip_2() + currentDeck,
|
help: tr.deckConfigNewGatherPriorityTooltip2() + currentDeck,
|
||||||
},
|
},
|
||||||
newCardSortOrder: {
|
newCardSortOrder: {
|
||||||
title: tr.deckConfigNewCardSortOrder(),
|
title: tr.deckConfigNewCardSortOrder(),
|
||||||
help: tr.deckConfigNewCardSortOrderTooltip_2() + currentDeck,
|
help: tr.deckConfigNewCardSortOrderTooltip2() + currentDeck,
|
||||||
},
|
},
|
||||||
newReviewPriority: {
|
newReviewPriority: {
|
||||||
title: tr.deckConfigNewReviewPriority(),
|
title: tr.deckConfigNewReviewPriority(),
|
||||||
|
|
|
@ -59,7 +59,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
$search = displayedSearch;
|
$search = displayedSearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
const year = tr.statisticsRange_1YearHistory();
|
const year = tr.statisticsRange1YearHistory();
|
||||||
const deck = tr.statisticsRangeDeck();
|
const deck = tr.statisticsRangeDeck();
|
||||||
const collection = tr.statisticsRangeCollection();
|
const collection = tr.statisticsRangeCollection();
|
||||||
const searchLabel = tr.statisticsRangeSearch();
|
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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// 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 "./bundles";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
// this is an ugly hack to inject code into a generated module
|
// 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 { FluentBundle, FluentResource } from "@fluent/bundle";
|
||||||
import { i18nResources } from "@tslib/backend";
|
import { i18nResources } from "@tslib/backend";
|
||||||
|
import type { ModuleName } from "@tslib/ftl";
|
||||||
|
|
||||||
import { firstLanguage, setBundles } from "./bundles";
|
import { firstLanguage, setBundles } from "./bundles";
|
||||||
import type { ModuleName } from "./modules";
|
|
||||||
|
|
||||||
export function supportsVerticalText(): boolean {
|
export function supportsVerticalText(): boolean {
|
||||||
const firstLang = firstLanguage();
|
const firstLang = firstLanguage();
|
||||||
|
|
Loading…
Reference in a new issue