German interface language (de)

Add the German catalog: every menu string, every `tl!` literal, the plural
forms and the status and error messages (3,818 rows), written from the meaning
of the English labels in the vocabulary German-speaking users of vector
illustration software know (Zeichenfläche, Farbfelder, Kontur, Fläche, Pfad,
Ankerpunkt, Ebene, Verlauf, Deckkraft). Conventions are in the header of
`de.tsv`.

- Register `de` ("Deutsch") in `i18n::LANGUAGES` as a complete language; every
  `de-*` locale (`de-DE`, `de-AT`, `de-CH` …) resolves to it.
- Tests: German joins the menu-label, menu-string, `tl!`-literal and message
  coverage checks; `german_reads_as_german` pins the core vocabulary, the
  plurals, a nested message, the joined "…format" style names and the theme
  context ("Erscheinungsbild" vs the Appearance panel's "Aussehen"); ä ö Ä Ö ß
  are checked in the bundled UI fonts. The "unsupported locale" cases use
  Dutch now that German is supported.
- README, ROADMAP and docs/development.md list German.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
dwetscher 2026-10-10 10:55:39 +02:00 committed by bflatastic
parent 58bb72ffc6
commit 194968bc27
7 changed files with 3921 additions and 14 deletions

View File

@ -213,7 +213,7 @@ doesn't write Affinity files. Current `.af` validation includes 33 pinned files,
and every-board SVG/PDF/PSD export, with fixes for Affinity 3 artboards, source-backed JPEGs and text runs.
[Scope and limits](crates/affinity/README.md); [source audit and remaining gaps](docs/affinity-validation.md).
The interface
speaks English, Japanese, Traditional and Simplified Chinese, Spanish, French, Italian, Russian and Ukrainian (and Czech and Brazilian Portuguese in the menus).
speaks English, German, Japanese, Traditional and Simplified Chinese, Spanish, French, Italian, Russian and Ukrainian (and Czech and Brazilian Portuguese in the menus).
The scores are
self-assessed, so the [honest assessment](ROADMAP.md#honest-assessment-2026-10-05) explains how far to trust them.

File diff suppressed because one or more lines are too long

View File

@ -733,7 +733,7 @@ mod tests {
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 ["es", "fr", "it", "ru"] {
for code in ["de", "es", "fr", "it", "ru"] {
let lang = crate::i18n::Lang::from_code(code).unwrap();
for s in [
"every search thread stopped on an internal error (please report this bug)",

File diff suppressed because it is too large Load Diff

View File

@ -82,13 +82,23 @@ fn plural_east_slavic(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 11] = [
pub static LANGUAGES: [LangInfo; 12] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
// Japanese: the whole interface (every menu string and `tl!` literal) and the status and error
// messages, keeping the product, workspace and perspective preset names in English (`MENU_KEEP_AS_IS`).
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
// Czech: every menu label (`menu_catalogs_translate_every_menu_label`); panels and dialogs not yet.
LangInfo { code: "cs", name: "Čeština", source: include_str!("cs.tsv"), plural: plural_czech, complete_menus: false, catalog: OnceLock::new() },
// German: the whole interface and the status and error messages, keeping the same names in
// English as Spanish; every `de-*` locale (`de-DE`, `de-AT`, `de-CH`, `de-LU` …) resolves here.
LangInfo {
code: "de",
name: "Deutsch",
source: include_str!("de.tsv"),
plural: plural_one_other,
complete_menus: true,
catalog: OnceLock::new(),
},
// Spanish: the whole interface in neutral, international Spanish, keeping the same names in
// English as Japanese; every `es-*` locale (`es-ES`, `es-MX`, `es-AR`, `es-419` …) resolves here.
LangInfo {

View File

@ -8,7 +8,11 @@ fn tags_map_to_languages() {
assert_eq!(lang_from_tag("en_US.UTF-8"), Some(Lang::EN));
assert_eq!(lang_from_tag("C"), Some(Lang::EN));
assert_eq!(lang_from_tag("POSIX"), Some(Lang::EN));
assert_eq!(lang_from_tag("de_DE"), None);
assert_eq!(lang_from_tag("nl_NL"), None);
// German: Germany, Austria, Switzerland and the rest share the one catalog.
for tag in ["de", "de_DE.UTF-8", "de-AT", "de_CH", "de-LU", "de_DE.UTF-8@euro"] {
assert_eq!(lang_from_tag(tag), Some(de()), "{tag}");
}
assert_eq!(lang_from_tag("ja_JP.UTF-8"), Lang::from_code("ja"));
assert_eq!(lang_from_tag("ja"), Lang::from_code("ja"));
// Spanish: every region (and Latin America as a whole) resolves to the one catalog.
@ -51,7 +55,8 @@ fn candidates_walk_from_specific_to_general() {
#[test]
fn os_language_lists_are_parsed() {
assert_eq!(first_supported("(\n \"zh-Hant-TW\",\n \"en-US\"\n)\n"), Some(ZH()));
assert_eq!(first_supported("(\n \"de-DE\",\n \"en-US\"\n)\n"), Some(Lang::EN));
assert_eq!(first_supported("(\n \"nl-NL\",\n \"en-US\"\n)\n"), Some(Lang::EN));
assert_eq!(first_supported("(\n \"de-DE\",\n \"en-US\"\n)\n"), Some(de()));
assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Lang::from_code("fr"));
assert_eq!(first_supported("("), None);
// Windows: `reg query HKCU\Control Panel\International /v LocaleName`.
@ -435,9 +440,9 @@ fn cs() -> Lang {
}
/// Languages whose catalogs leave [`MENU_KEEP_AS_IS`] in English.
const KEEPS_MENU_NAMES: [&str; 8] = ["cs", "es", "fr", "it", "ja", "pt-br", "ru", "uk"];
const KEEPS_MENU_NAMES: [&str; 9] = ["cs", "de", "es", "fr", "it", "ja", "pt-br", "ru", "uk"];
/// Menu labels the menu-complete catalogs (Czech, Spanish, Italian, Japanese, Brazilian Portuguese) show as they are: the product name, a format name, the built-in workspace
/// Menu labels the menu-complete catalogs (Czech, German, Spanish, Italian, Japanese, Brazilian Portuguese) show as they are: the product name, a format name, the built-in workspace
/// names and the perspective grid presets (names, shown untranslated wherever else they appear).
/// Each language's own name in the Language menu is left alone too.
const MENU_KEEP_AS_IS: &[&str] = &[
@ -527,7 +532,7 @@ fn toggled_labels() -> Vec<String> {
labels
}
/// Czech, Spanish, Italian, Japanese and Brazilian Portuguese cover every menu label, the Show/Hide pairs
/// Czech, German, Spanish, Italian, Japanese and Brazilian Portuguese cover every menu label, the Show/Hide pairs
/// and the canvas context menu included (panels and dialogs not yet).
#[test]
fn menu_catalogs_translate_every_menu_label() {
@ -565,9 +570,14 @@ fn menu_catalogs_translate_every_menu_label() {
}
assert_eq!(tr(Lang::from_code("pt-br").expect("pt-br"), "File"), "Arquivo");
assert_eq!(tr(es(), "File"), "Archivo");
assert_eq!(tr(de(), "File"), "Datei");
assert_eq!(tr(it(), "Edit"), "Modifica");
}
fn de() -> Lang {
Lang::from_code("de").expect("de registered")
}
fn es() -> Lang {
Lang::from_code("es").expect("es registered")
}
@ -609,6 +619,36 @@ fn spanish_reads_as_spanish() {
assert_eq!(trn(es(), 3, "{n} Layer", "{n} Layers"), "3 capas");
}
/// German uses the vector-illustration vocabulary its users know, has two plural forms like
/// English (zero takes the plural), translates the reason inside a message too, and tells the
/// interface theme ("Erscheinungsbild") from the Appearance panel ("Aussehen").
#[test]
fn german_reads_as_german() {
for (en, want) in [
("Artboard Tool", "Zeichenflächen-Werkzeug"),
("Swatches", "Farbfelder"),
("Pathfinder", "Pathfinder"),
("Stroke", "Kontur"),
("Fill", "Fläche"),
("Direct Selection Tool", "Direktauswahl-Werkzeug"),
("Save As…", "Speichern unter…"),
("Undo", "Rückgängig"),
] {
assert_eq!(tr(de(), en), want);
}
assert_eq!(trn(de(), 1, "{n} Layer", "{n} Layers"), "1 Ebene");
assert_eq!(trn(de(), 0, "{n} Layer", "{n} Layers"), "0 Ebenen");
assert_eq!(trn(de(), 3, "{n} Layer", "{n} Layers"), "3 Ebenen");
assert_eq!(tr_ctx(de(), "axis", "Both"), "Beide");
assert_eq!(tr(de(), "Appearance"), "Aussehen");
assert_eq!(tr_ctx(de(), "theme", "Appearance"), "Erscheinungsbild");
let nested = "command `object.group` is not available right now: nothing selected";
assert_eq!(message(de(), nested), "der Befehl `object.group` ist derzeit nicht verfügbar: nichts ausgewählt");
// Text styles are "…format" in German: the kind is joined to it ("Neues Zeichenformat erstellen").
assert_eq!(fmt(tr(de(), "Create New {kind} Style"), &[("kind", tr(de(), "Character"))]), "Neues Zeichenformat erstellen");
assert_eq!(fmt(tr(de(), "Show {kind} Brushes"), &[("kind", tr(de(), "Calligraphic"))]), "Kalligrafiepinsel einblenden");
}
/// French uses the vector-illustration vocabulary its users know, puts zero in the singular (« 0
/// calque »), and reads the same in the menus and in the panels.
#[test]
@ -769,7 +809,7 @@ fn russian_plurals_have_three_forms() {
assert_eq!(forms, [2, 0, 1, 1, 2, 2, 0, 1, 2, 2, 0]);
}
/// Czech, Spanish, French and Italian letters (and the punctuation their text uses) come from each family's own first font, not
/// Czech, German, Spanish, French and Italian letters (and the punctuation their text uses) come from each family's own first font, not
/// from a fallback further down the stack. (`has_glyph` can't tell: it counts characters of the
/// face that draws missing glyphs, the first one, as missing.)
#[test]
@ -784,7 +824,7 @@ fn latin_script_glyphs_are_available_without_system_fonts() {
let first = first.unwrap();
let mut font = fonts.fonts.font(&family);
let chars = font.characters();
for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–ñÑüÜ¿¡”àèìòùÀÈÌÒÙ«»’âêîôûçëïÿœæÂÊÎÔÛÇËÏŒÆ\u{a0}".chars()
for ch in "áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ„“‚‘…–ñÑüÜ¿¡”àèìòùÀÈÌÒÙ«»’âêîôûçëïÿœæÂÊÎÔÛÇËÏŒÆäöÄÖß\u{a0}".chars()
{
assert!(chars.get(&ch).is_some_and(|fonts| fonts.contains(&first)), "{first} ({family:?}) has no {ch}");
}
@ -884,7 +924,7 @@ fn complete_languages_translate_every_message() {
}
/// Languages whose catalogs cover every status and error message.
const COMPLETE_MESSAGES: &[&str] = &["es", "fr", "it", "ja", "ru", "uk"];
const COMPLETE_MESSAGES: &[&str] = &["de", "es", "fr", "it", "ja", "ru", "uk"];
/// Crates whose error and status messages reach the status bar.
const MESSAGE_CRATES: &[&str] = &["ui-egui", "engine", "doc", "format", "svg", "pdf", "eps", "text", "plugins", "metafile", "cad", "trace"];

View File

@ -166,7 +166,8 @@ always use the English ids and labels, so agents and scripts never see translate
Languages shipped: English (`en`, the source), Traditional Chinese (`zh-hant`, complete, in the vocabulary used
in Taiwan; `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-*` locales all resolve to it), Simplified Chinese (`zh-hans`,
complete, in the vocabulary used in mainland China; `zh-CN`, `zh-SG`, `zh-Hans-*` and a bare `zh` resolve to it,
so the two scripts never mix), Japanese (`ja`, complete), Spanish (`es`, complete, in neutral
so the two scripts never mix), Japanese (`ja`, complete), German (`de`, complete; `de-DE`, `de-AT`, `de-CH` and every
other `de-*` locale resolve to it), Spanish (`es`, complete, in neutral
international Spanish; every `es-*` locale such as `es-ES`, `es-MX`, `es-AR` or `es-419` resolves to it), French (`fr`,
complete; `fr-FR`, `fr-BE`, `fr-CA`, `fr-CH` and every other `fr-*` locale resolve to it), Italian (`it`,
complete; `it-IT`, `it-CH` and every other `it-*` locale resolve to it), Russian (`ru`, complete; every `ru-*`