diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 20980a0a1c41f..d3d81b7f3cc7d 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -42,14 +42,20 @@ void _updateWindowMetrics(double devicePixelRatio, typedef _LocaleClosure = String Function(); -String _localeClosure() => window._locale.toString(); +String _localeClosure() => window.locale.toString(); @pragma('vm:entry-point') _LocaleClosure _getLocaleClosure() => _localeClosure; @pragma('vm:entry-point') -void _updateLocale(String languageCode, String countryCode, String scriptCode, String variantCode) { - window._locale = new Locale(languageCode, countryCode); +void _updateLocales(List locales) { + const int stringsPerLocale = 4; + final int numLocales = locales.length ~/ stringsPerLocale; + window._locales = new List(numLocales); + for (int localeIndex = 0; localeIndex < numLocales; localeIndex++) { + window._locales[localeIndex] = new Locale(locales[localeIndex * stringsPerLocale], + locales[localeIndex * stringsPerLocale + 1]); + } _invoke(window.onLocaleChanged, window._onLocaleChangedZone); } diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 0b1318c997aaf..fa0431b3113d8 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -152,6 +152,9 @@ class Locale { /// the region subtag should be uppercase. const Locale(this._languageCode, [ this._countryCode ]) : assert(_languageCode != null); + /// Empty locale constant. This is an invalid locale. + static const Locale none = const Locale('', ''); + /// The primary language subtag for the locale. /// /// This must not be null. @@ -426,19 +429,39 @@ class Window { _onMetricsChangedZone = Zone.current; } - /// The system-reported locale. + /// The system-reported default locale of the device. + /// + /// This establishes the language and formatting conventions that application + /// should, if possible, use to render their user interface. + /// + /// This is the first locale selected by the user and is the user's + /// primary locale (the locale the device UI is displayed in) + /// + /// This is equivalent to `locales.first` and will provide an empty non-null locale + /// if the [locales] list has not been set or is empty. + Locale get locale { + if (_locales != null && _locales.isNotEmpty) { + return _locales.first; + } + return Locale.none; + } + + /// The full system-reported supported locales of the device. /// /// This establishes the language and formatting conventions that application /// should, if possible, use to render their user interface. /// + /// The list is ordered in order of priority, with lower-indexed locales being + /// preferred over higher-indexed ones. The first element is the primary [locale]. + /// /// The [onLocaleChanged] callback is called whenever this value changes. /// /// See also: /// /// * [WidgetsBindingObserver], for a mechanism at the widgets layer to /// observe when this value changes. - Locale get locale => _locale; - Locale _locale; + List get locales => _locales; + List _locales; /// A callback that is invoked whenever [locale] changes value. /// diff --git a/lib/ui/window/window.cc b/lib/ui/window/window.cc index 95be0076198cc..ce4626afa74e7 100644 --- a/lib/ui/window/window.cc +++ b/lib/ui/window/window.cc @@ -162,21 +162,14 @@ void Window::UpdateWindowMetrics(const ViewportMetrics& metrics) { }); } -void Window::UpdateLocale(const std::string& language_code, - const std::string& country_code, - const std::string& script_code, - const std::string& variant_code) { +void Window::UpdateLocales(const std::vector& locales) { std::shared_ptr dart_state = library_.dart_state().lock(); if (!dart_state) return; tonic::DartState::Scope scope(dart_state); - - DartInvokeField(library_.value(), "_updateLocale", + DartInvokeField(library_.value(), "_updateLocales", { - StdStringToDart(language_code), - StdStringToDart(country_code), - StdStringToDart(script_code), - StdStringToDart(variant_code), + tonic::ToDart>(locales), }); } diff --git a/lib/ui/window/window.h b/lib/ui/window/window.h index eb2a8c6613d07..bc1e3ba267f73 100644 --- a/lib/ui/window/window.h +++ b/lib/ui/window/window.h @@ -5,7 +5,9 @@ #ifndef FLUTTER_LIB_UI_WINDOW_WINDOW_H_ #define FLUTTER_LIB_UI_WINDOW_WINDOW_H_ +#include #include +#include #include "flutter/fml/time/time_point.h" #include "flutter/lib/ui/semantics/semantics_update.h" @@ -59,10 +61,7 @@ class Window final { void DidCreateIsolate(); void UpdateWindowMetrics(const ViewportMetrics& metrics); - void UpdateLocale(const std::string& language_code, - const std::string& country_code, - const std::string& script_code, - const std::string& variant_code); + void UpdateLocales(const std::vector& locales); void UpdateUserSettingsData(const std::string& data); void UpdateSemanticsEnabled(bool enabled); void UpdateAccessibilityFeatures(int32_t flags); diff --git a/runtime/runtime_controller.cc b/runtime/runtime_controller.cc index 7e0bd024c66cc..92fb962437969 100644 --- a/runtime/runtime_controller.cc +++ b/runtime/runtime_controller.cc @@ -124,8 +124,7 @@ std::unique_ptr RuntimeController::Clone() const { bool RuntimeController::FlushRuntimeStateToIsolate() { return SetViewportMetrics(window_data_.viewport_metrics) && - SetLocale(window_data_.language_code, window_data_.country_code, - window_data_.script_code, window_data_.variant_code) && + SetLocales(window_data_.locale_data) && SetSemanticsEnabled(window_data_.semantics_enabled) && SetAccessibilityFeatures(window_data_.accessibility_feature_flags_); } @@ -140,22 +139,15 @@ bool RuntimeController::SetViewportMetrics(const ViewportMetrics& metrics) { return false; } -bool RuntimeController::SetLocale(const std::string& language_code, - const std::string& country_code, - const std::string& script_code, - const std::string& variant_code) { - window_data_.language_code = language_code; - window_data_.country_code = country_code; - window_data_.script_code = script_code; - window_data_.variant_code = variant_code; +bool RuntimeController::SetLocales( + const std::vector& locale_data) { + window_data_.locale_data = locale_data; if (auto window = GetWindowIfAvailable()) { - window->UpdateLocale(window_data_.language_code, window_data_.country_code, - window_data_.script_code, window_data_.variant_code); + window->UpdateLocales(locale_data); return true; } - - return false; + return true; } bool RuntimeController::SetUserSettingsData(const std::string& data) { diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index 67c7f3e44cb83..532499cb51e1d 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -6,6 +6,7 @@ #define FLUTTER_RUNTIME_RUNTIME_CONTROLLER_H_ #include +#include #include "flutter/common/task_runners.h" #include "flutter/flow/layers/layer_tree.h" @@ -15,6 +16,8 @@ #include "flutter/lib/ui/window/pointer_data_packet.h" #include "flutter/lib/ui/window/window.h" #include "flutter/runtime/dart_vm.h" +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" namespace blink { class Scene; @@ -40,10 +43,7 @@ class RuntimeController final : public WindowClient { bool SetViewportMetrics(const ViewportMetrics& metrics); - bool SetLocale(const std::string& language_code, - const std::string& country_code, - const std::string& script_code, - const std::string& variant_code); + bool SetLocales(const std::vector& locale_data); bool SetUserSettingsData(const std::string& data); @@ -78,12 +78,29 @@ class RuntimeController final : public WindowClient { std::pair GetRootIsolateReturnCode(); private: + struct Locale { + Locale(std::string language_code_, + std::string country_code_, + std::string script_code_, + std::string variant_code_) + : language_code(language_code_), + country_code(country_code_), + script_code(script_code_), + variant_code(variant_code_) {} + + std::string language_code; + std::string country_code; + std::string script_code; + std::string variant_code; + }; + struct WindowData { ViewportMetrics viewport_metrics; std::string language_code; std::string country_code; std::string script_code; std::string variant_code; + std::vector locale_data; std::string user_settings_data = "{}"; bool semantics_enabled = false; bool assistive_technology_enabled = false; diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 5e1f53a55912d..3ab484c46f37c 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include "flutter/common/settings.h" #include "flutter/fml/eintr_wrapper.h" @@ -325,17 +326,22 @@ bool Engine::HandleLocalizationPlatformMessage( if (args == root.MemberEnd() || !args->value.IsArray()) return false; - const auto& language = args->value[0]; - const auto& country = args->value[1]; - const auto& script = args->value[2]; - const auto& variant = args->value[3]; - - if (!language.IsString() || !country.IsString()) + const size_t strings_per_locale = 4; + if (args->value.Size() % strings_per_locale != 0) return false; + std::vector locale_data; + for (size_t locale_index = 0; locale_index < args->value.Size(); + locale_index += strings_per_locale) { + if (!args->value[locale_index].IsString() || + !args->value[locale_index + 1].IsString()) + return false; + locale_data.push_back(args->value[locale_index].GetString()); + locale_data.push_back(args->value[locale_index + 1].GetString()); + locale_data.push_back(args->value[locale_index + 2].GetString()); + locale_data.push_back(args->value[locale_index + 3].GetString()); + } - return runtime_controller_->SetLocale(language.GetString(), - country.GetString(), script.GetString(), - variant.GetString()); + return runtime_controller_->SetLocales(locale_data); } void Engine::HandleSettingsPlatformMessage(blink::PlatformMessage* message) { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 8778877486875..4f1f151d74705 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -37,6 +37,7 @@ import org.json.JSONException; import org.json.JSONObject; +import java.lang.reflect.Method; import java.net.URI; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -170,7 +171,8 @@ public void surfaceDestroyed(SurfaceHolder holder) { mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mTextInputPlugin = new TextInputPlugin(this); - setLocale(getResources().getConfiguration().locale); + + setLocales(getResources().getConfiguration()); setUserSettings(); } @@ -316,14 +318,43 @@ private void setUserSettings() { mFlutterSettingsChannel.send(message); } - private void setLocale(Locale locale) { + private void setLocales(Configuration config) { + if (Build.VERSION.SDK_INT >= 24) { + try { + // Passes the full list of locales for android API >= 24 with reflection. + Object localeList = config.getClass().getDeclaredMethod("getLocales").invoke(config); + Method localeListGet = localeList.getClass().getDeclaredMethod("get", int.class); + Method localeListSize = localeList.getClass().getDeclaredMethod("size"); + int localeCount = (int)localeListSize.invoke(localeList); + List data = new ArrayList(); + for (int index = 0; index < localeCount; ++index) { + Locale locale = (Locale)localeListGet.invoke(localeList, index); + data.add(locale.getLanguage()); + data.add(locale.getCountry()); + data.add(locale.getScript()); + data.add(locale.getVariant()); + } + mFlutterLocalizationChannel.invokeMethod("setLocale", data); + return; + } catch (Exception exception) { + // Any exception is a failure. Resort to fallback of sending only one locale. + } + } + // Fallback single locale passing for android API < 24. Should work always. + Locale locale = config.locale; + List data = new ArrayList(); + data.add(locale.getLanguage()); + data.add(locale.getCountry()); + data.add(locale.getScript()); + data.add(locale.getVariant()); mFlutterLocalizationChannel.invokeMethod("setLocale", Arrays.asList(locale.getLanguage(), locale.getCountry(), locale.getScript(), locale.getVariant())); + } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - setLocale(newConfig.locale); + setLocales(newConfig); setUserSettings(); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 2e81213980423..9327e8fbbb5e6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -905,19 +905,26 @@ - (void)onMemoryWarning:(NSNotification*)notification { #pragma mark - Locale updates - (void)onLocaleUpdated:(NSNotification*)notification { - NSLocale* currentLocale = [NSLocale currentLocale]; - NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; - NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; - NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; - NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; - if (languageCode && countryCode) - // We pass empty strings for undefined scripts and variants to ensure the JSON encoder/decoder - // functions properly. - [_localizationChannel.get() invokeMethod:@"setLocale" - arguments:@[ - languageCode, countryCode, scriptCode ? scriptCode : @"", - variantCode ? variantCode : @"" - ]]; + NSArray* preferredLocales = [NSLocale preferredLanguages]; + NSMutableArray* data = [NSMutableArray new]; + for (NSString* localeID in preferredLocales) { + NSLocale* currentLocale = [[NSLocale alloc] initWithLocaleIdentifier:localeID]; + NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; + NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; + NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; + NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; + if (!languageCode || !countryCode) { + continue; + } + [data addObject:languageCode]; + [data addObject:countryCode]; + [data addObject:(scriptCode ? scriptCode : @"")]; + [data addObject:(variantCode ? variantCode : @"")]; + } + if (data.count == 0) { + return; + } + [_localizationChannel.get() invokeMethod:@"setLocale" arguments:data]; } #pragma mark - Set user settings diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index b56aaeee4d1e9..1e362f403ceb2 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -100,7 +100,7 @@ void main() { }; }); - _updateLocale('en', 'US', '', ''); + _updateLocales(['en', 'US', '', '']); expect(runZone, isNotNull); expect(runZone, same(innerZone)); expect(locale, equals(const Locale('en', 'US')));