mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Android native locale resolution algorithm (#19266)
This commit is contained in:
parent
5d2a22dd6e
commit
559d93d978
@ -450,6 +450,7 @@ action("robolectric_tests") {
|
||||
"test/io/flutter/plugin/common/StandardMethodCodecTest.java",
|
||||
"test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java",
|
||||
"test/io/flutter/plugin/editing/TextInputPluginTest.java",
|
||||
"test/io/flutter/plugin/localization/LocalizationPluginTest.java",
|
||||
"test/io/flutter/plugin/mouse/MouseCursorPluginTest.java",
|
||||
"test/io/flutter/plugin/platform/PlatformPluginTest.java",
|
||||
"test/io/flutter/plugin/platform/SingleViewPresentationTest.java",
|
||||
|
||||
@ -26,9 +26,25 @@ public class LocalizationPlugin {
|
||||
this.localizationChannel = localizationChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the {@link Locale} in supportedLocales that best matches the user's preferred locales.
|
||||
*
|
||||
* <p>FlutterEngine must be non-null when this method is invoked.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public Locale resolveNativeLocale(List<Locale> supportedLocales) {
|
||||
Locale platformResolvedLocale = null;
|
||||
if (supportedLocales == null || supportedLocales.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Android improved the localization resolution algorithms after API 24 (7.0, Nougat).
|
||||
// See https://developer.android.com/guide/topics/resources/multilingual-support
|
||||
//
|
||||
// LanguageRange and Locale.lookup was added in API 26 and is the preferred way to
|
||||
// select a locale. Pre-API 26, we implement a manual locale resolution.
|
||||
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// Modern locale resolution using LanguageRange
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
List<Locale.LanguageRange> languageRanges = new ArrayList<>();
|
||||
LocaleList localeList = context.getResources().getConfiguration().getLocales();
|
||||
int localeCount = localeList.size();
|
||||
@ -37,14 +53,60 @@ public class LocalizationPlugin {
|
||||
String localeString = locale.toString();
|
||||
// This string replacement converts the locale string into the ranges format.
|
||||
languageRanges.add(new Locale.LanguageRange(localeString.replace("_", "-")));
|
||||
languageRanges.add(new Locale.LanguageRange(locale.getLanguage()));
|
||||
languageRanges.add(new Locale.LanguageRange(locale.getLanguage() + "-*"));
|
||||
}
|
||||
|
||||
// TODO(garyq): This should be modified to achieve Android's full
|
||||
// locale resolution:
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support
|
||||
platformResolvedLocale = Locale.lookup(languageRanges, supportedLocales);
|
||||
Locale platformResolvedLocale = Locale.lookup(languageRanges, supportedLocales);
|
||||
if (platformResolvedLocale != null) {
|
||||
return platformResolvedLocale;
|
||||
}
|
||||
return supportedLocales.get(0);
|
||||
} else if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
// Modern locale resolution without languageRange
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
LocaleList localeList = context.getResources().getConfiguration().getLocales();
|
||||
for (int index = 0; index < localeList.size(); ++index) {
|
||||
Locale preferredLocale = localeList.get(index);
|
||||
// Look for exact match.
|
||||
for (Locale locale : supportedLocales) {
|
||||
if (preferredLocale.equals(locale)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
// Look for exact language only match.
|
||||
for (Locale locale : supportedLocales) {
|
||||
if (preferredLocale.getLanguage().equals(locale.toLanguageTag())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
// Look for any locale with matching language.
|
||||
for (Locale locale : supportedLocales) {
|
||||
if (preferredLocale.getLanguage().equals(locale.getLanguage())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
return supportedLocales.get(0);
|
||||
}
|
||||
return platformResolvedLocale;
|
||||
|
||||
// Legacy locale resolution
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support#preN
|
||||
Locale preferredLocale = context.getResources().getConfiguration().locale;
|
||||
if (preferredLocale != null) {
|
||||
// Look for exact match.
|
||||
for (Locale locale : supportedLocales) {
|
||||
if (preferredLocale.equals(locale)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
// Look for exact language only match.
|
||||
for (Locale locale : supportedLocales) {
|
||||
if (preferredLocale.getLanguage().equals(locale.toString())) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
return supportedLocales.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,6 +13,7 @@ import io.flutter.embedding.android.FlutterViewTest;
|
||||
import io.flutter.embedding.engine.FlutterEngineCacheTest;
|
||||
import io.flutter.embedding.engine.FlutterEnginePluginRegistryTest;
|
||||
import io.flutter.embedding.engine.FlutterJNITest;
|
||||
import io.flutter.embedding.engine.LocalizationPluginTest;
|
||||
import io.flutter.embedding.engine.RenderingComponentTest;
|
||||
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest;
|
||||
import io.flutter.embedding.engine.renderer.FlutterRendererTest;
|
||||
@ -52,6 +53,7 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
|
||||
FlutterRendererTest.class,
|
||||
FlutterViewTest.class,
|
||||
InputConnectionAdaptorTest.class,
|
||||
LocalizationPluginTest.class,
|
||||
PlatformPluginTest.class,
|
||||
PluginComponentTest.class,
|
||||
PreconditionsTest.class,
|
||||
|
||||
@ -96,7 +96,10 @@ public class FlutterJNITest {
|
||||
"en", "CA", ""
|
||||
};
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 0); // This should change when full algo is implemented.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "en");
|
||||
assertEquals(result[1], "CA");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
@ -137,7 +140,11 @@ public class FlutterJNITest {
|
||||
localeList = new LocaleList();
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 0);
|
||||
// The first locale is default.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
}
|
||||
|
||||
public void onDisplayPlatformView__callsPlatformViewsController() {
|
||||
|
||||
@ -0,0 +1,339 @@
|
||||
// Part of the embedding.engine package to allow access to FlutterJNI methods.
|
||||
package io.flutter.embedding.engine;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import io.flutter.embedding.engine.dart.DartExecutor;
|
||||
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
|
||||
import io.flutter.plugin.localization.LocalizationPlugin;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Locale;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
@Config(manifest = Config.NONE)
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@TargetApi(24) // LocaleList and scriptCode are API 24+.
|
||||
public class LocalizationPluginTest {
|
||||
// This test should be synced with the version for API 24.
|
||||
@Test
|
||||
public void computePlatformResolvedLocaleAPI26() {
|
||||
// --- Test Setup ---
|
||||
setApiVersion(26);
|
||||
FlutterJNI flutterJNI = new FlutterJNI();
|
||||
|
||||
Context context = mock(Context.class);
|
||||
Resources resources = mock(Resources.class);
|
||||
Configuration config = mock(Configuration.class);
|
||||
DartExecutor dartExecutor = mock(DartExecutor.class);
|
||||
LocaleList localeList =
|
||||
new LocaleList(new Locale("es", "MX"), new Locale("zh", "CN"), new Locale("en", "US"));
|
||||
when(context.getResources()).thenReturn(resources);
|
||||
when(resources.getConfiguration()).thenReturn(config);
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
|
||||
flutterJNI.setLocalizationPlugin(
|
||||
new LocalizationPlugin(context, new LocalizationChannel(dartExecutor)));
|
||||
|
||||
// Empty supportedLocales.
|
||||
String[] supportedLocales = new String[] {};
|
||||
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 0);
|
||||
|
||||
// Empty preferredLocales.
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"fr", "FR", "",
|
||||
"zh", "", "",
|
||||
"en", "CA", ""
|
||||
};
|
||||
localeList = new LocaleList();
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The first locale is default.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"fr", "", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"), new Locale("it", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "it");
|
||||
assertEquals(result[1], "IT");
|
||||
assertEquals(result[2], "");
|
||||
}
|
||||
|
||||
// This test should be synced with the version for API 26.
|
||||
@Test
|
||||
public void computePlatformResolvedLocaleAPI24() {
|
||||
// --- Test Setup ---
|
||||
setApiVersion(24);
|
||||
FlutterJNI flutterJNI = new FlutterJNI();
|
||||
|
||||
Context context = mock(Context.class);
|
||||
Resources resources = mock(Resources.class);
|
||||
Configuration config = mock(Configuration.class);
|
||||
DartExecutor dartExecutor = mock(DartExecutor.class);
|
||||
LocaleList localeList =
|
||||
new LocaleList(new Locale("es", "MX"), new Locale("zh", "CN"), new Locale("en", "US"));
|
||||
when(context.getResources()).thenReturn(resources);
|
||||
when(resources.getConfiguration()).thenReturn(config);
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
|
||||
flutterJNI.setLocalizationPlugin(
|
||||
new LocalizationPlugin(context, new LocalizationChannel(dartExecutor)));
|
||||
|
||||
// Empty supportedLocales.
|
||||
String[] supportedLocales = new String[] {};
|
||||
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 0);
|
||||
|
||||
// Empty preferredLocales.
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"fr", "FR", "",
|
||||
"zh", "", "",
|
||||
"en", "CA", ""
|
||||
};
|
||||
localeList = new LocaleList();
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The first locale is default.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"fr", "", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
localeList = new LocaleList(new Locale("fr", "CH"), new Locale("it", "CH"));
|
||||
when(config.getLocales()).thenReturn(localeList);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The call will use the new (> API 24) algorithm.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "it");
|
||||
assertEquals(result[1], "IT");
|
||||
assertEquals(result[2], "");
|
||||
}
|
||||
|
||||
// Tests the legacy pre API 24 algorithm.
|
||||
@Test
|
||||
public void computePlatformResolvedLocaleAPI16() {
|
||||
// --- Test Setup ---
|
||||
setApiVersion(16);
|
||||
FlutterJNI flutterJNI = new FlutterJNI();
|
||||
|
||||
Context context = mock(Context.class);
|
||||
Resources resources = mock(Resources.class);
|
||||
Configuration config = mock(Configuration.class);
|
||||
DartExecutor dartExecutor = mock(DartExecutor.class);
|
||||
Locale userLocale = new Locale("es", "MX");
|
||||
when(context.getResources()).thenReturn(resources);
|
||||
when(resources.getConfiguration()).thenReturn(config);
|
||||
setLegacyLocale(config, userLocale);
|
||||
|
||||
flutterJNI.setLocalizationPlugin(
|
||||
new LocalizationPlugin(context, new LocalizationChannel(dartExecutor)));
|
||||
|
||||
// Empty supportedLocales.
|
||||
String[] supportedLocales = new String[] {};
|
||||
String[] result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 0);
|
||||
|
||||
// Empty null preferred locale.
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"fr", "FR", "",
|
||||
"zh", "", "",
|
||||
"en", "CA", ""
|
||||
};
|
||||
userLocale = null;
|
||||
setLegacyLocale(config, userLocale);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
// The first locale is default.
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "FR");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
// Example from https://developer.android.com/guide/topics/resources/multilingual-support#postN
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
userLocale = new Locale("fr", "CH");
|
||||
setLegacyLocale(config, userLocale);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "en");
|
||||
assertEquals(result[1], "");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
userLocale = new Locale("it", "IT");
|
||||
setLegacyLocale(config, userLocale);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "it");
|
||||
assertEquals(result[1], "IT");
|
||||
assertEquals(result[2], "");
|
||||
|
||||
supportedLocales =
|
||||
new String[] {
|
||||
"en", "", "",
|
||||
"de", "DE", "",
|
||||
"es", "ES", "",
|
||||
"fr", "FR", "",
|
||||
"fr", "", "",
|
||||
"it", "IT", ""
|
||||
};
|
||||
userLocale = new Locale("fr", "CH");
|
||||
setLegacyLocale(config, userLocale);
|
||||
result = flutterJNI.computePlatformResolvedLocale(supportedLocales);
|
||||
assertEquals(result.length, 3);
|
||||
assertEquals(result[0], "fr");
|
||||
assertEquals(result[1], "");
|
||||
assertEquals(result[2], "");
|
||||
}
|
||||
|
||||
private static void setApiVersion(int apiVersion) {
|
||||
try {
|
||||
Field field = Build.VERSION.class.getField("SDK_INT");
|
||||
|
||||
field.setAccessible(true);
|
||||
Field modifiersField = Field.class.getDeclaredField("modifiers");
|
||||
modifiersField.setAccessible(true);
|
||||
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
|
||||
|
||||
field.set(null, apiVersion);
|
||||
} catch (Exception e) {
|
||||
assertTrue(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setLegacyLocale(Configuration config, Locale locale) {
|
||||
try {
|
||||
Field field = config.getClass().getField("locale");
|
||||
field.setAccessible(true);
|
||||
Field modifiersField = Field.class.getDeclaredField("modifiers");
|
||||
modifiersField.setAccessible(true);
|
||||
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
|
||||
|
||||
field.set(config, locale);
|
||||
} catch (Exception e) {
|
||||
assertTrue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user