未验证 提交 559d93d9 编写于 作者: G Gary Qian 提交者: GitHub

Android native locale resolution algorithm (#19266)

上级 5d2a22dd
......@@ -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() + "-*"));
}
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);
}
// 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);
// 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 platformResolvedLocale;
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() {
......
// 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);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册