French (fr) UI localization (#1224)

Adds a French catalog as a partial locale (missing strings fall back to English, like Russian), registered in the language list, with tests and attribution.
This commit is contained in:
rlartot authored and GitHub committed 2026-10-14 19:05:17 -04:00
1 parent 1cc65cbc91
commit e4ef6cfe29
7 files changed
+1083 -14

No files matched your search

+1
View File
@@ -89,6 +89,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/fr.json | rlartot | French translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 | | assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 | | assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
File diff suppressed because it is too large. Load diff
+43 -8
View File
@@ -34,10 +34,11 @@ impl Catalog {
static ENGLISH: Catalog = Catalog::new("en", "English", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/en.json"))); static ENGLISH: Catalog = Catalog::new("en", "English", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/en.json")));
static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ja.json"))); static JAPANESE: Catalog = Catalog::new("ja", "日本語", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ja.json")));
static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/de.json"))); static GERMAN: Catalog = Catalog::new("de", "Deutsch", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/de.json")));
static FRENCH: Catalog = Catalog::new("fr", "Français", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/fr.json")));
static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ko.json"))); static KOREAN: Catalog = Catalog::new("ko", "한국어", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ko.json")));
static RUSSIAN: Catalog = Catalog::new("ru", "Русский", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ru.json"))); static RUSSIAN: Catalog = Catalog::new("ru", "Русский", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/ru.json")));
static CHINESE: Catalog = Catalog::new("zh", "中文", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/zh.json"))); static CHINESE: Catalog = Catalog::new("zh", "中文", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../assets/locales/zh.json")));
pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &KOREAN, &RUSSIAN, &CHINESE]; pub const LOCALES: &[&Catalog] = &[&ENGLISH, &JAPANESE, &GERMAN, &FRENCH, &KOREAN, &RUSSIAN, &CHINESE];
pub fn is_supported(id: &str) -> bool { pub fn is_supported(id: &str) -> bool {
LOCALES.iter().any(|catalog| catalog.id == id) LOCALES.iter().any(|catalog| catalog.id == id)
@@ -188,9 +189,9 @@ mod tests {
} }
/// Catalogs delivered complete. English is the source catalog; Japanese was completed /// Catalogs delivered complete. English is the source catalog; Japanese was completed
/// upstream and Chinese lands complete here. German, Korean and Russian are partial /// upstream and Chinese lands complete here. German, Korean, Russian and French are partial
/// (German and Korean predate the coverage guards; Russian is kept partial so a new label /// (German and Korean predate the coverage guards; Russian and French are kept partial so a
/// doesn't need a sixth translation), so their gaps fall back to English and are reported by /// new label doesn't need more translations), so their gaps fall back to English and are reported by
/// `cargo xtask i18n` without failing the build, while nothing in the complete catalogs may /// `cargo xtask i18n` without failing the build, while nothing in the complete catalogs may
/// regress. /// regress.
const COMPLETE: &[&str] = &["ja", "zh"]; const COMPLETE: &[&str] = &["ja", "zh"];
@@ -260,6 +261,7 @@ mod tests {
("de", "Flansch", "Zebra-Analyse", "Bewegungsuntersuchung"), ("de", "Flansch", "Zebra-Analyse", "Bewegungsuntersuchung"),
("ko", "플랜지", "얼룩말줄무늬 분석", "운동 연구"), ("ko", "플랜지", "얼룩말줄무늬 분석", "운동 연구"),
("ru", "Фланец", "Зебра-анализ", "Исследование движения"), ("ru", "Фланец", "Зебра-анализ", "Исследование движения"),
("fr", "Bord tombé", "Analyse zébrée", "Étude de mouvement"),
]; ];
for (lang, flange, zebra, motion) in cases { for (lang, flange, zebra, motion) in cases {
let locale = Locale::for_language(lang); let locale = Locale::for_language(lang);
@@ -335,9 +337,13 @@ mod tests {
/// non-empty string" would not catch that. /// non-empty string" would not catch that.
#[test] #[test]
fn id_labels_resolve_to_their_own_translation_not_an_english_fallback() { fn id_labels_resolve_to_their_own_translation_not_an_english_fallback() {
for (lang, profile, points) in for (lang, profile, points) in [
[("de", "Profil", "Punkte"), ("ja", "プロファイル", "点"), ("ko", "프로파일", "점"), ("ru", "Профиль", "Точки")] ("de", "Profil", "Punkte"),
{ ("ja", "プロファイル", "点"),
("ko", "프로파일", "점"),
("ru", "Профиль", "Точки"),
("fr", "Profil", "Points"),
] {
let locale = Locale::for_language(lang); let locale = Locale::for_language(lang);
assert_eq!(locale.label("ui.palette_profile"), profile, "{lang}"); assert_eq!(locale.label("ui.palette_profile"), profile, "{lang}");
assert_eq!(locale.label("ui.palette_points"), points, "{lang}"); assert_eq!(locale.label("ui.palette_points"), points, "{lang}");
@@ -355,7 +361,13 @@ mod tests {
/// the row's two-word text must reach its own entry rather than that one. /// the row's two-word text must reach its own entry rather than that one.
#[test] #[test]
fn the_construction_geometries_row_reaches_its_own_entry_not_the_line_type() { fn the_construction_geometries_row_reaches_its_own_entry_not_the_line_type() {
let rows = [("de", "Konstruktionsgeometrien"), ("ja", "構築ジオメトリ"), ("ko", "구성 형상"), ("ru", "Вспомогательная геометрия")]; let rows = [
("de", "Konstruktionsgeometrien"),
("ja", "構築ジオメトリ"),
("ko", "구성 형상"),
("ru", "Вспомогательная геометрия"),
("fr", "Géométries de construction"),
];
for (lang, want) in rows { for (lang, want) in rows {
let locale = Locale::for_language(lang); let locale = Locale::for_language(lang);
assert_eq!(locale.label("Construction Geometries"), want, "{lang}"); assert_eq!(locale.label("Construction Geometries"), want, "{lang}");
@@ -428,6 +440,29 @@ mod tests {
assert_eq!(ru.dynamic("Sketch of bracket"), "Sketch of bracket"); assert_eq!(ru.dynamic("Sketch of bracket"), "Sketch of bracket");
} }
#[test]
fn french_catalog_translates_keys_labels_and_browser_patterns() {
let fr = Locale::for_language("fr");
assert_eq!(fr.name(), "Français");
assert!(is_supported("fr"));
assert_eq!(fr.text("ui.preferences"), "Préférences");
assert_eq!(fr.label("Extrude"), "Extrusion");
assert_eq!(fr.label("Fillet"), "Congé");
assert_eq!(fr.label("CREATE SKETCH"), "CRÉER UNE ESQUISSE");
assert_eq!(fr.label("NonExistentKey"), "NonExistentKey");
// French puts a no-break space before the two-part punctuation marks.
assert_eq!(fr.format("document.save_changes", &[("name", "My {design}")]), "Enregistrer les modifications de My {design}\u{a0}?");
assert_eq!(fr.format("dialog.selected_count", &[("count", "2")]), "↖ 2 sélectionné(s)");
assert_eq!(fr.dynamic("Sketch1"), "Esquisse1");
assert_eq!(fr.dynamic("Body2"), "Corps2");
assert_eq!(fr.dynamic("Component12"), "Composant12");
assert_eq!(fr.dynamic("Units: mm"), "Unités\u{a0}: mm");
assert_eq!(fr.dynamic("Precision: 3"), "Précision\u{a0}: 3");
assert_eq!(fr.dynamic("Parameters (5)"), "Paramètres (5)");
assert_eq!(fr.dynamic("4 degrees of freedom"), "4 degrés de liberté");
assert_eq!(fr.dynamic("Sketch of bracket"), "Sketch of bracket");
}
#[test] #[test]
fn render_scopes_restore_the_previous_language() { fn render_scopes_restore_the_previous_language() {
let _english = scope("en"); let _english = scope("en");
+1
View File
@@ -65,6 +65,7 @@ fn window_chrome_renders_in_the_active_language() {
("de", "BROWSER", "Auswahlfilter", "Flächen"), ("de", "BROWSER", "Auswahlfilter", "Flächen"),
("ko", "브라우저", "선택 필터", "면"), ("ko", "브라우저", "선택 필터", "면"),
("ru", "БРАУЗЕР", "Фильтр выделения", "Грани"), ("ru", "БРАУЗЕР", "Фильтр выделения", "Грани"),
("fr", "NAVIGATEUR", "Filtre de sélection", "Faces"),
] { ] {
let mut h = Harness::new(); let mut h = Harness::new();
h.app.preferences.language = lang.into(); h.app.preferences.language = lang.into();
+1 -1
View File
@@ -1584,7 +1584,7 @@ mod tests {
assert!(control(&mut app, &json!({"open": "Plugins"})).is_err()); assert!(control(&mut app, &json!({"open": "Plugins"})).is_err());
assert!(set(&mut app, &json!({"up_axis": "x"})).is_err()); assert!(set(&mut app, &json!({"up_axis": "x"})).is_err());
assert!(set(&mut app, &json!({"theme": "sepia"})).is_err()); assert!(set(&mut app, &json!({"theme": "sepia"})).is_err());
assert!(set(&mut app, &json!({"language": "fr"})).is_err()); assert!(set(&mut app, &json!({"language": "xx"})).is_err());
} }
#[test] #[test]
+1 -1
View File
@@ -127,7 +127,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o
| `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs | | `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs |
| `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) | | `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) |
| `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text | | `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text |
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `ko`, `ru`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
| `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits |
| `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it | | `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |
+4 -4
View File
@@ -1,7 +1,7 @@
# UI localization # UI localization
SolidCraft embeds its translations and works offline. English, Japanese, German, Korean, Russian and SolidCraft embeds its translations and works offline. English, Japanese, German, French, Korean, Russian
Chinese are currently registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an and Chinese are currently registered in `crates/ui-egui/src/i18n.rs`. Adding a language requires an
embedded catalog and one registration there; the preferences selector and validation use the same embedded catalog and one registration there; the preferences selector and validation use the same
registry. registry.
@@ -31,9 +31,9 @@ that looks untranslated. Two guards catch that in the build:
- `every_complete_catalog_carries_every_english_message` — adding a key to `en.json` - `every_complete_catalog_carries_every_english_message` — adding a key to `en.json`
without giving it to a complete catalog fails the tests. English is the source catalog, without giving it to a complete catalog fails the tests. English is the source catalog,
Japanese and Chinese are delivered complete; German, Korean and Russian are partial — their Japanese and Chinese are delivered complete; German, Korean, Russian and French are partial — their
gaps fall back to English and are reported by `cargo xtask i18n` without failing the build. A gaps fall back to English and are reported by `cargo xtask i18n` without failing the build. A
new label needs en, ja, zh, de and ko; Russian may follow later. new label needs en, ja, zh, de and ko; Russian and French may follow later.
- `every_toolbar_tab_and_panel_is_in_the_english_catalog` — tabs and panels reach the UI as bare - `every_toolbar_tab_and_panel_is_in_the_english_catalog` — tabs and panels reach the UI as bare
source strings, resolved only by the English-label adapter, so a heading missing from `en.json` source strings, resolved only by the English-label adapter, so a heading missing from `en.json`
can never translate. can never translate.