i18n: missing translations never fail; xtask ci prints one report (#3856)

* i18n tests: report missing translations instead of failing

The scans for untranslated menu strings, tl! literals, undo labels, menu labels and status
messages print what a complete language lacks. A missing translation shows the English
text until a translator adds the row, so it no longer fails the gate.

* i18n: one untranslated report in xtask ci; drop one-language wording tests

`i18n::tests::untranslated_report` lists, per language, the menu strings, tl! literals, undo
step names and templates, menu labels, render-site labels, status and error messages,
PostScript error meanings and default swatch names its catalog lacks; strings every language
lacks are listed once. It never fails on a gap, only on a scan that finds too few strings.
`cargo xtask ci` runs it in an `i18n-report` step, and `cargo xtask i18n-report` runs it alone.

The five scan tests, the render-site and swatch coverage tests and untranslated_messages.txt
fold into the report. Tests that pinned one language's wording are gone; where they also
checked code (lookups, plural rules, locale tags, menu and History step names, engine
parameter errors), that check stays and compares with the catalog lookup.
This commit is contained in:
aiongg
2026-10-23 16:01:11 -04:00
committed by GitHub
parent 5c008243cd
commit cb06c7b34e
36 changed files with 389 additions and 2415 deletions
-8
View File
@@ -243,12 +243,4 @@ mod tests {
assert_eq!(fileio::format_for_name(&name).map(|f| f.label), Some(label.as_str()), "{name}");
}
}
/// The menu's label is translated in every language with complete menus.
#[test]
fn the_label_is_translated() {
for lang in i18n::Lang::all().filter(|l| l.complete_menus()) {
assert!(i18n::has(lang, "Format:"), "{}", lang.code());
}
}
}
@@ -736,27 +736,6 @@ mod tests {
assert_eq!((d.str("state"), &d.fields["chosen"]), ("done".to_string(), &json!([])));
}
/// The status lines are translated with the reasons in them, and so are the reasons a search
/// failed and a file wasn't copied, which the code holds as constants.
#[test]
fn the_status_lines_read_in_the_interface_language() {
let fr = crate::i18n::Lang::from_code("fr").unwrap();
let shown = crate::i18n::message(fr, "2 font file(s) are now in /Fonts; 1 couldn't be added: a.otf: larger than 256 MB");
assert!(shown.contains("se trouvent maintenant dans /Fonts") && shown.contains("a.otf") && shown.contains("plus de 256 Mo"), "{shown}");
for code in ["de", "es", "fr", "hu", "it", "ka", "pl", "ru", "sv"] {
let lang = crate::i18n::Lang::from_code(code).unwrap();
for s in [
"every search thread stopped on an internal error (please report this bug)",
"not a file",
"not a font file",
"the files come to more than 1 GB: add fewer at a time",
"every numbered name for it is taken in the Fonts folder",
] {
assert_ne!(crate::i18n::message(lang, s), s, "{code}");
}
}
}
#[test]
fn the_web_says_how_many_fonts_are_missing() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
+2 -17
View File
@@ -236,21 +236,6 @@ fn form_dialog_axes_are_dropdowns() {
}
}
/// The axis choices are translated in the "axis" context: in Japanese the plain Horizontal and
/// Vertical are the type orientations (horizontal and vertical writing).
#[test]
fn axis_choices_read_as_axes_in_every_complete_language() {
use crate::i18n::{LANGUAGES, Lang, tr, tr_ctx};
let ja = Lang::from_code("ja").unwrap();
assert_eq!(tr_ctx(ja, "axis", "Horizontal"), "水平方向");
assert_ne!(tr_ctx(ja, "axis", "Horizontal"), tr(ja, "Horizontal"));
for l in LANGUAGES.iter().filter(|l| l.complete_menus) {
for s in ["Horizontal", "Vertical", "Both"] {
assert!(l.source.lines().any(|line| line.starts_with(&format!("axis\t{s}\t"))), "{}: no axis row for {s}", l.code);
}
}
}
/// Offset Path previews on the canvas; Cancel rolls the preview back.
#[test]
fn offset_path_previews() {
@@ -713,7 +698,7 @@ fn simplify_dialog_show_original_path_outlines_the_paths_before_the_preview() {
#[test]
fn printer_mark_labels_are_distinct_from_language_names() {
let cs = crate::i18n::Lang::from_code("cs").unwrap();
assert_eq!(save_pdf::choice_label(cs, "marks.kind", "Japanese"), "Japonské");
assert_eq!(save_pdf::choice_label(cs, "language", "Japanese"), "Japonština");
assert_eq!(save_pdf::choice_label(cs, "marks.kind", "Japanese"), crate::i18n::tr_ctx(cs, "mark-style", "Japanese"));
assert_eq!(save_pdf::choice_label(cs, "language", "Japanese"), crate::i18n::tr(cs, "Japanese"));
assert_eq!(save_pdf::choice_label(crate::i18n::Lang::EN, "marks.kind", "Japanese"), "Japanese");
}
+1 -1
View File
@@ -865,7 +865,7 @@ mod tests {
assert_eq!(super::script_label(3), "Thai");
let zh = crate::i18n::Lang::from_code("zh-hans").unwrap();
crate::i18n::set_current(zh);
assert_eq!(super::script_label(1), "汉字");
assert_eq!(super::script_label(1), crate::i18n::tr_ctx(zh, "font-script", "Japanese"));
crate::i18n::set_current(crate::i18n::Lang::EN);
assert_eq!(super::script_label(1), "Japanese");
}
+1 -1
View File
@@ -1350,7 +1350,7 @@ font-technology Variable Променливи
Clear History Изчистване на историята
Set History Limit… Задаване на ограничение на историята…
History States Състояния на историята
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Прилагане на набор от данни
Cancel Pattern Editing Отказ от редактирането на мостра
Copy Appearance Копиране на външния вид
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -2
View File
@@ -13,7 +13,7 @@
# režim míchání (blending mode), přednastavení (preset), rozložit (expand), uvolnit (release),
# vektorizace obrazu (Image Trace). Counts without a plural row read "Label: {n}".
#
# Coverage: every menu string, tl! literal, plural, status and error message (enforced in tests:
# Coverage: every menu string, tl! literal, plural, status and error message (`untranslated_report` lists gaps:
# `complete_menus` and `COMPLETE_MESSAGES`). Left in English on purpose (`MENU_KEEP_AS_IS` in the
# tests): the product name, language names, OpenType, the built-in workspace names and the
# bracketed perspective grid preset names. Keep placeholders, ellipses and the leading/trailing
@@ -1537,7 +1537,7 @@ lighting Light Světlo
Clear History Vymazat historii
Set History Limit… Nastavit limit historie…
Open a document to see its history. Otevřete dokument, abyste viděli jeho historii.
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Použít sadu dat
Cancel Pattern Editing Zrušit úpravy vzorku
Copy Appearance Kopírovat vzhled
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -3
View File
@@ -15,7 +15,7 @@
# error messages keep the case of their English first word, as they are often shown after "…: ".
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested) — and the status and error messages. Left in English on purpose (`MENU_KEEP_AS_IS` in the
# `untranslated_report` lists gaps) — and the status and error messages. Left in English on purpose (`MENU_KEEP_AS_IS` in the
# tests): the product name, language names, OpenType, the built-in workspace names and the bracketed
# perspective grid preset names; `de-DE`, `de-AT`, `de-CH` and every other `de-*` locale resolve here.
# Photoshop-style raster effects (Effect menu, their dialogs).
@@ -1371,7 +1371,7 @@ file-filter Templates Vorlagen
Clear History Protokoll löschen
Set History Limit… Protokollgrenze festlegen…
History States Protokollschritte
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Datensatz anwenden
Area Type Flächentext
Bloat Aufblasen
@@ -3678,7 +3678,7 @@ theme Appearance Erscheinungsbild
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` keine Bibliothek `{id}`
@msg select some type first wählen Sie zuerst Text aus
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -1313,7 +1313,7 @@ font-technology Variable Μεταβλητές
Clear History Εκκαθάριση ιστορικού
Set History Limit… Ορισμός ορίου ιστορικού…
History States Καταστάσεις ιστορικού
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Εφαρμογή συνόλου δεδομένων
Cancel Pattern Editing Ακύρωση επεξεργασίας μοτίβου
Copy Appearance Αντιγραφή εμφάνισης
1 # Ελληνικά (el) — VectorCraft
1313
1314 # History panel: the names of undo steps (`every_undo_label_is_translated`). # History panel: the names of undo steps (`untranslated_report` lists any that are missing).
1315
1316
1317
1318
1319
+3 -3
View File
@@ -9,7 +9,7 @@
# sentences address the user as "tú". Shortcut keys keep their key-cap names (Shift, Alt, Ctrl, Esc).
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# `untranslated_report` lists gaps). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Bordes resplandecientes…
@@ -1385,7 +1385,7 @@ font-technology Variable Variables
Clear History Borrar historial
Set History Limit… Definir límite de historial…
History States Estados de historial
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Aplicar conjunto de datos
Cancel Pattern Editing Cancelar edición de motivo
Copy Appearance Copiar apariencia
@@ -3754,7 +3754,7 @@ axis Both Ambos
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` no hay ninguna biblioteca `{id}`
@msg select some type first selecciona primero algún texto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -1511,7 +1511,7 @@ lighting Light Valo
Clear History Tyhjennä historia
Set History Limit… Aseta historian raja…
Open a document to see its history. Avaa asiakirja, niin näet sen historian.
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Käytä tietojoukkoa
Cancel Pattern Editing Peruuta kuvion muokkaus
Copy Appearance Kopioi ulkoasu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -3
View File
@@ -10,7 +10,7 @@
# « », the typographic apostrophe ’. Shortcut keys use their French key-cap names (Maj, Ctrl, Alt, Échap).
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# `untranslated_report` lists gaps). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Contour lumineux…
@@ -1384,7 +1384,7 @@ font-technology Variable Variables
Clear History Effacer l’historique
Set History Limit… Définir la limite de l’historique…
History States États de l’historique
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Appliquer le jeu de données
Cancel Pattern Editing Annuler la modification du motif
Copy Appearance Copier l’aspect
@@ -3751,7 +3751,7 @@ axis Both Les deux
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` aucune bibliothèque `{id}`
@msg select some type first sélectionnez d’abord du texte
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -2
View File
@@ -18,7 +18,7 @@
# English first word, as they are often shown after "…: ".
#
# Coverage: the whole interface (every menu string and `tl!` literal, `complete_menus`) and the
# status and error messages (`COMPLETE_MESSAGES`), enforced by the tests. Left in English on
# status and error messages (`COMPLETE_MESSAGES`), gaps listed by `untranslated_report`. Left in English on
# purpose (`MENU_KEEP_AS_IS`): the product name, language names, OpenType, the built-in workspace
# names and the bracketed perspective grid preset names; `hu-HU` and every other `hu-*` locale
# resolve here.
@@ -1777,7 +1777,7 @@ file-filter Templates Sablonok
Clear History Előzmények törlése
Set History Limit… Előzménykorlát beállítása…
History States Előzmények lépésszáma
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Adatkészlet alkalmazása
Cancel Pattern Editing Mintázatszerkesztés megszakítása
Copy Appearance Megjelenés másolása
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -3
View File
@@ -9,7 +9,7 @@
# user as "tu". Shortcut keys keep their key-cap names (Shift, Alt, Ctrl, Esc).
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# `untranslated_report` lists gaps). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Bordi luminosi…
@@ -1395,7 +1395,7 @@ file-filter Templates Modelli
Clear History Cancella cronologia
Set History Limit… Imposta limite cronologia…
History States Stati cronologia
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Applica set di dati
Area Type Testo in area
Bloat Gonfia
@@ -3794,7 +3794,7 @@ axis Both Entrambi
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` nessuna libreria `{id}`
@msg select some type first seleziona prima del testo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4 -4
View File
@@ -11,7 +11,7 @@
Smoothness 滑らかさ
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested) — and the status and error messages (`@msg` rows at the end, `COMPLETE_MESSAGES`, tested).
# `untranslated_report` lists gaps) — and the status and error messages (`@msg` rows at the end, `COMPLETE_MESSAGES`, `untranslated_report` lists gaps).
# Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
About VectorCraft VectorCraft について
@@ -1639,7 +1639,7 @@ font-technology Variable バリアブル
Clear History ヒストリーを消去
Set History Limit… ヒストリーの上限を設定…
History States ヒストリー数
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set データセットを適用
Area Type エリア内文字
Bloat 膨張
@@ -3807,7 +3807,7 @@ theme Appearance 外観
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
# Japanese: no final 。 (these are status lines, and reasons that follow a colon), unless the English ends
# with a period; a space between Japanese text and an adjacent Latin word, number or {placeholder}.
@@ -4654,7 +4654,7 @@ effect Vertical 垂直
Define a variable first 先に変数を定義してください
Text テキスト
Visibility 表示状態
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# `untranslated_report` lists gaps). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# Shared panel docking
Activate Panel パネルをアクティブにする
Change Panel Layout パネルの配置を変更
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -1634,7 +1634,7 @@ font-technology Variable 가변
Clear History 히스토리 지우기
Set History Limit… 히스토리 한도 설정…
History States 히스토리 상태
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set 데이터 세트 적용
Area Type 영역 문자
Bloat 부풀리기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -3
View File
@@ -9,8 +9,8 @@
//! English text, clean-room, see `zh-hant.tsv`).
//! 2. Add one row to [`LANGUAGES`] (code, native name, catalog, plural rule).
//!
//! That is all: the Preferences dropdown, the system-locale match and the catalog tests (parse,
//! placeholders, plural forms, menu coverage) pick it up from the registry.
//! That is all: the Preferences dropdown, the system-locale match, the catalog tests (parse,
//! placeholders, plural forms) and the untranslated report pick it up from the registry.
//!
//! # Looking strings up
//! - [`t`] / the `tl!` macro: a plain string in the language the UI is drawn in. [`tr`] takes the
@@ -41,7 +41,7 @@ pub struct LangInfo {
pub source: &'static str,
/// Plural form index for a count (English: 0 = one, 1 = other; Chinese: always 0).
pub plural: fn(u64) -> usize,
/// Must the catalog cover every menu string? (checked by the tests)
/// Does the catalog aim to cover every menu string? The untranslated report lists what it lacks.
pub complete_menus: bool,
catalog: OnceLock<Catalog>,
}
+2 -2
View File
@@ -17,7 +17,7 @@
#
# Plurals: one (1), few (2–4, 22–24, … but not 12–14), many (0, 5–21, 25–31, …).
#
# Coverage: every menu string, tl! literal, plural, status and error message (enforced in tests:
# Coverage: every menu string, tl! literal, plural, status and error message (`untranslated_report` lists gaps:
# `complete_menus` and `COMPLETE_MESSAGES`). Left in English on purpose (`MENU_KEEP_AS_IS` in the
# tests): the product name, language names, OpenType, the built-in workspace names and the
# bracketed perspective grid preset names. Keep placeholders, ellipses and the leading/trailing
@@ -1451,7 +1451,7 @@ lighting Light Światło
Clear History Wyczyść historię
Set History Limit… Ustaw limit historii…
Open a document to see its history. Otwórz dokument, aby zobaczyć jego historię.
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Zastosuj zestaw danych
Area Type Tekst akapitowy
Cancel Pattern Editing Anuluj edycję wzorka
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -2
View File
@@ -19,7 +19,7 @@
# commands is kept.
#
# Coverage: the whole interface (every menu string and every `tl!` literal, `complete_menus`,
# tested) and the status and error messages (`@msg` rows, `COMPLETE_MESSAGES`, tested). Left in
# `untranslated_report` lists gaps) and the status and error messages (`@msg` rows, `COMPLETE_MESSAGES`, `untranslated_report` lists gaps). Left in
# English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language names, OpenType,
# the built-in workspace names and the bracketed perspective grid preset names.
#
@@ -1191,7 +1191,7 @@
Clear History Limpar Histórico
Set History Limit… Definir Limite do Histórico…
History States Estados do Histórico
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Aplicar Conjunto de Dados
Cancel Pattern Editing Cancelar Edição de Padrão
Copy Appearance Copiar Aparência
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -7,7 +7,7 @@
# Перо, Прямое выделение). Shortcut keys keep their key-cap names (Shift, Alt, Ctrl, Esc).
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# `untranslated_report` lists gaps). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Светящиеся края…
@@ -1393,7 +1393,7 @@ file-filter Templates Шаблоны
Clear History Очистить историю
Set History Limit… Задать предел истории…
History States Состояния истории
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Применить набор данных
Area Type Текст в области
Bloat Раздувание
@@ -3790,7 +3790,7 @@ axis Both Оба
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` нет библиотеки `{id}`
@msg select some type first сначала выделите текст
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -1352,7 +1352,7 @@ font-technology Variable Variabla
Clear History Rensa historik
Set History Limit… Ange historikgräns…
History States Historiktillstånd
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Använd datauppsättning
Cancel Pattern Editing Avbryt mönsterredigering
Copy Appearance Kopiera utseende
Can't render this file because it contains an unexpected character in line 5 and column 25.
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -7,7 +7,7 @@
# монтажна область, шар, обведення, заливка, контур, опорна точка, зразки, пензель, виділення.
# Shortcut key names (Shift, Alt, Ctrl, Esc), command ids and placeholders remain unchanged.
#
# Coverage: every menu string, tl! literal, plural, status and error message (enforced in tests).
# Coverage: every menu string, tl! literal, plural, status and error message (`untranslated_report` lists gaps).
# Product, language, built-in workspace and perspective preset names follow MENU_KEEP_AS_IS.
# Keep placeholders, ellipses and leading/trailing spaces of fragments when editing.
Glowing Edges… Сяйливі краї…
@@ -1389,7 +1389,7 @@ file-filter Templates Шаблони
Clear History Очистити історію
Set History Limit… Задати межу історії…
History States Стан історії
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Застосувати набір даних
Area Type Текст в області
Bloat Роздування
Can't render this file because it contains an unexpected character in line 737 and column 40.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1607,7 +1607,7 @@ file-filter Templates Mẫu
Clear History Xóa lịch sử
Set History Limit… Đặt giới hạn lịch sử…
History States Trạng thái lịch sử
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set Áp dụng tập dữ liệu
Area Type Chữ vùng
Bloat Phình
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1607
1608 # History panel: the names of undo steps (`every_undo_label_is_translated`). # History panel: the names of undo steps (`untranslated_report` lists any that are missing).
1609
1610
1611
1612
1613
+2 -2
View File
@@ -1328,7 +1328,7 @@ file-filter Templates 模板
Clear History 清除历史记录
Set History Limit… 设置历史记录上限…
History States 历史记录步数
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set 应用数据集
Bloat 膨胀
Cancel Pattern Editing 取消图案编辑
@@ -3595,7 +3595,7 @@ imageTrace Outline 轮廓
Use Default 使用默认值
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
# Simplified Chinese: no final 。 unless the English ends with a period; a space between Chinese text and
# an adjacent Latin word, number or {placeholder}; 、 between the items of a list.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -2
View File
@@ -1360,7 +1360,7 @@ file-filter Templates 範本
Clear History 清除步驟記錄
Set History Limit… 設定步驟記錄上限…
History States 步驟記錄狀態
# History panel: the names of undo steps (`every_undo_label_is_translated`).
# History panel: the names of undo steps (`untranslated_report` lists any that are missing).
Apply Data Set 套用資料集
Area Type 區域文字
Bloat 膨脹
@@ -1584,7 +1584,7 @@ file-filter Templates 範本
Keyboard Shortcuts… 鍵盤快速鍵…
# Status and error messages (`@msg`), translated where the status bar shows them; the message itself
# stays English for agents and tests. `{_1}`, `{_2}` … stand for the `{}` of the format string, in order
# (`complete_languages_translate_every_message` lists any that are missing). Command ids, parameter
# (`untranslated_report` lists any that are missing). Command ids, parameter
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
# Traditional Chinese: no final 。 unless the English ends with a period; a space between Chinese text and
# an adjacent Latin word, number or {placeholder}; 、 between the items of a list; 「」 around the name of
Can't render this file because it contains an unexpected character in line 6 and column 47.
+15 -6
View File
@@ -4435,10 +4435,14 @@ mod tests {
_ => panic!("{id} is not a menu item"),
}
};
assert_eq!(shown(&app, "edit.undo"), "ぼかしを取り消す");
let step = |template: &str, name: &str| {
let name = crate::panels::history::step_name(ja, name).unwrap_or_else(|| name.to_string());
crate::i18n::fmt(crate::i18n::tr(ja, template), &[("name", &name)])
};
assert_eq!(shown(&app, "edit.undo"), step("Undo {name}", "Feather"));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(shown(&app, "edit.redo"), "ぼかしをやり直す");
assert_eq!(shown(&app, "edit.undo"), "長方形を取り消す", "a step with a plain row");
assert_eq!(shown(&app, "edit.redo"), step("Redo {name}", "Feather"));
assert_eq!(shown(&app, "edit.undo"), step("Undo {name}", "Rectangle"), "a step with a plain row");
crate::i18n::set_current(crate::i18n::Lang::EN);
assert_eq!(shown(&app, "edit.redo"), "Redo Feather");
}
@@ -4543,10 +4547,15 @@ mod tests {
let text = app.run("text.create", json!({"x": 0, "y": 40, "text": "Hello"})).unwrap()["id"].clone();
app.run("charStyle.new", json!({"name": "Brush"})).unwrap();
app.run("charStyle.apply", json!({"name": "Brush", "id": text})).unwrap();
crate::i18n::set_current(crate::i18n::Lang::from_code("ja").expect("ja registered"));
assert_eq!(display_label(&app, "edit.undo", "Undo"), "スタイル「Brush」を適用を取り消す");
let ja = crate::i18n::Lang::from_code("ja").expect("ja registered");
crate::i18n::set_current(ja);
let step = |template: &str| {
let applied = crate::i18n::fmt(crate::i18n::tr(ja, "Apply {name}"), &[("name", "Brush")]);
crate::i18n::fmt(crate::i18n::tr(ja, template), &[("name", &applied)])
};
assert_eq!(display_label(&app, "edit.undo", "Undo"), step("Undo {name}"));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(display_label(&app, "edit.redo", "Redo"), "スタイル「Brush」を適用をやり直す");
assert_eq!(display_label(&app, "edit.redo", "Redo"), step("Redo {name}"));
crate::i18n::set_current(crate::i18n::Lang::EN);
assert_eq!(display_label(&app, "edit.redo", "Redo"), "Redo Apply Brush");
}
+6 -4
View File
@@ -1277,14 +1277,16 @@ mod tests {
}
/// Those choices read as their dialogs show them in the UI language: in French, the channel's
/// RGB is RVB, and in Spanish, Extrude & Bevel's Rounded is the "3d" row's Redondeado, not the
/// plain row's Redondeada. Rasterize names the document's color mode.
/// RGB as the "effect" row translates it, and in Spanish, Extrude & Bevel's Rounded as the "3d"
/// row does, not the plain row. Rasterize names the document's color mode.
#[test]
fn the_effect_editor_shows_those_choices_as_their_dialogs_do() {
crate::i18n::set_current(crate::i18n::Lang::from_code("fr").unwrap());
let fr = crate::i18n::Lang::from_code("fr").unwrap();
crate::i18n::set_current(fr);
let (mut app, ctx, _) = editing("adjust.levels");
let texts = shown_texts(&mut app, &ctx);
assert!(texts.iter().any(|s| s == "RVB"), "{texts:?}");
let rgb = crate::i18n::tr_ctx(fr, "effect", "RGB");
assert!(texts.iter().any(|s| s == rgb), "{rgb}: {texts:?}");
let es = crate::i18n::Lang::from_code("es").unwrap();
crate::i18n::set_current(es);
let (mut app, ctx, _) = editing("threeD.extrude");
+5 -3
View File
@@ -1080,11 +1080,13 @@ mod tests {
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
assert!(text.contains("Weight\n") && text.contains("Width\n") && text.contains("400") && text.contains("100"), "{text}");
// The labels are the font's names in every interface language, not catalog words (ja
// translates the interface's Weight as 線幅, the stroke weight).
crate::i18n::set_current(crate::i18n::Lang::from_code("ja").unwrap());
// translates the interface's Weight as the stroke weight).
let ja = crate::i18n::Lang::from_code("ja").unwrap();
crate::i18n::set_current(ja);
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
crate::i18n::set_current(crate::i18n::Lang::EN);
assert!(text.contains("Weight\n") && text.contains("Width\n") && !text.contains("線幅"), "{text}");
let weight = crate::i18n::tr(ja, "Weight");
assert!(text.contains("Weight\n") && text.contains("Width\n") && (weight == "Weight" || !text.contains(weight)), "{text}");
// Screen readers name each slider by its axis.
let tree = crate::widgets::accessibility_tests::tree(|ui| axis_sliders(&mut app, ui, &face));
for name in ["Weight", "Width"] {
+17 -11
View File
@@ -154,19 +154,21 @@ mod tests {
let ja = crate::i18n::Lang::from_code("ja").expect("ja registered");
crate::i18n::set_current(ja);
let rows = listed(&mut app);
let step = |template: &str, name: &str| crate::i18n::fmt(crate::i18n::tr(ja, template), &[("name", name)]);
let apply = |name: &str| step("Apply {name}", name);
let want = [
"グラフィックスタイル「S」を再定義",
"スタイル「Body」を適用",
"スタイル「{name}」を適用",
"スタイル「Apply」を適用",
"スタイル「Brush」を適用",
"スタイル「[標準文字スタイル]」を適用",
"スタイル「Graphic Style “X”」を再定義",
step("Redefine Graphic Style “{name}”", "S"),
apply("Body"),
apply("{name}"),
apply("Apply"),
apply("Brush"),
apply(crate::i18n::tr(ja, "[Normal Character Style]")),
step("Redefine {name}", "Graphic Style “X”"),
];
assert!(rows.contains(&want[0].to_string()), "{rows:#?}");
assert!(rows.contains(&want[0]), "{rows:#?}");
assert_eq!(rows[rows.len() - 6..], want[1..], "{rows:#?}");
app.session.execute("edit.undo", &json!({})).unwrap();
assert_eq!(listed(&mut app).last().map(String::as_str), Some(want[6]), "an undone step");
assert_eq!(listed(&mut app).last(), Some(&want[6]), "an undone step");
crate::i18n::set_current(crate::i18n::Lang::EN);
assert_eq!(listed(&mut app)[rows.len() - 6..rows.len() - 4], ["Apply Body", "Apply {name}"]);
}
@@ -176,8 +178,12 @@ mod tests {
#[test]
fn effect_steps_read_the_menu_row() {
let ja = crate::i18n::Lang::from_code("ja").expect("ja registered");
assert_eq!(step_name(ja, "Revolve").as_deref(), Some("回転体"));
assert_eq!(step_name(ja, "Feather").as_deref(), Some("ぼかし"));
for effect in ["Revolve", "Feather"] {
let menu_row =
crate::i18n::has(ja, &format!("{effect}…")).then(|| crate::i18n::tr(ja, &format!("{effect}…")).trim_end_matches('…').to_string());
let plain_row = crate::i18n::has(ja, effect).then(|| crate::i18n::tr(ja, effect).to_string());
assert_eq!(step_name(ja, effect), plain_row.or(menu_row), "{effect}");
}
assert_eq!(step_name(ja, "No Such Step"), None);
}
-1
View File
@@ -1794,7 +1794,6 @@ mod tests {
let (stored, painted) = names(rect);
let inner = stored.trim_start_matches('<').trim_end_matches('>');
assert_eq!(painted, format!("<{}>", crate::i18n::tr(zh, inner)));
assert_ne!(painted, stored, "translated");
}
#[test]
-1
View File
@@ -147,7 +147,6 @@ fn language_and_appearance_are_in_the_app_menu() {
assert_eq!(appearance.len(), crate::theme::Brightness::ALL.len());
assert_eq!(appearance.iter().filter(|(_, c)| *c == Some(true)).count(), 1, "{appearance:?}");
assert!(appearance.iter().all(|(_, c)| c.is_some()));
assert_eq!(crate::i18n::tr_ctx(Lang::from_code("ja").unwrap(), "theme", "Appearance"), "外観", "the theme, not the Appearance panel");
}
/// Everything the in-window bar lists is in the Mac menu bar, once: what moved to the app menu
+18 -12
View File
@@ -258,24 +258,30 @@ other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE
rule). The Language menu, the dropdown, locale matching and the catalog tests (well-formed, no duplicates,
placeholders and ellipses agree, command ids exist, every row of a partial catalog is a string the UI
shows, the UI fonts have every glyph, no Simplified characters in `zh-hant`) pick it up. Set `complete_menus`
once every menu string and `tl!` literal is translated; `i18n::tests` then enforce it
(`VECTORCRAFT_I18N_DUMP=strings.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them).
once the menus and `tl!` literals are translated; the untranslated report then lists what the catalog lacks
(`VECTORCRAFT_I18N_DUMP=strings.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists every
English string).
- A missing translation never fails a test or a build: the interface shows the English text until a translator
adds the row. `cargo xtask i18n-report` prints, per language, the strings its catalog lacks (menu strings,
`tl!` literals, undo step names, menu labels, render-site labels, status and error messages, PostScript
error meanings, default swatch names), and `cargo xtask ci` prints the same list in its `i18n-report` step.
The report is the test `i18n::tests::untranslated_report`, run with `--nocapture`; it fails only when a
scan finds too few strings to be working. Tests don't check one language's exact wording: a test that needs a
translation compares with the catalog lookup (`tr(lang, "English")`).
- Translations are clean-room: written from the meaning of the English text in ordinary vocabulary, never
from another product's localisation resources. Product and technology names stay in Latin letters.
- Status-bar messages and errors stay English where they are made (`app.ui.status`, `EngineError`: the
control channel, MCP and tests read them) and are translated only where the status bar draws them
(`i18n::msg`). `@msg` catalog rows hold a whole message or a template such as
`Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Georgian, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian, Vietnamese and Finnish cover every
message literal the test scan finds (`complete_languages_translate_every_message`, languages listed in
`COMPLETE_MESSAGES`), except the messages listed in `crates/ui-egui/src/i18n/untranslated_messages.txt`.
A new `Err("…")`, `Other(…)`, `#[error(…)]` or `status(…)` message needs a row in each of those catalogs, and so
does a new message given to an error enum's variant (`PsError::Limit(…)`), to the engine's `bad(cmd, …)`, `err(…)` or
`error(…)`, or to the PDF writer's `bad(…)` or `failed(…)` (`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui
complete_languages_translate_every_message` lists them all). The list also holds the messages that open pull
requests add without those rows. A change that adds the rows for a listed message removes its line from the list
(`VECTORCRAFT_I18N_DUMP_UNTRANSLATED=<file>` writes the messages in the sources that lack a row). Other languages
show messages in English until they add `@msg` rows.
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Georgian, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian, Vietnamese, Finnish and Turkish
translate status and error messages (`COMPLETE_MESSAGES` in `i18n::tests`); the untranslated report lists the
message literals the scan finds that one of them has no row for. The scan reads `Err("…")`, `Other(…)`,
`#[error(…)]` and `status(…)` messages, messages given to an error enum's variant (`PsError::Limit(…)`), to the
engine's `bad(cmd, …)`, `err(…)` or `error(…)`, and to the PDF writer's `bad(…)` or `failed(…)`
(`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them
all). A missing row shows the English message. Other languages show messages in English until they add `@msg`
rows.
- Not translated on purpose: names that are user data (layers, swatches, fonts, documents), the tab
title's colour mode, command ids and parameter names inside messages. Not done yet: locale-aware number
and date formats, right-to-left layout. Chinese and Japanese UI text
+27 -29
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Georgian (`ka`) interface catalog; Bulgarian (`bg`) and Greek (`el`) catalogs complete under the tests, #1909, #2076; Finnish (`fi`) catalog added, complete under the tests; Swedish (`sv`) catalog added, complete under the tests; scores and estimates unchanged; Brazilian Portuguese (`pt-br`) catalog completed under the tests: menus, `tl!` literals and messages; 12-language score 71% → 74%; #1914) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** patch (missing translations never fail; `cargo xtask ci` prints the untranslated report)
Per-language status of VectorCraft's interface. How the catalogs work and how to add a language:
[`development.md` › Localisation](development.md#localisation). Part of
@@ -8,18 +8,15 @@ Per-language status of VectorCraft's interface. How the catalogs work and how to
**How it is measured:** rows with a translation in each `crates/ui-egui/src/i18n/<code>.tsv`, counted on
origin/main `01e165af`, except the rows that give a count date, which were counted on `main` at `cb0a7d9` (2026-10-21).
The reference is the French catalog, which the tests hold complete: 3,416 interface
The reference is the French catalog, which was complete when counted: 3,416 interface
strings (menus, command labels, `tl!` literals: panels, dialogs, tooltips, Preferences) plus 604 `@msg` status and
error messages, 4,020 in all. A language's % is its rows over 4,020 (capped at 100% per part). The
`complete_menus` flag and `COMPLETE_MESSAGES` list make `i18n::tests` fail if a complete language misses a
string, so "full" here is enforced by tests. The messages listed in `crates/ui-egui/src/i18n/untranslated_messages.txt`
are not translated yet, and the tests let them through. No language has help content: there is no translated (or English)
in-app help, and the docs are English.
error messages, 4,020 in all. A language's % is its rows over 4,020 (capped at 100% per part).
`i18n::tests` fail if a language with the `complete_menus` flag misses a menu string or `tl!` literal, or if a
language in the `COMPLETE_MESSAGES` list misses a status or error message. Every non-English language marked
"full" has the flag, and all of them except Bulgarian are in the list. No language has help content: there is
no translated (or English) in-app help, and the docs are English.
error messages, 4,020 in all. A language's % is its rows over 4,020 (capped at 100% per part). A missing
translation never fails a test or a build: the interface shows the English text until a translator adds the row.
`cargo xtask ci` prints the untranslated report (`cargo xtask i18n-report` alone), which lists per language the
strings missing from each catalog with the `complete_menus` flag and the messages missing from each language in
the `COMPLETE_MESSAGES` list. Every non-English language marked "full" has the flag, and all of them except
Bulgarian, Danish, Indonesian and Dutch are in the list. No language has help content: there is no translated (or
English) in-app help, and the docs are English.
**Native review:** every catalog was written clean-room from the English meaning, most of it by agents
(`bflatastic`), with community contributors adding or correcting (German by `dwetscher`, Japanese and Traditional
@@ -30,17 +27,17 @@ Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker
| Language | Code | UI strings translated | Dialogs / tooltips / messages | Script support | Native review | Status | To `full` (h) |
|---|---|---|---|---|---|---|---|
| English | `en` | source, 100% | all | Latin | — | full | — |
| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete (tests) | all | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) |
| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) |
| Spanish | `es` | 4,009 / 4,020 (99.7%) | all (604 messages) | Latin | no | full | — (review) |
| Hindi | `hi` | 0 | none | Devanagari: the document's text shapes (HarfRust), with tests of conjuncts, reph, pre-base matras, nukta forms and caret stops that run when an OpenType Devanagari font is installed, but the egui interface does no complex shaping | no | none | 20–30 |
| Arabic | `ar` | 0 | none | document text: bidi layout and Paragraph Direction; interface: no right-to-left layout, no Arabic shaping in egui | no | none | 25–40 |
| French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) |
| Portuguese | `pt-br` | 5,194 catalog entries (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) |
| Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete (tests); the tests do not hold its messages complete; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) |
| Portuguese | `pt-br` | 5,194 catalog entries (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) |
| Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) |
| Japanese | `ja` | 4,004 / 4,020 (99.6%) | all | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) |
| German | `de` | 3,952 / 4,020 (98%; tests hold menus, literals and messages complete) | all (602 of 604 message rows; landed 2026-10-10) | Latin | contributor-written, not reviewed | full | — (review) |
| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) |
| Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (precomposed forms; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) |
| German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (602 of 604 message rows; landed 2026-10-10) | Latin | contributor-written, not reviewed | full | — (review) |
| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) |
| Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (precomposed forms; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) |
**12-language score: ~82%** (equal weights: 100 + 85 + 100 + 0 + 0 + 100 + 100 + 100 + 100 + 98 + 100 + 100, over 12).
**To `full` for all twelve: ~50–85 h**, most of it Arabic (right-to-left interface) and Hindi (interface
@@ -51,19 +48,19 @@ plus review. Native-speaker review is human work and isn't in the hours.
| Language | Code | Translated | Status |
|---|---|---|---|
| Traditional Chinese (Taiwan) | `zh-hant` | 4,892 rows, 790 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete under the tests (80% overall) | partial |
| Traditional Chinese (Taiwan) | `zh-hant` | 4,892 rows, 790 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted (80% overall) | partial |
| Italian | `it` | 4,011 / 4,020 | full |
| Dutch | `nl` | 5,317 rows, 777 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete (tests); the tests do not hold its messages complete; one/other plurals | full |
| Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete (tests); the tests do not hold its messages complete; one/other plurals | full |
| Dutch | `nl` | 5,317 rows, 777 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full |
| Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full |
| Russian | `ru` | 4,009 / 4,020 | full |
| Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full |
| Hungarian | `hu` | 5,253 rows (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one plural form | full |
| Polish | `pl` | 5,130 rows (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one/few/many plurals | full; native review pending |
| Finnish | `fi` | 5,318 rows (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one/other plurals, 797 `@msg` rows | full; AI-assisted, native proofreading pending |
| Czech | `cs` | 5,240 catalog entries, including 795 `@msg` rows (counted 2026-10-21); menu, literal and message coverage enforced by tests | catalog complete; render-site gaps reduced; native review pending |
| Swedish | `sv` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete (tests); one/other plurals | full; native review welcome |
| Bulgarian | `bg` | 5,245 rows, 763 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete (tests); the tests do not hold its messages complete | full; AI-drafted, native review pending |
| Greek | `el` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete (tests) | full; native review welcome |
| Hungarian | `hu` | 5,253 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | full |
| Polish | `pl` | 5,130 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/few/many plurals | full; native review pending |
| Finnish | `fi` | 5,318 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals, 797 `@msg` rows | full; AI-assisted, native proofreading pending |
| Czech | `cs` | 5,240 catalog entries, including 795 `@msg` rows (counted 2026-10-21); menus, literals and messages complete when counted | catalog complete; render-site gaps reduced; native review pending |
| Swedish | `sv` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals | full; native review welcome |
| Bulgarian | `bg` | 5,245 rows, 763 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial | full; AI-drafted, native review pending |
| Greek | `el` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | full; native review welcome |
| Georgian | `ka` | 5,200 rows (counted 2026-10-21): menus, panels, dialogs, plurals (one count-neutral form) and messages, tested complete; reviewed by a native speaker | full |
Twenty-three non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
@@ -82,7 +79,7 @@ contextual radial adjective. PDF/Print mark styles use `mark-style`, so `Japanes
The search placeholder, Home screen, toolbar/panel tooltips and Preferences already use lookups.
No new `tl!("literal")` keys were needed: only `cs.tsv` adds 19 AI-draft rows and revises the gradient
template. Every other complete catalog (`complete_menus`) has the same rows and a `Roman` row for the
other mark style; `complete_catalogs_translate_render_site_labels` fails when one of them lacks a row.
other mark style; the untranslated report lists a catalog that lacks one of them.
Native review is still needed, and catalog coverage does not prove that every variable in the UI is
translated.
@@ -95,6 +92,7 @@ font for the web build.
| Date | Change | Summary |
|---|---|---|
| 2026-10-23 | patch | A missing translation never fails a test or a build; one untranslated report, printed by `cargo xtask ci` and `cargo xtask i18n-report`, lists the missing strings per language; scores unchanged |
| 2026-10-16 | minor | Bulgarian (`bg`, #1909) and Greek (`el`, #2076) catalogs added, complete under the tests; Swedish (#2139) and Finnish (#1994) rows for strings added since; eighteen non-English catalogs |
| 2026-10-16 | minor | Finnish (`fi`) catalog added, complete under the tests (menus, literals, messages; one/other plurals); AI-assisted, native proofreading pending |
| 2026-10-15 | minor | Swedish (`sv`) catalog added, complete under the tests (menus, literals, messages; one/other plurals); every `sv-*` locale resolves to it; AI-drafted, native review welcome; scores and estimates unchanged |
+2 -2
View File
@@ -549,10 +549,10 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
- The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.
- Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).
- The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`, `every_undo_label_is_translated`).
- The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`).
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian and Georgian ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
- Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`).
- The History panel translates the undo step names the engine, the tools and the shell write as literals: each has a row in every complete catalog (`every_undo_label_is_translated`), and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis.
- The History panel translates the undo step names the engine, the tools and the shell write as literals: the untranslated report (`untranslated_report`) lists any a complete catalog lacks, and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis.
- The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. Not done: a new document from the selected state.
- The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear.
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
+12
View File
@@ -27,6 +27,8 @@ commands:
assets check that every icon/image/font/asset is attributed in ASSETS.md
brands check that user-visible text (labels, params docs, MCP, docs, packaging) names no other vendor's products
cursors warn about Cursor values with no entry in crates/tools/src/cursors.toml (never fails)
i18n-report list the strings each language's catalog lacks, shown in English (fails only on a
broken scan; ci runs it too)
pdf-deps verify immutable shared PDF writer and patched Krilla sources
layers enforce the crate dependency layering (plan/architecture.md §3)
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates and the web app
@@ -49,6 +51,7 @@ fn main() -> ExitCode {
Some("assets") => assets::run(&root()),
Some("brands") => brands::run(&root()),
Some("cursors") => cursors::run(&root()),
Some("i18n-report") => cmd_i18n_report(),
Some("layers") => cmd_layers(),
Some("pdf-deps") => pdf_deps::run(&root()),
Some("wasm") => cmd_wasm(),
@@ -271,6 +274,7 @@ fn cmd_ci() -> Result<(), String> {
("assets", Box::new(|| assets::run(&root()))),
("brands", Box::new(|| brands::run(&root()))),
("cursors", Box::new(|| cursors::run(&root()))),
("i18n-report", Box::new(cmd_i18n_report)),
("layers", Box::new(cmd_layers)),
("wasm", Box::new(cmd_wasm)),
];
@@ -317,3 +321,11 @@ Tests that use a corpus skip cleanly when it is absent.
}
pngsuite::fetch()
}
/// Prints the strings each language's catalog has no row for (`i18n::tests::untranslated_report`).
/// The interface shows them in English, so a gap never fails it; only a scan that finds nothing does.
fn cmd_i18n_report() -> Result<(), String> {
let mut c = cargo();
c.args(["test", "-p", "vectorcraft-ui-egui", "--lib", "--", "i18n::tests::untranslated_report", "--exact", "--nocapture"]);
run(c, "cargo test -p vectorcraft-ui-egui --lib -- i18n::tests::untranslated_report --exact --nocapture")
}