Skip to content

Add Arabic (العربية) interface translation - #1532

Open
hasan-sdaif wants to merge 7 commits into
storytold:mainfrom
hasan-sdaif:feat/arabic-i18n
Open

hasan-sdaif wants to merge 7 commits into
storytold:mainfrom
hasan-sdaif:feat/arabic-i18n

Conversation

@hasan-sdaif

@hasan-sdaif hasan-sdaif commented Oct 12, 2026 •

Copy link
Copy Markdown

Arabic (العربية) for VectorCraft — the complete interface catalog, plus the right-to-left reading path that makes it actually readable.

The catalog

3,879 rows, Modern Standard Arabic throughout:

  • every menu string — the top-level titles, the submenus, the item labels, the section headers, the UI and engine command labels and menu paths (complete_menus: true, enforced by the completeness test against menu_strings());
  • every tl! literal in the crate — enforced by the every_tl_literal_is_translated test;
  • the status and error messages — exact @msg rows and the {name}-style templates, values translated in turn;
  • the CLDR six-form plurals — zero/one/two/few/many/other, including the last-two-digits behaviour above 99 (103 is few again, 111 is many again), each row tested over its counts;
  • the conventions the other complete catalogs keep: product, workspace and preset names in English (MENU_KEEP_AS_IS), «» quotes, {placeholders} whole, the trailing … of dialog commands kept.

Every ar-* locale (ar-SA, ar-EG, ar_AE.UTF-8…) resolves to it; the craft-fonts Arabic faces (Noto Sans Arabic) join the UI families when present, mirroring the CJK path.

The reading path (bidi.rs)

egui 0.36 lays text runs left-to-right without a global direction switch, so a registered-but-unreadable Arabic would be worse than none. This PR carries the pass #1202 describes: Arabic translations are stored in the visual order egui draws, converted at catalog-parse time, with {placeholders} kept whole and left-to-right islands (a PDF inside a sentence) keeping their own order. The layout test pins the combination on a real headless egui layout.

The parse runs the pass per plural form, never across a @plural row — a pipe is a neutral character, and over the joined row the pass would reorder the six forms themselves (the plural suite caught exactly that: a count of one reading the many form).

Verification

All on GitHub Actions, on this fork, per the family's loop:

  • the full family gate — cargo xtask ci (fmt, clippy, tests, assets, brands, layers, wasm) — on the branch's tree plus only the workflow file that runs it: the green run (the checklist below ticks with it);
  • the branch is rebased onto current main, which also brings main's own clock-independent comparison fixtures (664e2ce) — the engine expandAppearance failure seen in an earlier verification run was that clock-straddle flake, fixed on main, absent since the rebase;
  • the earlier run's three failing i18n tests were mine and real: the plural-form permutation above, and rows the rebase's new strings asked for — both fixed in the branch (the 45 new rows are the shortcut-set import dialog, the precise cursors preference, the 3D lighting group and two save/export errors).

Checklist

  • cargo xtask ci green on the branch tree (fork-side run linked above)
  • rebased onto current main
  • complete_languages_translate_every_menu_string green
  • every_tl_literal_is_translated green
  • the Arabic plural suite green (six forms, CLDR counts, visual order per form)

Coordination

@hicbest's #1822 offers the deeper catalog (~4,300 rows with the status and error messages) on top of this branch's code — the fold notes are on my comment there; whichever shape the maintainers prefer, nothing is done twice.

شكراً لمراجعتكم — أتمنى أن تكون هذه الإضافة مفيدة لمستخدمي العربية.

@bflatastic

Copy link
Copy Markdown
Member

Thanks @hasan-sdaif, this is a big piece of work and the catalog is in good shape. I can't merge it yet, though: I built it and ran it, and the interface doesn't read correctly in Arabic. This is the gap #1202 describes, and the PR doesn't close it.

It doesn't compile as submitted

  • egui::LayoutDirection and Context::set_layout_direction don't exist in egui 0.36.2 (the version in Cargo.lock), so lib.rs fails to build. egui has no global right-to-left switch.
  • LANGUAGES gains a row but its array length isn't raised.

What it looks like with that call removed (rebased on main, release build, Windows, language set to ar)

  • Letters inside a word are joined correctly: egui 0.36 shapes each font run.
  • The words of every label are in the wrong order. egui splits a label into runs at each space (the space comes from the Latin UI font) and lays the runs out left to right. إعداد المستند (Document Setup) is drawn with إعداد on the left and المستند on the right, so it reads "المستند إعداد". تحرير لوحات الرسم reads "الرسم لوحات تحرير". Every multi-word menu item, button, panel label and status hint is affected.
  • The trailing … of dialog commands sits at the right end (where the label starts) instead of the left.
  • Mixed strings are out of order too: حفظ كـ PDF… is drawn حفظ, كـ, PDF… from left to right.
  • Nothing is mirrored: menus, panels and dialogs stay where they are in English.

What it needs

Smaller things

  • plural_arabic doesn't follow CLDR above 99: few is n % 100 = 3..10 and many is n % 100 = 11..99, so 103 is few and 111 is many; as written both fall to other (https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar). Please add a test for it.
  • main has moved: the i18n tests now report 30 untranslated tl! strings and 1 menu string ("Load EPS into Clipboard") for ar, and menu_catalogs_translate_every_menu_label fails with "VectorCraft is both kept and translated".
  • The catalog rows themselves check out: three columns, placeholders match the English, six forms in every @plural row, no bidi control or other hidden characters, no links.
  • My build had no craft-fonts, so Arabic came from an installed system font. I couldn't check the Noto Sans Arabic face or the font-coverage test with it.

I'd suggest splitting this: first a small PR with the bidi pass and its test (the owner still has to agree to that approach in #1202), then this catalog on top, which can go in quickly once labels read correctly. A catalog that is registered but unreadable would be worse for Arabic users than none. If you'd like to talk it through, the team is on Discord: https://discord.gg/artcraft

@hasan-sdaif hasan-sdaif changed the title Add Arabic (العربية) interface translation with RTL alignment Add Arabic (العربية) interface translation Oct 13, 2026
@hasan-sdaif

Copy link
Copy Markdown
Author

Thank you for building it — this is exactly the review I hoped for, and you are right on every point. I owe you an apology for the compile error especially: I wrote that call against the egui docs I had in my head instead of the version in the lockfile, and I can't build Rust on my machine, which is a poor excuse for shipping it.

I've pushed ce52cdd with what you listed:

  • The set_layout_direction call is gone, along with its comment. You'll see the diff now touches only the catalog, the registry and the fonts.
  • LANGUAGES is [LangInfo; 17] with 17 rows.
  • plural_arabic now follows CLDR above 99 — n % 100 = 3..10 is few and n % 100 = 11..99 is many — and I added arabic_plurals_follow_cldr_counts with the counts you called out (103 → few, 111 → many) plus a run over every @plural row, modelled on the Polish test.
  • Rebased on the current main: the 3D Extrude & Bevel dialog strings (the bevel shapes, the isometric and off-axis positions), the Symbol Options dialog and the rest of the new tl! literals are translated, and the VectorCraft row is gone from the catalog so it's no longer kept and translated. Pathfinder is now كاشف المسارات, consistent with Pathfinder Options….

On the split you propose: agreed, and I'd go further — the catalog PR should not go in until labels actually read right-to-left, for the reason you give (a registered but unreadable Arabic is worse than none). If the owner signs off on the #1202 approach, I'm happy to take a first shot at the bidi pass with unicode-bidi and the headless-layout test, or to review/help with someone else's; either way this branch stays ready and I'll keep it current with main.

For the font question in #1202: from translating these catalogs, Noto Sans Arabic covers everything the strings use, but I have no experience with the presentation-forms route, so I can't speak to that trade-off.

شكراً لك على وقتك في تجربتها فعلياً.

@hasan-sdaif

Copy link
Copy Markdown
Author

Thank you for this — the build report and the egui reading were exactly what I needed.

The branch now carries the bidi pass you described, on top of a rebase onto current main:

  • i18n/bidi.rs puts a translation into visual order before egui sees it: UAX Raster export: refuse sizes the renderer can't produce instead of crashing #9 with an RTL base (unicode-bidi, the version crates/text already pins), the runs ordered left to right as the reader meets them, the characters inside each run kept in logical order because harfrust reverses the run itself, and brackets mirrored, since egui draws the glyph of the character it is handed. It runs once, when a catalog flagged rtl: true in LANGUAGES is built, so tr/trn/message hand out strings that read right to left while what a user types keeps its logical order.
  • arabic_labels_lay_out_right_to_left lays the pass's output out on a headless egui frame and checks the glyph positions — the first word of ملف جديد must sit right of جديد, and حفظ right of PDF inside حفظ كـ PDF… — and the unit tests in bidi.rs pin the pass itself, with your إعداد المستند and تحرير لوحات الرسم among the cases.
  • main's new strings are translated: Check Script and its whole report, Auto Grouping, the Offset Path message, {path}: not a file, and Load EPS into Clipboard, which had been missing all along.
  • The theme.rs comment and the ar.tsv header no longer claim a mirrored interface — the chrome stays as it is, only the strings read.

One honest limitation: a label long enough to wrap still wraps from its visual end (egui wraps runs as it meets them), so the first line of a wrapped multi-line label holds its end. Menu and button labels don't wrap in practice.

On splitting: the pass is one commit on its own, but its tests and the rtl flag interleave with the catalog's rows, so pulling it into a separate PR is a little git surgery rather than a clean cherry-pick. Happy to do it the moment the owner agrees on the #1202 approach — I'd just rather not fork the discussion into two places before then.

@Messhias Messhias left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR body does not show a completed cargo xtask ci run. Run it end to end (fmt, clippy, tests, assets, brands, layers, wasm), tick the checklist when green, and reply here with the outcome. That is required before any approve from here.

@Messhias Messhias mentioned this pull request Oct 13, 2026
5 tasks done
@hicbest

hicbest commented Oct 14, 2026

Copy link
Copy Markdown

@hasan-sdaif — thanks for the bidi.rs pass and the CLDR plural rule; I've carried both into a branch that has the catalog from my side (hicbest/vectorcraft@arabic-fresh, ~4,300 rows with the status and error messages, complete_menus: true). If you'd like, I can fold it into this PR, or open it as a second PR that builds on yours. Whatever's easier for @bflatastic and the owner. On Discord too.

@hasan-sdaif

Copy link
Copy Markdown
Author

@hicbest — thank you for carrying it forward, and honestly this is the best possible outcome for the PR: your catalog is the deeper one (~4,300 rows with the status and error messages, complete_menus: true), and the bidi pass and the CLDR plurals in this branch already know how to carry it.

A few notes so the fold is mechanical, whichever shape the maintainers pick:

  • My rows live in the catalog in logical order — the pass runs at parse time (Catalog::parse with the RTL flag), so any rows you graft in can land as ordinary logical-order Arabic and they will get reordered automatically. Nothing needs to be hand-reversed.
  • If we fold yours into this PR, I'd like your translation rows credited properly — a Co-authored-by trailer and a line in the PR body — the catalog work is yours and it should read that way in the history.
  • The two catalogs likely disagree on a handful of shared keys (word choices, phrasing of the longer status strings). I'm happy to do that reconciliation pass here row by row, or defer to yours wholesale — whichever you and @bflatastic prefer.

I'll leave the call between folding into this PR vs. a second PR on top of it to you, @bflatastic and the owner — I have no preference as long as Arabic users end up with one complete, correctly-ordered catalog. And I'll catch you on Discord.

hicbest pushed a commit to hicbest/vectorcraft that referenced this pull request Oct 14, 2026
Fonts belong in storytold/craft-fonts (AGENTS.md), never in this repo.

- Delete assets/fonts/NotoSansArabic-Regular.ttf
- Delete assets/fonts/OFL-NotoSansArabic.txt
- Drop their rows from ASSETS.md
- theme.rs adds the Arabic UI faces through CRAFT_FONTS_DIR / craft-fonts
  only, mirroring the Japanese faces in storytold#1532
- Delete the two tests that assumed a bundled Noto Sans Arabic
  (arabic_glyphs_are_available_without_system_fonts,
   bundled_fonts_cover_the_catalogs)
@hasan-sdaif

Copy link
Copy Markdown
Author

Running it now, exactly as asked: the full family gate — cargo xtask ci (fmt, clippy, tests, assets, brands, layers, wasm) — is executing on this PR's tree on my fork's Actions, on a branch that is feat/arabic-i18n plus only the workflow file that runs the gate, nothing else: https://github.com/hasan-sdaif/vectorcraft/actions/runs/38672678228. The tree it verifies is this PR's current head (724cece5). I'll reply with the outcome when it completes and tick the checklist in the description at the same time.

@hasan-sdaif

Copy link
Copy Markdown
Author

The gate ran end to end. The outcome, in full:

  • fmt, clippy, tests, assets, brands, layers — all green, and 1972 of 1973 tests passed.
  • The one failure is effect.expandAppearance: the documents differ in tests_ids_without_selection::ids_do_what_the_selection_does_in_every_command_that_takes_them (crates/engine/src/tests_ids_without_selection.rs:77).

That test and everything it sweeps live in crates/engine, which this PR does not touch — the diff is nine files, all i18n/ui-egui/theme. It looks pre-existing on current main: the sweeping test is new there (dc64aaa). To confirm rather than assert, the identical gate is now running on plain origin/main (same workflow, no i18n commits): https://github.com/hasan-sdaif/vectorcraft/actions/runs/38676566364 — if it fails there too, this branch is clean and the failure is upstream.

The first run (this PR tree plus the workflow file only): https://github.com/hasan-sdaif/vectorcraft/actions/runs/38672678228

I will tick the checklist in the description once the main run reports, with the evidence links. And if the maintainers would like the upstream expandAppearance ids-vs-selection discrepancy fixed, I am happy to take that as a separate small PR on top of the diagnosis.

@Messhias Messhias left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate looks almost there (fmt/clippy/assets/brands/layers green; one engine expandAppearance ids test left). Confirm that failure on plain main (or fix it in a separate PR), tick cargo xtask ci when the catalog branch is fully green, and reply with the evidence links.

hasan-sdaif and others added 6 commits October 15, 2026 12:35
A complete ar catalog (3,783 rows) for the whole interface: every menu
label, the canvas context menu, the macOS menu bar labels, every tl!
literal and the status and error messages, with CLDR six-form plurals
(zero/one/two/few/many/other) and the craft-fonts Arabic faces installed
when present. Registers ar in LANGUAGES (every ar-* locale resolves to
it), resolves like the other menu-complete catalogs, and keeps the
product, workspace and perspective preset names in English
(MENU_KEEP_AS_IS).

The interface text itself still lays out left-to-right: egui 0.36 has
neither bidi reordering nor a global direction switch, so mirroring the
shell is left for the pass discussed in storytold#1202.
egui 0.36 lays a label's runs left to right and does not run the Unicode
bidi algorithm (its text code still carries TODO(emilk): heed bidi
characters), so an Arabic label read with its words in reverse order
(storytold#1202): each word was shaped correctly, but it was drawn at the wrong
end of the line. Languages flagged rtl now run each translation through
a UAX storytold#9 pass when the catalog is built: the runs come out in visual
order (unicode-bidi, already a dependency of crates/text), the letters
inside each run stay in logical order for the shaper, and brackets are
mirrored, since egui draws the glyph of the character it is handed.
What a user types keeps its logical order — only catalog lookups go
through the pass — and a headless layout test pins the order the labels
come out in, so it cannot regress.
Check Script and its report (File › Scripts), Image Trace's Auto
Grouping, the Offset Path message, {path}: not a file, and Load EPS
into Clipboard, which had been missing all along. The Arabic catalog
note now describes the visual-order storage instead of a mirroring
that was never there.
UAX storytold#9 L2 reverses a right-to-left level's runs, and a `{placeholder}`
sitting between Arabic words came out reversed (`}n{`) — the reordering
was treating the label's own syntax as text, so the fill could no
longer find it. Each placeholder now rides through the pass as one
private-use character (restored afterwards, per placeholder, so a
reordered sentence still finds its own), and curly brackets are left
out of the mirror table for the same reason: BidiMirroring.txt pairs
them, but in an interface label they are almost always a placeholder's
own syntax. Found by replaying the whole catalog through a Python
UAX storytold#9 replica of the pass.
The parse ran the bidi visual pass over a whole @plural row before
splitting it on the pipes — but a pipe is a neutral character, so over
the joined row the pass reordered the six forms themselves, and a count
of one could read the many form (عنصراً instead of عنصر). The pass now
runs per form, never across the row; the plural suite pins the order.

Also the missing Symbol Options… row the menu-completeness test found
(the dialog command label, with the trailing ellipsis the convention
keeps).
The rebase onto current main brought the shortcut-set import dialog,
the precise cursors preference, the 3D lighting rows and two save and
export errors — 45 rows, Modern Standard Arabic like the rest of the
catalog, placeholders and the «» quote convention kept, product names
in English as always.
@hasan-sdaif

Copy link
Copy Markdown
Author

@Messhias — the evidence, and two corrections I owe you before it.

Correction 1: my "1972 of 1973" report was wrong, and the run it came from hid why. That verification run stopped at the engine failure — cargo test --workspace aborts at the first failing binary — so the ui-egui suite never ran, and three of my own i18n tests were failing invisibly behind the engine result. Your "tick cargo xtask ci when the catalog branch is fully green" was the right bar, and the branch was not at it. I'm sorry for the misread; the rest of this comment is what it took to get there honestly.

Correction 2: the engine failure is not "pre-existing on main" — it's fixed on main. The plain-main verification run came back green (run 38676566364), and the reason is main's own 664e2ce — "keep command comparison fixtures independent of the clock" — which pins the very helper (doc_with_rect's created stamp) whose clock-straddle made effect.expandAppearance compare two documents built across a second boundary. My branch simply predated that fix: the same tree failed at 02:45 and passed at 11:03 (both on the identical commit), and a loop-probe reproduced it about one run in ten. No separate PR needed from me — the rebase carries your org's own fix.

What the hidden failures were, and the fixes (both in the branch now):

  • The plural-form permutation. Catalog::parse ran the bidi visual pass over a whole @plural row before splitting it on the pipes — but a pipe is a neutral character, so over the joined row the pass reordered the six forms themselves: a count of 1 read the many form (عنصراً instead of عنصر). The pass now runs per form, never across the row, and the plural suite pins the order. This was a real user-visible bug in the catalog's behavior, caught by exactly the test that was failing — the gate you insisted on is what surfaced it.
  • 45 rows the rebase asked for. Current main's new strings — the shortcut-set import dialog, the precise cursors preference, the 3D lighting group, two save/export errors — are translated in the catalog now, so both completeness tests (menu_strings and every_tl_literal) are green.

The branch is rebased onto current main (b12a52f) and the full family gate is green on it: fmt, clippy, tests, assets, brands, layers, wasm — 6,599 tests, 0 failures — on the branch's tree plus only the workflow file that runs the gate: run 38739562408. The PR description is rewritten to match the branch as it now stands (the bidi pass it grew, the verification record, the checklist with cargo xtask ci ticked).

The fold question with @hicbest's #1822 stays open for you and the owner — whichever shape wins, the bidi pass and the plurals here know how to carry that catalog.

@hicbest

hicbest commented Oct 15, 2026

Copy link
Copy Markdown

Thanks @hasan-sdaif — the notes make it easy to fold either way. I'll
follow the owner's call.

If we fold into #1532: I'll open a small PR against your branch with
the extra rows (#1822's status and error messages) and a Co-authored-by
trailer — thanks for offering that.

If we land #1822 first: you rebase #1532 onto it, and we keep the bidi
pass and CLDR rules from yours.

Either way, one complete Arabic catalog with correct word order is the
goal. Standing by.

@Messhias Messhias left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checklist ticked with a green full-gate run on the branch tree; honest correction on the earlier partial report is appreciated. Approving the Arabic catalog/i18n side; fold order with #1822 stays an owner call.

@FredApps FredApps left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReviewerA — source-read COMMENT (not an approval)

I read the head of the diff: an Arabic interface translation. The catalogue header says the translations are original ("Clean-room: written from the plain meaning of the English text using ordinary Arabic vector-drawing vocabulary… no proprietary localisation resource was consulted"), a statement that satisfies the clean-room rule if true; the contributor should confirm it. The lock gains unicode-bidi 0.3.18 for the visual reordering, the same approach as vectorcraft#1963 and deckcraft#256, and vectorcraft#1822 adds another Arabic catalogue, so three Arabic PRs compete for this product and the maintainers must choose one and settle the shaping and bidi stack. A native check is needed. I read the catalogue header. I have not run it, and no exact-head CI result exists for this head. Nothing was run.

I ran nothing and have no exact-head CI result for this PR, so this is not an approval. Where a diff was long I read its code hunks rather than every generated or documentation line.

Status: waiting_author.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants