diff --git a/CHANGELOG.md b/CHANGELOG.md index 1957e4b..5a8f0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 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 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/.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/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/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/config/playx_locale_config.dart b/lib/src/config/playx_locale_config.dart index bd17e4a..6e7ea97 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,24 @@ 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; + + /// 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( @@ -75,8 +95,16 @@ class PlayxLocaleConfig { this.fallbackLocale, this.useOnlyLangCode = false, this.useFallbackTranslations = true, + this.useFallbackTranslationsForEmptyResources = false, + this.ignorePluralRules = true, + this.preloadSupportedLocales = false, 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..5932e7a 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,12 +44,19 @@ 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; 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) { @@ -72,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); @@ -126,15 +137,13 @@ 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 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 = @@ -168,9 +177,17 @@ class PlayxLocaleController extends ValueNotifier { ); _translations = res.translations; _fallbackTranslations = res.fallbackTranslations; - - Localization.load(locale.locale, - translations: translations, fallbackTranslations: fallbackTranslations); + _preloadedTranslations = res.preloadedTranslations; + + Localization.load( + locale.locale, + translations: _translations, + fallbackTranslations: _fallbackTranslations, + preloadedTranslations: _preloadedTranslations, + useFallbackTranslationsForEmptyResources: + config.useFallbackTranslationsForEmptyResources, + ignorePluralRules: config.ignorePluralRules, + ); } /// update the locale to be one of the supported locales. @@ -234,13 +251,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 +276,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,12 +293,30 @@ 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; - return updateByLanguageCode( + final foundPlatformLocale = await findSystemLocale(); + final locale = foundPlatformLocale.toLocale(); + deviceLocale = locale; + final search = searchLocaleByLanguageCode( languageCode: locale.languageCode, countryCode: locale.countryCode, - 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. + 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. @@ -284,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); @@ -298,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/controller/translation_manager.dart b/lib/src/controller/translation_manager.dart index f6ab38f..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, ); } } @@ -75,17 +94,45 @@ class TranslationManager { static Future> loadTranslationData( {required XLocale locale, required PlayxLocaleConfig config}) async { - late Map? data; + final result = {}; + final loaderFutures = ?>>[]; + + final Locale desiredLocale = config.useOnlyLangCode + ? Locale.fromSubtags( + languageCode: locale.languageCode, scriptCode: locale.scriptCode) + : locale.locale; - if (config.useOnlyLangCode) { - data = await config.assetLoader - .load(config.path, Locale(locale.languageCode)); - } else { - data = await config.assetLoader.load(config.path, 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 data; + return result; + } +} + +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..4772293 100644 --- a/lib/src/delegate/playx_localization_delegate.dart +++ b/lib/src/delegate/playx_localization_delegate.dart @@ -25,15 +25,23 @@ 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, + preloadedTranslations: localizationController!.preloadedTranslations, + 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..4b4ce87 --- /dev/null +++ b/lib/src/easy_localization/file_loaders/io_file_loader.dart @@ -0,0 +1,2 @@ +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/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/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/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/extensions/playx_localization_extensions.dart b/lib/src/extensions/playx_localization_extensions.dart index 40cb425..ff331fa 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, ); } @@ -236,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/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]; } 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. 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: