i18n: add Hungarian (hu) UI translation (#3831)

* i18n(cs): drop duplicate catalog entries

The Czech catalog listed Tool, Clone Stamp, History Brush, Dodge, Burn,
Sponge and Stroke Path… twice, which fails `cargo xtask i18n-coverage`
and the bundled_catalogs_are_consistent test. Keep one row each; for
Stroke Path… keep the later row, the one the loader already used.

* test(crop): pin the UI language to English in crop_default_mode

The test queries widgets by their English labels ("Ratio", "Swaps height
and width") but its harness followed the system locale, so it failed on a
machine whose locale has a registered catalog. Set interface.language to
"en" in the harness, as paint_bucket_all_layers and export_as_scaling do.

* i18n: add Hungarian (hu) UI translation

A complete Hungarian catalog (hu.tsv, 3908 entries: 3829 plain, 50
contextual, 21 plural, 8 command-id overrides) registered with the
complete-menu languages; hu-HU locales resolve to it.

Plural rule: plural_none. A Hungarian count takes the singular noun
("3 réteg"), so every @plural entry has one form.

Clean room: written from the plain meaning of the English labels with
ordinary Hungarian image-editing vocabulary; no proprietary localisation
resource, string table or product UI was used.

Also listed in the coverage test, LICENSE-translations.txt,
ATTRIBUTION.md and docs/localization.md, with tag, system-locale and
plural tests.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bihacsy László
2026-10-12 11:07:17 -04:00
committed by GitHub
co-authored by Brandon Thomas Claude Opus 5.5
parent b3309e27d7
commit 9e623e7a45
8 changed files with 4006 additions and 5 deletions
+1
View File
@@ -19,6 +19,7 @@ next to them.
| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese 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/uk.tsv` | Ukrainian 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/cs.tsv` | Czech 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/hu.tsv` | Hungarian 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/vi.tsv` | Vietnamese 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/tr.tsv` | Turkish 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) |
@@ -1,6 +1,6 @@
PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch, Czech and Turkish UI translation catalogs are
licensed under MIT OR Apache-2.0, at your option. The full license texts are in ../../../../LICENSE-MIT
and ../../../../LICENSE-APACHE.
PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch, Czech, Turkish and
Hungarian UI translation catalogs are licensed under MIT OR Apache-2.0, at your option. The full
license texts are in ../../../../LICENSE-MIT and ../../../../LICENSE-APACHE.
Korean translations: PhotoCraft contributors, original translations of
PhotoCraft's English labels; no proprietary translation resources were used.
@@ -25,3 +25,6 @@ PhotoCraft's English labels; no proprietary translation resources were used.
Turkish translations: PhotoCraft contributors, original translations of
PhotoCraft's English labels; no proprietary translation resources were used.
Hungarian translations: PhotoCraft contributors, original translations of
PhotoCraft's English labels; no proprietary translation resources were used.
File diff suppressed because it is too large Load Diff
+22 -2
View File
@@ -39,7 +39,7 @@ pub struct LangInfo {
/// Catalog file contents, raw-deflated by build.rs (empty for the built-in English); see
/// [`LangInfo::text`].
pub source: &'static [u8],
/// Plural form index for a count (English: 0 = one, 1 = other; Japanese and Chinese: always 0;
/// Plural form index for a count (English: 0 = one, 1 = other; Japanese, Chinese and Hungarian: always 0;
/// Czech: 0 = one, 1 = few (2–4), 2 = other; French: 0 = one (0 and 1), 1 = other). A catalog's `@plural` entries list one form per
/// index.
pub plural: fn(u64) -> usize,
@@ -141,7 +141,7 @@ fn inflate(source: &[u8]) -> Result<String, String> {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 19] = [
pub static LANGUAGES: [LangInfo; 20] = [
LangInfo { code: "en", name: "English", source: b"", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
LangInfo { code: "ja", name: "日本語", source: catalog!("ja"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "zh-hans", name: "简体中文", source: catalog!("zh-hans"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
@@ -164,6 +164,8 @@ pub static LANGUAGES: [LangInfo; 19] = [
// Dutch; `nl-NL` and `nl-BE` locales both resolve here.
LangInfo { code: "nl", name: "Nederlands", source: catalog!("nl"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() },
LangInfo { code: "it", name: "Italiano", source: catalog!("it"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() },
// Hungarian: a count takes the singular noun ("3 réteg"), so one plural form.
LangInfo { code: "hu", name: "Magyar", source: catalog!("hu"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
// Turkish; `tr`, `tr-TR` and `tr_TR` locales resolve here.
LangInfo { code: "tr", name: "Türkçe", source: catalog!("tr"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
// Vietnamese; `vi-VN` resolves here.
@@ -420,6 +422,8 @@ mod tests {
assert_eq!(lang_from_tag("it-CH"), Lang::from_code("it"));
assert_eq!(lang_from_tag("nl_NL.UTF-8"), Lang::from_code("nl"));
assert_eq!(lang_from_tag("nl-BE"), Lang::from_code("nl"));
assert_eq!(lang_from_tag("hu_HU.UTF-8"), Lang::from_code("hu"));
assert_eq!(lang_from_tag("hu-HU"), Lang::from_code("hu"));
assert_eq!(lang_from_tag("tr_TR.UTF-8"), Lang::from_code("tr"));
assert_eq!(lang_from_tag("tr-TR"), Lang::from_code("tr"));
assert_eq!(lang_from_tag("tr"), Lang::from_code("tr"));
@@ -518,6 +522,22 @@ mod tests {
assert_eq!(trn(es, 3, "{n} item", "{n} items"), "3 elementos");
}
#[test]
fn hungarian_resolves_and_keeps_the_singular_after_counts() {
let hu = Lang::from_code("hu").expect("hu registered");
for tag in ["hu", "hu_HU.UTF-8", "hu-HU"] {
assert_eq!(lang_from_tag(tag), Some(hu), "{tag}");
}
assert!(hu.complete_menus());
assert_eq!(hu.name(), "Magyar");
assert_eq!(tr(hu, "Layer"), "Réteg");
for (n, expected) in [(0, "0 réteg"), (1, "1 réteg"), (3, "3 réteg"), (21, "21 réteg")] {
assert_eq!(trn(hu, n, "{n} layer", "{n} layers"), expected);
}
assert_eq!(tr_ctx(hu, "layerLabel", "Red"), "Piros");
assert_eq!(tr(hu, "Red"), "Vörös");
}
#[test]
fn brazilian_portuguese_resolves_and_pluralises() {
let pt = Lang::from_code("pt-br").expect("pt-br registered");
+1
View File
@@ -96,6 +96,7 @@ mod tests {
(vec!["pt-PT"], "pt-br"),
(vec!["it-IT", "en-US"], "it"),
(vec!["nl-BE", "fr-BE"], "nl"),
(vec!["hu-HU", "en-US"], "hu"),
(vec!["tr-TR", "en-US"], "tr"),
(vec![], "en"),
] {
@@ -24,6 +24,8 @@ fn harness() -> Option<Harness> {
egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 640.0)).with_max_steps(64).wgpu().build_eframe(|cc| {
PhotocraftApp::setup_context(&cc.egui_ctx, Default::default());
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
// The queries below use English labels; don't follow the system locale.
app.session.prefs.edit(|p| p.interface.language = "en".into());
if let Some(rs) = cc.wgpu_render_state.as_ref() {
app.set_wgpu(rs.clone());
}
+1
View File
@@ -25,6 +25,7 @@ contextual, command-ID and plural translations; missing entries fall back to Eng
| Polski | `pl` | one / few (2–4, not 12–14) / many |
| Ελληνικά | `el` | one / other |
| Nederlands | `nl` | one / other |
| Magyar | `hu` | one form |
| Tiếng Việt | `vi` | one form |
Languages registered with `complete_menus` cover the current menu labels, `tl!` literals, blend
+1
View File
@@ -579,6 +579,7 @@ mod tests {
("el", "el.tsv"),
("uk", "uk.tsv"),
("nl", "nl.tsv"),
("hu", "hu.tsv"),
("tr", "tr.tsv"),
("vi", "vi.tsv"),
] {