From dca33dce83df762804ca2fb036e9e782f86a7fba Mon Sep 17 00:00:00 2001 From: basemosama Date: Wed, 1 Apr 2026 15:46:44 +0200 Subject: [PATCH 1/4] feat: support scriptCode, linked translation files, and extra asset loaders This update introduces several enhancements to the localization system, including support for script codes, the ability to resolve linked translation files, and support for multiple asset loaders. Key changes: - **XLocale & Locale Extensions**: Added `scriptCode` support to `XLocale` and updated `XLocaleExtension` to handle `Locale.fromSubtags` when a script code is present. - **Linked Files Support**: Introduced `LinkedFileResolver` and `JsonLinkedFileResolver` to allow translations to reference other JSON files using the `:/filename.json` syntax. - **Flexible File Loading**: Added `FileLoader` interface with `RootBundleFileLoader` (for Flutter) and `IOFileLoader` (for CLI/Dart IO) implementations. - **AssetLoader Enhancements**: `AssetLoader` now requires a `fileLoader` and `linkedFileResolver`. `RootBundleAssetLoader` includes factory methods for root bundle and IO-based loading. - **Multiple Asset Loaders**: `PlayxLocaleConfig` now accepts `extraAssetLoaders`, and `TranslationManager` has been updated to merge translation data from all provided loaders recursively. - **New Configuration Options**: Added `useFallbackTranslationsForEmptyResources`, `ignorePluralRules`, and a custom `errorWidget` to `PlayxLocaleConfig`. - **Controller Improvements**: - Updated locale searching to use `Locale.supports` for better matching. - Enhanced `updateToDeviceLocale` and added `resetLocale` to better handle platform-specific locale detection. - Updated translation loading logic to pass new configuration flags to the underlying `Localization` instance. --- lib/src/config/playx_locale_config.dart | 24 +++- lib/src/controller/controller.dart | 55 ++++++--- lib/src/controller/translation_manager.dart | 44 ++++++-- .../delegate/playx_localization_delegate.dart | 15 ++- lib/src/easy_localization/asset_loader.dart | 45 +++++++- .../file_loaders/file_loader.dart | 5 + .../file_loaders/io_file_loader.dart | 17 +++ .../file_loaders/root_bundle_file_loader.dart | 13 +++ .../linked_file_resolver.dart | 106 ++++++++++++++++++ lib/src/extensions/locale_extensions.dart | 11 +- lib/src/model/x_locale.dart | 5 +- 11 files changed, 303 insertions(+), 37 deletions(-) create mode 100644 lib/src/easy_localization/file_loaders/file_loader.dart create mode 100644 lib/src/easy_localization/file_loaders/io_file_loader.dart create mode 100644 lib/src/easy_localization/file_loaders/root_bundle_file_loader.dart create mode 100644 lib/src/easy_localization/linked_file_resolver.dart diff --git a/lib/src/config/playx_locale_config.dart b/lib/src/config/playx_locale_config.dart index bd17e4a..4f3f1ee 100644 --- a/lib/src/config/playx_locale_config.dart +++ b/lib/src/config/playx_locale_config.dart @@ -2,6 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:playx_localization/src/delegate/playx_localization_delegate.dart'; import '../../playx_localization.dart'; +import '../easy_localization/file_loaders/root_bundle_file_loader.dart'; +import '../easy_localization/linked_file_resolver.dart'; /// Locale config : /// used to configure out app locales by providing the app with the supported locales and localization settings. @@ -64,6 +66,19 @@ class PlayxLocaleConfig { /// Additional custom delegates, e.g., from third-party packages. final List? extraDelegates; + /// If a localization key is empty in the locale file, try to use the fallbackLocale file. + /// Does not take effect if [useFallbackTranslations] is false. + /// @Default value false + final bool useFallbackTranslationsForEmptyResources; + + /// Ignore usage of plural strings for languages that do not use plural rules. + /// @Default value true + final bool ignorePluralRules; + + /// Class loader for localization files that belong to other packages. + /// @Default value `null` + final List? extraAssetLoaders; + /// Custom localization delegate builder. /// This allows you to create a custom list of delegates based on the provided [PlayxLocalizationDelegate]. final List Function( @@ -75,8 +90,15 @@ class PlayxLocaleConfig { this.fallbackLocale, this.useOnlyLangCode = false, this.useFallbackTranslations = true, + this.useFallbackTranslationsForEmptyResources = false, + this.ignorePluralRules = true, this.path = 'assets/translations', - this.assetLoader = const RootBundleAssetLoader(), + this.assetLoader = const RootBundleAssetLoader( + fileLoader: RootBundleFileLoader(), + linkedFileResolver: + JsonLinkedFileResolver(fileLoader: RootBundleFileLoader()), + ), + this.extraAssetLoaders, this.saveLocale = true, this.logMissingKeys = false, this.logLocaleChanges = true, diff --git a/lib/src/controller/controller.dart b/lib/src/controller/controller.dart index e39036c..871da61 100644 --- a/lib/src/controller/controller.dart +++ b/lib/src/controller/controller.dart @@ -129,12 +129,10 @@ class PlayxLocaleController extends ValueNotifier { if (config.startLocale != null) return config.startLocale!; if (deviceLocale != null) { - final searchedLocaleByCountryCode = supportedXLocales.firstWhereOrNull( - (e) => - e.languageCode == deviceLocale!.languageCode && - e.countryCode == deviceLocale!.countryCode); - if (searchedLocaleByCountryCode != null) { - return searchedLocaleByCountryCode; + final searchedLocale = supportedXLocales.firstWhereOrNull( + (e) => e.locale.supports(deviceLocale!)); + if (searchedLocale != null) { + return searchedLocale; } final searchedLocaleByOnlyLanguageCode = @@ -169,8 +167,14 @@ class PlayxLocaleController extends ValueNotifier { _translations = res.translations; _fallbackTranslations = res.fallbackTranslations; - Localization.load(locale.locale, - translations: translations, fallbackTranslations: fallbackTranslations); + Localization.load( + locale.locale, + translations: translations, + fallbackTranslations: fallbackTranslations, + useFallbackTranslationsForEmptyResources: + config.useFallbackTranslationsForEmptyResources, + ignorePluralRules: config.ignorePluralRules, + ); } /// update the locale to be one of the supported locales. @@ -234,13 +238,17 @@ class PlayxLocaleController extends ValueNotifier { /// Search for locale by language code and country code if available. XLocale? searchLocaleByLanguageCode( - {required String languageCode, String? countryCode}) { - final searchedLocaleByCountryCode = supportedXLocales.firstWhereOrNull( - (e) => e.languageCode == languageCode && e.countryCode == countryCode); - if (searchedLocaleByCountryCode != null) { - return searchedLocaleByCountryCode; + {required String languageCode, String? countryCode, String? scriptCode}) { + final searchLocale = Locale.fromSubtags( + languageCode: languageCode, + countryCode: countryCode, + scriptCode: scriptCode); + final searchedLocale = supportedXLocales.firstWhereOrNull( + (e) => e.locale.supports(searchLocale)); + if (searchedLocale != null) { + return searchedLocale; } - //if not found by country code then search by language code only. + //if not found then search by language code only. final searchedLocaleByOnlyLanguageCode = supportedXLocales .firstWhereOrNull((e) => e.languageCode == languageCode); if (searchedLocaleByOnlyLanguageCode != null) { @@ -255,9 +263,10 @@ class PlayxLocaleController extends ValueNotifier { Future updateByLanguageCode( {required String languageCode, String? countryCode, + String? scriptCode, bool forceAppUpdate = false}) async { final locale = searchLocaleByLanguageCode( - languageCode: languageCode, countryCode: countryCode); + languageCode: languageCode, countryCode: countryCode, scriptCode: scriptCode); if (locale != null) { return updateTo( locale, @@ -271,14 +280,26 @@ class PlayxLocaleController extends ValueNotifier { /// if the locale is not supported it will return false. /// if [forceAppUpdate] is true it will force the app to update. Future updateToDeviceLocale({bool forceAppUpdate = false}) async { - final locale = deviceLocale; - if (locale == null) return false; + final foundPlatformLocale = await findSystemLocale(); + final locale = foundPlatformLocale.toLocale(); + deviceLocale = locale; return updateByLanguageCode( languageCode: locale.languageCode, countryCode: locale.countryCode, + scriptCode: locale.scriptCode, forceAppUpdate: forceAppUpdate); } + /// Reset locale to platform locale or fallback locale. + Future resetLocale({bool forceAppUpdate = false}) async { + final foundPlatformLocale = await findSystemLocale(); + deviceLocale = foundPlatformLocale.toLocale(); + final locale = _getStartLocale(savedLocale: null); + + logger?.i('Reset locale to ${locale.name} while the platform locale is $deviceLocale'); + await updateTo(locale, forceAppUpdate: forceAppUpdate); + } + /// Update the locale to be one of the supported locales. /// if [forceAppUpdate] is true it will force the app to update. Future _updateLocale({ diff --git a/lib/src/controller/translation_manager.dart b/lib/src/controller/translation_manager.dart index f6ab38f..6772dbf 100644 --- a/lib/src/controller/translation_manager.dart +++ b/lib/src/controller/translation_manager.dart @@ -75,17 +75,45 @@ class TranslationManager { static Future> loadTranslationData( {required XLocale locale, required PlayxLocaleConfig config}) async { - late Map? data; + final result = {}; + final loaderFutures = ?>>[]; - if (config.useOnlyLangCode) { - data = await config.assetLoader - .load(config.path, Locale(locale.languageCode)); - } else { - data = await config.assetLoader.load(config.path, locale.locale); + final Locale desiredLocale = config.useOnlyLangCode + ? Locale.fromSubtags( + languageCode: locale.languageCode, scriptCode: locale.scriptCode) + : locale.locale; + + List loaders = [ + config.assetLoader, + if (config.extraAssetLoaders != null) ...config.extraAssetLoaders! + ]; + + for (final loader in loaders) { + loaderFutures.add(loader.load(config.path, desiredLocale)); } - if (data == null) return {}; + await Future.wait(loaderFutures).then((List?> value) { + for (final Map? map in value) { + if (map != null) { + result.addAllRecursive(map); + } + } + }); + + return result; + } +} - return data; +extension MapExtension on Map { + void addAllRecursive(Map other) { + other.forEach((key, value) { + if (this[key] == null) { + this[key] = value; + } else if (this[key] is Map && value is Map) { + (this[key] as Map).addAllRecursive(value); + } else { + this[key] = value; + } + }); } } diff --git a/lib/src/delegate/playx_localization_delegate.dart b/lib/src/delegate/playx_localization_delegate.dart index 8f96597..40e1354 100644 --- a/lib/src/delegate/playx_localization_delegate.dart +++ b/lib/src/delegate/playx_localization_delegate.dart @@ -25,15 +25,22 @@ class PlayxLocalizationDelegate extends LocalizationsDelegate { Future load(Locale locale) async { if (localizationController!.translations == null) { final xLocale = localizationController!.searchLocaleByLanguageCode( - languageCode: locale.languageCode, countryCode: locale.countryCode); + languageCode: locale.languageCode, + countryCode: locale.countryCode, + scriptCode: locale.scriptCode); if (xLocale == null) { throw UnsupportedError('Locale not found'); } await localizationController!.loadTranslations(xLocale); } - Localization.load(locale, - translations: localizationController!.translations, - fallbackTranslations: localizationController!.fallbackTranslations); + Localization.load( + locale, + translations: localizationController!.translations, + fallbackTranslations: localizationController!.fallbackTranslations, + useFallbackTranslationsForEmptyResources: localizationController! + .config.useFallbackTranslationsForEmptyResources, + ignorePluralRules: localizationController!.config.ignorePluralRules, + ); return Future.value(Localization.instance); } diff --git a/lib/src/easy_localization/asset_loader.dart b/lib/src/easy_localization/asset_loader.dart index 7858df3..c14eb1e 100644 --- a/lib/src/easy_localization/asset_loader.dart +++ b/lib/src/easy_localization/asset_loader.dart @@ -1,8 +1,12 @@ import 'dart:convert'; import 'dart:ui'; -import 'package:flutter/services.dart'; -import 'package:playx_localization/src/extensions/locale_extensions.dart'; +import '../../playx_localization.dart'; +import '../controller/controller.dart'; +import 'file_loaders/file_loader.dart'; +import 'file_loaders/io_file_loader.dart'; +import 'file_loaders/root_bundle_file_loader.dart'; +import 'linked_file_resolver.dart'; /// abstract class used to building your Custom AssetLoader /// Example: @@ -16,7 +20,13 @@ import 'package:playx_localization/src/extensions/locale_extensions.dart'; ///} /// ``` abstract class AssetLoader { - const AssetLoader(); + // Place inside class RootBundleAssetLoader + final FileLoader fileLoader; + final LinkedFileResolver linkedFileResolver; + + const AssetLoader( + {required this.linkedFileResolver, required this.fileLoader}); + Future?> load(String path, Locale locale); } @@ -24,7 +34,23 @@ abstract class AssetLoader { /// default used is RootBundleAssetLoader which uses flutter's assetloader /// class RootBundleAssetLoader extends AssetLoader { - const RootBundleAssetLoader(); + const RootBundleAssetLoader( + {required super.linkedFileResolver, required super.fileLoader}); + + factory RootBundleAssetLoader.fromRootBundle() { + return const RootBundleAssetLoader( + linkedFileResolver: + JsonLinkedFileResolver(fileLoader: RootBundleFileLoader()), + fileLoader: RootBundleFileLoader(), + ); + } + + factory RootBundleAssetLoader.fromIOFile() { + return const RootBundleAssetLoader( + linkedFileResolver: JsonLinkedFileResolver(fileLoader: IOFileLoader()), + fileLoader: IOFileLoader(), + ); + } String getLocalePath(String basePath, Locale locale) { return '$basePath/${locale.toStringWithSeparator(separator: "-")}.json'; @@ -33,6 +59,15 @@ class RootBundleAssetLoader extends AssetLoader { @override Future?> load(String path, Locale locale) async { var localePath = getLocalePath(path, locale); - return json.decode(await rootBundle.loadString(localePath)); + PlayxLocaleController.logger?.debug('Load asset from $path'); + + Map baseJson = + json.decode(await fileLoader.loadString(localePath)); + return await linkedFileResolver.resolveLinkedFiles( + basePath: path, + languageCode: locale.languageCode, + countryCode: locale.countryCode, + baseJson: baseJson, + ); } } diff --git a/lib/src/easy_localization/file_loaders/file_loader.dart b/lib/src/easy_localization/file_loaders/file_loader.dart new file mode 100644 index 0000000..822b162 --- /dev/null +++ b/lib/src/easy_localization/file_loaders/file_loader.dart @@ -0,0 +1,5 @@ +/// Abstract file loader interface to allow different implementations +/// for Flutter runtime (using rootBundle) and CLI (using dart:io) +abstract class FileLoader { + Future loadString(String path); +} diff --git a/lib/src/easy_localization/file_loaders/io_file_loader.dart b/lib/src/easy_localization/file_loaders/io_file_loader.dart new file mode 100644 index 0000000..3016bbf --- /dev/null +++ b/lib/src/easy_localization/file_loaders/io_file_loader.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'file_loader.dart'; + +/// File loader implementation for Dart CLI applications using dart:io +class IOFileLoader implements FileLoader { + const IOFileLoader(); + + @override + Future loadString(String path) async { + final file = File(path); + if (!file.existsSync()) { + throw FileSystemException('File not found', path); + } + return file.readAsString(); + } +} diff --git a/lib/src/easy_localization/file_loaders/root_bundle_file_loader.dart b/lib/src/easy_localization/file_loaders/root_bundle_file_loader.dart new file mode 100644 index 0000000..9407c70 --- /dev/null +++ b/lib/src/easy_localization/file_loaders/root_bundle_file_loader.dart @@ -0,0 +1,13 @@ +import 'package:flutter/services.dart'; + +import 'file_loader.dart'; + +/// File loader implementation for Flutter applications using rootBundle +class RootBundleFileLoader implements FileLoader { + const RootBundleFileLoader(); + + @override + Future loadString(String path) async { + return rootBundle.loadString(path); + } +} diff --git a/lib/src/easy_localization/linked_file_resolver.dart b/lib/src/easy_localization/linked_file_resolver.dart new file mode 100644 index 0000000..93999c9 --- /dev/null +++ b/lib/src/easy_localization/linked_file_resolver.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'file_loaders/file_loader.dart'; + +/// Resolves linked translation files by loading referenced files and merging them +/// into the base JSON structure. Handles the ':/filename.json' syntax used in linked files. + +abstract class LinkedFileResolver { + final int maxLinkedDepth = 32; + final FileLoader fileLoader; + + const LinkedFileResolver({required this.fileLoader}); + + Future> resolveLinkedFiles({ + required String basePath, + required String languageCode, + required Map baseJson, + Set? visited, + int depth = 0, + String? countryCode, + }); + + String getLinkedLocalePath( + String basePath, String filePath, String languageCode, + {String? countryCode}) { + if (countryCode != null) { + return '$basePath/$languageCode-$countryCode/$filePath'; + } + + return '$basePath/$languageCode/$filePath'; + } +} + +class JsonLinkedFileResolver extends LinkedFileResolver { + const JsonLinkedFileResolver({required FileLoader fileLoader}) + : super(fileLoader: fileLoader); + + @override + Future> resolveLinkedFiles({ + required String basePath, + required String languageCode, + required Map baseJson, + Set? visited, + int depth = 0, + String? countryCode, + }) async { + visited ??= {}; + + if (depth > maxLinkedDepth) { + throw StateError( + 'Maximum linked files depth ($maxLinkedDepth) exceeded for $languageCode at $basePath.'); + } + + final Map fullJson = Map.from(baseJson); + + for (final entry in baseJson.entries) { + final key = entry.key; + var value = entry.value; + + if (value is String && value.startsWith(':/')) { + final rawPath = value.substring(2).trim(); + final linkedAssetPath = getLinkedLocalePath( + basePath, rawPath, languageCode, + countryCode: countryCode); + + if (visited.contains(linkedAssetPath)) { + throw StateError( + 'Cyclic linked files detected at "$linkedAssetPath" (key: "$key").'); + } + + try { + final linkedContent = await fileLoader.loadString(linkedAssetPath); + final Map linkedJson = + json.decode(linkedContent) as Map; + + visited.add(linkedAssetPath); + + final resolved = await resolveLinkedFiles( + basePath: basePath, + languageCode: languageCode, + baseJson: linkedJson, + visited: visited, + depth: depth + 1, + countryCode: countryCode, + ); + fullJson[key] = resolved; + } catch (e) { + throw StateError( + 'Error resolving linked file "$linkedAssetPath" for key "$key": $e', + ); + } + } else if (value is Map) { + fullJson[key] = await resolveLinkedFiles( + basePath: basePath, + languageCode: languageCode, + baseJson: value, + visited: visited, + depth: depth, + countryCode: countryCode, + ); + } + } + + return fullJson; + } +} diff --git a/lib/src/extensions/locale_extensions.dart b/lib/src/extensions/locale_extensions.dart index 8fd7682..785bffb 100644 --- a/lib/src/extensions/locale_extensions.dart +++ b/lib/src/extensions/locale_extensions.dart @@ -38,7 +38,16 @@ extension LocaleExtension on Locale { } extension XLocaleExtension on XLocale { - Locale get locale => Locale(languageCode, countryCode); + Locale get locale { + if (scriptCode != null && scriptCode!.isNotEmpty) { + return Locale.fromSubtags( + languageCode: languageCode, + countryCode: countryCode, + scriptCode: scriptCode, + ); + } + return Locale(languageCode, countryCode); + } String toStringWithSeparator({String separator = '-'}) { return locale.toStringWithSeparator(separator: separator); diff --git a/lib/src/model/x_locale.dart b/lib/src/model/x_locale.dart index eb36ec2..b06d0d5 100644 --- a/lib/src/model/x_locale.dart +++ b/lib/src/model/x_locale.dart @@ -8,6 +8,7 @@ class XLocale extends Equatable { final String name; final String languageCode; final String? countryCode; + final String? scriptCode; final String? fontFamily; const XLocale({ @@ -15,9 +16,11 @@ class XLocale extends Equatable { required this.name, required this.languageCode, this.countryCode, + this.scriptCode, this.fontFamily, }); @override - List get props => [id, name, languageCode, countryCode, fontFamily]; + List get props => + [id, name, languageCode, countryCode, scriptCode, fontFamily]; } From 299c1b7cdf9d357f9c4d3c7b108ef8d07a69ebaa Mon Sep 17 00:00:00 2001 From: basemosama Date: Wed, 1 Apr 2026 15:50:15 +0200 Subject: [PATCH 2/4] Feat: Update to v0.4.0 - CU-86c6yv89p feat: Update dependencies and improve cross-platform file loading This release introduces support for conditional imports for the `IOFileLoader` to ensure compatibility with web platforms where `dart:io` is unavailable. Other changes include: - Bumping `playx_core` to `^1.0.0`. - Updating `flutter_secure_storage` to `v10.0.0` and migrating macOS/iOS integration to use `flutter_secure_storage_darwin`. - Bumping SDK constraints to Dart `^3.9.0` and Flutter `^3.35.0`. - Updating various internal dependencies including `get_it`, `shared_preferences`, and `meta`. - Adding new localization features: fallback translations for empty resources, `scriptCode` support in `XLocale`, deep translation merging, and device sync reset. - Updating package version to `0.4.0`. --- CHANGELOG.md | 6 ++ example/.flutter-plugins-dependencies | 2 +- .../Flutter/GeneratedPluginRegistrant.swift | 4 +- example/pubspec.lock | 78 +++++++++---------- .../file_loaders/io_file_loader.dart | 19 +---- .../file_loaders/io_file_loader_io.dart | 17 ++++ .../file_loaders/io_file_loader_stub.dart | 11 +++ pubspec.yaml | 8 +- 8 files changed, 78 insertions(+), 67 deletions(-) create mode 100644 lib/src/easy_localization/file_loaders/io_file_loader_io.dart create mode 100644 lib/src/easy_localization/file_loaders/io_file_loader_stub.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 1957e4b..f4ee542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.2 +- **Enhanced Locale Config**: Added `useFallbackTranslationsForEmptyResources`, `ignorePluralRules`, `extraAssetLoaders`, and `errorWidget` to `PlayxLocaleConfig`. +- **Script Code Support**: Added `scriptCode` natively to `XLocale` and updated locale matching in `PlayxLocaleController` to properly utilize it. +- **Deep Translation Merging**: `TranslationManager` now supports combining JSON translation maps securely from multiple asset loaders. +- **Device Sync Reset**: Introduced `resetLocale()` to safely recalculate and synchronize the application's locale against device-level configurations. + ## 0.3.1 - Bumping `playx_core` dependency to `^0.7.4`. - Renaming `isRtl` and `isLtr` getters to `isCurrentLocaleRtl` and `isCurrentLocaleLtr` respectively for better clarity. diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index b0d50d8..55df1fd 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_secure_storage","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_secure_storage","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage-9.2.4/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.10/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_secure_storage_macos","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_secure_storage_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"flutter_secure_storage_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"flutter_secure_storage_web","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_linux","flutter_secure_storage_macos","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_macos","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2025-11-18 00:44:55.505853","version":"3.35.7","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_secure_storage_darwin","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_darwin-0.2.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"flutter_secure_storage","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage-10.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_android-2.2.17/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_android","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.10/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"flutter_secure_storage_darwin","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_darwin-0.2.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"flutter_secure_storage_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_linux-3.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false}],"windows":[{"name":"flutter_secure_storage_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_windows-4.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false}],"web":[{"name":"flutter_secure_storage_web","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/flutter_secure_storage_web-2.1.0/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/Users/basemosama/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"flutter_secure_storage","dependencies":["flutter_secure_storage_darwin","flutter_secure_storage_linux","flutter_secure_storage_web","flutter_secure_storage_windows"]},{"name":"flutter_secure_storage_darwin","dependencies":[]},{"name":"flutter_secure_storage_linux","dependencies":[]},{"name":"flutter_secure_storage_web","dependencies":[]},{"name":"flutter_secure_storage_windows","dependencies":["path_provider"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2026-04-01 15:49:06.975748","version":"3.38.7","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 37af1fe..66f641b 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,12 @@ import FlutterMacOS import Foundation -import flutter_secure_storage_macos +import flutter_secure_storage_darwin import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/example/pubspec.lock b/example/pubspec.lock index 7d7ae1f..3df66a5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" fake_async: dependency: transitive description: @@ -119,50 +119,50 @@ packages: dependency: transitive description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 url: "https://pub.dev" source: hosted - version: "9.2.4" - flutter_secure_storage_linux: + version: "10.0.0" + flutter_secure_storage_darwin: dependency: transitive description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" url: "https://pub.dev" source: hosted - version: "1.2.3" - flutter_secure_storage_macos: + version: "0.2.0" + flutter_secure_storage_linux: dependency: transitive description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -177,10 +177,10 @@ packages: dependency: transitive description: name: get_it - sha256: "84792561b731b6463d053e9761a5236da967c369da10b134b8585a5e18429956" + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" url: "https://pub.dev" source: hosted - version: "9.0.5" + version: "9.2.1" intl: dependency: transitive description: @@ -189,14 +189,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" leak_tracker: dependency: transitive description: @@ -249,10 +241,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -321,17 +313,17 @@ packages: dependency: transitive description: name: playx_core - sha256: b49e07caeda91353f04f68d52c751fdec0513faf46646c74ef785ca258fd1683 + sha256: "1d92e7d33d1132b2fabd779149b9a95b2dbf4b8b0f8f6ea28cad76950ef12f94" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "1.0.0" playx_localization: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.3.1" + version: "0.4.0" plugin_platform_interface: dependency: transitive description: @@ -344,10 +336,10 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.5" shared_preferences_android: dependency: transitive description: @@ -376,10 +368,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -445,10 +437,10 @@ packages: dependency: transitive description: name: talker_logger - sha256: "8218836d871ea5ab1ec616cffe3cdae84e8fb44022d5cc04c95d7b220572b8fb" + sha256: cea1b8283a28c2118a0b197057fc5beb5b0672c75e40a48725e5e452c0278ff3 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.1.16" term_glyph: dependency: transitive description: @@ -461,10 +453,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" vector_math: dependency: transitive description: @@ -514,5 +506,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/lib/src/easy_localization/file_loaders/io_file_loader.dart b/lib/src/easy_localization/file_loaders/io_file_loader.dart index 3016bbf..4b4ce87 100644 --- a/lib/src/easy_localization/file_loaders/io_file_loader.dart +++ b/lib/src/easy_localization/file_loaders/io_file_loader.dart @@ -1,17 +1,2 @@ -import 'dart:io'; - -import 'file_loader.dart'; - -/// File loader implementation for Dart CLI applications using dart:io -class IOFileLoader implements FileLoader { - const IOFileLoader(); - - @override - Future loadString(String path) async { - final file = File(path); - if (!file.existsSync()) { - throw FileSystemException('File not found', path); - } - return file.readAsString(); - } -} +export 'io_file_loader_stub.dart' + if (dart.library.io) 'io_file_loader_io.dart'; diff --git a/lib/src/easy_localization/file_loaders/io_file_loader_io.dart b/lib/src/easy_localization/file_loaders/io_file_loader_io.dart new file mode 100644 index 0000000..3016bbf --- /dev/null +++ b/lib/src/easy_localization/file_loaders/io_file_loader_io.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'file_loader.dart'; + +/// File loader implementation for Dart CLI applications using dart:io +class IOFileLoader implements FileLoader { + const IOFileLoader(); + + @override + Future loadString(String path) async { + final file = File(path); + if (!file.existsSync()) { + throw FileSystemException('File not found', path); + } + return file.readAsString(); + } +} diff --git a/lib/src/easy_localization/file_loaders/io_file_loader_stub.dart b/lib/src/easy_localization/file_loaders/io_file_loader_stub.dart new file mode 100644 index 0000000..5ecc573 --- /dev/null +++ b/lib/src/easy_localization/file_loaders/io_file_loader_stub.dart @@ -0,0 +1,11 @@ +import 'file_loader.dart'; + +/// Stub File loader implementation for web applications where dart:io is unavailable. +class IOFileLoader implements FileLoader { + const IOFileLoader(); + + @override + Future loadString(String path) async { + throw UnsupportedError('dart:io is not supported on the web.'); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0e8ff66..cfac4d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: playx_localization description: Easily manage and update app localization with a simple implementation and a lot of utilities. -version: 0.3.1 +version: 0.4.0 homepage: https://sourcya.io repository: https://github.com/playx-flutter/playx_localization issue_tracker: https://github.com/playx-flutter/playx_localization/issues @@ -19,15 +19,15 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - playx_core: ^0.7.4 + playx_core: ^1.0.0 intl: ^0.20.2 - shared_preferences_platform_interface: ^2.4.1 + shared_preferences_platform_interface: ^2.4.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - lints: ^6.0.0 + lints: ^6.1.0 flutter: From 95e5041f749f646cb695dc8df38ab6ebf77d878b Mon Sep 17 00:00:00 2001 From: basemosama Date: Wed, 1 Apr 2026 17:05:13 +0200 Subject: [PATCH 3/4] feat: Synchronous Any-Locale Translation - **Synchronous Any-Locale Translation**: Added `preloadSupportedLocales` to `PlayxLocaleConfig`. When enabled, it caches all dictionaries during initialization, allowing developers to query translations for any loaded `Locale? locale` parametrically using `tr(..., locale: ...)` synchronously, completely out-of-bounds, without overriding the app environment. --- CHANGELOG.md | 4 +- lib/src/config/playx_locale_config.dart | 6 ++ lib/src/controller/controller.dart | 10 ++- lib/src/controller/translation_manager.dart | 25 +++++- .../delegate/playx_localization_delegate.dart | 1 + lib/src/easy_localization/localization.dart | 78 ++++++++++++++----- lib/src/easy_localization/public.dart | 8 +- .../playx_localization_extensions.dart | 15 +++- 8 files changed, 116 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ee542..33895eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## 0.3.2 +## 0.4.0 - **Enhanced Locale Config**: Added `useFallbackTranslationsForEmptyResources`, `ignorePluralRules`, `extraAssetLoaders`, and `errorWidget` to `PlayxLocaleConfig`. - **Script Code Support**: Added `scriptCode` natively to `XLocale` and updated locale matching in `PlayxLocaleController` to properly utilize it. - **Deep Translation Merging**: `TranslationManager` now supports combining JSON translation maps securely from multiple asset loaders. - **Device Sync Reset**: Introduced `resetLocale()` to safely recalculate and synchronize the application's locale against device-level configurations. - +- **Synchronous Any-Locale Translation**: Added `preloadSupportedLocales` to `PlayxLocaleConfig`. When enabled, it caches all dictionaries during initialization, allowing developers to query translations for any loaded `Locale? locale` parametrically using `tr(..., locale: ...)` synchronously, completely out-of-bounds, without overriding the app environment. ## 0.3.1 - Bumping `playx_core` dependency to `^0.7.4`. - Renaming `isRtl` and `isLtr` getters to `isCurrentLocaleRtl` and `isCurrentLocaleLtr` respectively for better clarity. diff --git a/lib/src/config/playx_locale_config.dart b/lib/src/config/playx_locale_config.dart index 4f3f1ee..6e7ea97 100644 --- a/lib/src/config/playx_locale_config.dart +++ b/lib/src/config/playx_locale_config.dart @@ -79,6 +79,11 @@ class PlayxLocaleConfig { /// @Default value `null` final List? extraAssetLoaders; + /// When true, all supported locales are preloaded into memory when the controller initializes. + /// This allows translating to arbitrary locales synchronously using `tr(locale: ...)` without + /// updating the app locale. Warning: Doing this might consume more memory if there are many locales. + final bool preloadSupportedLocales; + /// Custom localization delegate builder. /// This allows you to create a custom list of delegates based on the provided [PlayxLocalizationDelegate]. final List Function( @@ -92,6 +97,7 @@ class PlayxLocaleConfig { this.useFallbackTranslations = true, this.useFallbackTranslationsForEmptyResources = false, this.ignorePluralRules = true, + this.preloadSupportedLocales = false, this.path = 'assets/translations', this.assetLoader = const RootBundleAssetLoader( fileLoader: RootBundleFileLoader(), diff --git a/lib/src/controller/controller.dart b/lib/src/controller/controller.dart index 871da61..d748646 100644 --- a/lib/src/controller/controller.dart +++ b/lib/src/controller/controller.dart @@ -36,6 +36,7 @@ class PlayxLocaleController extends ValueNotifier { late PlayxLocalizationDelegate delegate; Translations? _translations, _fallbackTranslations; + Map? _preloadedTranslations; /// current translations loaded from assets. Translations? get translations => _translations; @@ -43,6 +44,9 @@ class PlayxLocaleController extends ValueNotifier { /// current fallback translations loaded from assets. Translations? get fallbackTranslations => _fallbackTranslations; + /// all preloaded translations loaded from assets, if config.preloadSupportedLocales is true. + Map? get preloadedTranslations => _preloadedTranslations; + // Returns the device locale. Locale? deviceLocale; @@ -166,11 +170,13 @@ class PlayxLocaleController extends ValueNotifier { ); _translations = res.translations; _fallbackTranslations = res.fallbackTranslations; + _preloadedTranslations = res.preloadedTranslations; Localization.load( locale.locale, - translations: translations, - fallbackTranslations: fallbackTranslations, + translations: _translations, + fallbackTranslations: _fallbackTranslations, + preloadedTranslations: _preloadedTranslations, useFallbackTranslationsForEmptyResources: config.useFallbackTranslationsForEmptyResources, ignorePluralRules: config.ignorePluralRules, diff --git a/lib/src/controller/translation_manager.dart b/lib/src/controller/translation_manager.dart index 6772dbf..5df53e3 100644 --- a/lib/src/controller/translation_manager.dart +++ b/lib/src/controller/translation_manager.dart @@ -12,6 +12,7 @@ class TranslationManager { ({ Translations? translations, Translations? fallbackTranslations, + Map? preloadedTranslations, })> loadTranslations({ required XLocale locale, bool useFallbackTranslations = true, @@ -19,7 +20,19 @@ class TranslationManager { required XLocale fallbackLocale, }) async { Map data; + Map? preloadedTranslations; + try { + if (config.preloadSupportedLocales) { + preloadedTranslations = {}; + for (final supportedXLocale in config.supportedLocales) { + final supportedData = await loadTranslationData( + locale: supportedXLocale, config: config); + preloadedTranslations[supportedXLocale.locale] = + Translations(Map.from(supportedData)); + } + } + data = Map.from(await loadTranslationData(locale: locale, config: config)); final translations = Translations(data); @@ -41,15 +54,20 @@ class TranslationManager { final fallbackTranslations = Translations(data); return ( translations: translations, - fallbackTranslations: fallbackTranslations + fallbackTranslations: fallbackTranslations, + preloadedTranslations: preloadedTranslations, ); } - return (translations: translations, fallbackTranslations: null); + return ( + translations: translations, + fallbackTranslations: null, + preloadedTranslations: preloadedTranslations, + ); } on FlutterError catch (e, s) { // onLoadError(e); PlayxLocaleController.logger ?.error('Error loading translations: ', error: e, stackTrace: s); - return (translations: null, fallbackTranslations: null); + return (translations: null, fallbackTranslations: null, preloadedTranslations: null); } catch (e, s) { PlayxLocaleController.logger ?.error('Error loading translations: ', error: e, stackTrace: s); @@ -57,6 +75,7 @@ class TranslationManager { return ( translations: null, fallbackTranslations: null, + preloadedTranslations: null, ); } } diff --git a/lib/src/delegate/playx_localization_delegate.dart b/lib/src/delegate/playx_localization_delegate.dart index 40e1354..4772293 100644 --- a/lib/src/delegate/playx_localization_delegate.dart +++ b/lib/src/delegate/playx_localization_delegate.dart @@ -37,6 +37,7 @@ class PlayxLocalizationDelegate extends LocalizationsDelegate { locale, translations: localizationController!.translations, fallbackTranslations: localizationController!.fallbackTranslations, + preloadedTranslations: localizationController!.preloadedTranslations, useFallbackTranslationsForEmptyResources: localizationController! .config.useFallbackTranslationsForEmptyResources, ignorePluralRules: localizationController!.config.ignorePluralRules, diff --git a/lib/src/easy_localization/localization.dart b/lib/src/easy_localization/localization.dart index 68af8c5..8a8384c 100644 --- a/lib/src/easy_localization/localization.dart +++ b/lib/src/easy_localization/localization.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; +import 'package:playx_localization/playx_localization.dart'; import '../controller/controller.dart'; import 'plural_rules.dart'; @@ -7,6 +8,7 @@ import 'translations.dart'; class Localization { Translations? _translations, _fallbackTranslations; + Map? _preloadedTranslations; late Locale _locale; final RegExp _replaceArgRegex = RegExp('{}'); @@ -36,12 +38,14 @@ class Localization { Locale locale, { Translations? translations, Translations? fallbackTranslations, + Map? preloadedTranslations, bool useFallbackTranslationsForEmptyResources = false, bool ignorePluralRules = true, }) { instance._locale = locale; instance._translations = translations; instance._fallbackTranslations = fallbackTranslations; + instance._preloadedTranslations = preloadedTranslations; instance._useFallbackTranslationsForEmptyResources = useFallbackTranslationsForEmptyResources; instance._ignorePluralRules = ignorePluralRules; @@ -53,6 +57,7 @@ class Localization { List? args, Map? namedArgs, String? gender, + Locale? locale, }) { late String res; bool logMissingKeys = true; @@ -63,9 +68,9 @@ class Localization { } if (gender != null) { - res = _gender(key, gender: gender); + res = _gender(key, gender: gender, locale: locale); } else { - res = _resolve(key, logging: logMissingKeys); + res = _resolve(key, logging: logMissingKeys, locale: locale); } res = _replaceLinks(res, logging: logMissingKeys); @@ -153,31 +158,33 @@ class Localization { Map? namedArgs, String? name, NumberFormat? format, + Locale? locale, }) { late String res; - final pluralRule = _pluralRule(_locale.languageCode, value); + final pLangCode = locale?.languageCode ?? _locale.languageCode; + final pluralRule = _pluralRule(pLangCode, value); final pluralCase = pluralRule != null ? pluralRule() : _pluralCaseFallback(value); switch (pluralCase) { case PluralCase.ZERO: - res = _resolvePlural(key, 'zero'); + res = _resolvePlural(key, 'zero', locale: locale); break; case PluralCase.ONE: - res = _resolvePlural(key, 'one'); + res = _resolvePlural(key, 'one', locale: locale); break; case PluralCase.TWO: - res = _resolvePlural(key, 'two'); + res = _resolvePlural(key, 'two', locale: locale); break; case PluralCase.FEW: - res = _resolvePlural(key, 'few'); + res = _resolvePlural(key, 'few', locale: locale); break; case PluralCase.MANY: - res = _resolvePlural(key, 'many'); + res = _resolvePlural(key, 'many', locale: locale); break; case PluralCase.OTHER: - res = _resolvePlural(key, 'other'); + res = _resolvePlural(key, 'other', locale: locale); break; } @@ -191,31 +198,64 @@ class Localization { return _replaceArgs(res, args ?? [formattedValue]); } - String _gender(String key, {required String gender}) { - return _resolve('$key.$gender'); + String _gender(String key, {required String gender, Locale? locale}) { + return _resolve('$key.$gender', locale: locale); } - String _resolvePlural(String key, String subKey) { - if (subKey == 'other') return _resolve('$key.other'); + String _resolvePlural(String key, String subKey, {Locale? locale}) { + if (subKey == 'other') return _resolve('$key.other', locale: locale); final tag = '$key.$subKey'; var resource = - _resolve(tag, logging: false, fallback: _fallbackTranslations != null); + _resolve(tag, logging: false, fallback: _fallbackTranslations != null, locale: locale); if (resource == tag) { - resource = _resolve('$key.other'); + resource = _resolve('$key.other', locale: locale); } return resource; } - String _resolve(String key, {bool logging = true, bool fallback = true}) { - var resource = _translations?.get(key); + String _resolve(String key, {bool logging = true, bool fallback = true, Locale? locale}) { + String? resource; + bool isRequestedLanguageFallback = false; + + if (locale != null) { + final xLocale = PlayxLocaleController.controller.config.supportedLocales.firstWhereOrNull((e) => e.locale.supports(locale)); + if (xLocale != null) { + if (xLocale.locale == _locale) { + resource = _translations?.get(key); + } else { + final fallbackLocale = PlayxLocaleController.controller.getFallbackLocale(); + if (xLocale.locale == fallbackLocale.locale) { + resource = _fallbackTranslations?.get(key); + isRequestedLanguageFallback = true; + } else if (_preloadedTranslations != null && _preloadedTranslations!.containsKey(xLocale.locale)) { + resource = _preloadedTranslations![xLocale.locale]?.get(key); + } else { + if (logging) { + PlayxLocaleController.logger?.warning( + 'Translations for $locale are not loaded. Set preloadSupportedLocales to true in PlayxLocaleConfig to support this, or only request current/fallback locales.'); + } + return key; + } + } + } else { + if (logging) { + PlayxLocaleController.logger?.warning( + 'Translations for $locale are not supported in config.'); + } + return key; + } + } else { + resource = _translations?.get(key); + } + if (resource == null || (_useFallbackTranslationsForEmptyResources && resource.isEmpty)) { - if (logging) { + if (logging && !isRequestedLanguageFallback) { PlayxLocaleController.logger ?.warning('Localization key [$key] not found'); } - if (_fallbackTranslations == null || !fallback) { + if (_fallbackTranslations == null || !fallback || isRequestedLanguageFallback) { return key; } else { resource = _fallbackTranslations?.get(key); diff --git a/lib/src/easy_localization/public.dart b/lib/src/easy_localization/public.dart index 0dddad5..58d31e9 100644 --- a/lib/src/easy_localization/public.dart +++ b/lib/src/easy_localization/public.dart @@ -38,12 +38,13 @@ String tr( List? args, Map? namedArgs, String? gender, + Locale? locale, }) { return context != null ? Localization.of(context)! - .tr(key, args: args, namedArgs: namedArgs, gender: gender) + .tr(key, args: args, namedArgs: namedArgs, gender: gender, locale: locale) : Localization.instance - .tr(key, args: args, namedArgs: namedArgs, gender: gender); + .tr(key, args: args, namedArgs: namedArgs, gender: gender, locale: locale); } bool trExists(String key, {BuildContext? context}) { @@ -110,10 +111,11 @@ String plural( Map? namedArgs, String? name, NumberFormat? format, + Locale? locale, }) { final Localization localization = (context != null ? Localization.of(context) : null) ?? Localization.instance; return localization.plural(key, value, - args: args, namedArgs: namedArgs, name: name, format: format); + args: args, namedArgs: namedArgs, name: name, format: format, locale: locale); } diff --git a/lib/src/extensions/playx_localization_extensions.dart b/lib/src/extensions/playx_localization_extensions.dart index 40cb425..26e9cdf 100644 --- a/lib/src/extensions/playx_localization_extensions.dart +++ b/lib/src/extensions/playx_localization_extensions.dart @@ -22,9 +22,10 @@ extension PlayxLocalizationStringExtensions on String { List? args, Map? namedArgs, String? gender, + Locale? locale, }) => ez.tr(this, - context: context, args: args, namedArgs: namedArgs, gender: gender); + context: context, args: args, namedArgs: namedArgs, gender: gender, locale: locale); bool trExists() => ez.trExists(this); @@ -44,6 +45,7 @@ extension PlayxLocalizationStringExtensions on String { Map? namedArgs, String? name, NumberFormat? format, + Locale? locale, }) => ez.plural( this, @@ -53,6 +55,7 @@ extension PlayxLocalizationStringExtensions on String { name: name, format: format, context: context, + locale: locale, ); } @@ -68,7 +71,8 @@ extension TextTranslateExtension on Text { {List? args, BuildContext? context, Map? namedArgs, - String? gender}) => + String? gender, + Locale? locale}) => Text( ez.tr( data ?? '', @@ -76,6 +80,7 @@ extension TextTranslateExtension on Text { namedArgs: namedArgs, gender: gender, context: context, + locale: locale, ), key: key, style: style, @@ -98,6 +103,7 @@ extension TextTranslateExtension on Text { Map? namedArgs, String? name, NumberFormat? format, + Locale? locale, }) => Text( ez.plural( @@ -108,6 +114,7 @@ extension TextTranslateExtension on Text { namedArgs: namedArgs, name: name, format: format, + locale: locale, ), key: key, style: style, @@ -158,6 +165,7 @@ extension BuildContextLocalizationExtension on BuildContext { List? args, Map? namedArgs, String? gender, + Locale? locale, }) { final localization = Localization.of(this); @@ -170,6 +178,7 @@ extension BuildContextLocalizationExtension on BuildContext { args: args, namedArgs: namedArgs, gender: gender, + locale: locale, ); } @@ -190,6 +199,7 @@ extension BuildContextLocalizationExtension on BuildContext { Map? namedArgs, String? name, NumberFormat? format, + Locale? locale, }) { final localization = Localization.of(this); @@ -204,6 +214,7 @@ extension BuildContextLocalizationExtension on BuildContext { namedArgs: namedArgs, name: name, format: format, + locale: locale, ); } From 529f9a7b33ba45f9d49b13cc0a3cc3a7f97270fd Mon Sep 17 00:00:00 2001 From: basemosama Date: Wed, 1 Apr 2026 17:19:15 +0200 Subject: [PATCH 4/4] feat: Device Sync Persistence - **Device Sync Persistence**: `updateToDeviceLocale()` now saves a special flag in preferences (index `-1`). When the application boots, instead of overriding the device's current locale with a historically cached exact locale or `startLocale`, the controller auto-routes strictly to whatever the current `deviceLocale` dictates. This allows developers to offer a persistent "system default" setting. --- CHANGELOG.md | 3 +- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 37 ++++++ example/ios/Runner.xcodeproj/project.pbxproj | 118 +++++++++++++++++- .../contents.xcworkspacedata | 3 + example/lib/main.dart | 10 ++ lib/src/controller/controller.dart | 33 +++-- .../playx_localization_extensions.dart | 3 + lib/src/playx_localization.dart | 3 + 10 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 example/ios/Podfile.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 33895eb..5a8f0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ - **Enhanced Locale Config**: Added `useFallbackTranslationsForEmptyResources`, `ignorePluralRules`, `extraAssetLoaders`, and `errorWidget` to `PlayxLocaleConfig`. - **Script Code Support**: Added `scriptCode` natively to `XLocale` and updated locale matching in `PlayxLocaleController` to properly utilize it. - **Deep Translation Merging**: `TranslationManager` now supports combining JSON translation maps securely from multiple asset loaders. -- **Device Sync Reset**: Introduced `resetLocale()` to safely recalculate and synchronize the application's locale against device-level configurations. +- **Device Sync Persistence**: `updateToDeviceLocale()` now saves a special flag in preferences (index `-1`). When the application boots, instead of overriding the device's current locale with a historically cached exact locale or `startLocale`, the controller auto-routes strictly to whatever the current `deviceLocale` dictates. This allows developers to offer a persistent "system default" setting. - **Synchronous Any-Locale Translation**: Added `preloadSupportedLocales` to `PlayxLocaleConfig`. When enabled, it caches all dictionaries during initialization, allowing developers to query translations for any loaded `Locale? locale` parametrically using `tr(..., locale: ...)` synchronously, completely out-of-bounds, without overriding the app environment. + ## 0.3.1 - Bumping `playx_core` dependency to `^0.7.4`. - Renaming `isRtl` and `isLtr` getters to `isCurrentLocaleRtl` and `isCurrentLocaleLtr` respectively for better clarity. diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index e549ee2..620e46e 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..d3ab58f --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,37 @@ +PODS: + - Flutter (1.0.0) + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index f109a11..69c3c1f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,8 +8,10 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 16CD4ECFBE07BDC4CA6BF884 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2322ACF59C699FB546362789 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4D1582C0AC34C1CB875C0726 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C2C1BDF23FC2D0A835E2D70 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -42,12 +44,19 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2322ACF59C699FB546362789 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A366468B591D70BECBCFCC7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 61FA2CD2201312E546D2F2BE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6C2C1BDF23FC2D0A835E2D70 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7784ABE32650A73F12B90C3C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 832C59ECF11E0EC739EFFEAD /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 8EFCD8E2BC201DBAE2FA8EAD /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,13 +64,23 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CFC30A6A7E5CB2FD3AF9BE5D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0F2B610A87E313099CCC1B99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4D1582C0AC34C1CB875C0726 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 16CD4ECFBE07BDC4CA6BF884 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,29 @@ path = RunnerTests; sourceTree = ""; }; + 4966E70334CBA96494A45E4D /* Pods */ = { + isa = PBXGroup; + children = ( + CFC30A6A7E5CB2FD3AF9BE5D /* Pods-Runner.debug.xcconfig */, + 61FA2CD2201312E546D2F2BE /* Pods-Runner.release.xcconfig */, + 8EFCD8E2BC201DBAE2FA8EAD /* Pods-Runner.profile.xcconfig */, + 832C59ECF11E0EC739EFFEAD /* Pods-RunnerTests.debug.xcconfig */, + 7784ABE32650A73F12B90C3C /* Pods-RunnerTests.release.xcconfig */, + 3A366468B591D70BECBCFCC7 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 6A130E9E42AB80BB44C04A6D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2322ACF59C699FB546362789 /* Pods_Runner.framework */, + 6C2C1BDF23FC2D0A835E2D70 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 4966E70334CBA96494A45E4D /* Pods */, + 6A130E9E42AB80BB44C04A6D /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 2A7FAE604EB726A2C50B5838 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 0F2B610A87E313099CCC1B99 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 948DA71D134D31F494F59C83 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 7A26DFE72E842B242B098E44 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2A7FAE604EB726A2C50B5838 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +308,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 7A26DFE72E842B242B098E44 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 948DA71D134D31F494F59C83 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -346,7 +455,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 832C59ECF11E0EC739EFFEAD /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7784ABE32650A73F12B90C3C /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3A366468B591D70BECBCFCC7 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -473,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -524,7 +636,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 30daaf5..70b2fe0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -16,6 +16,7 @@ Future main() async { startLocale: locales.first, fallbackLocale: locales.first, useFallbackTranslations: true, + preloadSupportedLocales: true, ); await PlayxLocalization.boot(config: config); @@ -66,6 +67,15 @@ class MyHomePage extends StatelessWidget { ? Colors.blueAccent : Colors.black), ), + Text( + context.tr(AppTrans.changeLanguageTitle, + locale: Locale('ar')), + style: TextStyle( + fontSize: 18, + color: (context.tr(AppTrans.changeLanguageTitle)).isArabic + ? Colors.blueAccent + : Colors.black), + ), const SizedBox( height: 20, ), diff --git a/lib/src/controller/controller.dart b/lib/src/controller/controller.dart index d748646..5932e7a 100644 --- a/lib/src/controller/controller.dart +++ b/lib/src/controller/controller.dart @@ -53,6 +53,10 @@ class PlayxLocaleController extends ValueNotifier { static PlayxBaseLogger? get logger => PlayxLogger.getLogger('Playx Localization'); + /// Whether the app locale is actively synced to the device locale + bool get isDeviceLocaleSelected => _isDeviceLocaleSelected; + bool _isDeviceLocaleSelected = false; + /// current locale index int get currentIndex { if (value == null) { @@ -76,9 +80,12 @@ class PlayxLocaleController extends ValueNotifier { deviceLocale = foundPlatformLocale.toLocale(); logger.i('Device Locale ${deviceLocale?.toStringWithSeparator()}'); - XLocale? lastSavedLocale = config.supportedLocales.atOrNull( - lastKnownIndex ?? -1, - ); + _isDeviceLocaleSelected = lastKnownIndex == -1; + + XLocale? lastSavedLocale; + if (lastKnownIndex != null && lastKnownIndex >= 0) { + lastSavedLocale = config.supportedLocales.atOrNull(lastKnownIndex); + } final locale = _getStartLocale(savedLocale: lastSavedLocale); @@ -130,7 +137,7 @@ class PlayxLocaleController extends ValueNotifier { XLocale _getStartLocale({XLocale? savedLocale}) { if (savedLocale != null) return savedLocale; - if (config.startLocale != null) return config.startLocale!; + if (!_isDeviceLocaleSelected && config.startLocale != null) return config.startLocale!; if (deviceLocale != null) { final searchedLocale = supportedXLocales.firstWhereOrNull( @@ -289,11 +296,17 @@ class PlayxLocaleController extends ValueNotifier { final foundPlatformLocale = await findSystemLocale(); final locale = foundPlatformLocale.toLocale(); deviceLocale = locale; - return updateByLanguageCode( + final search = searchLocaleByLanguageCode( languageCode: locale.languageCode, countryCode: locale.countryCode, - scriptCode: locale.scriptCode, - forceAppUpdate: forceAppUpdate); + scriptCode: locale.scriptCode); + if (search != null) { + return _updateLocale( + locale: search, + forceAppUpdate: forceAppUpdate, + saveAsDeviceLocale: true); + } + return false; } /// Reset locale to platform locale or fallback locale. @@ -311,6 +324,7 @@ class PlayxLocaleController extends ValueNotifier { Future _updateLocale({ required XLocale locale, bool forceAppUpdate = false, + bool saveAsDeviceLocale = false, }) async { try { final index = supportedXLocales.indexOf(locale); @@ -325,8 +339,11 @@ class PlayxLocaleController extends ValueNotifier { locale, ); if (config.saveLocale) { - await PlayxAsyncPrefs.setInt(_lastKnownIndexKey, index); + final savedIndex = saveAsDeviceLocale ? -1 : index; + await PlayxAsyncPrefs.setInt(_lastKnownIndexKey, savedIndex); } + _isDeviceLocaleSelected = saveAsDeviceLocale; + final oldLocale = value; value = locale; diff --git a/lib/src/extensions/playx_localization_extensions.dart b/lib/src/extensions/playx_localization_extensions.dart index 26e9cdf..ff331fa 100644 --- a/lib/src/extensions/playx_localization_extensions.dart +++ b/lib/src/extensions/playx_localization_extensions.dart @@ -247,4 +247,7 @@ extension BuildContextLocalizationExtension on BuildContext { /// Returns true if the current locale is LTR. bool get isCurrentLocaleLtr => !isCurrentLocaleRtl; + + /// Returns true if the app is currently synced to the device locale. + bool get isDeviceLocaleSelected => PlayxLocalization.isDeviceLocaleSelected; } diff --git a/lib/src/playx_localization.dart b/lib/src/playx_localization.dart index 403a329..f5848f0 100644 --- a/lib/src/playx_localization.dart +++ b/lib/src/playx_localization.dart @@ -52,6 +52,9 @@ abstract class PlayxLocalization { /// Returns the locale of device. static Locale? get deviceLocale => _controller.deviceLocale; + /// Returns whether the app locale is actively synced to the device locale. + static bool get isDeviceLocaleSelected => _controller.isDeviceLocaleSelected; + /// Returns the current fallback Locale that is used in the app. /// If [useFallbackTranslations] in config is false, it will return null. /// Else it will return the fallback locale based on config.