Merge pull request #872 from devius/ka-language

i18n: Georgian (ka) interface language
This commit is contained in:
BFlat
2026-10-20 03:57:09 +07:00
committed by GitHub
11 changed files with 2422 additions and 20 deletions
+1 -1
View File
@@ -32,4 +32,4 @@ jobs:
uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main
with:
run-package-check: true
craft-fonts-ref: 43913056ce82a3fdad0ef69f6b88d1dc922a8104
craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
with:
app-name: figcraft
app-title: FigCraft
craft-fonts-ref: 43913056ce82a3fdad0ef69f6b88d1dc922a8104
craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc
has-server: true
version: ${{ inputs.version }}
secrets:
+1 -1
View File
@@ -227,7 +227,7 @@ The desktop app also offers a loopback-only control channel (`figcraft --control
- **Its own renderer:** tiny-skia on every core for the canvas and exports, with layer blending, effects and masks; booleans from linesweeper, geometry from kurbo, text from skrifa and HarfRust.
- **Runs in the browser:** the whole engine and UI compile to WebAssembly.
- **Accessible:** screen readers get names, roles, values and states for the tools, layer rows, fields, dropdowns, tabs and menus (AccessKit on desktop), F6 moves the keyboard between the toolbar, layers, canvas and properties (Tab within them, Escape back to the canvas), the system's reduce-motion setting is honoured, and panel text meets WCAG AA contrast in both themes.
- **Languages:** English, Russian and Turkish (reviewed by native speakers), and Spanish, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech and Indonesian drafts, picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
- **Languages:** English, Russian and Turkish (reviewed by native speakers), and Spanish, French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian and Georgian drafts, picked in Preferences or taken from the system or browser and switched live; adding a language is one catalog file ([`docs/translating.md`](docs/translating.md)).
- **Plugins:** sandboxed WebAssembly plugins (a metered, pure-Rust interpreter with a memory cap and no file or network access, in the browser too) that act through the same commands, each run one undo step, within the permissions their manifest asks for. A Rust SDK and three example plugins are in [`plugins/`](plugins); see [`docs/plugins.md`](docs/plugins.md).
- **Live sessions (preview):** share the open file from the app (Share ▸ Start sharing: online through a server, or on your local network), give the others the short join code it shows (or the invite link), and edit it together: everyone's changes sync live, with cursors, selections, avatars and follow, and undo takes back only your own changes. Others join from the desktop app (invite links open it) or from the browser build. See [`docs/collab-protocol.md`](docs/collab-protocol.md).
- **A server of your own (preview):** `figcraft-server` keeps a team's files, publishes them from the app (File ▸ Publish to server…, or Share ▸ Online with your server in Preferences) and hands out share links and join codes that let people edit or only view; files survive restarts and apps reconnect where they were. No accounts yet: links are the access. Releases ship it for Linux, and the desktop app joins over `wss://`; [hosting it on a rented Linux server](docs/server.md#deploy-on-a-linux-server) takes a systemd unit and Caddy or nginx (or Docker). See [`docs/server.md`](docs/server.md).
+1 -1
View File
@@ -427,7 +427,7 @@ pub fn specs() -> Vec<CommandSpec> {
spec!(
"pref.language",
"Interface language",
"{language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…; else English}",
"{language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|zh-Hans|…; else English}",
false,
|s, p| {
let lang = req_string("pref.language", p, "language")?;
+4 -2
View File
@@ -65,8 +65,10 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result<String, Stri
if cjk && std::env::var_os("FIGCRAFT_EMBED_CJK").is_none() {
continue;
}
// Web builds have a size budget: UI fonts, only the SemiBold wordmark, and optionally CJK.
if wasm && *family != "Inter" && !(*family == "Archivo" && *style == "SemiBold") && !cjk {
// Web builds have a size budget: Inter, Noto Sans Georgian (small, for the Georgian UI), only the
// SemiBold wordmark, and optionally Noto Sans CJK (every CJK script, for CJK UI text).
let wordmark = *family == "Archivo" && *style == "SemiBold";
if wasm && !matches!(*family, "Inter" | "Noto Sans Georgian") && !wordmark && !cjk {
continue;
}
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
File diff suppressed because it is too large Load Diff
+23 -5
View File
@@ -69,13 +69,22 @@ pub fn language(code: &str) -> u8 {
}
/// Whether the UI can draw language `code`: always on desktop (a system face is found when it's
/// needed; tofu at worst), on the web only with built-in faces for its scripts. The language
/// picker and the system language skip a language it can't.
/// needed; tofu at worst), on the web only with built-in faces for its scripts (Georgian: craft-fonts'
/// Noto Sans Georgian). The language picker and the system language skip a language it can't.
pub fn can_show(code: &str) -> bool {
shows(code, cfg!(target_arch = "wasm32"))
}
/// [`can_show`] in a desktop or (`web`) a web build.
fn shows(code: &str, web: bool) -> bool {
if !web {
return true;
}
if code.split('-').next() == Some("ka") {
return figcraft_text::face_with_char('ა', &["Noto Sans Georgian"]).is_some();
}
let need = language(code);
need == 0
|| !cfg!(target_arch = "wasm32")
|| samples(need, 0).iter().all(|(c, _)| figcraft_text::face_with_char(*c, &["BIZ UDPGothic", "Noto Sans CJK SC"]).is_some())
need == 0 || samples(need, 0).iter().all(|(c, _)| figcraft_text::face_with_char(*c, &["BIZ UDPGothic", "Noto Sans CJK SC"]).is_some())
}
/// A character standing for each script in `need`, with the families to look for it in, the UI
@@ -133,6 +142,15 @@ mod tests {
assert!(["en", "es", "es-419", "fr", "de", "pt-BR", "tr", "id"].iter().all(|c| can_show(c)));
}
#[test]
fn the_web_offers_georgian_only_with_its_face() {
assert!(shows("ka", false), "desktop always lists Georgian");
let face = figcraft_text::face_with_char('ა', &["Noto Sans Georgian"]).is_some();
assert_eq!(shows("ka", true), face);
assert_eq!(shows("ka-GE", true), face);
assert!(shows("en", true) && shows("tr", true));
}
#[test]
fn the_ui_language_script_comes_first() {
let order = |need, ui| samples(need, ui).into_iter().map(|(c, _)| c).collect::<String>();
+35
View File
@@ -421,6 +421,41 @@ fn japanese_korean_and_chinese_have_no_plural_forms() {
}
}
#[test]
fn georgian_is_a_draft_with_count_neutral_plurals() {
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
for tag in ["ka", "ka-GE", "ka_GE.UTF-8"] {
assert_eq!(code("system", Some(tag)), "ka", "{tag}");
}
let ka = LANGS.iter().position(|l| l.code == "ka").unwrap();
assert!(LANGS[ka].draft && LANGS[ka].name == "ქართული");
// A noun stays singular after a numeral: both forms read the same where English counts in both.
for (k, t) in LANGS[ka].ui {
if let (Text::Plural { one: Some(one), other, .. }, Some(Text::Plural { one: Some(en_one), .. })) = (t, find(english().ui, k)) {
assert!(!en_one.contains("{n}") || one == other, "ka: {k:?}");
}
}
}
/// Release builds embed craft-fonts' Noto Sans Georgian (`theme::install_fonts`' built-in faces),
/// since Inter and egui's fonts have no Georgian: it draws every Georgian letter the catalog uses.
#[test]
fn the_built_in_georgian_face_draws_the_georgian_catalog() {
let Some(face) = figcraft_text::find_face("Noto Sans Georgian", "Regular").filter(|f| f.family == "Noto Sans Georgian") else {
eprintln!("built without craft-fonts' Noto Sans Georgian; skipping");
return;
};
let ka = &LANGS[LANGS.iter().position(|l| l.code == "ka").unwrap()];
let georgian = |c: &char| matches!(u32::from(*c), 0x10A0..=0x10FF | 0x1C90..=0x1CBF | 0x2D00..=0x2D2F);
let mut letters: BTreeSet<char> = BTreeSet::new();
for (_, t) in ka.ui.iter().chain(ka.commands).chain(ka.menu) {
letters.extend(forms(t).into_iter().flat_map(str::chars).filter(georgian));
}
assert!(letters.len() > 30, "{letters:?}");
let missing: String = letters.iter().filter(|c| !face.has_char(**c)).collect();
assert!(missing.is_empty(), "no glyph for {missing:?}");
}
#[test]
fn chinese_reads_simplified_and_traditional_has_no_catalog() {
let code = |pref: &str, system: Option<&str>| LANGS[resolve(pref, system)].code;
+1 -1
View File
@@ -296,7 +296,7 @@ pub fn install_fonts(ctx: &Context, system: bool, cjk: u8) {
brand.extend(fonts.families.get(&FontFamily::Name("semibold".into())).cloned().unwrap_or_default());
fonts.families.insert(FontFamily::Name("brand".into()), brand);
// CJK and other scripts from craft-fonts as fallbacks (Noto Sans CJK only with FIGCRAFT_EMBED_CJK).
const BUILT_IN: [&str; 4] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic"];
const BUILT_IN: [&str; 5] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic", "Noto Sans Georgian"];
let lang = crate::i18n::current().map_or("en", |l| l.code);
// A Chinese UI draws Han in a Chinese face: the built-in Japanese one goes after the system faces.
let late = |family: &str| family == "BIZ UDPGothic" && crate::cjk::language(lang) == crate::cjk::HAN;
+1 -2
View File
@@ -436,8 +436,7 @@ on; `key` resolves keys the way the canvas does. Commands behind keys:
- `pref.startOnHome {on?}`: the desktop app starts on the Home screen instead of in a new file
(off by default; files named on the command line still open, and `--control` starts in a new
file anyway, so control callers have a document).
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ko|zh-Hans|…}`: the app's interface language, applied live ("system":
- `pref.language {language: system|en|es|es-419|fr|de|pt-BR|ja|ko|sv|zh-Hans|…}`: the app's interface language, applied live ("system":
- `pref.language {language: system|en|cs|ru|es|es-419|fr|de|pt-BR|ja|ka|ko|sv|zh-Hans|…}`: the app's interface language, applied live ("system":
the OS or browser language; a language without a catalog shows English). Command ids, labels and
every result stay English.
+11 -6
View File
@@ -17,7 +17,7 @@ Simplified.
The catalogs: English (`en`, the source), and Spanish (`es`, with the Latin American overlay
`es-419`), French (`fr`), German (`de`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`), Japanese
(`ja`), Korean (`ko`), Simplified Chinese (`zh-Hans`), Czech (`cs`), Indonesian (`id`), Finnish
(`fi`) and Polish (`pl`), machine-drafted and waiting for native speakers' review. Russian (`ru`, Русский) was
(`fi`), Polish (`pl`) and Georgian (`ka`), machine-drafted and waiting for native speakers' review. Russian (`ru`, Русский) was
reviewed and approved by VanTan; its catalog keeps Frame, Dev Mode and Smart animate as familiar
tool names and uses « » quotes. Turkish (`tr`) was machine-drafted and reviewed by a native speaker,
and Swedish (`sv`) translates every UI string, command label and menu wording.
@@ -57,14 +57,19 @@ komponenta, instance, varianta, automatické rozvržení, omezení, výplň, tah
proměnná, styl). Indonesian uses bare imperatives for buttons and menu items (Simpan, Hapus), the
formal Anda (never kamu), keeps established design terms in English (frame, layer, component,
instance, auto layout, plugin, file) and keeps a noun unchanged after a count, so its plural entries
give only `other`.
give only `other`. Georgian has no letter case (never write Mtavruli for capitals), uses verbal nouns
for commands (გვერდის დამატება) and keeps a noun singular after a numeral, so counts read
"შრე: {n}" and its plural entries give the same text for `one` and `other`.
CJK text needs a CJK font, which egui and Inter lack: the desktop app adds an installed one when
something on screen needs it (see `crates/ui-egui/src/cjk.rs`). The web build has no system fonts,
so Japanese, Korean and Chinese appear there only in a build with craft-fonts and
`FIGCRAFT_EMBED_CJK=1` (Noto Sans CJK SC, +16 MB); otherwise they are left out of the picker. A
Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes. A new CJK or other non-Latin
language also needs its script in `cjk::language`.
Chinese UI puts its Chinese face first so Han takes Simplified Chinese shapes. Georgian is drawn in
craft-fonts' Noto Sans Georgian, one of the built-in faces in `theme::install_fonts`. Web builds
keep it only when built with craft-fonts (`crates/text/build.rs`); the web release isn't, so its
picker leaves Georgian out. A new CJK or other non-Latin language also needs its script in
`cjk::language`, or a built-in craft-fonts face.
## Adding a language
@@ -86,8 +91,8 @@ language also needs its script in `cjk::language`.
4. To require full coverage, add the code to `COMPLETE` in
[`crates/ui-egui/src/i18n_tests.rs`](../crates/ui-egui/src/i18n_tests.rs). Every later PR that
adds a string then has to translate it into each complete language, so only Spanish is there:
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech and Indonesian
are registered as drafts, Russian and Turkish are reviewed but not required, and a string added
French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Czech, Indonesian and
Georgian are registered as drafts, Russian and Turkish are reviewed but not required, and a string added
later shows English in them until it is translated. To list what a draft lacks,
add its code to `COMPLETE` locally and run the check.
5. Run the check and look at the panels in the app (longer words must fit the right panel):