feat(i18n): UI language selector (Settings → Appearance → Language) (T-462)
Make the localization usable: an app.locale select (English / Nederlands) in the Appearance category, applied live by root_shell — it parses app.locale and calls i18n.setLocale on boot + on settings change (setLocale is a no-op when unchanged). nl_NL registered in availableLocales. The ui-design skill now documents the locale-dir config and the rule to design for string-length variation (translations run ~20% longer; never hard-size to the English label). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,25 @@ These apply across every reference and every surface:
|
|||||||
namespace (framework chrome uses `core`). Contribution manifests carry
|
namespace (framework chrome uses `core`). Contribution manifests carry
|
||||||
`titleKey`/`labelKey` for the same reason.
|
`titleKey`/`labelKey` for the same reason.
|
||||||
|
|
||||||
|
## Localization & string length (D-21/D-102)
|
||||||
|
|
||||||
|
- **Config / layout.** Catalogs are bundled assets at
|
||||||
|
`assets/i18n/<locale>/<namespace>.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)
|
## Conversation-panel cards (T-305)
|
||||||
|
|
||||||
The Claude conversation stream has **three** card categories. They are NOT one
|
The Claude conversation stream has **three** card categories. They are NOT one
|
||||||
|
|||||||
@@ -18,5 +18,8 @@
|
|||||||
"settings.appearance.field.monoFont.label": { "translation": "Monospace font" },
|
"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.help": { "translation": "Terminal, diffs, code, and IDs; applies live." },
|
||||||
"settings.appearance.field.monoFont.option.jetBrainsMono": { "translation": "JetBrains Mono" },
|
"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." }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/builtin/theme_picker/src/theme_status_item.dart';
|
||||||
import 'package:clide/extension/extension.dart';
|
import 'package:clide/extension/extension.dart';
|
||||||
import 'package:clide/kernel/kernel.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 {
|
class ThemePickerExtension extends ClideExtension {
|
||||||
@override
|
@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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -394,6 +394,10 @@ Future<void> main() async {
|
|||||||
bundledThemes: themes,
|
bundledThemes: themes,
|
||||||
i18nLoader: AssetCatalogLoader(bundle: rootBundle),
|
i18nLoader: AssetCatalogLoader(bundle: rootBundle),
|
||||||
preloadNamespaces: _tier0Namespaces,
|
preloadNamespaces: _tier0Namespaces,
|
||||||
|
// Languages the UI can switch to (Settings → Appearance, T-462). Each needs
|
||||||
|
// an assets/i18n/<locale>/ catalog folder; root_shell applies the persisted
|
||||||
|
// app.locale on boot.
|
||||||
|
availableLocales: const [Locale('en', 'US'), Locale('nl', 'NL')],
|
||||||
autoStartDaemonClient: false,
|
autoStartDaemonClient: false,
|
||||||
toolchain: toolchain,
|
toolchain: toolchain,
|
||||||
minLogLevel: bootLogLevel,
|
minLogLevel: bootLogLevel,
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ class RootShellState extends State<RootShell> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_keyFocus = FocusNode()..requestFocus();
|
_keyFocus = FocusNode()..requestFocus();
|
||||||
widget.services.textZoom.addListener(_onZoom);
|
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);
|
widget.services.settings.addListener(_onZoom);
|
||||||
|
_applyLocale(); // apply the persisted UI language at boot
|
||||||
_globalSeq = SequenceMatcher(
|
_globalSeq = SequenceMatcher(
|
||||||
keymap: () => widget.services.keymap.keymap ?? Keymap(const []),
|
keymap: () => widget.services.keymap.keymap ?? Keymap(const []),
|
||||||
context: () => widget.services.keymap.scope,
|
context: () => widget.services.keymap.scope,
|
||||||
@@ -69,7 +70,20 @@ class RootShellState extends State<RootShell> {
|
|||||||
super.dispose();
|
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<String>(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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ const List<String> clideUiFamilyFallback = [
|
|||||||
const String kUiFontSettingKey = 'app.ui.font';
|
const String kUiFontSettingKey = 'app.ui.font';
|
||||||
const String kMonoFontSettingKey = 'app.mono.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
|
// Monospace face — JetBrains Mono
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user