Move i18n helpers into ftl/, with a single main.rs
Clap gives us a nice help message and better arg parsing
This commit is contained in:
parent
78b4a391cc
commit
13572a86b2
|
@ -190,17 +190,6 @@ dependencies = [
|
|||
"unic-langid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anki_i18n_helpers"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"fluent-syntax",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anki_io"
|
||||
version = "0.0.0"
|
||||
|
@ -1295,8 +1284,17 @@ dependencies = [
|
|||
name = "ftl"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anki_io",
|
||||
"anki_process",
|
||||
"anyhow",
|
||||
"camino",
|
||||
"clap",
|
||||
"fluent-syntax",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"snafu",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -9,7 +9,6 @@ edition = "2021"
|
|||
members = [
|
||||
"rslib",
|
||||
"rslib/i18n",
|
||||
"rslib/i18n_helpers",
|
||||
"rslib/linkchecker",
|
||||
"rslib/proto",
|
||||
"rslib/io",
|
||||
|
|
|
@ -8,11 +8,11 @@ use ninja_gen::build::FilesHandle;
|
|||
use ninja_gen::cargo::CargoBuild;
|
||||
use ninja_gen::cargo::CargoClippy;
|
||||
use ninja_gen::cargo::CargoFormat;
|
||||
use ninja_gen::cargo::CargoRun;
|
||||
use ninja_gen::cargo::CargoTest;
|
||||
use ninja_gen::cargo::RustOutput;
|
||||
use ninja_gen::git::SyncSubmodule;
|
||||
use ninja_gen::glob;
|
||||
use ninja_gen::hash::simple_hash;
|
||||
use ninja_gen::input::BuildInput;
|
||||
use ninja_gen::inputs;
|
||||
use ninja_gen::Build;
|
||||
|
@ -60,21 +60,30 @@ fn prepare_translations(build: &mut Build) -> Result<()> {
|
|||
)?;
|
||||
|
||||
build.add_action(
|
||||
"ftl:sync",
|
||||
CargoRun {
|
||||
binary_name: "ftl-sync",
|
||||
cargo_args: "-p ftl",
|
||||
bin_args: "",
|
||||
deps: inputs![":ftl:repo", glob!["ftl/{core,core-repo,qt,qt-repo}/**"]],
|
||||
"ftl:bin",
|
||||
CargoBuild {
|
||||
inputs: inputs![glob!["ftl/**"],],
|
||||
outputs: &[RustOutput::Binary("ftl")],
|
||||
target: None,
|
||||
extra_args: "-p ftl",
|
||||
release_override: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
// These don't use :group notation, as it doesn't make sense to invoke multiple
|
||||
// commands as a group.
|
||||
build.add_action(
|
||||
"ftl-sync",
|
||||
FtlCommand {
|
||||
args: "sync",
|
||||
deps: inputs![":ftl:repo", glob!["ftl/**"]],
|
||||
},
|
||||
)?;
|
||||
|
||||
build.add_action(
|
||||
"ftl:deprecate",
|
||||
CargoRun {
|
||||
binary_name: "deprecate_ftl_entries",
|
||||
cargo_args: "-p anki_i18n_helpers",
|
||||
bin_args: "ftl/core ftl/qt -- pylib qt rslib ts --keep ftl/usage",
|
||||
"ftl-deprecate",
|
||||
FtlCommand {
|
||||
args: "deprecate --ftl-roots ftl/core ftl/qt --source-roots pylib qt rslib ts --json-roots ftl/usage",
|
||||
deps: inputs!["ftl/core", "ftl/qt", "pylib", "qt", "rslib", "ts"],
|
||||
},
|
||||
)?;
|
||||
|
@ -82,6 +91,24 @@ fn prepare_translations(build: &mut Build) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
struct FtlCommand {
|
||||
args: &'static str,
|
||||
deps: BuildInput,
|
||||
}
|
||||
|
||||
impl BuildAction for FtlCommand {
|
||||
fn command(&self) -> &str {
|
||||
"$ftl_bin $args"
|
||||
}
|
||||
|
||||
fn files(&mut self, build: &mut impl FilesHandle) {
|
||||
build.add_inputs("", &self.deps);
|
||||
build.add_inputs("ftl_bin", inputs![":ftl:bin"]);
|
||||
build.add_variable("args", self.args);
|
||||
build.add_output_stamp(format!("ftl/stamp.{}", simple_hash(self.args)));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<()> {
|
||||
let outputs = vec![
|
||||
RustOutput::Data("descriptors.bin", "rslib/proto/descriptors.bin"),
|
||||
|
|
|
@ -6,11 +6,17 @@ edition.workspace = true
|
|||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ftl-sync"
|
||||
path = "sync.rs"
|
||||
description = "Helpers for Anki's i18n system"
|
||||
|
||||
[dependencies]
|
||||
anki_io.workspace = true
|
||||
anki_process.workspace = true
|
||||
anyhow.workspace = true
|
||||
camino.workspace = true
|
||||
clap.workspace = true
|
||||
fluent-syntax.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
snafu.workspace = true
|
||||
walkdir.workspace = true
|
||||
|
|
|
@ -5,7 +5,12 @@ use std::collections::HashSet;
|
|||
use std::fs;
|
||||
use std::io::BufReader;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anki_io::create_file;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use clap::Args;
|
||||
use fluent_syntax::ast;
|
||||
use fluent_syntax::ast::Resource;
|
||||
use fluent_syntax::parser;
|
||||
|
@ -17,42 +22,61 @@ use walkdir::WalkDir;
|
|||
|
||||
use crate::serialize;
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct WriteJsonArgs {
|
||||
target_filename: PathBuf,
|
||||
source_roots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct GarbageCollectArgs {
|
||||
json_root: String,
|
||||
ftl_roots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct DeprecateEntriesArgs {
|
||||
#[clap(long, num_args(1..), required(true))]
|
||||
ftl_roots: Vec<String>,
|
||||
#[clap(long, num_args(1..), required(true))]
|
||||
source_roots: Vec<String>,
|
||||
#[clap(long, num_args(1..), required(true))]
|
||||
json_roots: Vec<String>,
|
||||
}
|
||||
|
||||
const DEPCRATION_WARNING: &str =
|
||||
"NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future.";
|
||||
|
||||
/// Extract references from all Rust, Python, TS, Svelte, Swift, Kotlin and
|
||||
/// Designer files in the `roots`, convert them to kebab case and write them as
|
||||
/// a json to the target file.
|
||||
pub fn write_ftl_json<S1: AsRef<str>, S2: AsRef<str>>(roots: &[S1], target: S2) {
|
||||
let refs = gather_ftl_references(roots);
|
||||
pub fn write_ftl_json(args: WriteJsonArgs) -> Result<()> {
|
||||
let refs = gather_ftl_references(&args.source_roots);
|
||||
let mut refs = Vec::from_iter(refs);
|
||||
refs.sort();
|
||||
serde_json::to_writer_pretty(
|
||||
fs::File::create(target.as_ref()).expect("failed to create file"),
|
||||
&refs,
|
||||
)
|
||||
.expect("failed to write file");
|
||||
serde_json::to_writer_pretty(create_file(args.target_filename)?, &refs)
|
||||
.context("writing json")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete every entry in `ftl_root` that is not mentioned in another message
|
||||
/// or any json in `json_root`.
|
||||
pub fn garbage_collect_ftl_entries(ftl_roots: &[impl AsRef<str>], json_root: impl AsRef<str>) {
|
||||
let used_ftls = get_all_used_messages_and_terms(json_root.as_ref(), ftl_roots);
|
||||
strip_unused_ftl_messages_and_terms(ftl_roots, &used_ftls);
|
||||
pub fn garbage_collect_ftl_entries(args: GarbageCollectArgs) -> Result<()> {
|
||||
let used_ftls = get_all_used_messages_and_terms(&args.json_root, &args.ftl_roots);
|
||||
strip_unused_ftl_messages_and_terms(&args.ftl_roots, &used_ftls);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves every entry in `ftl_roots` that is not mentioned in another message, a
|
||||
/// source file or any json in `json_roots` to the bottom of its file below a
|
||||
/// deprecation warning.
|
||||
pub fn deprecate_ftl_entries(
|
||||
ftl_roots: &[impl AsRef<str>],
|
||||
source_roots: &[impl AsRef<str>],
|
||||
json_roots: &[impl AsRef<str>],
|
||||
) {
|
||||
let mut used_ftls = gather_ftl_references(source_roots);
|
||||
import_messages_from_json(json_roots, &mut used_ftls);
|
||||
extract_nested_messages_and_terms(ftl_roots, &mut used_ftls);
|
||||
deprecate_unused_ftl_messages_and_terms(ftl_roots, &used_ftls);
|
||||
pub fn deprecate_ftl_entries(args: DeprecateEntriesArgs) -> Result<()> {
|
||||
let mut used_ftls = gather_ftl_references(&args.source_roots);
|
||||
import_messages_from_json(&args.json_roots, &mut used_ftls);
|
||||
extract_nested_messages_and_terms(&args.ftl_roots, &mut used_ftls);
|
||||
deprecate_unused_ftl_messages_and_terms(&args.ftl_roots, &used_ftls);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_all_used_messages_and_terms(
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
pub mod garbage_collection;
|
||||
pub mod serialize;
|
||||
|
||||
mod sync;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::Subcommand;
|
||||
use garbage_collection::deprecate_ftl_entries;
|
||||
use garbage_collection::garbage_collect_ftl_entries;
|
||||
use garbage_collection::write_ftl_json;
|
||||
use garbage_collection::DeprecateEntriesArgs;
|
||||
use garbage_collection::GarbageCollectArgs;
|
||||
use garbage_collection::WriteJsonArgs;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Update commit references to the latest translations,
|
||||
/// and copy source files to the translation repos. Requires access to the
|
||||
/// i18n repos to run.
|
||||
Sync,
|
||||
/// Extract references from all Rust, Python, TS, Svelte and Designer files
|
||||
/// in the given roots, convert them to ftl names case and write them as
|
||||
/// a json to the target file.
|
||||
WriteJson(WriteJsonArgs),
|
||||
/// Delete every entry in the ftl files that is not mentioned in another
|
||||
/// message or a given json.
|
||||
GarbageCollect(GarbageCollectArgs),
|
||||
/// Deprecate unused ftl entries by moving them to the bottom of the file
|
||||
/// and adding a deprecation warning. An entry is considered unused if
|
||||
/// cannot be found in a source or JSON file.
|
||||
Deprecate(DeprecateEntriesArgs),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
match Cli::parse().command {
|
||||
Command::Sync => sync::sync(),
|
||||
Command::WriteJson(args) => write_ftl_json(args),
|
||||
Command::GarbageCollect(args) => garbage_collect_ftl_entries(args),
|
||||
Command::Deprecate(args) => deprecate_ftl_entries(args),
|
||||
}
|
||||
}
|
|
@ -1,17 +1,13 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
//! A helper script to update commit references to the latest translations,
|
||||
//! and copy source files to the translation repos. Requires access to the
|
||||
//! i18n repos to run.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::bail;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use camino::Utf8Path;
|
||||
use snafu::prelude::*;
|
||||
use snafu::Whatever;
|
||||
|
||||
type Result<T> = std::result::Result<T, Whatever>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Module {
|
||||
|
@ -23,8 +19,7 @@ struct Module {
|
|||
/// remote is used to push via authenticated ssh.
|
||||
const GIT_REMOTE: &str = "ssh";
|
||||
|
||||
#[snafu::report]
|
||||
fn main() -> Result<()> {
|
||||
pub fn sync() -> Result<()> {
|
||||
let modules = [
|
||||
Module {
|
||||
template_folder: "ftl/core".into(),
|
||||
|
@ -41,8 +36,7 @@ fn main() -> Result<()> {
|
|||
fetch_new_translations(&module)?;
|
||||
push_new_templates(&module)?;
|
||||
}
|
||||
commit(".", "Update translations")
|
||||
.whatever_context("failure expected if no translations changed")?;
|
||||
commit(".", "Update translations").context("failure expected if no translations changed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -50,47 +44,41 @@ fn check_clean() -> Result<()> {
|
|||
let output = Command::new("git")
|
||||
.arg("diff")
|
||||
.output()
|
||||
.whatever_context("git diff")?;
|
||||
ensure_whatever!(output.status.success(), "git diff");
|
||||
ensure_whatever!(
|
||||
output.stdout.is_empty(),
|
||||
"please commit any outstanding changes first"
|
||||
);
|
||||
Ok(())
|
||||
.context("git diff")?;
|
||||
if !output.status.success() {
|
||||
bail!("git diff");
|
||||
}
|
||||
|
||||
fn run(command: &mut Command) -> Result<()> {
|
||||
let status = command
|
||||
.status()
|
||||
.with_whatever_context(|_| format!("{:?}", command))?;
|
||||
if !status.success() {
|
||||
whatever!("{:?} exited with code: {:?}", command, status.code());
|
||||
if !output.stdout.is_empty() {
|
||||
bail!("please commit any outstanding changes first");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_new_translations(module: &Module) -> Result<()> {
|
||||
run(Command::new("git")
|
||||
Command::new("git")
|
||||
.current_dir(module.translation_repo)
|
||||
.args(["checkout", "main"]))?;
|
||||
run(Command::new("git")
|
||||
.args(["checkout", "main"])
|
||||
.ensure_success()?;
|
||||
Command::new("git")
|
||||
.current_dir(module.translation_repo)
|
||||
.args(["pull", "origin", "main"]))?;
|
||||
.args(["pull", "origin", "main"])
|
||||
.ensure_success()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_new_templates(module: &Module) -> Result<()> {
|
||||
run(Command::new("rsync")
|
||||
Command::new("rsync")
|
||||
.args(["-ai", "--delete", "--no-perms", "--no-times", "-c"])
|
||||
.args([
|
||||
format!("{}/", module.template_folder),
|
||||
format!("{}/", module.translation_repo.join("templates")),
|
||||
]))?;
|
||||
])
|
||||
.ensure_success()?;
|
||||
let changes_pending = !Command::new("git")
|
||||
.current_dir(module.translation_repo)
|
||||
.args(["diff", "--exit-code"])
|
||||
.status()
|
||||
.whatever_context("git")?
|
||||
.context("git")?
|
||||
.success();
|
||||
if changes_pending {
|
||||
commit(module.translation_repo, "Update templates")?;
|
||||
|
@ -100,11 +88,15 @@ fn push_new_templates(module: &Module) -> Result<()> {
|
|||
}
|
||||
|
||||
fn push(repo: &Utf8Path) -> Result<()> {
|
||||
run(Command::new("git")
|
||||
Command::new("git")
|
||||
.current_dir(repo)
|
||||
.args(["push", GIT_REMOTE, "main"]))?;
|
||||
.args(["push", GIT_REMOTE, "main"])
|
||||
.ensure_success()?;
|
||||
// ensure origin matches ssh remote
|
||||
run(Command::new("git").current_dir(repo).args(["fetch"]))?;
|
||||
Command::new("git")
|
||||
.current_dir(repo)
|
||||
.args(["fetch"])
|
||||
.ensure_success()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -112,8 +104,9 @@ fn commit<F>(folder: F, message: &str) -> Result<()>
|
|||
where
|
||||
F: AsRef<str>,
|
||||
{
|
||||
run(Command::new("git")
|
||||
Command::new("git")
|
||||
.current_dir(folder.as_ref())
|
||||
.args(["commit", "-a", "-m", message]))?;
|
||||
.args(["commit", "-a", "-m", message])
|
||||
.ensure_success()?;
|
||||
Ok(())
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "anki_i18n_helpers"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
description = "Helpers for Anki's i18n system"
|
||||
|
||||
[lib]
|
||||
name = "anki_i18n_helpers"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
fluent-syntax.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
walkdir.workspace = true
|
|
@ -1,49 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// Deprecate unused ftl entries by moving them to the bottom of the file and
|
||||
/// adding a deprecation warning. An entry is considered unused if cannot be
|
||||
/// found in a source or JSON file.
|
||||
/// Arguments before `--` are roots of ftl files, arguments after that are
|
||||
/// source roots. JSON roots must be preceded by `--keep` or `-k`.
|
||||
fn main() {
|
||||
let args = Arguments::new();
|
||||
anki_i18n_helpers::garbage_collection::deprecate_ftl_entries(
|
||||
&args.ftl_roots,
|
||||
&args.source_roots,
|
||||
&args.json_roots,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Arguments {
|
||||
ftl_roots: Vec<String>,
|
||||
source_roots: Vec<String>,
|
||||
json_roots: Vec<String>,
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
fn new() -> Self {
|
||||
let mut args = Self::default();
|
||||
let mut past_separator = false;
|
||||
let mut keep_flag = false;
|
||||
for arg in std::env::args() {
|
||||
match arg.as_str() {
|
||||
"--" => {
|
||||
past_separator = true;
|
||||
}
|
||||
"--keep" | "-k" => {
|
||||
keep_flag = true;
|
||||
}
|
||||
_ if keep_flag => {
|
||||
keep_flag = false;
|
||||
args.json_roots.push(arg)
|
||||
}
|
||||
_ if past_separator => args.source_roots.push(arg),
|
||||
_ => args.ftl_roots.push(arg),
|
||||
};
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// Delete every entry in the ftl files that is not mentioned in another message
|
||||
/// or a given json.
|
||||
/// First argument is the root of the json files, following are the roots of the
|
||||
/// ftl files.
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
anki_i18n_helpers::garbage_collection::garbage_collect_ftl_entries(&args[2..], &args[1]);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// Extract references from all Rust, Python, TS, Svelte and Designer files in
|
||||
/// the given roots, convert them to ftl names case and write them as a json to
|
||||
/// the target file.
|
||||
/// First argument is the target file name, following are source roots.
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
anki_i18n_helpers::garbage_collection::write_ftl_json(&args[2..], &args[1]);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
pub mod garbage_collection;
|
||||
pub mod serialize;
|
Loading…
Reference in New Issue