From 98cceae59d676ad069e29d9c693b4f9894d32679 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 8 Sep 2026 23:10:10 +0200 Subject: [PATCH] fix(i18n): point static catalog translation at weblate (#2619) --- tools/i18n_auto/src/json_catalog.rs | 291 ++++++++++++++++++++++++++++ tools/i18n_auto/src/lib.rs | 1 + tools/i18n_auto/src/runner.rs | 77 ++++++-- 3 files changed, 349 insertions(+), 20 deletions(-) create mode 100644 tools/i18n_auto/src/json_catalog.rs diff --git a/tools/i18n_auto/src/json_catalog.rs b/tools/i18n_auto/src/json_catalog.rs new file mode 100644 index 000000000..f6d06de2e --- /dev/null +++ b/tools/i18n_auto/src/json_catalog.rs @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use anyhow::{Context, Result, bail}; +use serde_json::{Map, Value}; + +use crate::po::{Entry, Translation}; +use crate::ts_catalog::StaticTsCatalogKind; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StaticJsonCatalogConfig { + pub kind: StaticTsCatalogKind, + pub source_path: PathBuf, +} + +fn parse_object(content: &str, what: &str) -> Result> { + let value: Value = + serde_json::from_str(content).with_context(|| format!("failed to parse {what}"))?; + match value { + Value::Object(map) => Ok(map), + _ => bail!("{what} must be a JSON object"), + } +} + +fn flatten( + kind: &StaticTsCatalogKind, + map: &Map, +) -> Result> { + let mut out = BTreeMap::new(); + for (key, value) in map { + match kind { + StaticTsCatalogKind::SimpleMessages => match value { + Value::String(text) => { + out.insert(key.clone(), text.clone()); + } + _ => bail!("{key} must be a string"), + }, + StaticTsCatalogKind::EmailTemplates => match value { + Value::Object(fields) => { + for (field, text) in fields { + match text { + Value::String(text) => { + out.insert(format!("{key}.{field}"), text.clone()); + } + _ => bail!("{key}.{field} must be a string"), + } + } + } + _ => bail!("{key} must be an object of template fields"), + }, + } + } + Ok(out) +} + +fn render(kind: &StaticTsCatalogKind, values: &BTreeMap) -> Result { + let mut root = Map::new(); + match kind { + StaticTsCatalogKind::SimpleMessages => { + for (key, text) in values { + root.insert(key.clone(), Value::String(text.clone())); + } + } + StaticTsCatalogKind::EmailTemplates => { + for (context, text) in values { + let Some((template, field)) = context.split_once('.') else { + bail!("{context} must name a template field as