mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2045 from devius/ka-language
i18n: Georgian (ka) interface language
This commit is contained in:
@@ -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
File diff suppressed because one or more lines are too long
@@ -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