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:
2026-06-20 17:34:08 +02:00
co-authored by Claude Opus 4.8
parent bee7f98e45
commit 43c650de10
6 changed files with 68 additions and 4 deletions
+21 -1
View File
@@ -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'),
],
),
],
),
],
),
),
+4
View File
@@ -394,6 +394,10 @@ Future<void> 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/<locale>/ 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,
+16 -2
View File
@@ -48,8 +48,9 @@ class RootShellState extends State<RootShell> {
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<RootShell> {
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
Widget build(BuildContext context) {
+4
View File
@@ -44,6 +44,10 @@ const List<String> 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
// ---------------------------------------------------------------------------