i18n: add German (de) UI translation (#309) (#663)

* i18n: add German (de) UI translation (#309)

A complete German catalog plus the registry row:

- de.tsv: the full key set (menus, tl! literals, blend-mode names, Camera
  Raw labels, generated preference labels), plural rule one|other, header
  documenting the format and the clean-room policy like ja.tsv.
- Registered with complete_menus: true; the locale resolver and the
  language-list tests gain de (de-DE/de-AT/de-CH resolve to it).
- ATTRIBUTION.md row and the docs/localization.md language table entry.

Translations are original work from the plain meaning of the English
labels in ordinary German image-editing vocabulary (Ebene, Maske,
Auswahl, Weichzeichnen, Tonwertkorrektur, Smart-Objekt ...); no other
product's localisation resources were consulted. Product and technology
names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab, Camera Raw) stay in
Latin letters; the Camera Raw curve bands are distinct (Schwarz, Tiefen,
Dunkle, Helle, Lichter, Weiss).

Verified: catalog tests and preference-label tests green,
cargo xtask i18n-coverage reports de 1404/1404 (100%), the full
ui-egui suite passes (except the known machine-local wgpu driver
failure that also fails on clean main), clippy and wasm32 clean, and
an offscreen snapshot sequence switches de -> en -> de in one process
and ends fully German.

* de: translate Metadata (Export As gains the label in the #647 fix)

* Revert "de: translate Metadata (Export As gains the label in the #647 fix)"

This reverts commit 948a8d01dd83579a899d938611101daa50a9e1e1.

* de: apply the native-speaker review (#663)

All meaning errors from the review: Auto Tone is the tonal range
(Automatische Tonwertkorrektur), the Divide blend mode is arithmetic
(Dividieren), Note Paper is Notizpapier, Legal is the US paper format,
Fuzziness widens the colour tolerance (Toleranz:), Perspective Warp
planes are Flaechen (not Ebenen, which reads as layers), Revert is
Zuruecksetzen (matching its confirm text), Fade is Verblassen.
Typos: Tonwerthistogramm, Magischer-Radiergummi-Werkzeug,
Einfrier-Lasso. Consistency: Kanalmixer everywhere, Zoom In/Out as
Vergrössern/Verkleinern, a space before % in the scale labels.

* xtask i18n-coverage: gate the German catalog too

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-07 15:48:02 -04:00
1 parent e70deecbe5
commit e79dcb6f7f
5 files changed
+2118 -4

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ next to them.
| Path | Title | Author | Source | License |
|---|---|---|---|---|
| `crates/ui-egui/src/i18n/de.tsv` | German UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) |
| `crates/ui-egui/src/i18n/ko.tsv` | Korean UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) |
| `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | <https://github.com/rsms/inter> | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) |
| `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono 2.305 (numeric font) | The JetBrains Mono Project Authors | <https://github.com/JetBrains/JetBrainsMono> | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) |
File diff suppressed because it is too large. Load diff
+5 -3
View File
@@ -75,7 +75,7 @@ fn plural_fr(n: u64) -> usize {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 10] = [
pub static LANGUAGES: [LangInfo; 11] = [
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
LangInfo {
@@ -92,6 +92,7 @@ pub static LANGUAGES: [LangInfo; 10] = [
LangInfo { code: "fr", name: "Français", source: include_str!("fr.tsv"), plural: plural_fr, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "id", name: "Bahasa Indonesia", source: include_str!("id.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "ko", name: "한국어", source: include_str!("ko.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "de", name: "Deutsch", source: include_str!("de.tsv"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() },
];
impl LangInfo {
@@ -355,7 +356,8 @@ mod tests {
assert_eq!(lang_from_tag("cs_CZ.UTF-8"), Some(CS()));
assert_eq!(lang_from_tag("cs-CZ"), Some(CS()));
assert_eq!(lang_from_tag("fr_FR"), Lang::from_code("fr"));
assert_eq!(lang_from_tag("de_DE"), None);
assert_eq!(lang_from_tag("de_DE"), Lang::from_code("de"));
assert_eq!(lang_from_tag("de-AT"), Lang::from_code("de"));
// Traditional Chinese: by region, by script, and with a region after the script.
assert_eq!(lang_from_tag("zh_TW.UTF-8"), Some(ZH()));
assert_eq!(lang_from_tag("zh-TW"), Some(ZH()));
@@ -385,7 +387,7 @@ mod tests {
fn macos_language_list_is_parsed() {
assert_eq!(first_supported("(\n \"ja-JP\",\n \"en-US\"\n)\n"), Some(JA()));
assert_eq!(first_supported("(\n \"fr-FR\",\n \"en-US\"\n)\n"), Lang::from_code("fr"));
assert_eq!(first_supported("(\n \"de-DE\",\n \"en-US\"\n)\n"), Some(Lang::EN));
assert_eq!(first_supported("(\n \"de-DE\",\n \"en-US\"\n)\n"), Lang::from_code("de"));
assert_eq!(first_supported("(\n \"zh-Hant-TW\",\n \"en-US\"\n)\n"), Some(ZH()));
assert_eq!(first_supported("("), None);
}
+1
View File
@@ -17,6 +17,7 @@ contextual, command-ID and plural translations; missing entries fall back to Eng
| 繁體中文 | `zh-hant` | one form |
| Español | `es` | one / other |
| Čeština | `cs` | one / few / other |
| Deutsch | `de` | one / other |
| Bahasa Indonesia | `id` | one form |
Every non-English catalog covers the current menu labels, `tl!` literals, blend mode names,
+1 -1
View File
@@ -559,7 +559,7 @@ mod tests {
fn source_key_set_is_covered_by_complete_catalogs() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap_or(Path::new("."));
let keys = source_keys(root).unwrap_or_default();
for (code, path) in [("ja", "ja.tsv"), ("zh-hant", "zh-hant.tsv"), ("es", "es.tsv"), ("ru", "ru.tsv"), ("cs", "cs.tsv")] {
for (code, path) in [("ja", "ja.tsv"), ("zh-hant", "zh-hant.tsv"), ("es", "es.tsv"), ("ru", "ru.tsv"), ("cs", "cs.tsv"), ("de", "de.tsv")] {
let catalog_path = root.join("crates/ui-egui/src/i18n").join(path);
let text = fs::read_to_string(&catalog_path).unwrap_or_default();
let translations = parse_catalog(&catalog_path, &text).unwrap_or_default();