mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-06 18:51:26 +00:00
CP4.3: a language and region name is translated in parts, not looked up whole
Swedish (Finland) is built at run time, so no catalog row can match it. One helper
(ribbon::display_name) now translates the language, the region and the {language} ({region})
template separately and joins them; the Set Proofing Language list, the Language menu, the status
bar and the dialog all use it. English output is unchanged and matching never uses a name.
gap 8 no longer describes the missing-pack bar, which is in its own branch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCYmv6UJmY2QcuhfXco
This commit is contained in:
1 parent
26a8182e91
commit
29f99006e0
6 files changed
+81
-18
No files matched your search
@@ -322,7 +322,7 @@ impl LanguageForm {
|
||||
|
||||
/// A language's name in the interface language (the tag itself when we don't know it).
|
||||
pub(super) fn language_name(tag: &str) -> String {
|
||||
tl!(&crate::ribbon::language_name(Some(tag))).to_string()
|
||||
crate::ribbon::display_name(Some(tag))
|
||||
}
|
||||
|
||||
pub(super) fn language_ui(app: &mut WordApp, ui: &mut Ui, f: &mut LanguageForm) -> bool {
|
||||
|
||||
@@ -142,9 +142,8 @@ pub(crate) const MORE_LANGUAGES: [(&str, &str); 28] = [
|
||||
/// the Language dialog name it; else the pack's own name; else its locale.
|
||||
pub(crate) fn language_of(pack_locale: &str, endonym: &str) -> String {
|
||||
let tag = crate::ribbon::tag_for_pack(pack_locale);
|
||||
let named = crate::ribbon::language_name(Some(&tag));
|
||||
if !named.eq_ignore_ascii_case(&tag) {
|
||||
return tl!(&named).to_string();
|
||||
if !crate::ribbon::language_name(Some(&tag)).eq_ignore_ascii_case(&tag) {
|
||||
return crate::ribbon::display_name(Some(&tag));
|
||||
}
|
||||
if endonym.is_empty() { pack_locale.to_string() } else { endonym.to_string() }
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub(crate) use language::sort_key;
|
||||
pub(crate) use language::{
|
||||
caret_language, caret_language_proofed, caret_missing_dictionary, caret_no_proof, caret_unreadable_dictionary, language_menu,
|
||||
};
|
||||
pub(crate) use language::{language_list, language_name, same_language, tag_for_pack};
|
||||
pub(crate) use language::{display_name, language_list, language_name, same_language, tag_for_pack};
|
||||
use layout::layout;
|
||||
#[cfg(test)]
|
||||
use layout::{columns_items, margins_items, orientation_items};
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) fn short_languages() -> Vec<(String, String)> {
|
||||
more.push((tag, name));
|
||||
}
|
||||
}
|
||||
more.sort_by_cached_key(|(_, n)| sort_key(tl!(n)));
|
||||
more.sort_by_cached_key(|(t, n)| sort_key(&display_of(t, n)));
|
||||
out.extend(more);
|
||||
out
|
||||
}
|
||||
@@ -113,7 +113,7 @@ pub(crate) fn all_languages() -> Vec<(String, String)> {
|
||||
});
|
||||
for (tag, name) in builtin.chain(packs) {
|
||||
if !out.iter().any(|(t, _)| t.eq_ignore_ascii_case(&tag)) {
|
||||
out.push((tag, tl!(&name).to_string()));
|
||||
out.push((tag.clone(), display_of(&tag, &name)));
|
||||
}
|
||||
}
|
||||
out.sort_by_cached_key(|(_, n)| sort_key(n));
|
||||
@@ -126,7 +126,7 @@ pub(crate) fn language_list(current: &str) -> Vec<(String, String)> {
|
||||
let mut list = all_languages();
|
||||
// A tag the list has under another spelling (`nl` for `nl-NL`) is that row, not another.
|
||||
if !current.is_empty() && !list.iter().any(|(t, _)| same_language(current, t)) {
|
||||
list.push((current.to_string(), tl!(&language_name(Some(current))).to_string()));
|
||||
list.push((current.to_string(), display_name(Some(current))));
|
||||
list.sort_by_cached_key(|(_, n)| sort_key(n));
|
||||
}
|
||||
list
|
||||
@@ -170,15 +170,53 @@ pub(crate) fn sort_key(name: &str) -> (String, String) {
|
||||
(folded, lower)
|
||||
}
|
||||
|
||||
/// A language's name in English, as the lists build it: a whole name (`Polish`, `Serbian (Latin)`)
|
||||
/// or a language with a region the lists have no entry for (`Swedish` and `Finland`).
|
||||
enum NameParts {
|
||||
Whole(String),
|
||||
Regioned { language: String, region: String },
|
||||
}
|
||||
|
||||
/// The English name of a proofing language tag, as the status bar shows it (no tag: WordCraft's
|
||||
/// default, US English): a listed language by its name; another region of one by the language
|
||||
/// and the region ("French (Canada)"); a bare language subtag by its usual variant; else the tag.
|
||||
pub(crate) fn language_name(tag: Option<&str>) -> String {
|
||||
match name_parts(tag) {
|
||||
NameParts::Whole(n) => n,
|
||||
NameParts::Regioned { language, region } => format!("{language} ({region})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`language_name`] in the interface language: a whole name is looked up as it is, a language
|
||||
/// with a region is the translated `{language} ({region})` of the translated language and region
|
||||
/// (a name built at run time has no catalog row of its own). English is unchanged.
|
||||
pub(crate) fn display_name(tag: Option<&str>) -> String {
|
||||
display_name_with(tag, &|s| tl!(s).to_string())
|
||||
}
|
||||
|
||||
/// [`display_name`] with the lookup given (`translate`: English in, interface language out).
|
||||
pub(super) fn display_name_with(tag: Option<&str>, translate: &dyn Fn(&str) -> String) -> String {
|
||||
match name_parts(tag) {
|
||||
NameParts::Whole(n) => translate(&n),
|
||||
NameParts::Regioned { language, region } => {
|
||||
crate::i18n::fmt(&translate("{language} ({region})"), &[("language", &translate(&language)), ("region", &translate(®ion))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The name a list shows for `tag` whose English name is `name`: [`display_name`] for the name
|
||||
/// WordCraft has for the tag, else `name` as it is (a pack's own name for a language WordCraft
|
||||
/// has none for).
|
||||
pub(crate) fn display_of(tag: &str, name: &str) -> String {
|
||||
if name == language_name(Some(tag)) { display_name(Some(tag)) } else { name.to_string() }
|
||||
}
|
||||
|
||||
fn name_parts(tag: Option<&str>) -> NameParts {
|
||||
let tag = tag.unwrap_or("en-US");
|
||||
let named = |t: &str| known().find(|(l, _)| l.eq_ignore_ascii_case(t)).map(|(_, n)| *n);
|
||||
// Any language the Language dialog offers (French, German…) by its name, not its tag (#860).
|
||||
if let Some(name) = named(tag) {
|
||||
return name.to_string();
|
||||
return NameParts::Whole(name.to_string());
|
||||
}
|
||||
let mut parts = tag.split(['-', '_']);
|
||||
let primary = parts.next().unwrap_or("");
|
||||
@@ -191,12 +229,12 @@ pub(crate) fn language_name(tag: Option<&str>) -> String {
|
||||
.find(|(l, _)| l.split('-').next().is_some_and(|p| p.eq_ignore_ascii_case(primary)))
|
||||
.map(|(_, n)| n.split(" (").next().unwrap_or(n));
|
||||
match language {
|
||||
Some(language) => format!("{language} ({})", region_name(r)),
|
||||
None => tag.to_string(),
|
||||
Some(language) => NameParts::Regioned { language: language.to_string(), region: region_name(r) },
|
||||
None => NameParts::Whole(tag.to_string()),
|
||||
}
|
||||
}
|
||||
// A bare language subtag ("pt", "pl"): the variant WordCraft proofs.
|
||||
None => match wordcraft_engine::proof::ProofLang::from_tag(Some(tag)) {
|
||||
None => NameParts::Whole(match wordcraft_engine::proof::ProofLang::from_tag(Some(tag)) {
|
||||
wordcraft_engine::proof::ProofLang::Pl => "Polish".into(),
|
||||
wordcraft_engine::proof::ProofLang::Hr => "Croatian".into(),
|
||||
wordcraft_engine::proof::ProofLang::Pt => "Portuguese (Brazil)".into(),
|
||||
@@ -213,7 +251,7 @@ pub(crate) fn language_name(tag: Option<&str>) -> String {
|
||||
_ => named_starting(tag)
|
||||
.or_else(|| known().find(|(t, _)| t.contains('-') && wordcraft_engine::packs::without_region(t).eq_ignore_ascii_case(tag)))
|
||||
.map_or_else(|| tag.to_string(), |(_, n)| (*n).to_string()),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +333,7 @@ fn region_name(code: &str) -> String {
|
||||
|
||||
/// The language at the caret (resolved through the styles), translated, for the status bar.
|
||||
pub(crate) fn caret_language(app: &WordApp) -> String {
|
||||
tl!(&language_name(caret_lang_tag(app).as_deref())).to_string()
|
||||
display_name(caret_lang_tag(app).as_deref())
|
||||
}
|
||||
|
||||
/// The formatting at the caret, resolved through the styles and the document default.
|
||||
@@ -349,11 +387,11 @@ pub(crate) fn language_menu(ui: &mut Ui, app: &mut WordApp) {
|
||||
let no_proof = st.get("noProof").and_then(Value::as_bool).unwrap_or(false);
|
||||
let entries = short_languages();
|
||||
let checks = language_checks(¤t, &entries);
|
||||
if let Some(name) = checks.extra {
|
||||
mi_check(ui, app, &name, true, "review.language", json!({"lang": current}));
|
||||
if checks.extra.is_some() {
|
||||
mi_check(ui, app, &display_name(Some(¤t)), true, "review.language", json!({"lang": current}));
|
||||
}
|
||||
for ((tag, name), on) in entries.iter().zip(checks.listed) {
|
||||
mi_check(ui, app, name, on, "review.language", json!({"lang": tag}));
|
||||
mi_check(ui, app, &display_of(tag, name), on, "review.language", json!({"lang": tag}));
|
||||
}
|
||||
ui.separator();
|
||||
mi_check(ui, app, "Do Not Check Spelling or Grammar", no_proof, "review.language", json!({"noProof": !no_proof}));
|
||||
|
||||
@@ -308,6 +308,32 @@ fn rows_are_matched_by_tag_not_by_display_name() {
|
||||
assert_eq!(language_checks("pt", &[("pt-PT".to_string(), "Portuguese (Brazil)".to_string())]).listed, [false]);
|
||||
}
|
||||
|
||||
/// CP4.3 review: a name built from a language and a region (`Swedish (Finland)`) has no catalog row
|
||||
/// of its own, so the language, the region and the `{language} ({region})` template are looked up
|
||||
/// separately and joined. English stays as `language_name` builds it.
|
||||
#[test]
|
||||
fn a_language_and_region_name_is_translated_in_parts() {
|
||||
use super::language::display_name_with;
|
||||
let fake = |s: &str| -> String {
|
||||
match s {
|
||||
"Swedish" => "X".into(),
|
||||
"Finland" => "Y".into(),
|
||||
"{language} ({region})" => "{language} \u{2013} {region}".into(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
};
|
||||
assert_eq!(display_name_with(Some("sv-FI"), &fake), "X \u{2013} Y");
|
||||
// A whole name is looked up as it is; the template isn't used for it.
|
||||
assert_eq!(display_name_with(Some("sv-SE"), &fake), "X");
|
||||
// Without a catalogue (English) the name is byte-identical to the English name.
|
||||
for tag in ["sv-FI", "es-CU", "fr-CA", "de-CH", "pt-AO", "en-SG", "sr-Latn-BA", "pl-PL", "nl", "xx-YY", "xx", "ca-valencia"] {
|
||||
assert_eq!(super::language::display_name(Some(tag)), language_name(Some(tag)), "{tag}");
|
||||
}
|
||||
assert_eq!(language_name(Some("sv-FI")), "Swedish (Finland)");
|
||||
// Matching never uses a translated name: Finnish Swedish and Swedish of Sweden stay two rows.
|
||||
assert!(!super::language::same_language("sv-FI", "sv-SE"));
|
||||
}
|
||||
|
||||
/// #860 review: a tag the language list doesn't have shows by language and region, not as
|
||||
/// the tag; Portuguese (Angola) isn't Brazil's, and no name shows twice in the menu.
|
||||
#[test]
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ Text context menus now include the existing formatting mini toolbar (#2959), inc
|
||||
| 6 | **Print dialog depth**: desktop printer choice, copies, collation, duplex and grayscale submit through craft-print; File › Print retains preview, page ranges and odd/even (#641) and scales pages to another paper size (#2108); driver tray/options and pages per sheet remain; web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
|
||||
| 7 | **Dialog depth**: Word-observed spelling/grammar task pane added (shared review commands, current-story traversal; see [spelling-review.md](spelling-review.md)); 61 modal dialogs vs Word's ~100 (Custom Document Properties from File › Info, #2982; Drop Cap Options matches Word's menu and dialog; Split Cells and Convert Text to Table match Word's observed limits and defaults; Format Page Numbers carries Word's number formats and Continue from previous section / Start at, #1113) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs`; Cell Margins (#1960) now opens with current defaults and applies only confirmed edits | Power users reach for dialogs the ribbon alone doesn't cover | 11–19 | [ui-parity.md](ui-parity.md) |
|
||||
| 9 | **Password-protected `.doc` files with an AES header in the Word 97-2003 format** can't be opened: [MS-OFFCRYPTO] defines AES only for encrypted packages, so these are refused with a clear message. Encrypted packages open under any Word extension (`.docx`, `.doc`, `.dot`…) with either agile (Word 2010+) or Standard (Word 2007, AES-ECB) encryption; protected `.docx` saves with agile encryption (#55, #281, gap 9) | | `crates/docx/src/crypt.rs` (agile and Standard); `crates/docbin/src/crypt.rs` opens XOR/RC4/RC4 CryptoAPI `.doc` with a password (#1079). All synthetic fixtures; untested on Word-written files | Corporate and legal users with old files | 0 (refused by design; nothing left to build) | [file-format-parity.md](file-format-parity.md) |
|
||||
| 8 | **Built-in proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish only**; the other languages Word proofs get spelling and their own hyphenation only from an installed Language pack (without one their text is left unmarked and hyphenated with the English patterns; Bulgarian and German have their own), and none of them has grammar rules | | craft-proof (storytold/craft-libs `crates/proof`, data in craft-i18n `language-data/`) `ProofLang::PROOFED`: English (American, British-spelling and Canadian, each checked against its own SCOWL 2026 dictionary and flagging the other's spellings, #1281, #40, #520), Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) Russian (the installed `ru_RU` dictionary, 2026-10-12) Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16) and Spanish (es-ES, es-MX, es-419 and es-UY via the installed RLA-ES dictionaries, Uruguayan `vos` forms, #3360, #3553, 2026-10-22), a language per run; Bulgarian (#826) and German (TeX patterns, 2026-10-14) have hyphenation only; open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #100 open. **Language packs** (CP4, 2026-11-01): a pack per locale (spelling dictionary, hyphenation, licence texts) installs from File › Options › Language or the missing-pack bar through the font store and is read at once (the bar offers a language before the list is fetched, and the caret's typing language counts); language-packs-1 (pinned, 61 packs) has Lithuanian [#4707](https://github.com/storytold/wordcraft/issues/4707), Romanian [#3438](https://github.com/storytold/wordcraft/issues/3438), Catalan [#2895](https://github.com/storytold/wordcraft/issues/2895)/[#4947](https://github.com/storytold/wordcraft/issues/4947), French [#1325](https://github.com/storytold/wordcraft/issues/1325), Greek [#2561](https://github.com/storytold/wordcraft/issues/2561), Slovak [#3806](https://github.com/storytold/wordcraft/issues/3806), Serbian [#4968](https://github.com/storytold/wordcraft/issues/4968) and Latvian [#4438](https://github.com/storytold/wordcraft/issues/4438), with Polish, Portuguese, Italian, Russian, Czech, Spanish, Bulgarian, Bosnian, Slovenian and the rest; Dutch [#4145](https://github.com/storytold/wordcraft/issues/4145)/[#2505](https://github.com/storytold/wordcraft/issues/2505), German, Danish, Swedish, Norwegian, Hungarian and the other compounding languages are packs in language-packs-2 (craft-proof's flag-based compounding, storytold/craft-libs#497); Mongolian waits on faster suggestions, Thai on word segmentation, and Basque, Ukrainian and Uyghur are excluded on licence grounds (see [installing languages](spelling-dictionaries.md#language-packs)) | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish | 20–40 | [localization-parity.md](localization-parity.md) |
|
||||
| 8 | **Built-in proofing covers English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish only**; the other languages Word proofs get spelling and their own hyphenation only from an installed Language pack (without one their text is left unmarked and hyphenated with the English patterns; Bulgarian and German have their own), and none of them has grammar rules | | craft-proof (storytold/craft-libs `crates/proof`, data in craft-i18n `language-data/`) `ProofLang::PROOFED`: English (American, British-spelling and Canadian, each checked against its own SCOWL 2026 dictionary and flagging the other's spellings, #1281, #40, #520), Polish, Portuguese (pt-BR via the installed VERO dictionary, #797), Italian (it-IT via the installed Hunspell dictionary), Croatian (hr-HR, 2026-10-13), Turkish (tr_TR spelling, grammar, TDK hyphenation, 2026-10-13) Russian (the installed `ru_RU` dictionary, 2026-10-12) Indonesian (the installed `id_ID` dictionary, #498, 2026-10-16) and Spanish (es-ES, es-MX, es-419 and es-UY via the installed RLA-ES dictionaries, Uruguayan `vos` forms, #3360, #3553, 2026-10-22), a language per run; Bulgarian (#826) and German (TeX patterns, 2026-10-14) have hyphenation only; open PRs add Danish (#723), Russian (#894) and Czech (#1048); Word ships 120 proofing tools; issues #25, #100 open. **Language packs** (CP4, 2026-11-01): a pack per locale (spelling dictionary, hyphenation, licence texts) installs from File › Options › Language or the missing-pack bar through the font store and is read at once; language-packs-1 (pinned, 61 packs) has Lithuanian [#4707](https://github.com/storytold/wordcraft/issues/4707), Romanian [#3438](https://github.com/storytold/wordcraft/issues/3438), Catalan [#2895](https://github.com/storytold/wordcraft/issues/2895)/[#4947](https://github.com/storytold/wordcraft/issues/4947), French [#1325](https://github.com/storytold/wordcraft/issues/1325), Greek [#2561](https://github.com/storytold/wordcraft/issues/2561), Slovak [#3806](https://github.com/storytold/wordcraft/issues/3806), Serbian [#4968](https://github.com/storytold/wordcraft/issues/4968) and Latvian [#4438](https://github.com/storytold/wordcraft/issues/4438), with Polish, Portuguese, Italian, Russian, Czech, Spanish, Bulgarian, Bosnian, Slovenian and the rest; Dutch [#4145](https://github.com/storytold/wordcraft/issues/4145)/[#2505](https://github.com/storytold/wordcraft/issues/2505), German, Danish, Swedish, Norwegian, Hungarian and the other compounding languages are packs in language-packs-2 (craft-proof's flag-based compounding, storytold/craft-libs#497); Mongolian waits on faster suggestions, Thai on word segmentation, and Basque, Ukrainian and Uyghur are excluded on licence grounds (see [installing languages](spelling-dictionaries.md#language-packs)) | Every writer outside English, Polish, Portuguese, Italian, Croatian, Turkish, Russian, Czech, Indonesian and Spanish | 20–40 | [localization-parity.md](localization-parity.md) |
|
||||
| 10 | **Objects**: text in a freely rotated text box (free rotation and flips landed, #332; live rotation preview while dragging fixed for pictures and shapes without text stories, #2277; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt 3-D effects and connector routing around shapes (WordArt effects and warps and glued connectors landed, #416), dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 11/11, Layout 22/22); group and ungroup landed (#267) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) |
|
||||
| 11 | **Interface languages**: 2 of the 12 key languages missing (hi, id) and pt-PT (Arabic landed with a mirrored shell, #1195; French landed, #729; Vietnamese, #1434; Korean, #1216; 28 interface languages in `crates/ui-egui/src/i18n`); open PRs for Indonesian (#572), pt-PT (#1028) and Hebrew with RTL (#20); ~5% of strings still English in the translated catalogs | | [localization-parity.md](localization-parity.md) | Most of the world's writers | 40–70 | [localization-parity.md](localization-parity.md) |
|
||||
| 12 | **Draw tab / ink**: pens, pencil, highlighter, eraser, Select and Hide Ink landed (#307); Lasso Select, Add Pen, Ink to Shape and Ink Replay landed (#395); Ink to Shape also recognises single-stroke arrows (a line plus a V head drawn in one stroke becomes a straight connector with an arrowhead); two-stroke arrows (a line and a separate V or closed-triangle head) and one-stroke arrows with a closed triangle head landed (#1778); missing: Ink to Math, pressure, rendering/editing Word's own ink (drawing-level and direct run-level InkML content parts now survive DOCX save) | | [parity-checklist.md](parity-checklist.md) (Draw 10/11) | Pen and tablet users | 4–8 | [hardware-parity.md](hardware-parity.md) |
|
||||
|
||||
Reference in new issue
Block a user