Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 55 additions & 39 deletions flutter_app/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'l10n/app_localizations.dart';
import 'providers/locale_provider.dart';
import 'providers/setup_provider.dart';
import 'providers/gateway_provider.dart';
import 'providers/node_provider.dart';
Expand Down Expand Up @@ -41,6 +44,7 @@ class OpenClawApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LocaleProvider()..load()),
ChangeNotifierProvider(create: (_) => SetupProvider()),
ChangeNotifierProvider(create: (_) => GatewayProvider()),
ChangeNotifierProxyProvider<GatewayProvider, NodeProvider>(
Expand All @@ -51,13 +55,53 @@ class OpenClawApp extends StatelessWidget {
},
),
],
child: MaterialApp(
title: 'OpenClaw',
debugShowCheckedModeBanner: false,
theme: _buildLightTheme(),
darkTheme: _buildDarkTheme(),
themeMode: ThemeMode.system,
home: const SplashScreen(),
child: Consumer<LocaleProvider>(
builder: (context, localeProvider, _) => MaterialApp(
debugShowCheckedModeBanner: false,
onGenerateTitle: (context) => context.l10n.t('appName'),
locale: localeProvider.locale,
localeListResolutionCallback: (deviceLocales, supportedLocales) {
if (localeProvider.locale != null) {
return localeProvider.locale;
}

for (final deviceLocale in deviceLocales ?? const <Locale>[]) {
for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == deviceLocale.languageCode &&
supportedLocale.scriptCode == deviceLocale.scriptCode &&
supportedLocale.countryCode == deviceLocale.countryCode) {
return supportedLocale;
}
}

for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == deviceLocale.languageCode &&
supportedLocale.scriptCode == deviceLocale.scriptCode) {
return supportedLocale;
}
}

for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == deviceLocale.languageCode) {
return supportedLocale;
}
}
}

return supportedLocales.first;
},
Comment on lines +63 to +105
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localeListResolutionCallback returns a supported Locale without preserving zh-HK/zh-TW/etc. country info; this means Traditional Chinese users can resolve to Locale('zh') (Simplified) even though AppLocalizations can map HK/TW to Hant when the Locale contains countryCode. Consider explicitly mapping zh locales with countryCode in {TW, HK, MO} to the supported Hant locale (Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')) before falling back to language-only matching.

Copilot uses AI. Check for mistakes.
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
theme: _buildLightTheme(),
darkTheme: _buildDarkTheme(),
themeMode: ThemeMode.system,
home: const SplashScreen(),
),
),
);
}
Expand Down Expand Up @@ -95,15 +139,7 @@ class OpenClawApp extends StatelessWidget {
color: Colors.white,
),
),
cardTheme: CardTheme(
elevation: 0,
color: AppColors.darkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.darkBorder),
),
margin: const EdgeInsets.symmetric(vertical: 4),
),
cardColor: AppColors.darkSurface,
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.accent,
Expand Down Expand Up @@ -165,13 +201,7 @@ class OpenClawApp extends StatelessWidget {
color: AppColors.darkBorder,
space: 1,
),
dialogTheme: DialogTheme(
backgroundColor: AppColors.darkSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.darkBorder),
),
),
dialogBackgroundColor: AppColors.darkSurface,
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.darkSurfaceAlt,
contentTextStyle: GoogleFonts.inter(color: Colors.white),
Expand Down Expand Up @@ -226,15 +256,7 @@ class OpenClawApp extends StatelessWidget {
color: const Color(0xFF0A0A0A),
),
),
cardTheme: CardTheme(
elevation: 0,
color: AppColors.lightBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.lightBorder),
),
margin: const EdgeInsets.symmetric(vertical: 4),
),
cardColor: AppColors.lightBg,
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: AppColors.accent,
Expand Down Expand Up @@ -296,13 +318,7 @@ class OpenClawApp extends StatelessWidget {
color: AppColors.lightBorder,
space: 1,
),
dialogTheme: DialogTheme(
backgroundColor: AppColors.lightBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: AppColors.lightBorder),
),
),
dialogBackgroundColor: AppColors.lightBg,
snackBarTheme: SnackBarThemeData(
backgroundColor: const Color(0xFF0A0A0A),
contentTextStyle: GoogleFonts.inter(color: Colors.white),
Expand Down
97 changes: 97 additions & 0 deletions flutter_app/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';

import 'app_strings_en.dart';
import 'app_strings_ja.dart';
import 'app_strings_zh_hans.dart';
import 'app_strings_zh_hant.dart';

class AppLocalizations {
AppLocalizations(this.locale);

final Locale locale;

static const supportedLocales = [
Locale('en'),
Locale('zh'),
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
Locale('ja'),
];

static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();

static AppLocalizations of(BuildContext context) {
final localizations = Localizations.of<AppLocalizations>(
context,
AppLocalizations,
);
assert(localizations != null, 'AppLocalizations not found in context');
return localizations!;
}

String t(String key, [Map<String, Object?> params = const {}]) {
final localeKey = _localeToKey(locale);
final localized = _localizedValues[localeKey] ??
_localizedValues[locale.languageCode] ??
_localizedValues['en']!;
final fallback = _localizedValues['en']!;

var value = localized[key] ?? fallback[key] ?? key;
for (final entry in params.entries) {
value = value.replaceAll('{${entry.key}}', '${entry.value ?? ''}');
}
return value;
}

static bool isLocaleSupported(Locale locale) {
final localeKey = _localeToKey(locale);
if (_localizedValues.containsKey(localeKey)) {
return true;
}
return _localizedValues.containsKey(locale.languageCode);
}

static String _localeToKey(Locale locale) {
final scriptCode = locale.scriptCode?.toLowerCase();
final countryCode = locale.countryCode?.toUpperCase();

if (locale.languageCode == 'zh') {
if (scriptCode == 'hant') {
return 'zh-Hant';
}

if (countryCode == 'TW' || countryCode == 'HK' || countryCode == 'MO') {
return 'zh-Hant';
}
}

return locale.languageCode;
}

static final Map<String, Map<String, String>> _localizedValues = {
'en': appStringsEn,
'zh': appStringsZhHans,
'zh-Hant': appStringsZhHant,
'ja': appStringsJa,
};
}

class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();

@override
bool isSupported(Locale locale) => AppLocalizations.isLocaleSupported(locale);

@override
Future<AppLocalizations> load(Locale locale) async {
return AppLocalizations(locale);
}

@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}

extension AppLocalizationsContextExtension on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
Loading