Arabic interface language, combined from the community catalogs

Adds Arabic (ar, العربية) as an interface language. The catalog combines
the open Arabic catalogs of #2505, #4520, #4569, #5593, #5915, #6092 and
#6287: each row takes the best of their translations, with one Arabic
term per concept across the UI, and the strings added since are filled
in. Modern Standard Arabic, Western digits, the six CLDR plural forms
(zero|one|two|few|many|other).

The two tests that used ar-SA as an unsupported locale now use sw-KE, and
the direction test expects Arabic to be the one right-to-left catalog.

Co-authored-by: azizr12 <163084230+azizr12@users.noreply.github.com>
Co-authored-by: hicbest <213873494+hicbest@users.noreply.github.com>
Co-authored-by: hasan-sdaif <306196098+hasan-sdaif@users.noreply.github.com>
Co-authored-by: NibrasAz7 <34162952+NibrasAz7@users.noreply.github.com>
Co-authored-by: vfggf95565 <74471233+vfggf95565@users.noreply.github.com>
Co-authored-by: syrrevshin4202-prog <243624842+syrrevshin4202-prog@users.noreply.github.com>
Co-authored-by: test-bit-sys <336050374+test-bit-sys@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Mann
2026-10-28 21:22:26 +01:00
parent 2fccdcba8e
commit bd9590d060
12 changed files with 3622 additions and 20 deletions
+1
View File
@@ -23,6 +23,7 @@ next to them.
| `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/ar.tsv` | Arabic 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) |
| `crates/ui-egui/src/i18n/sr.tsv` | Serbian (Latin, Ekavian) 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/ru.tsv` | Russian 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) |
+16 -1
View File
@@ -115,6 +115,18 @@ fn plural_polish(n: u64) -> usize {
}
}
/// Arabic (CLDR): 0 → zero, 1 → one, 2 → two, n % 100 in 3–10 → few, in 11–99 → many, else other.
fn plural_arabic(n: u64) -> usize {
match (n, n % 100) {
(0, _) => 0,
(1, _) => 1,
(2, _) => 2,
(_, 3..=10) => 3,
(_, 11..=99) => 4,
_ => 5,
}
}
/// A catalog file as packed by build.rs (`pack_catalogs`; the binary would otherwise carry ~2.2 MB
/// of translations, which the web build's size gate can't afford). See [`unpack`].
macro_rules! catalog {
@@ -187,7 +199,7 @@ fn expand(packed: &str, keys: &[&str]) -> Result<String, String> {
}
/// The registry. English first: it is the fallback and the source language.
pub static LANGUAGES: [LangInfo; 22] = [
pub static LANGUAGES: [LangInfo; 23] = [
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() },
@@ -222,6 +234,9 @@ pub static LANGUAGES: [LangInfo; 22] = [
LangInfo { code: "vi", name: "Tiếng Việt", source: catalog!("vi"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() },
// Hindi; `hi`, `hi-IN` locales resolve here.
LangInfo { code: "hi", name: "हिन्दी", source: catalog!("hi"), plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
// Arabic (Modern Standard); `ar-SA`, `ar-EG` and the other regional locales resolve here. The
// chrome mirrors right to left (`dir_for_code`), and egui lays each label out right to left.
LangInfo { code: "ar", name: "العربية", source: catalog!("ar"), plural: plural_arabic, complete_menus: true, catalog: OnceLock::new() },
];
impl LangInfo {
@@ -40,3 +40,8 @@ PhotoCraft's English labels; no proprietary translation resources were used.
Russian translations: PhotoCraft contributors, original translations of
PhotoCraft's English labels; no proprietary translation resources were used.
Arabic translations: PhotoCraft contributors, combined from the catalogs contributed by @azizr12,
@hicbest, @hasan-sdaif, @NibrasAz7, @vfggf95565, @syrrevshin4202-prog and @test-bit-sys and
reviewed with Claude (Anthropic) as an AI contributor; original translations of PhotoCraft's
English labels, and no proprietary translation resources were used.
File diff suppressed because it is too large Load Diff
+22 -1
View File
@@ -244,7 +244,7 @@ fn first_launch_renders_the_system_language_without_overwriting_auto() {
#[test]
fn missing_or_unsupported_system_languages_render_english() {
for tags in [vec![], vec!["sv-SE", "ar-SA"]] {
for tags in [vec![], vec!["sv-SE", "sw-KE"]] {
system::with_system_tags(&tags, || {
with_language(Lang::EN, || {
let h = harness();
@@ -449,3 +449,24 @@ fn czech_shortcut_conflicts_keep_warning_state_and_shortcut_codes() {
assert_eq!(fields["overrides"]["file.new"], json!("Cmd+O"));
});
}
/// The catalog stays in logical order: egui puts a right-to-left label into display order itself,
/// so the first word of an Arabic label lands to the right of its last, and a Latin brand name at
/// the end of the sentence lands at the left.
#[test]
fn arabic_labels_are_laid_out_right_to_left() {
with_language(Lang::EN, || {
let mut h = harness();
h.state_mut().run("prefs.set", json!({"path": "interface.language", "value": "ar"})).unwrap();
h.run_steps(4);
let x_of = |text: &str, c: char| {
let galley = h.ctx.fonts_mut(|f| f.layout_no_wrap(text.to_string(), egui::FontId::proportional(13.0), egui::Color32::WHITE));
galley.rows.iter().flat_map(|r| r.glyphs.iter()).find(|g| g.chr == c).map(|g| g.pos.x).expect("glyph")
};
let label = tr(lang("ar"), "No properties");
assert_eq!(label, "لا توجد خصائص");
assert!(x_of(label, 'ل') > x_of(label, 'ص'), "{label}: first word at the right");
let join = tr(lang("ar"), "Join us on Discord");
assert!(x_of(join, 'D') < x_of(join, 'ن'), "{join}: Discord at the left");
});
}
+2 -1
View File
@@ -91,7 +91,8 @@ mod tests {
(vec!["zh-Hant-HK", "zh-CN"], "zh-hant"),
(vec!["zh-Hans-CN", "zh-TW"], "zh-hans"),
(vec!["en-US", "ru-RU"], "en"),
(vec!["sv-SE", "ar-SA"], "en"),
(vec!["sv-SE", "ar-SA"], "ar"),
(vec!["sv-SE", "sw-KE"], "en"),
(vec!["de-AT", "en-US"], "de"),
(vec!["pt-PT"], "pt-br"),
(vec!["it-IT", "en-US"], "it"),
+22
View File
@@ -194,6 +194,28 @@ fn ukrainian_plural_rule_handles_teens_and_compound_counts() {
assert_eq!(forms, [2, 0, 1, 1, 2, 2, 2, 2, 2, 0, 1, 1, 2, 2, 0, 2, 2, 2, 0, 1, 2]);
}
#[test]
fn arabic_resolves_locales_and_preferences() {
let ar = Lang::from_code("ar").expect("ar registered");
assert_eq!(ar.name(), "العربية");
for tag in ["ar", "AR", "ar-SA", "ar_EG", "ar_EG.UTF-8", "ar-AE", "ar-MA"] {
assert_eq!(lang_from_tag(tag), Some(ar), "{tag}");
assert_eq!(Lang::from_pref(tag), ar, "{tag}");
}
assert_eq!(dir_for_code("ar"), Dir::Rtl);
assert_eq!(tr(ar, "File"), "ملف");
}
#[test]
fn arabic_plural_rule_has_six_forms() {
let ar = Lang::from_code("ar").expect("ar registered");
let counts = [0, 1, 2, 3, 10, 11, 99, 100, 101, 102, 103, 111, 199, 1000, u64::MAX];
let forms: Vec<usize> = counts.into_iter().map(ar.0.plural).collect();
assert_eq!(forms, [0, 1, 2, 3, 3, 4, 4, 5, 5, 5, 3, 4, 4, 5, 4]);
assert_eq!(trn(ar, 2, "{n} layer", "{n} layers"), "2 طبقتان");
assert_eq!(trn(ar, 5, "{n} layer", "{n} layers"), "5 طبقات");
}
#[test]
fn serbian_resolves_locales_and_preferences() {
let sr = Lang::from_code("sr").expect("sr registered");
+5 -4
View File
@@ -1,6 +1,6 @@
//! Right-to-left shell layout: the direction rules, and where the mirrored chrome lands in a
//! running shell. No right-to-left catalog is registered yet, so the shell tests force the
//! direction with `direction_scope`.
//! running shell. The shell tests force the direction with `direction_scope`, so they don't
//! depend on a right-to-left language being selected.
use super::*;
use crate::PhotocraftApp;
@@ -17,9 +17,10 @@ fn right_to_left_scripts_by_language_code() {
for code in ["en", "ja", "zh-hans", "pt-br", "ku", "arn", ""] {
assert_eq!(i18n::dir_for_code(code), Dir::Ltr, "{code}");
}
// No right-to-left catalog is registered yet, so the shell stays as it was.
// Arabic is the one right-to-left catalog; every other registered language stays left to right.
for lang in Lang::all() {
assert_eq!(lang.dir(), Dir::Ltr, "{}", lang.code());
let want = if lang.code() == "ar" { Dir::Rtl } else { Dir::Ltr };
assert_eq!(lang.dir(), want, "{}", lang.code());
}
}
+6 -5
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-22 · **Change:** minor (Arabic: paragraph direction buttons, #6793; previously minor: Russian dynamic labels) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-28 · **Change:** minor (Arabic UI catalog; previously minor: Arabic paragraph direction buttons, #6793) · **Target:** Adobe Photoshop 2026
Per-language status of PhotoCraft's UI. How catalogs work and how to add one:
[`localization.md`](localization.md). Language notes: [Korean](localization-ko.md), [Spanish](localization-es.md),
@@ -24,7 +24,7 @@ Vietnamese UI; Arabic and Hebrew users get English "Middle East" builds with the
composer, and its text engine handles RTL, Devanagari and Indic scripts.
**Summary.** 7 of the 12 key languages at 100% of measured keys (status `partial` because of the
engine messages), 3 absent (Hindi, Arabic, Vietnamese), plus 8 other languages. Weighted by
engine messages), 2 absent (Hindi, Vietnamese), plus 8 other languages. Weighted by
speakers, ~55% ready. Remaining: 80–140 h, plus native-speaker review (recorded only for Italian,
2026-10-12).
@@ -36,7 +36,7 @@ speakers, ~55% ready. Remaining: 80–140 h, plus native-speaker review (recorde
| Simplified Chinese | `zh-hans` | 1,608 / 1,608 (100%) | 2,366 | dialogs, tooltips yes; engine messages English | CJK fonts (lazy loader), IME preedit/commit, vertical type | no | partial | 8–12 h |
| Spanish | `es` | 1,844 / 1,844 (100%) | 4,010 | as above; complete dynamic UI key set | Latin | no | partial | 6–10 h |
| Hindi | `hi` | partial | 448 | Top menus, toolbox tools, camera raw, dialogs, plurals | Devanagari shaping in the text engine (parley/HarfRust); no Devanagari fallback font (TYPE-218-3) | no | partial | 15–20 h |
| Arabic | `ar` | 0 (0%) | – | – | Arabic shaping and bidi in the text engine, Arabic fallback font; RTL shell layout but not inside panels, and UI labels in LTR word order (DIST-220-4), paragraph direction buttons but no visual RTL caret (TYPE-218-2 partial), layer names as tofu (#1408, #1693) | no | none | 40–60 h |
| Arabic | `ar` | 2,175 / 2,175 (100%, 2026-10-28) | 3,410 | as above; combined from the community catalogs with one term per concept | Arabic shaping and bidi in the UI (egui 0.37) and the text engine, Arabic fallback font; RTL shell layout but not inside panels (DIST-220-4), paragraph direction buttons but no visual RTL caret (TYPE-218-2 partial) | no | partial | 15–25 h |
| French | `fr` | 1,608 / 1,608 (100%) | 2,766 | as above | Latin | no | partial | 6–10 h |
| Portuguese (Brazil) | `pt-br` | 1,608 / 1,608 (100%) | 2,788 | as above | Latin; every `pt` locale uses it | no | partial | 6–10 h |
| Indonesian | `id` | 1,608 / 1,608 (100%) | 2,764 | as above | Latin | no | partial | 6–10 h |
@@ -70,8 +70,8 @@ engine localization or native review is complete.
- Engine error, status and parameter-help messages are English in every language.
- Right-to-left UI layout is partial (2026-10-10): the shell mirrors for Arabic, Hebrew, Persian,
Urdu and other right-to-left codes (`direction.rs`), but panel interiors, dialogs, menu rows,
submenus and document tabs stay left to right. UI labels also lack an Arabic fallback font, and
egui 0.36 joins letters within a word but orders a line's words left to right (no bidi).
submenus and document tabs stay left to right. Word order within a label is right: egui 0.37
lays text out with the Unicode Bidirectional Algorithm.
- Web build: no browser-locale detection, no CJK web fonts.
- WebAssembly plug-ins disappear from Filter › Plug-ins when the UI isn't English (#2540).
- A user couldn't find the language option (#2532): it is in Preferences › Interface.
@@ -92,3 +92,4 @@ engine localization or native review is complete.
| 2026-10-13 | minor | Italian: the `Screen` blend mode read *Schermo* (the display) and is now *Scherma*; the Dodge/Burn/Sponge tool names were already Adobe's (*Scherma*, *Brucia*, *Spugna*) and stay. Pinned by a test |
| 2026-10-18 | minor | Russian: 1,229 dynamic labels added (filter and adjustment parameters, Camera Raw, lighting, History/Layers/paths menus, metadata, System Info, status templates, dialogs) with a terminology note ([localization-ru](localization-ru.md)) |
| 2026-10-22 | minor | Arabic row: paragraph direction has Auto / Left-to-right / Right-to-left buttons in the Paragraph panel and Type options bar (#6793); the visual RTL caret is still missing (TYPE-218-2 partial) |
| 2026-10-28 | minor | Arabic (`ar`) UI catalog: every measured key, six plural forms; layer names and the title bar no longer reversed on egui 0.37 |
+8 -6
View File
@@ -103,13 +103,15 @@ title, options and status bars read from the right (`crates/ui-egui/src/directio
direction follows the language code, so a catalog needs no extra setting.
Not mirrored yet: panel interiors, dialogs, menu rows (shortcuts stay on the right), submenus
(they open to the right), document tabs and the canvas. Before an Arabic catalog reads
correctly, the UI also needs an Arabic fallback font (the bundled UI fonts have no Arabic
glyphs) and bidirectional word order: egui 0.36 shapes each right-to-left word with HarfRust,
so letters join, but lays a line's words out left to right, and its caret indexing is off in
right-to-left text (emilk/egui#8576, #8577). Document text is laid out by the text engine.
(they open to the right), document tabs and the canvas. egui 0.37 lays text out with the Unicode
Bidirectional Algorithm (each paragraph takes the direction of its first strong letter), so
catalogs, layer names and document names are handed to it in logical order, as typed; never
pre-reorder a label. The title line forces a left-to-right paragraph so "— PhotoCraft" stays at
the right of a right-to-left document name, and `bidi_text::elide` cuts a right-to-left name at
its logical end. Document text is laid out by the text engine.
No right-to-left catalog ships yet. To check the mirrored layout with any language, launch with
Arabic (`ar`) is the first right-to-left catalog: Modern Standard Arabic, Western digits, the six
CLDR plural forms (zero|one|two|few|many|other). To check the mirrored layout with any language, launch with
`PHOTOCRAFT_UI_DIRECTION=rtl` (or `ltr` to force left-to-right); it is read once per process
and has no effect on the web build:
+1 -1
View File
@@ -290,7 +290,7 @@ Never-crash attribute on 30 of 30 crate roots.
| DIST-220-1 | Help > Check for Updates plus an opt-out periodic release-feed check on all desktops | partial | [#220](https://github.com/storytold/photocraft/issues/220) | 2026-10-22 (#5485): Help > Check for Updates and an opt-out check once per launch (updates_cmds + the desktop HTTPS transport; none inside Flatpak, where flatpak update owns updates). Missing: a periodic recheck while the app stays open |
| DIST-220-2 | Windows ARM64 (aarch64-pc-windows-msvc) in the release matrix, signed like x64 | done | [#220](https://github.com/storytold/photocraft/issues/220) | release.yml has { arch: arm64, target: aarch64-pc-windows-msvc } in the Windows matrix, and .github/workflows/windows-arm64.yml packages that MSI, installs it on a Windows 11 ARM64 runner, checks the installed binaries are ARM64 and runs the CLI natively before uninstalling. Signing takes the same path as x64: the secrets are optional and a missing one produces an unsigned artifact and a warning (see DIST-2) |
| DIST-220-3 | In-app bug report with reviewable diagnostics that opens a prefilled issue | missing | [#220](https://github.com/storytold/photocraft/issues/220) | No bug report dialog in crates |
| DIST-220-4 | Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew | partial | [#220](https://github.com/storytold/photocraft/issues/220) | `cargo xtask i18n-coverage` reports per-language coverage (#427); RTL languages mirror the shell (`direction.rs`), not yet panel interiors or dialogs |
| DIST-220-4 | Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew | partial | [#220](https://github.com/storytold/photocraft/issues/220) | `cargo xtask i18n-coverage` reports per-language coverage (#427); RTL languages mirror the shell (`direction.rs`), not yet panel interiors or dialogs; Arabic (`ar`) is the first RTL catalog |
| DIST-220-5 | Linux AppImage x86_64 + aarch64 ship; Flatpak kept; Flathub later (#173) | partial | [#220](https://github.com/storytold/photocraft/issues/220) | AppImage and Flatpak ship (release.yml:162-216); Flathub not done |
## Open bugs
+1 -1
View File
@@ -66,7 +66,7 @@ id = "DIST-220-4"
target = "Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew"
status = "partial"
issue = 220
note = "`cargo xtask i18n-coverage` reports per-language coverage (#427); RTL languages mirror the shell (`direction.rs`), not yet panel interiors or dialogs"
note = "`cargo xtask i18n-coverage` reports per-language coverage (#427); RTL languages mirror the shell (`direction.rs`), not yet panel interiors or dialogs; Arabic (`ar`) is the first RTL catalog"
[[item]]
id = "DIST-220-5"