Merge pull request #1018 from kler/swedish-ui

i18n: Swedish (sv) interface translation
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-11-01 14:54:59 -05:00
commit a288a87bf6
8 files changed
+3069 -5

No files matched your search

+5 -1
View File
@@ -354,7 +354,7 @@ A string without a translation shows in English and fails no check; `cargo xtask
| Vietnamese | vi | none | 0% |
Also shipped: Czech (99%), Italian (85%), Ukrainian (83%), Polish (99%).
Also shipped: Italian (85%), Ukrainian (83%), Georgian (100%).
Also shipped: Italian (85%), Ukrainian (83%), Georgian (100%), Swedish (99%).
## Upcoming
@@ -540,6 +540,10 @@ Newest first.
- **2026-10-16** · Layout: autoflow, inserted pages and new parents follow dragged column guides (#1096). Autoflow sets one frame per dragged column and counts the pages it adds from them; Insert Pages and New Parent keep the custom positions with the column count and gutter; `document.inspect` reports each page's gutter, column rects and whether its columns are custom. No readiness percentage change.
- **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
- **2026-10-15** · Swedish interface (`sv`, Preferences ▸ General ▸ Interface Language ▸ Svenska): menus, panels,
dialogs and messages, 2,857 of the 2,891 interface strings (99%) in the missing-translations report; the other 34
show in English. AI-drafted with Claude Code from the English source; spot-checked by a native speaker, not every
string reviewed. No readiness percentage change.
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
- **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change.
- **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `<Image FillColor>` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change.
+65 -1
View File
@@ -19,6 +19,7 @@ mod ka;
mod pl;
mod pt_br;
mod ru;
mod sv;
mod uk;
/// Supported interface languages: (code, name in that language).
@@ -38,6 +39,7 @@ pub const LANGUAGES: &[(&str, &str)] = &[
("pl", "Polski"),
("ka", "ქართული"),
("hu", "Magyar"),
("sv", "Svenska"),
];
/// The interface languages in the order the Preferences picker lists them: English first (the
@@ -98,6 +100,7 @@ fn lookup<'a>(lang: &str, s: &'a str) -> &'a str {
if lang == "hu" {
return HUNGARIAN.get_or_init(|| hu::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
static SWEDISH: OnceLock<HashMap<&str, &str>> = OnceLock::new();
if lang == "ar" {
return ARABIC.get_or_init(|| ar::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
@@ -131,6 +134,9 @@ fn lookup<'a>(lang: &str, s: &'a str) -> &'a str {
if lang == "ka" {
return GEORGIAN.get_or_init(|| ka::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
if lang == "sv" {
return SWEDISH.get_or_init(|| sv::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
if lang == "uk" {
return UKRAINIAN.get_or_init(|| uk::TABLE.iter().copied().collect()).get(s).copied().unwrap_or(s);
}
@@ -160,6 +166,7 @@ pub fn tr_context<'a>(lang: &str, s: &'a str, context: &str) -> &'a str {
"pl" => pl::CONTEXT,
"ja" => ja::CONTEXT,
"ar" => ar::CONTEXT,
"sv" => sv::CONTEXT,
_ => &[],
};
let written = |text: &&str| !text.trim().is_empty();
@@ -185,6 +192,12 @@ pub(crate) fn russian_catalog() -> Vec<&'static str> {
ru::TABLE.iter().map(|(_, text)| *text).chain(ru::CONTEXT.iter().map(|(_, _, text)| *text)).collect()
}
/// Whether the Swedish catalog has an entry for `s` (many Swedish labels read like the English ones).
#[cfg(test)]
pub(crate) fn swedish_has(s: &str) -> bool {
sv::TABLE.iter().any(|(key, _)| *key == s)
}
/// Ukrainian, Russian, Polish and Georgian put the category before the value in count captions
/// ("сторінок: 5", "страниц: 5", "Strony: 5", "გვერდი: 5"): every number stays grammatical in the
/// languages with several plural forms, and Georgian nouns stay singular after numerals. Other
@@ -193,12 +206,29 @@ pub fn category_first(lang: &str) -> bool {
matches!(lang, "uk" | "ru" | "pl" | "ka")
}
/// Swedish singular for a count of one, where it differs from the catalog's plural ("1 sida",
/// "5 sidor"); "fel", "objekt" and "lager" read the same either way.
fn swedish_singular(noun: &str) -> Option<&'static str> {
match noun {
"pages" => Some("sida"),
"stories" => Some("artikel"),
"warnings" => Some("varning"),
_ => None,
}
}
/// Caption for a counted category, e.g. `count_label("ru", "pages", 5)` is "страниц: 5". Czech
/// picks the plural form for the count (1, 2–4, other).
/// picks the plural form for the count (1, 2–4, other); Swedish the singular for one.
pub fn count_label(lang: &str, noun: &str, count: usize) -> String {
if category_first(lang) {
return format!("{}: {count}", tr(lang, noun));
}
if lang == "sv"
&& count == 1
&& let Some(one) = swedish_singular(noun)
{
return format!("{count} {one}");
}
if lang == "cs"
&& let Some((_, [one, few, other])) = cs::PLURALS.iter().find(|(key, _)| *key == noun)
{
@@ -240,6 +270,40 @@ pub fn workspace_name<'a>(lang: &str, name: &'a str) -> &'a str {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod swedish_tests {
use super::*;
#[test]
fn swedish_catalog_preserves_placeholders_and_ellipses() {
let catalog: HashMap<_, _> = sv::TABLE.iter().copied().collect();
assert_eq!(catalog.len(), sv::TABLE.len(), "duplicate Swedish keys");
let placeholders = |s: &str| {
s.split('{').skip(1).filter_map(|part| part.split_once('}').map(|(name, _)| name.to_owned())).collect::<std::collections::BTreeSet<_>>()
};
for (key, translation) in sv::TABLE {
assert!(!translation.trim().is_empty(), "empty {key}");
assert_eq!(placeholders(key), placeholders(translation), "placeholders: {key}");
assert_eq!(key.ends_with('…'), translation.ends_with('…'), "ellipsis: {key}");
assert_eq!(tr("sv", key), *translation, "sv: {key}");
}
let contexts: std::collections::HashSet<_> = sv::CONTEXT.iter().map(|(key, scope, _)| (*key, *scope)).collect();
assert_eq!(contexts.len(), sv::CONTEXT.len(), "duplicate Swedish context keys");
for (key, context, expected) in sv::CONTEXT {
assert_eq!(placeholders(key), placeholders(expected), "placeholders: {key} ({context})");
assert_eq!(tr_context("sv", key, context), *expected, "sv {context}: {key}");
}
assert_eq!(tr("sv", "File"), "Arkiv");
assert_eq!(tr("sv", "Save As…"), "Spara som…");
assert_eq!(tr("sv", "Unknown label"), "Unknown label");
assert!(!is_rtl("sv"));
assert_eq!(count_label("sv", "pages", 5), "5 sidor");
assert_eq!(count_label("sv", "pages", 1), "1 sida");
assert_eq!(count_label("sv", "warnings", 1), "1 varning");
assert_eq!(count_label("sv", "errors", 1), "1 fel");
}
}
/// `name` lowercased with the Latin accents that occur in language names folded away, so a
/// byte-wise sort reads alphabetically. Other scripts are left as they are.
fn name_sort_key(name: &str) -> String {
File diff suppressed because it is too large. Load diff
+2
View File
@@ -14,6 +14,7 @@ const LANGUAGE_TABLES: &[(&str, &[(&str, &str)])] = &[
("pl", pl::TABLE),
("pt-br", pt_br::TABLE),
("ru", ru::TABLE),
("sv", sv::TABLE),
("uk", uk::TABLE),
];
@@ -25,6 +26,7 @@ const CONTEXT_TABLES: &[(&str, &[(&str, &str, &str)])] = &[
("ja", ja::CONTEXT),
("pl", pl::CONTEXT),
("ru", ru::CONTEXT),
("sv", sv::CONTEXT),
("uk", uk::CONTEXT),
];
+34
View File
@@ -65,3 +65,37 @@ fn cmd_item(id: &str, params: Value) -> Item {
#[cfg(test)]
mod tests;
#[cfg(test)]
mod swedish_tests {
use super::*;
#[test]
fn swedish_interface_is_selectable_and_translates_every_menu_label() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
run_ui(&mut app, "app.language", &json!({"lang": "sv"})).unwrap().unwrap();
assert_eq!(app.ui.language, "sv");
assert_eq!(checked(&app, "app.language", &json!({"lang": "sv"})), Some(true));
assert_eq!(crate::i18n::tr("sv", "File"), "Arkiv");
// Every menu title, item and submenu has a Swedish entry.
let mut untranslated = Vec::new();
for (title, entries) in menu_tree() {
let mut labels = vec![title.to_string()];
let mut stack: Vec<&Item> = entries.iter().collect();
while let Some(entry) = stack.pop() {
match entry {
Item::Sub(label, children) => {
labels.push(label.clone());
stack.extend(children.iter());
}
Item::Cmd { label, id, .. } if id != "app.language" => labels.push(label.clone()),
_ => {}
}
}
// Swedish shares many words with English ("Layout", "Cyan"), so look for a catalog entry.
let size = |l: &str| l.strip_suffix(" pt").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()));
untranslated.extend(labels.into_iter().filter(|l| l.chars().any(char::is_alphabetic) && !size(l) && !crate::i18n::swedish_has(l)));
}
assert!(untranslated.is_empty(), "untranslated Swedish menu labels: {untranslated:?}");
}
}
+1 -1
View File
@@ -71,7 +71,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
"app.language",
"Interface Language",
None,
"{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka|hu} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting",
"{lang?: \"\"|de|fr|es|ja|zh|ar|pt-br|it|uk|ru|cs|pl|ka|hu|sv} — menus and panel names (the macOS menu bar follows at once); without lang, opens Preferences, where it is the first setting",
),
(
"app.flattener",
+4 -2
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** minor (Hungarian interface catalog, bundled-font coverage and flyout regression checks) · **Target:** Adobe InDesign 2026 (21.6)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** minor (Hungarian interface catalog, bundled-font coverage and flyout regression checks; Swedish interface) · **Target:** Adobe InDesign 2026 (21.6)
Per-language status of the DesignCraft interface and of document typesetting in each script.
Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-typography.md),
@@ -11,7 +11,7 @@ Script-specific typesetting detail: [cjk.md](cjk.md), [cjk-typography.md](cjk-ty
## How it was measured
Interface strings live in `crates/ui-egui/src/i18n/table.rs` (a shared table with German, French,
Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,de,fr,hu,it,ja,ka,pl,pt_br,ru,uk}.rs`.
Spanish, Japanese and Simplified Chinese columns) and `crates/ui-egui/src/i18n/{ar,cs,de,fr,hu,it,ja,ka,pl,pt_br,ru,sv,uk}.rs`.
Untranslated strings fall back to English. A script (2026-10-10) built the set of English
interface strings as the union of every catalog's keys, every engine command label and every
literal passed to `tr`/`t` in the UI crate: **1,724 strings**. A further ~145 capitalized literals
@@ -85,6 +85,7 @@ scrolling, complete label/shortcut accessibility and selecting a scrolled row at
| Ukrainian | uk | 1,432 (83%), plural categories handled | partial |
| Polish | pl | 1,717 (99%), count captions category-first; AI-drafted, awaiting native review | partial |
| Georgian | ka | 1,724 (~100%: every catalog key plus the command labels the others lack), count captions as `noun: n` | partial (reviewed by a native speaker) |
| Swedish | sv | 2,857 of 2,891 (99%, 2026-11-01 missing-translations report): menus, panels, dialogs and messages; singular for a count of one; AI-drafted with Claude Code from the English source, spot-checked by a native speaker (not every string) | partial |
DesignCraft ships **6** interface languages beyond the twelve; InDesign ships 17 beyond its
eight of the twelve (cs, da, el, en_GB, fi, hu, it, nb, nl, pl, ro, ru, sq, sv, tr, uk, zh_TW).
@@ -118,6 +119,7 @@ German variants have no automatic hyphenation; their German shaping and quotes a
|---|---|---|
| 2026-11-01 | minor | Initial Hungarian interface: 1,906 entries, four context captions, native review, bundled-font glyph coverage and wide/narrow flyout regressions |
| 2026-10-11 | minor | Localized Japanese print, import and table controls; checked Japanese and English renders |
| 2026-10-15 | minor | Swedish (`sv`) interface added |
| 2026-10-14 | minor | Georgian (`ka`) interface added |
| 2026-10-10 | major | Created: catalogs measured (1,724 strings), the twelve key languages plus Italian and Ukrainian |
| 2026-10-11 | minor | Czech (cs) added: 1,722 strings, plural categories handled |
+1
View File
@@ -23,6 +23,7 @@
<string>pl</string>
<string>pt-BR</string>
<string>ru</string>
<string>sv</string>
<string>uk</string>
<string>zh-Hans</string>
</array>