mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-22 19:08:39 +00:00
Merge pull request #2045 from devius/ka-language
i18n: Georgian (ka) interface language
This commit is contained in:
14 files changed
+5251
-31
No files matched your search
@@ -238,7 +238,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, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Polish, Brazilian Portuguese, Russian, Swedish, Ukrainian, Finnish, Bulgarian, Greek and Czech;
|
||||
speaks English, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Polish, Brazilian Portuguese, Russian, Swedish, Ukrainian, Finnish, Bulgarian, Greek, Czech and Georgian;
|
||||
[localization-parity.md](docs/localization-parity.md) has the details.
|
||||
The scores are self-assessed from our code against Illustrator's public documentation;
|
||||
[target-app-parity.md](docs/target-app-parity.md#how-this-was-measured-2026-10-10) explains how far to trust them.
|
||||
|
||||
+3
-2
@@ -112,8 +112,8 @@ Weights, evidence and the missing items per area: [target-app-parity.md](docs/ta
|
||||
| Korean | `ko` | full | 100% |
|
||||
| Vietnamese | `vi` | full | 100% |
|
||||
|
||||
Also shipped: Traditional Chinese (partial, 80%), Bulgarian, Finnish, Greek, Hungarian, Italian, Polish, Russian, Swedish and Ukrainian (full), Czech (catalog complete; render-site gaps reduced; native review pending).
|
||||
No catalog has had a native speaker's review. Details: [localization-parity.md](docs/localization-parity.md).
|
||||
Also shipped: Traditional Chinese (partial, 80%), Bulgarian, Finnish, Greek, Hungarian, Italian, Polish, Russian, Swedish, Ukrainian and Georgian (full), Czech (catalog complete; render-site gaps reduced; native review pending).
|
||||
The Georgian catalog has had a native speaker's review; no other catalog has. Details: [localization-parity.md](docs/localization-parity.md).
|
||||
|
||||
## Upcoming
|
||||
|
||||
@@ -164,6 +164,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
||||
| 2026-10-12 | A `.ai` file with placed (linked) pictures opens from its editing data, with its guides, sublayers and hidden objects: each picture is a linked image in its place, showing the low-resolution preview the file keeps until its file is found or relinked (#1264). Scores unchanged. |
|
||||
| 2026-10-12 | Desktop printing adopts the shared `craft-print` spooler with exact physical sheet sizes. Windows 7 retains its WMI/viewer transport and excludes the shared backend; compatibility CI checks the dependency tree and PE imports. Scores unchanged. |
|
||||
| 2026-10-12 | Graphs: Object › Graph › Design… and Marker… (designs with a preview, New Design, Rename, Delete Design, Paste Design, Select Unused, and markers drawn with a design or None) (#1092); Graph Data opens as a worksheet with Import Data…, column width and decimals (#1138), and takes Transpose, Switch x/y, `|` line breaks in labels and the pie Legend, Position and Sort options (#1115); Graph Type adds Add Drop Shadow and Add Legend Across Top (#1119), widths past 100%, First Row and First Column in Front, and Draw Filled Lines (#1136); graphs keep their label formatting when drawn again (#1137). Align: a Horizontal & Vertical Align Center button (#1073); Align to Key Object makes the topmost selected object the key (#1221), and the palette's key entry reads Cancel Key Object (#1224). Pen: the nearest end wins over snapping, Alt on the last anchor edits it, and Enter, Escape or a tool switch finish a handle preview as one undo step (#821). Direct Selection: a click on an anchor of an already selected path selects that anchor alone (#1237). Make Compound Path keeps the backmost object's paint, and a selected member reads and changes the compound's fill rule (#1225). Shape Builder and Live Paint no longer turn a line's loose end past a crossing into a piece that runs out and back (#1230). Art moved or duplicated with an artboard follows Transform Pattern Tiles (#1219). A click with the Rectangular Grid tool opens its options dialog (#1169). Commands that change nothing (Lock, Hide, Transform Each, Rearrange Artboards and others) add no undo step (#1233). Saving with missing placed files keeps their relink data, and .ai export honours the chosen artboards (#718). SVG export keeps Unicode data-attribute names, and SVG Options refuses an invalid artboard range (#989). CMYK colours given in percent with a component under 1 keep their scale (#1249). Colour Image Trace keeps close dark tones apart (#1247). macOS: the Save sheet has a Format menu (#1235). The Hand and Zoom tools show drawn cursors on every platform (#1238). Czech: the remaining strings are translated, and Pathfinder's Add reads "Sjednotit" (#1226). MCP: a lost reply reports that the edit may have happened instead of replaying it (#1246); `select.key` checks its id and agents can read the key object (#1222). Tests: the command sweeps and import fuzz tests fail on a panic caught by the engine's guard (#1218), and the envelope export test ignores PDF dates and file IDs (#1220). Effect › Artistic › Film Grain (Grain, Highlight Area, Intensity; Photoshop-style filters 29/57) (#1282), and Craquelure, Mosaic Tiles and Stained Glass redraw 25–30% faster with identical pixels (#1265). Direct Selection: clicking a segment selects only that segment, and Delete removes just it (#1300); Shift-click deselects one anchor of a path selected whole (#1263). Pen + Alt converts anchors of paths inside a selected group or compound path (#1306). The Curvature tool shows circular point markers without direction handles (#1284). Snap to Point: pressing near an anchor drags by that anchor, with Smart Guides on or off (#1313). Transform Again repeats the options of the transform it repeats (#1303). Aligning anchors that are already aligned adds no undo step (#1276), and type commands that change nothing record none either (#1316). Simplify keeps the path selected with its new anchors, and its dialog previews and shows the anchor count (#1305). Adding a stroke colour to an unpainted zero-width stroke gives it a visible 1 pt width (#1256). The Eyedropper samples locked objects, template layers and art outside isolation mode (#1260), and shows a pipette cursor (#1288). Fonts named by their full name resolve to the right face (#1267), and Proportional Metrics brings the matching kerning (`apkn`, `vkrn`, `vapk`) (#1291). Artboards: Escape leaves Artboard editing (#1296), Document Setup's Edit Artboards switches to the Artboard tool (#1294), and the Artboard tool starts on the active artboard (#1309). New and imported documents use the Gridline every and Subdivisions preferences (#1314). Send to Current Layer refuses a hidden or locked current layer (#1301), and layer row commands check explicit ids (#1255). Links: Find in Folder… finds missing linked files in a folder and its subfolders and can copy them into the document's folder (#1287). Image Trace traces images over 64 megapixels scaled down instead of refusing them (#1315). DXF export marks closed curves as closed SPLINE entities (#1320). EPS import: closepath respects the path size limit (#1311). The interface is available in Hungarian (#1302), and more Czech labels are translated (#1259). Linux RPMs declare shared-library capabilities (#1290). CI pins every third-party action to a verified commit (#1321). Select menu commands skip hidden and locked art and stay inside the isolated group in isolation mode (#1325). Fill, stroke, gradient and Appearance panel commands that change nothing add no undLine truncated
|
||||
| 2026-10-12 | Georgian interface (`ka`, ქართული): the whole catalog, plurals and messages, tested complete; Georgian letters come from an installed font (SF Georgian, Sylfaen, Noto Sans Georgian, DejaVu Sans) from the first frame; Type › Change Case › Title Case and Sentence case leave Georgian lower case instead of turning it into Mtavruli. |
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). Spot colours defined in Lab open with their Lab values from `.ai` and PDF files, instead of as wrong RGB colours or their CMYK equivalents (#1032). Build scripts leave a generated source they wouldn't change untouched, so the next build doesn't compile most of the workspace again where file times lose their sub-second part (#1051). A fill colour recolours the gradient mesh point last clicked with the Mesh tool or Direct Selection, and a Mesh tool click on a mesh's point selects that mesh (#1052). The All Tools drawer stays two tools wide instead of filling a wide window (#1054). Text Wrap's Make, Release and Options refuse a list of object ids with a malformed or unknown id instead of skipping it (#1055). A freeform gradient line with a malformed point index is refused instead of being shortened to the indices that are valid (#1056). The Corners dialog (`ui.corners`) refuses a malformed object id or a listed anchor that isn't a corner instead of editing the selection or the other corners (#1057). New View refuses a centre that isn't exactly [x, y] and a rotation that isn't a number, instead of trimming the one and saving the other as 0° (#1058). The command sweeps try junk scale, spacing and artboard values too, which they had skipped since before those bugs were fixed (#1059). EPS and PostScript `arc` and `arcn` with huge angles no longer hang the import, and an arc whose angles are whole turns apart is empty, as the PostScript reference defines it (#1062). Align and DisLine truncated
|
||||
| 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) |
|
||||
| 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day |
|
||||
|
||||
@@ -160,6 +160,13 @@ fn edit_runs<S: Default>(s: &mut Session, label: &str, ids: &[NodeId], f: impl F
|
||||
|
||||
// ---------- Change Case ----------
|
||||
|
||||
/// Does `c` take a capital at the start of a word or sentence? Georgian has no word capitals:
|
||||
/// Mtavruli is a separate all-caps style, so title and sentence case leave Mkhedruli (and turn
|
||||
/// Mtavruli back into) lower case. Upper case still gives Mtavruli, as Unicode defines it.
|
||||
fn capitalizes(c: char) -> bool {
|
||||
!matches!(c, '\u{10D0}'..='\u{10FF}' | '\u{1C90}'..='\u{1CBF}')
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CaseState {
|
||||
/// Previous character (across runs).
|
||||
@@ -187,7 +194,7 @@ fn change_case(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"upper" | "uppercase" => out.extend(c.to_uppercase()),
|
||||
"lower" | "lowercase" => out.extend(c.to_lowercase()),
|
||||
"title" => {
|
||||
if st.prev.is_none_or(|p| p.is_whitespace() || matches!(p, '-' | '(' | '"' | '“' | '/')) {
|
||||
if capitalizes(c) && st.prev.is_none_or(|p| p.is_whitespace() || matches!(p, '-' | '(' | '"' | '“' | '/')) {
|
||||
out.extend(c.to_uppercase())
|
||||
} else {
|
||||
out.extend(c.to_lowercase())
|
||||
@@ -195,7 +202,11 @@ fn change_case(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
_ => {
|
||||
if c.is_alphabetic() && st.sentence {
|
||||
out.extend(c.to_uppercase());
|
||||
if capitalizes(c) {
|
||||
out.extend(c.to_uppercase());
|
||||
} else {
|
||||
out.extend(c.to_lowercase());
|
||||
}
|
||||
st.sentence = false;
|
||||
} else {
|
||||
out.extend(c.to_lowercase());
|
||||
|
||||
@@ -1595,6 +1595,23 @@ fn change_case_variants() {
|
||||
assert!(s.execute("type.changeCase", &json!({"case": "weird"})).is_err());
|
||||
}
|
||||
|
||||
/// Georgian has no word capitals: title and sentence case leave Mkhedruli lower case (and bring
|
||||
/// Mtavruli back to it) instead of capitalizing it; upper case still gives Mtavruli.
|
||||
#[test]
|
||||
fn change_case_leaves_georgian_uncapitalized() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "ფენის სახელი. ახალი Layer");
|
||||
sel(&mut s, &[t]);
|
||||
s.execute("type.changeCase", &json!({"case": "title"})).unwrap();
|
||||
assert_eq!(plain(&s, t), "ფენის სახელი. ახალი Layer");
|
||||
s.execute("type.changeCase", &json!({"case": "sentence"})).unwrap();
|
||||
assert_eq!(plain(&s, t), "ფენის სახელი. ახალი layer");
|
||||
s.execute("type.changeCase", &json!({"case": "upper"})).unwrap();
|
||||
assert_eq!(plain(&s, t), "ᲤᲔᲜᲘᲡ ᲡᲐᲮᲔᲚᲘ. ᲐᲮᲐᲚᲘ LAYER");
|
||||
s.execute("type.changeCase", &json!({"case": "title"})).unwrap();
|
||||
assert_eq!(plain(&s, t), "ფენის სახელი. ახალი Layer");
|
||||
}
|
||||
|
||||
/// Change Case is one undo step, and Change Case to the case the text has already records no undo
|
||||
/// step: the step to redo stays and the document stays unmodified. It still replies with the
|
||||
/// objects.
|
||||
|
||||
@@ -43,6 +43,10 @@ impl CraftFont {
|
||||
pub fn is_thai(&self) -> bool {
|
||||
self.scripts.contains(&"Thai")
|
||||
}
|
||||
/// Is the font for Georgian text?
|
||||
pub fn is_georgian(&self) -> bool {
|
||||
self.scripts.contains(&"Geor")
|
||||
}
|
||||
/// A serif (Mincho) family, for document text.
|
||||
pub fn is_mincho(&self) -> bool {
|
||||
self.family.contains("Mincho")
|
||||
@@ -72,8 +76,8 @@ fn japanese_document_fonts_from(fonts: &[CraftFont]) -> Vec<&CraftFont> {
|
||||
/// The craft-fonts faces the document font database loads: the Japanese ones in
|
||||
/// [`japanese_document_fonts`] order, then the Arabic ones in manifest order (Noto Sans Arabic
|
||||
/// first: the fallback for Arabic text; the other families are picked by name), then the Thai ones
|
||||
/// (Noto Sans Thai: the fallback for Thai text where no Thai font is installed, and on the web).
|
||||
/// Remaining faces follow in manifest order, so every explicitly supplied family can be selected.
|
||||
/// (Noto Sans Thai: the fallback for Thai text where no Thai font is installed, and on the web),
|
||||
/// then the Georgian ones (Noto Sans Georgian, the same for Georgian). Remaining faces follow in manifest order, so every explicitly supplied family can be selected.
|
||||
/// Empty when built without craft-fonts.
|
||||
pub fn document_fonts() -> Vec<&'static CraftFont> {
|
||||
document_fonts_from(CRAFT_FONTS)
|
||||
@@ -83,10 +87,18 @@ fn document_fonts_from(fonts: &[CraftFont]) -> Vec<&CraftFont> {
|
||||
let mut v = japanese_document_fonts_from(fonts);
|
||||
v.extend(fonts.iter().filter(|f| f.is_arabic() && !f.is_japanese()));
|
||||
v.extend(fonts.iter().filter(|f| f.is_thai() && !f.is_japanese() && !f.is_arabic()));
|
||||
v.extend(fonts.iter().filter(|f| !f.is_japanese() && !f.is_arabic() && !f.is_thai()));
|
||||
v.extend(fonts.iter().filter(|f| f.is_georgian() && !f.is_japanese() && !f.is_arabic() && !f.is_thai()));
|
||||
v.extend(fonts.iter().filter(|f| !f.is_japanese() && !f.is_arabic() && !f.is_thai() && !f.is_georgian()));
|
||||
v
|
||||
}
|
||||
|
||||
/// The craft-fonts faces for Georgian UI text (Noto Sans Georgian), in manifest order. The UI fonts
|
||||
/// have no Georgian letters. Empty when built without craft-fonts, and on the web, which fetches
|
||||
/// the face at startup (`web-fonts.txt`) instead of embedding it.
|
||||
pub fn georgian_ui_fonts() -> Vec<&'static CraftFont> {
|
||||
CRAFT_FONTS.iter().filter(|f| f.is_georgian() && !f.is_japanese()).collect()
|
||||
}
|
||||
|
||||
/// The craft-fonts faces for Japanese text, best for UI text first: Gothic (sans) families, then
|
||||
/// the others; `bold` puts bold faces before regular ones. Empty when built without craft-fonts.
|
||||
pub fn japanese_ui_fonts(bold: bool) -> Vec<&'static CraftFont> {
|
||||
@@ -110,6 +122,7 @@ mod tests {
|
||||
font("Arabic", "Regular", &["Arab"]),
|
||||
font("Mincho", "Regular", &["Jpan", "Hans"]),
|
||||
font("Thai only", "Regular", &["Thai"]),
|
||||
font("Georgian", "Regular", &["Geor"]),
|
||||
font("Latin", "Regular", &["Latn"]),
|
||||
];
|
||||
let ordered: Vec<_> = document_fonts_from(&fonts).into_iter().map(|f| (f.family, f.style)).collect();
|
||||
@@ -122,6 +135,7 @@ mod tests {
|
||||
("Thai", "Regular"),
|
||||
("Arabic", "Regular"),
|
||||
("Thai only", "Regular"),
|
||||
("Georgian", "Regular"),
|
||||
("Chinese", "Regular"),
|
||||
("Latin", "Regular"),
|
||||
]
|
||||
@@ -150,10 +164,11 @@ mod tests {
|
||||
assert!(name.ends_with(".ttf") || name.ends_with(".otf"), "{}", w.url);
|
||||
assert!(w.integrity.starts_with("sha256-") && w.integrity.len() == "sha256-".len() + 44, "{}", w.integrity);
|
||||
}
|
||||
// The fallbacks the first frame may need: Noto Sans Arabic and Noto Sans Thai, where the
|
||||
// checkout has them.
|
||||
// The fallbacks the first frame may need: Noto Sans Arabic, Noto Sans Thai and Noto Sans
|
||||
// Georgian, where the checkout has them.
|
||||
let startup: Vec<_> = WEB_FONTS.iter().filter(|w| w.startup).map(|w| w.family).collect();
|
||||
let expected: Vec<_> = ["Noto Sans Arabic", "Noto Sans Thai"].into_iter().filter(|n| CRAFT_FONTS.iter().any(|f| f.family == *n)).collect();
|
||||
let expected: Vec<_> =
|
||||
["Noto Sans Arabic", "Noto Sans Thai", "Noto Sans Georgian"].into_iter().filter(|n| CRAFT_FONTS.iter().any(|f| f.family == *n)).collect();
|
||||
assert_eq!(startup, expected);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
# Read by build.rs (the WEB_FONTS table) and `cargo xtask web-fonts` (copies the files, a Trunk hook).
|
||||
startup | Noto Sans Arabic | Regular
|
||||
startup | Noto Sans Thai | Regular
|
||||
startup | Noto Sans Georgian | Regular
|
||||
background | Cairo | Regular
|
||||
background | Tajawal | ExtraLight
|
||||
background | Tajawal | Light
|
||||
|
||||
@@ -743,7 +743,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 ["de", "es", "fr", "hu", "it", "pl", "ru", "sv"] {
|
||||
for code in ["de", "es", "fr", "hu", "it", "ka", "pl", "ru", "sv"] {
|
||||
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
@@ -94,7 +94,7 @@ fn plural_polish(n: u64) -> usize {
|
||||
}
|
||||
|
||||
/// The registry. English first: it is the fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 23] = [
|
||||
pub static LANGUAGES: [LangInfo; 24] = [
|
||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() },
|
||||
// Greek: the whole interface and the status and error messages, keeping the same names in
|
||||
// English as Spanish; every `el-*` locale (including `el-GR` and `el-CY`) resolves here.
|
||||
@@ -154,6 +154,12 @@ pub static LANGUAGES: [LangInfo; 23] = [
|
||||
complete_menus: true,
|
||||
catalog: OnceLock::new(),
|
||||
},
|
||||
// Georgian: the whole interface and the status and error messages, keeping the same names in
|
||||
// English as Spanish. Georgian nouns stay singular after a number, so one plural form;
|
||||
// `ka-GE` and `ka_GE.UTF-8` resolve here.
|
||||
LangInfo {
|
||||
code: "ka", name: "ქართული", source: include_str!("ka.tsv"), plural: plural_none, complete_menus: true, catalog: OnceLock::new()
|
||||
},
|
||||
// Russian: the whole interface and the status and error messages, keeping the same names in
|
||||
// English as Spanish; every `ru-*` locale (`ru-RU`, `ru-BY`, `ru-KZ` …) resolves here.
|
||||
LangInfo {
|
||||
|
||||
@@ -23,6 +23,9 @@ fn tags_map_to_languages() {
|
||||
for tag in ["fr", "fr_FR.UTF-8", "fr-BE", "fr_CA", "fr-CH", "fr-LU"] {
|
||||
assert_eq!(lang_from_tag(tag), Lang::from_code("fr"), "{tag}");
|
||||
}
|
||||
for tag in ["ka", "ka-GE", "ka_GE.UTF-8", "KA-ge"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(ka()), "{tag}");
|
||||
}
|
||||
for tag in ["uk", "uk-UA", "uk_UA.UTF-8", "UK-ua", "uk_UA.UTF-8@euro"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(uk()), "{tag}");
|
||||
}
|
||||
@@ -271,14 +274,21 @@ fn is_cjk(c: char) -> bool {
|
||||
matches!(c as u32, 0x1100..=0x11FF | 0x3000..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF | 0x20000..=0x2FFFF)
|
||||
}
|
||||
|
||||
/// Georgian letters (Mkhedruli, Mtavruli, Asomtavruli): only craft-fonts' Noto Sans Georgian has them.
|
||||
fn is_georgian(c: char) -> bool {
|
||||
matches!(c as u32, 0x10A0..=0x10FF | 0x1C90..=0x1CBF | 0x2D00..=0x2D2F)
|
||||
}
|
||||
|
||||
/// The UI fonts draw every character the catalogs use, in every family the UI draws text with
|
||||
/// (the web build has no system fonts to fall back to). Chinese and Japanese characters come from
|
||||
/// the craft-fonts build input, which is optional: without it they are left to the installed
|
||||
/// fonts, and with it the Japanese catalog is checked against BIZ UDPGothic (the Traditional
|
||||
/// Chinese one waits for a Traditional Chinese UI font in craft-fonts).
|
||||
/// Chinese one waits for a Traditional Chinese UI font in craft-fonts). Georgian letters are the
|
||||
/// same: checked against Noto Sans Georgian when craft-fonts has it.
|
||||
#[test]
|
||||
fn bundled_fonts_cover_the_catalogs() {
|
||||
let japanese_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_japanese());
|
||||
let georgian_ui_font = vectorcraft_text::CRAFT_FONTS.iter().any(|f| f.is_georgian());
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |_| {});
|
||||
@@ -294,8 +304,11 @@ fn bundled_fonts_cover_the_catalogs() {
|
||||
for l in LANGUAGES.iter().filter(|l| !l.source.is_empty()) {
|
||||
let (entries, _) = parse_entries(l.source);
|
||||
let cjk_checked = japanese_ui_font && l.code == "ja";
|
||||
let chars: std::collections::BTreeSet<char> =
|
||||
entries.iter().flat_map(|(_, _, tr)| tr.chars()).filter(|c| !c.is_whitespace() && (cjk_checked || !is_cjk(*c))).collect();
|
||||
let chars: std::collections::BTreeSet<char> = entries
|
||||
.iter()
|
||||
.flat_map(|(_, _, tr)| tr.chars())
|
||||
.filter(|c| !c.is_whitespace() && (cjk_checked || !is_cjk(*c)) && (georgian_ui_font || !is_georgian(*c)))
|
||||
.collect();
|
||||
for family in &families {
|
||||
let font = egui::FontId::new(13.0, family.clone());
|
||||
let gone: String = chars.iter().filter(|c| !fonts.has_glyph(&font, **c)).collect();
|
||||
@@ -851,7 +864,7 @@ fn cs() -> Lang {
|
||||
}
|
||||
|
||||
/// Languages whose catalogs leave [`MENU_KEEP_AS_IS`] in English.
|
||||
const KEEPS_MENU_NAMES: [&str; 16] = ["cs", "de", "el", "es", "fr", "hu", "it", "ja", "ko", "pl", "pt-br", "ru", "sv", "uk", "vi", "fi"];
|
||||
const KEEPS_MENU_NAMES: [&str; 17] = ["cs", "de", "el", "es", "fr", "hu", "it", "ja", "ka", "ko", "pl", "pt-br", "ru", "sv", "uk", "vi", "fi"];
|
||||
|
||||
/// 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).
|
||||
@@ -1009,6 +1022,40 @@ fn ru() -> Lang {
|
||||
Lang::from_code("ru").expect("ru registered")
|
||||
}
|
||||
|
||||
fn ka() -> Lang {
|
||||
Lang::from_code("ka").expect("ka registered")
|
||||
}
|
||||
|
||||
/// Georgian uses verbal nouns for commands, genitive + "ხელსაწყო" for tools, one count-neutral
|
||||
/// plural form (nouns stay singular after a number), and translates the reason inside a message.
|
||||
#[test]
|
||||
fn georgian_reads_as_georgian() {
|
||||
for (en, want) in [
|
||||
("Artboard Tool", "სამუშაო დაფის ხელსაწყო"),
|
||||
("Swatches", "ფერის ნიმუშები"),
|
||||
("Pathfinder", "ტრაექტორიების ოპერაციები"),
|
||||
("Stroke", "კონტური"),
|
||||
("Fill", "შევსება"),
|
||||
("Direct Selection Tool", "პირდაპირი მონიშვნის ხელსაწყო"),
|
||||
("Save As…", "შენახვა როგორც…"),
|
||||
("Undo", "დაბრუნება"),
|
||||
("File", "ფაილი"),
|
||||
] {
|
||||
assert_eq!(tr(ka(), en), want);
|
||||
}
|
||||
for n in [0, 1, 2, 5, 21] {
|
||||
assert_eq!(trn(ka(), n, "{n} Layer", "{n} Layers"), format!("შრე: {n}"));
|
||||
}
|
||||
assert_eq!(tr_ctx(ka(), "axis", "Both"), "ორივე");
|
||||
assert_eq!(tr_id(ka(), "pathfinder.add", "Add"), "გაერთიანება");
|
||||
let nested = "command `object.group` is not available right now: nothing selected";
|
||||
assert_eq!(message(ka(), nested), "ბრძანება `object.group` ახლა ხელმისაწვდომი არ არის: არაფერია მონიშნული");
|
||||
// No Mtavruli: Georgian UI text has no capitals.
|
||||
let (entries, _) = parse_entries(ka().0.source);
|
||||
let caps: Vec<_> = entries.iter().filter(|(_, _, tr)| tr.chars().any(|c| matches!(c, '\u{1C90}'..='\u{1CBF}'))).collect();
|
||||
assert!(caps.is_empty(), "Mtavruli in ka.tsv: {caps:?}");
|
||||
}
|
||||
|
||||
fn hu() -> Lang {
|
||||
Lang::from_code("hu").expect("hu registered")
|
||||
}
|
||||
@@ -1777,7 +1824,8 @@ fn select_key_param_error_is_translated() {
|
||||
}
|
||||
|
||||
/// Languages whose catalogs cover every status and error message.
|
||||
const COMPLETE_MESSAGES: &[&str] = &["cs", "de", "el", "es", "fr", "hu", "it", "ja", "ko", "pl", "pt-br", "ru", "sv", "uk", "vi", "zh-hans", "fi"];
|
||||
const COMPLETE_MESSAGES: &[&str] =
|
||||
&["cs", "de", "el", "es", "fr", "hu", "it", "ja", "ka", "ko", "pl", "pt-br", "ru", "sv", "uk", "vi", "zh-hans", "fi"];
|
||||
|
||||
/// 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"];
|
||||
|
||||
@@ -531,16 +531,18 @@ pub(crate) fn craft_font_name(f: &vectorcraft_text::CraftFont) -> String {
|
||||
format!("craft-fonts {} {}", f.family, f.style)
|
||||
}
|
||||
|
||||
/// The Japanese craft-fonts faces (when built with `CRAFT_FONTS_DIR`) as fallbacks at the end of
|
||||
/// every family, after the app's own fonts and before the installed fonts `ui_fonts` adds: BIZ
|
||||
/// UDPGothic first (bold first in the semibold family), then the Mincho faces.
|
||||
/// The Japanese and Georgian craft-fonts faces (when built with `CRAFT_FONTS_DIR`) as fallbacks at
|
||||
/// the end of every family, after the app's own fonts and before the installed fonts `ui_fonts`
|
||||
/// adds: BIZ UDPGothic first (bold first in the semibold family), then the Mincho faces, then Noto
|
||||
/// Sans Georgian.
|
||||
fn add_craft_fonts(fonts: &mut FontDefinitions) {
|
||||
for f in vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.is_japanese()) {
|
||||
for f in vectorcraft_text::CRAFT_FONTS.iter().filter(|f| f.is_japanese() || f.is_georgian()) {
|
||||
fonts.font_data.insert(craft_font_name(f), Arc::new(FontData::from_static(f.bytes)));
|
||||
}
|
||||
for (family, stack) in fonts.families.iter_mut() {
|
||||
let bold = *family == FontFamily::Name(FONT_UI_SEMIBOLD.into());
|
||||
stack.extend(vectorcraft_text::craft_fonts::japanese_ui_fonts(bold).into_iter().map(craft_font_name));
|
||||
stack.extend(vectorcraft_text::craft_fonts::georgian_ui_fonts().into_iter().map(craft_font_name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-5
@@ -142,12 +142,15 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese-
|
||||
so they are the fallback for Japanese after the requested font; installed system fonts come after.
|
||||
Noto Sans Thai (when the checkout has it) is the fallback for Thai where no Thai font is
|
||||
installed, and the web build fetches it before the app starts (`crates/text/web-fonts.txt`).
|
||||
Noto Sans Georgian (when the checkout has it) is the same for Georgian.
|
||||
All other supplied faces follow those existing document fallbacks in manifest order, so they
|
||||
can also be selected by family name. This does not change which faces the web build embeds
|
||||
or fetches, and no craft-fonts faces are registered when the optional input is absent.
|
||||
- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first), after
|
||||
the UI fonts and before the installed fonts `ui_fonts.rs` discovers for other scripts.
|
||||
- Tests that need Japanese glyphs skip with a message when built without craft-fonts. The FreeBSD
|
||||
- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first, then
|
||||
Noto Sans Georgian for the Georgian interface), after the UI fonts and before the installed fonts
|
||||
`ui_fonts.rs` discovers for other scripts. On the web, Noto Sans Georgian is fetched at startup
|
||||
and reaches the UI through `ui_fonts.rs`.
|
||||
- Tests that need Japanese or Georgian glyphs skip with a message when built without craft-fonts. The FreeBSD
|
||||
CI job and every release job (`release.yml`) check craft-fonts out at a pinned commit; release
|
||||
packages carry each embedded font's `OFL-<family>.txt`.
|
||||
|
||||
@@ -205,7 +208,7 @@ locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, com
|
||||
`uk_UA.UTF-8` and every other `uk-*` locale resolve to it; integer plurals use one/few/many), Hungarian (`hu`, complete;
|
||||
`hu-HU` and every other `hu-*` locale resolve to it; a count takes the singular, so plurals have one form), Polish (`pl`, complete; `pl-PL` and every other `pl-*`
|
||||
locale resolve to it; plurals use one/few/many, with 21, 31 … taking many), Swedish (`sv`, complete; `sv-SE`, `sv-FI` and every
|
||||
other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian Portuguese
|
||||
other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE.UTF-8` resolve to it; one count-neutral plural form), Czech (`cs`, every menu label) and Brazilian Portuguese
|
||||
(`pt-br`, complete; `pt-BR`, `pt_BR.UTF-8` and other tags that resolve to `pt-br` through `candidates`). Untranslated text falls back to English until its rows are added.
|
||||
|
||||
- `tl!("…")` translates a literal into the language the UI is drawn in; `i18n::t(s)` is the same for a
|
||||
@@ -233,7 +236,7 @@ other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian
|
||||
control channel, MCP and tests read them) and are translated only where the status bar draws them
|
||||
(`i18n::msg`). `@msg` catalog rows hold a whole message or a template such as
|
||||
`Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the
|
||||
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Swedish, Ukrainian, Vietnamese and Finnish cover every
|
||||
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Georgian, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Swedish, Ukrainian, Vietnamese and Finnish cover every
|
||||
message literal the test scan finds (`complete_languages_translate_every_message`, languages listed in
|
||||
`COMPLETE_MESSAGES`): a new `Err("…")`, `Other(…)`, `#[error(…)]` or `status(…)` message needs a row in each of those catalogs (`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui
|
||||
complete_languages_translate_every_message` lists them all). Other languages show messages in English
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Bulgarian (`bg`) and Greek (`el`) catalogs complete under the tests, #1909, #2076; Finnish (`fi`) catalog added, complete under the tests; Swedish (`sv`) catalog added, complete under the tests; scores and estimates unchanged; Brazilian Portuguese (`pt-br`) catalog completed under the tests: menus, `tl!` literals and messages; 12-language score 71% → 74%; #1914) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Georgian (`ka`) interface catalog; Bulgarian (`bg`) and Greek (`el`) catalogs complete under the tests, #1909, #2076; Finnish (`fi`) catalog added, complete under the tests; Swedish (`sv`) catalog added, complete under the tests; scores and estimates unchanged; Brazilian Portuguese (`pt-br`) catalog completed under the tests: menus, `tl!` literals and messages; 12-language score 71% → 74%; #1914) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Per-language status of VectorCraft's interface. How the catalogs work and how to add a language:
|
||||
[`development.md` › Localisation](development.md#localisation). Part of
|
||||
@@ -17,7 +17,7 @@ in-app help, and the docs are English.
|
||||
|
||||
**Native review:** every catalog was written clean-room from the English meaning, most of it by agents
|
||||
(`bflatastic`), with community contributors adding or correcting (German by `dwetscher`, Japanese and Traditional
|
||||
Chinese by `imachus`). None is recorded as reviewed by a native speaker; the catalog headers ask for one.
|
||||
Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker; the other catalog headers ask for one.
|
||||
|
||||
## The twelve key languages
|
||||
|
||||
@@ -56,8 +56,9 @@ plus review. Native-speaker review is human work and isn't in the hours.
|
||||
| Swedish | `sv` | 4,770 rows, 668 of them `@msg` (counted 2026-10-15): menus, `tl!` literals and messages complete (tests); one/other plurals | full; native review welcome |
|
||||
| Bulgarian | `bg` | 4,910 rows, 703 of them `@msg` (counted 2026-10-16): menus, `tl!` literals and messages complete (tests) | full; AI-drafted, native review pending |
|
||||
| Greek | `el` | 4,880 rows, 709 of them `@msg` (counted 2026-10-16): menus, `tl!` literals and messages complete (tests) | full; native review welcome |
|
||||
| Georgian | `ka` | 5,066 rows (counted 2026-10-18): menus, panels, dialogs, plurals (one count-neutral form) and messages, tested complete; reviewed by a native speaker | full |
|
||||
|
||||
Eighteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
|
||||
Nineteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
|
||||
interface languages (including Polish, Dutch, Turkish, the Nordic languages, Hungarian, and Middle East
|
||||
editions with Arabic and Hebrew); it doesn't ship Hindi, Indonesian or Vietnamese.
|
||||
|
||||
@@ -99,5 +100,6 @@ font for the web build.
|
||||
| 2026-10-12 | minor | Korean (`ko`) catalog added, complete under the tests (menus, literals, messages); 12-language score 54% → 63%, hours 70–110 → 60–100 |
|
||||
| 2026-10-12 | minor | Polish (`pl`) catalog added, complete under the tests (menus, literals, messages; one/few/many plurals) |
|
||||
| 2026-10-12 | minor | Hungarian (`hu`) catalog added, complete under the tests |
|
||||
| 2026-10-12 | minor | Georgian (`ka`) catalog, complete |
|
||||
| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged |
|
||||
| 2026-10-10 | major | First per-language table, measured from the catalog row counts against the complete French catalog |
|
||||
@@ -463,7 +463,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done.
|
||||
- Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).
|
||||
- The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`, `every_undo_label_is_translated`).
|
||||
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian and Ukrainian ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||
- Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian and Georgian ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins).
|
||||
- Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`).
|
||||
- The History panel translates the undo step names the engine, the tools and the shell write as literals: each has a row in every complete catalog (`every_undo_label_is_translated`), and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis.
|
||||
- The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. Not done: a new document from the selected state.
|
||||
|
||||
Reference in new issue
Block a user