From 8d4d91933a2d85a2fc64472211b46ddcf75ff520 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 23 May 2025 13:18:19 +0900 Subject: [PATCH 1/2] Bullet proof-localized-feature --- assets/lang/en.json | 26 ++++++++++++ lib/l10n/app_ru.arb => assets/lang/ru.json | 2 +- lib/l10n/app_th.arb => assets/lang/th.json | 2 +- lib/l10n/app_zh.arb => assets/lang/zh.json | 2 +- l10n.yaml | 4 -- lib/l10n/app_en.arb | 26 ------------ lib/localization_service.dart | 32 ++++++++++++++ lib/main.dart | 49 ++++++++-------------- lib/pages/apps/apps_page.dart | 8 ++-- lib/pages/main/location_widget.dart | 4 +- lib/pages/main/main_btn.dart | 27 +++++------- lib/pages/main/main_page.dart | 4 +- lib/pages/servers/servers_list.dart | 16 +++---- lib/pages/servers/servers_page.dart | 8 ++-- lib/search_dialog.dart | 12 +++--- pubspec.yaml | 18 ++++---- 16 files changed, 127 insertions(+), 113 deletions(-) create mode 100644 assets/lang/en.json rename lib/l10n/app_ru.arb => assets/lang/ru.json (97%) rename lib/l10n/app_th.arb => assets/lang/th.json (98%) rename lib/l10n/app_zh.arb => assets/lang/zh.json (97%) delete mode 100644 l10n.yaml delete mode 100644 lib/l10n/app_en.arb create mode 100644 lib/localization_service.dart diff --git a/assets/lang/en.json b/assets/lang/en.json new file mode 100644 index 0000000..a5c0797 --- /dev/null +++ b/assets/lang/en.json @@ -0,0 +1,26 @@ +{ + + "app_name": "VPN Client", + "apps_selection": "App Selection", + "search": "Search", + "your_location": "Your Location", + "auto_select": "Auto Select", + "kazakhstan": "Kazakhstan", + "turkey": "Turkey", + "poland": "Poland", + "fastest": "Fastest", + "selected_server": "Selected server", + "server_selection": "Server selection", + "all_servers": "All servers", + "country_name": "Country name", + "all_apps": "All Applications", + "done": "Done", + "cancel": "Cancel", + "recently_searched": "Recently searched", + "nothing_found": "Nothing found", + "connected": "CONNECTED", + "disconnected": "DISCONNECTED", + "reconnecting": "RECONNECTING", + "connecting": "CONNECTING", + "disconnecting": "DISCONNECTING" +} diff --git a/lib/l10n/app_ru.arb b/assets/lang/ru.json similarity index 97% rename from lib/l10n/app_ru.arb rename to assets/lang/ru.json index 067094e..b495f72 100644 --- a/lib/l10n/app_ru.arb +++ b/assets/lang/ru.json @@ -1,5 +1,5 @@ { - "@@locale": "ru", + "app_name": "VPN Клиент", "apps_selection": "Выбор приложений", "search": "Поиск", diff --git a/lib/l10n/app_th.arb b/assets/lang/th.json similarity index 98% rename from lib/l10n/app_th.arb rename to assets/lang/th.json index c258b47..420c726 100644 --- a/lib/l10n/app_th.arb +++ b/assets/lang/th.json @@ -1,5 +1,5 @@ { - "@@locale": "th", + "app_name": "VPN Client", "apps_selection": "เลือกแอป", "search": "ค้นหา", diff --git a/lib/l10n/app_zh.arb b/assets/lang/zh.json similarity index 97% rename from lib/l10n/app_zh.arb rename to assets/lang/zh.json index fae8b5e..3985ada 100644 --- a/lib/l10n/app_zh.arb +++ b/assets/lang/zh.json @@ -1,5 +1,5 @@ { - "@@locale": "zh", + "app_name": "VPN客户端", "apps_selection": "应用选择", "search": "搜索", diff --git a/l10n.yaml b/l10n.yaml deleted file mode 100644 index d5830f6..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,4 +0,0 @@ -synthetic-package: true -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 385ce8c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "@@locale": "en", - "app_name": "VPN Client", - "apps_selection": "App Selection", - "search": "Search", - "your_location": "Your Location", - "auto_select": "Auto Select", - "kazakhstan": "Kazakhstan", - "turkey": "Turkey", - "poland": "Poland", - "fastest": "Fastest", - "selected_server": "Selected server", - "server_selection": "Server selection", - "all_servers": "All servers", - "country_name": "Country name", - "all_apps": "All Applications", - "done": "Done", - "cancel": "Cancel", - "recently_searched": "Recently searched", - "nothing_found": "Nothing found", - "connected": "CONNECTED", - "disconnected": "DISCONNECTED", - "reconnecting": "RECONNECTING", - "connecting": "CONNECTING", - "disconnecting": "DISCONNECTING" -} diff --git a/lib/localization_service.dart b/lib/localization_service.dart new file mode 100644 index 0000000..259fbbd --- /dev/null +++ b/lib/localization_service.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +class LocalizationService { + static Map _localizedStrings = {}; + static late Locale _currentLocale; + + static Future load(Locale locale) async { + _currentLocale = locale; + String langCode = locale.languageCode; + + // Try loading the file, fallback to English + try { + final String jsonString = await rootBundle.loadString( + 'assets/lang/$langCode.json', + ); + _localizedStrings = json.decode(jsonString); + } catch (_) { + final String fallback = await rootBundle.loadString( + 'assets/lang/en.json', + ); + _localizedStrings = json.decode(fallback); + } + } + + static String to(String key) { + return _localizedStrings[key] ?? '[$key]'; + } + + static Locale get currentLocale => _currentLocale; +} diff --git a/lib/main.dart b/lib/main.dart index 334992a..341fccc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; - import 'package:vpn_client/pages/apps/apps_page.dart'; +import 'dart:ui' as ui; import 'package:vpn_client/pages/main/main_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; import 'package:vpn_client/theme_provider.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:vpn_client/vpn_state.dart'; +import 'package:vpn_client/localization_service.dart'; import 'design/colors.dart'; import 'nav_bar.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Locale userLocale = ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + await LocalizationService.load(userLocale); + runApp( MultiProvider( providers: [ @@ -36,28 +41,23 @@ class App extends StatelessWidget { final Locale? manualLocale = null; // ← use system by default return MaterialApp( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], debugShowCheckedModeBanner: false, title: 'VPN Client', theme: lightTheme, darkTheme: darkTheme, locale: manualLocale, - localeResolutionCallback: (locale, supportedLocales) { + localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); // Check for exact match - for (var supportedLocale in supportedLocales) { - if (supportedLocale.languageCode == locale.languageCode && - (supportedLocale.countryCode == null || - supportedLocale.countryCode == locale.countryCode)) { - return supportedLocale; - } - } - - // If Chinese variants are not supported, fallback to zh - if (locale.languageCode == 'zh') { - return supportedLocales.contains(const Locale('zh')) - ? const Locale('zh') - : const Locale('en'); + final supported = ['en', 'ru', 'th', 'zh']; + if (supported.contains(locale.languageCode)) { + return Locale(locale.languageCode); } // Fallback to 'en' if not found @@ -66,19 +66,6 @@ class App extends StatelessWidget { themeMode: themeProvider.themeMode, home: const MainScreen(), - - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), - Locale('ru'), - Locale('th'), - Locale('zh'), - ], ); } } diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index 9e45e79..fdd99ba 100644 --- a/lib/pages/apps/apps_page.dart +++ b/lib/pages/apps/apps_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/pages/apps/apps_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); @@ -19,7 +19,7 @@ class AppsPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.app_name, + placeholder: LocalizationService.to('app_name'), items: _apps, type: 1, ); @@ -40,7 +40,7 @@ class AppsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.apps_selection), + title: Text(LocalizationService.to('apps_selection')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -60,7 +60,7 @@ class AppsPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 889911f..53b724c 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class LocationWidget extends StatelessWidget { final Map? selectedServer; @@ -27,7 +27,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.your_location, + LocalizationService.to('your_location'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 1a1b40f..7e5d82a 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( @@ -47,21 +47,16 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { super.dispose(); } - String get connectionStatusText { - final localizations = AppLocalizations.of(context)!; + String connectionStatusText(BuildContext context) { final vpnState = Provider.of(context, listen: false); - switch (vpnState.connectionStatus) { - case ConnectionStatus.connected: - return localizations.connected; - case ConnectionStatus.disconnected: - return localizations.disconnected; - case ConnectionStatus.reconnecting: - return localizations.reconnecting; - case ConnectionStatus.disconnecting: - return localizations.disconnecting; - case ConnectionStatus.connecting: - return localizations.connecting; - } + + return { + ConnectionStatus.connected: LocalizationService.to('connected'), + ConnectionStatus.disconnected: LocalizationService.to('disconnected'), + ConnectionStatus.reconnecting: LocalizationService.to('reconnecting'), + ConnectionStatus.disconnecting: LocalizationService.to('disconnecting'), + ConnectionStatus.connecting: LocalizationService.to('connecting'), + }[vpnState.connectionStatus]!; } Future _toggleConnection(BuildContext context) async { @@ -160,7 +155,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { ), const SizedBox(height: 20), Text( - connectionStatusText, + connectionStatusText(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index f08a8e4..de76bd8 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:vpn_client/pages/main/main_btn.dart'; import 'package:vpn_client/pages/main/location_widget.dart'; import 'package:vpn_client/pages/main/stat_bar.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -55,7 +55,7 @@ class MainPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.app_name), + title: Text(LocalizationService.to('app_name')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index 7a89d8d..c4343ce 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class ServersList extends StatefulWidget { @@ -62,25 +62,25 @@ class ServersListState extends State { List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': AppLocalizations.of(context)!.auto_select, - 'ping': AppLocalizations.of(context)!.fastest, + 'text': LocalizationService.to('auto_select'), + 'ping': LocalizationService.to('fastest'), 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': AppLocalizations.of(context)!.kazakhstan, + 'text': LocalizationService.to('kazakhstan'), 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': AppLocalizations.of(context)!.turkey, + 'text': LocalizationService.to('turkey'), 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': AppLocalizations.of(context)!.poland, + 'text': LocalizationService.to('poland'), 'ping': '298', 'isActive': false, }, @@ -178,7 +178,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.selected_server, + LocalizationService.to('selected_server'), style: TextStyle(color: Colors.grey), ), ), @@ -197,7 +197,7 @@ class ServersListState extends State { Container( margin: const EdgeInsets.only(left: 10), child: Text( - AppLocalizations.of(context)!.all_servers, + LocalizationService.to('all_servers'), style: TextStyle(color: Colors.grey), ), ), diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index 41b0ad2..dda5741 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -23,7 +23,7 @@ class ServersPageState extends State { context: context, builder: (BuildContext context) { return SearchDialog( - placeholder: AppLocalizations.of(context)!.country_name, + placeholder: LocalizationService.to('country_name'), items: _servers, type: 2, ); @@ -47,7 +47,7 @@ class ServersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.selected_server), + title: Text(LocalizationService.to('selected_server')), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -67,7 +67,7 @@ class ServersPageState extends State { color: Theme.of(context).colorScheme.primary, ), onPressed: () => _showSearchDialog(context), - tooltip: AppLocalizations.of(context)!.search, + tooltip: LocalizationService.to('search'), ), ), ), diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 67d3a06..9aaa304 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/apps/apps_list_item.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; class SearchDialog extends StatefulWidget { @@ -137,7 +137,7 @@ class _SearchDialogState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.search, + LocalizationService.to('search'), style: TextStyle( fontSize: 24, fontWeight: FontWeight.w600, @@ -155,7 +155,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(widget.items); }, child: Text( - AppLocalizations.of(context)!.done, + LocalizationService.to('done'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -174,7 +174,7 @@ class _SearchDialogState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel, + LocalizationService.to('cancel'), textAlign: TextAlign.center, style: TextStyle( color: Colors.blue, @@ -248,7 +248,7 @@ class _SearchDialogState extends State { Container( margin: const EdgeInsets.only(left: 20), child: Text( - AppLocalizations.of(context)!.recently_searched, + LocalizationService.to('recently_searched'), style: TextStyle(color: Colors.grey), ), ), @@ -311,7 +311,7 @@ class _SearchDialogState extends State { ? _filteredItems.isEmpty ? Center( child: Text( - AppLocalizations.of(context)!.nothing_found, + LocalizationService.to('nothing_found'), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), diff --git a/pubspec.yaml b/pubspec.yaml index 5644e73..ef2d86e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - + fonts: - family: CustomIcons fonts: @@ -80,6 +80,10 @@ flutter: assets: - assets/images/ - assets/images/flags/ + - assets/lang/en.json + - assets/lang/zh.json + - assets/lang/ru.json + - assets/lang/th.json # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images @@ -107,12 +111,12 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package - generate: true + generate: false -l10n: - arb-dir: l10n - template-arb-file: app_en.arb - output-localization-file: app_localizations.dart - untranslated-messages-file: lib/l10n/untranslated_messages.txt +# l10n: +# arb-dir: l10n +# template-arb-file: app_en.arb +# output-localization-file: app_localizations.dart +# untranslated-messages-file: lib/l10n/untranslated_messages.txt From 60560d893de956cf5776d531922499013b65a7b9 Mon Sep 17 00:00:00 2001 From: Titan Date: Fri, 23 May 2025 13:22:11 +0900 Subject: [PATCH 2/2] Bullet proof-localized-feature --- lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 341fccc..7415286 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,8 @@ import 'nav_bar.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - Locale userLocale = ui.PlatformDispatcher.instance.locale; // <-- Get the system locale + Locale userLocale = + ui.PlatformDispatcher.instance.locale; // <-- Get the system locale await LocalizationService.load(userLocale); runApp(