diff --git a/.claude/skills/ui-design/SKILL.md b/.claude/skills/ui-design/SKILL.md index 999524a4..ead6b0d3 100644 --- a/.claude/skills/ui-design/SKILL.md +++ b/.claude/skills/ui-design/SKILL.md @@ -53,6 +53,25 @@ These apply across every reference and every surface: namespace (framework chrome uses `core`). Contribution manifests carry `titleKey`/`labelKey` for the same reason. +## Localization & string length (D-21/D-102) + +- **Config / layout.** Catalogs are bundled assets at + `assets/i18n//.json` — the locale is a *directory* + (`en_us`, `nl_nl`, `nl_be`, `en_eu`, …); a new language is a new folder of the + same namespace files. The active language is `app.locale` (Settings → + Appearance → Language), applied live by `root_shell` via `i18n.setLocale`; + add the `Locale` to `availableLocales` in `main.dart` and a folder under + `assets/i18n/`. `en_US` is default; `nl_NL` ships. +- **Design for length variation.** Translations are not the same width — Dutch + runs ~20% longer than English, German more. So **never hard-size a surface to + its English label.** Tight surfaces (status-bar items, chips, buttons, tab + titles, menu items) must tolerate ~30% growth: let them wrap, ellipsis, or + `Flexible`/`Expanded`, not a fixed width tuned to English. When you add or + translate a label, sanity-check the length delta on those tight surfaces (an + `*.semantics` label is screen-reader-only, so its length never deforms + layout). A quick audit: compare `len(nl)/len(en)` per key and eyeball the + short-but-grew cases on real (non-semantics) surfaces. + ## Conversation-panel cards (T-305) The Claude conversation stream has **three** card categories. They are NOT one diff --git a/assets/i18n/en_us/builtin.theme-picker.json b/assets/i18n/en_us/builtin.theme-picker.json index 973429ea..d2a5f5c4 100644 --- a/assets/i18n/en_us/builtin.theme-picker.json +++ b/assets/i18n/en_us/builtin.theme-picker.json @@ -18,5 +18,8 @@ "settings.appearance.field.monoFont.label": { "translation": "Monospace font" }, "settings.appearance.field.monoFont.help": { "translation": "Terminal, diffs, code, and IDs; applies live." }, "settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" }, - "settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" } + "settings.appearance.field.monoFont.option.firaMono": { "translation": "Fira Mono" }, + "settings.appearance.section.language": { "translation": "Language" }, + "settings.appearance.field.language.label": { "translation": "Language" }, + "settings.appearance.field.language.help": { "translation": "Language for the app interface; applies live." } } diff --git a/lib/builtin/theme_picker/src/extension.dart b/lib/builtin/theme_picker/src/extension.dart index e08da28d..3736ec43 100644 --- a/lib/builtin/theme_picker/src/extension.dart +++ b/lib/builtin/theme_picker/src/extension.dart @@ -4,7 +4,7 @@ import 'package:clide/builtin/theme_picker/src/settings_view.dart'; import 'package:clide/builtin/theme_picker/src/theme_status_item.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; -import 'package:clide/widgets/widgets.dart' show kMonoFontSettingKey, kUiFontSettingKey; +import 'package:clide/widgets/widgets.dart' show kLocaleSettingKey, kMonoFontSettingKey, kUiFontSettingKey; class ThemePickerExtension extends ClideExtension { @override @@ -101,6 +101,26 @@ class ThemePickerExtension extends ClideExtension { ), ], ), + SettingsSection( + label: 'Language', + labelKey: 'settings.appearance.section.language', + fields: [ + SettingsField( + key: kLocaleSettingKey, + kind: SettingsFieldKind.select, + label: 'Language', + labelKey: 'settings.appearance.field.language.label', + help: 'Language for the app interface; applies live.', + helpKey: 'settings.appearance.field.language.help', + defaultValue: 'en_US', + // Language names stay in their own language (no labelKey). + options: [ + SettingsOption(value: 'en_US', label: 'English'), + SettingsOption(value: 'nl_NL', label: 'Nederlands'), + ], + ), + ], + ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index 340548f3..7ca3dbd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -394,6 +394,10 @@ Future main() async { bundledThemes: themes, i18nLoader: AssetCatalogLoader(bundle: rootBundle), preloadNamespaces: _tier0Namespaces, + // Languages the UI can switch to (Settings → Appearance, T-462). Each needs + // an assets/i18n// catalog folder; root_shell applies the persisted + // app.locale on boot. + availableLocales: const [Locale('en', 'US'), Locale('nl', 'NL')], autoStartDaemonClient: false, toolchain: toolchain, minLogLevel: bootLogLevel, diff --git a/lib/src/shell/root_shell.dart b/lib/src/shell/root_shell.dart index 264f852e..ddd4eae5 100644 --- a/lib/src/shell/root_shell.dart +++ b/lib/src/shell/root_shell.dart @@ -48,8 +48,9 @@ class RootShellState extends State { super.initState(); _keyFocus = FocusNode()..requestFocus(); widget.services.textZoom.addListener(_onZoom); - // Re-apply the UI font when its setting changes (T-460). + // Re-apply the UI font + language when their settings change (T-460/T-462). widget.services.settings.addListener(_onZoom); + _applyLocale(); // apply the persisted UI language at boot _globalSeq = SequenceMatcher( keymap: () => widget.services.keymap.keymap ?? Keymap(const []), context: () => widget.services.keymap.scope, @@ -69,7 +70,20 @@ class RootShellState extends State { super.dispose(); } - void _onZoom() => setState(() {}); + void _onZoom() { + _applyLocale(); + setState(() {}); + } + + /// Apply the persisted UI language (app.locale) to the i18n service. setLocale + /// is a no-op when the locale is unchanged, so this is safe on every tick. + void _applyLocale() { + final raw = widget.services.settings.get(kLocaleSettingKey); + if (raw == null || raw.isEmpty) return; + final parts = raw.split(RegExp('[_-]')); + final loc = parts.length >= 2 ? Locale(parts[0], parts[1]) : Locale(parts[0]); + widget.services.i18n.setLocale(loc); + } @override Widget build(BuildContext context) { diff --git a/lib/widgets/src/typography.dart b/lib/widgets/src/typography.dart index 9834d249..049e2e04 100644 --- a/lib/widgets/src/typography.dart +++ b/lib/widgets/src/typography.dart @@ -44,6 +44,10 @@ const List clideUiFamilyFallback = [ const String kUiFontSettingKey = 'app.ui.font'; const String kMonoFontSettingKey = 'app.mono.font'; +/// Settings key for the UI language (Settings → Appearance, T-462). Value is a +/// `lang_country` string (e.g. `en_US`, `nl_NL`); unset → the default locale. +const String kLocaleSettingKey = 'app.locale'; + // --------------------------------------------------------------------------- // Monospace face — JetBrains Mono // ---------------------------------------------------------------------------