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..7415286 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,24 @@ 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 +42,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 +67,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.lock b/pubspec.lock index 7c0530a..d3e7c33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -273,18 +273,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -598,10 +598,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: 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