Unverified Commit a7af0b31 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

Expose basicLocaleListResolution in widget library (#81898)

parent df158290
......@@ -79,6 +79,134 @@ typedef LocaleListResolutionCallback = Locale? Function(List<Locale>? locales, I
/// {@endtemplate}
typedef LocaleResolutionCallback = Locale? Function(Locale? locale, Iterable<Locale> supportedLocales);
/// The default locale resolution algorithm.
///
/// Custom resolution algorithms can be provided through
/// [WidgetsApp.localeListResolutionCallback] or
/// [WidgetsApp.localeResolutionCallback].
///
/// When no custom locale resolution algorithms are provided or if both fail
/// to resolve, Flutter will default to calling this algorithm.
///
/// This algorithm prioritizes speed at the cost of slightly less appropriate
/// resolutions for edge cases.
///
/// This algorithm will resolve to the earliest preferred locale that
/// matches the most fields, prioritizing in the order of perfect match,
/// languageCode+countryCode, languageCode+scriptCode, languageCode-only.
///
/// In the case where a locale is matched by languageCode-only and is not the
/// default (first) locale, the next preferred locale with a
/// perfect match can supersede the languageCode-only match if it exists.
///
/// When a preferredLocale matches more than one supported locale, it will
/// resolve to the first matching locale listed in the supportedLocales.
///
/// When all preferred locales have been exhausted without a match, the first
/// countryCode only match will be returned.
///
/// When no match at all is found, the first (default) locale in
/// [supportedLocales] will be returned.
///
/// To summarize, the main matching priority is:
///
/// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode]
/// 1. [Locale.languageCode] and [Locale.scriptCode] only
/// 1. [Locale.languageCode] and [Locale.countryCode] only
/// 1. [Locale.languageCode] only (with caveats, see above)
/// 1. [Locale.countryCode] only when all [preferredLocales] fail to match
/// 1. Returns the first element of [supportedLocales] as a fallback
///
/// This algorithm does not take language distance (how similar languages are to each other)
/// into account, and will not handle edge cases such as resolving `de` to `fr` rather than `zh`
/// when `de` is not supported and `zh` is listed before `fr` (German is closer to French
/// than Chinese).
Locale basicLocaleListResolution(List<Locale>? preferredLocales, Iterable<Locale> supportedLocales) {
// preferredLocales can be null when called before the platform has had a chance to
// initialize the locales. Platforms without locale passing support will provide an empty list.
// We default to the first supported locale in these cases.
if (preferredLocales == null || preferredLocales.isEmpty) {
return supportedLocales.first;
}
// Hash the supported locales because apps can support many locales and would
// be expensive to search through them many times.
final Map<String, Locale> allSupportedLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndCountryLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndScriptLocales = HashMap<String, Locale>();
final Map<String, Locale> languageLocales = HashMap<String, Locale>();
final Map<String?, Locale> countryLocales = HashMap<String?, Locale>();
for (final Locale locale in supportedLocales) {
allSupportedLocales['${locale.languageCode}_${locale.scriptCode}_${locale.countryCode}'] ??= locale;
languageAndScriptLocales['${locale.languageCode}_${locale.scriptCode}'] ??= locale;
languageAndCountryLocales['${locale.languageCode}_${locale.countryCode}'] ??= locale;
languageLocales[locale.languageCode] ??= locale;
countryLocales[locale.countryCode] ??= locale;
}
// Since languageCode-only matches are possibly low quality, we don't return
// it instantly when we find such a match. We check to see if the next
// preferred locale in the list has a high accuracy match, and only return
// the languageCode-only match when a higher accuracy match in the next
// preferred locale cannot be found.
Locale? matchesLanguageCode;
Locale? matchesCountryCode;
// Loop over user's preferred locales
for (int localeIndex = 0; localeIndex < preferredLocales.length; localeIndex += 1) {
final Locale userLocale = preferredLocales[localeIndex];
// Look for perfect match.
if (allSupportedLocales.containsKey('${userLocale.languageCode}_${userLocale.scriptCode}_${userLocale.countryCode}')) {
return userLocale;
}
// Look for language+script match.
if (userLocale.scriptCode != null) {
final Locale? match = languageAndScriptLocales['${userLocale.languageCode}_${userLocale.scriptCode}'];
if (match != null) {
return match;
}
}
// Look for language+country match.
if (userLocale.countryCode != null) {
final Locale? match = languageAndCountryLocales['${userLocale.languageCode}_${userLocale.countryCode}'];
if (match != null) {
return match;
}
}
// If there was a languageCode-only match in the previous iteration's higher
// ranked preferred locale, we return it if the current userLocale does not
// have a better match.
if (matchesLanguageCode != null) {
return matchesLanguageCode;
}
// Look and store language-only match.
Locale? match = languageLocales[userLocale.languageCode];
if (match != null) {
matchesLanguageCode = match;
// Since first (default) locale is usually highly preferred, we will allow
// a languageCode-only match to be instantly matched. If the next preferred
// languageCode is the same, we defer hastily returning until the next iteration
// since at worst it is the same and at best an improved match.
if (localeIndex == 0 &&
!(localeIndex + 1 < preferredLocales.length && preferredLocales[localeIndex + 1].languageCode == userLocale.languageCode)) {
return matchesLanguageCode;
}
}
// countryCode-only match. When all else except default supported locale fails,
// attempt to match by country only, as a user is likely to be familiar with a
// language from their listed country.
if (matchesCountryCode == null && userLocale.countryCode != null) {
match = countryLocales[userLocale.countryCode];
if (match != null) {
matchesCountryCode = match;
}
}
}
// When there is no languageCode-only match. Fallback to matching countryCode only. Country
// fallback only applies on iOS. When there is no countryCode-only match, we return first
// supported locale.
final Locale resolvedLocale = matchesLanguageCode ?? matchesCountryCode ?? supportedLocales.first;
return resolvedLocale;
}
/// The signature of [WidgetsApp.onGenerateTitle].
///
/// Used to generate a value for the app's [Title.title], which the device uses
......@@ -721,6 +849,7 @@ class WidgetsApp extends StatefulWidget {
///
/// * [MaterialApp.localeListResolutionCallback], which sets the callback of the
/// [WidgetsApp] it creates.
/// * [basicLocaleListResolution], the default locale resolution algorithm.
final LocaleListResolutionCallback? localeListResolutionCallback;
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
......@@ -736,6 +865,7 @@ class WidgetsApp extends StatefulWidget {
///
/// * [MaterialApp.localeResolutionCallback], which sets the callback of the
/// [WidgetsApp] it creates.
/// * [basicLocaleListResolution], the default locale resolution algorithm.
final LocaleResolutionCallback? localeResolutionCallback;
/// {@template flutter.widgets.widgetsApp.supportedLocales}
......@@ -804,6 +934,7 @@ class WidgetsApp extends StatefulWidget {
/// when the device's locale changes.
/// * [localizationsDelegates], which collectively define all of the localized
/// resources used by this app.
/// * [basicLocaleListResolution], the default locale resolution algorithm.
final Iterable<Locale> supportedLocales;
/// Turns on a performance overlay.
......@@ -1311,134 +1442,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return basicLocaleListResolution(preferredLocales, supportedLocales);
}
/// The default locale resolution algorithm.
///
/// Custom resolution algorithms can be provided through
/// [WidgetsApp.localeListResolutionCallback] or
/// [WidgetsApp.localeResolutionCallback].
///
/// When no custom locale resolution algorithms are provided or if both fail
/// to resolve, Flutter will default to calling this algorithm.
///
/// This algorithm prioritizes speed at the cost of slightly less appropriate
/// resolutions for edge cases.
///
/// This algorithm will resolve to the earliest preferred locale that
/// matches the most fields, prioritizing in the order of perfect match,
/// languageCode+countryCode, languageCode+scriptCode, languageCode-only.
///
/// In the case where a locale is matched by languageCode-only and is not the
/// default (first) locale, the next preferred locale with a
/// perfect match can supersede the languageCode-only match if it exists.
///
/// When a preferredLocale matches more than one supported locale, it will
/// resolve to the first matching locale listed in the supportedLocales.
///
/// When all preferred locales have been exhausted without a match, the first
/// countryCode only match will be returned.
///
/// When no match at all is found, the first (default) locale in
/// [supportedLocales] will be returned.
///
/// To summarize, the main matching priority is:
///
/// 1. [Locale.languageCode], [Locale.scriptCode], and [Locale.countryCode]
/// 1. [Locale.languageCode] and [Locale.scriptCode] only
/// 1. [Locale.languageCode] and [Locale.countryCode] only
/// 1. [Locale.languageCode] only (with caveats, see above)
/// 1. [Locale.countryCode] only when all [preferredLocales] fail to match
/// 1. Returns the first element of [supportedLocales] as a fallback
///
/// This algorithm does not take language distance (how similar languages are to each other)
/// into account, and will not handle edge cases such as resolving `de` to `fr` rather than `zh`
/// when `de` is not supported and `zh` is listed before `fr` (German is closer to French
/// than Chinese).
static Locale basicLocaleListResolution(List<Locale>? preferredLocales, Iterable<Locale> supportedLocales) {
// preferredLocales can be null when called before the platform has had a chance to
// initialize the locales. Platforms without locale passing support will provide an empty list.
// We default to the first supported locale in these cases.
if (preferredLocales == null || preferredLocales.isEmpty) {
return supportedLocales.first;
}
// Hash the supported locales because apps can support many locales and would
// be expensive to search through them many times.
final Map<String, Locale> allSupportedLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndCountryLocales = HashMap<String, Locale>();
final Map<String, Locale> languageAndScriptLocales = HashMap<String, Locale>();
final Map<String, Locale> languageLocales = HashMap<String, Locale>();
final Map<String?, Locale> countryLocales = HashMap<String?, Locale>();
for (final Locale locale in supportedLocales) {
allSupportedLocales['${locale.languageCode}_${locale.scriptCode}_${locale.countryCode}'] ??= locale;
languageAndScriptLocales['${locale.languageCode}_${locale.scriptCode}'] ??= locale;
languageAndCountryLocales['${locale.languageCode}_${locale.countryCode}'] ??= locale;
languageLocales[locale.languageCode] ??= locale;
countryLocales[locale.countryCode] ??= locale;
}
// Since languageCode-only matches are possibly low quality, we don't return
// it instantly when we find such a match. We check to see if the next
// preferred locale in the list has a high accuracy match, and only return
// the languageCode-only match when a higher accuracy match in the next
// preferred locale cannot be found.
Locale? matchesLanguageCode;
Locale? matchesCountryCode;
// Loop over user's preferred locales
for (int localeIndex = 0; localeIndex < preferredLocales.length; localeIndex += 1) {
final Locale userLocale = preferredLocales[localeIndex];
// Look for perfect match.
if (allSupportedLocales.containsKey('${userLocale.languageCode}_${userLocale.scriptCode}_${userLocale.countryCode}')) {
return userLocale;
}
// Look for language+script match.
if (userLocale.scriptCode != null) {
final Locale? match = languageAndScriptLocales['${userLocale.languageCode}_${userLocale.scriptCode}'];
if (match != null) {
return match;
}
}
// Look for language+country match.
if (userLocale.countryCode != null) {
final Locale? match = languageAndCountryLocales['${userLocale.languageCode}_${userLocale.countryCode}'];
if (match != null) {
return match;
}
}
// If there was a languageCode-only match in the previous iteration's higher
// ranked preferred locale, we return it if the current userLocale does not
// have a better match.
if (matchesLanguageCode != null) {
return matchesLanguageCode;
}
// Look and store language-only match.
Locale? match = languageLocales[userLocale.languageCode];
if (match != null) {
matchesLanguageCode = match;
// Since first (default) locale is usually highly preferred, we will allow
// a languageCode-only match to be instantly matched. If the next preferred
// languageCode is the same, we defer hastily returning until the next iteration
// since at worst it is the same and at best an improved match.
if (localeIndex == 0 &&
!(localeIndex + 1 < preferredLocales.length && preferredLocales[localeIndex + 1].languageCode == userLocale.languageCode)) {
return matchesLanguageCode;
}
}
// countryCode-only match. When all else except default supported locale fails,
// attempt to match by country only, as a user is likely to be familiar with a
// language from their listed country.
if (matchesCountryCode == null && userLocale.countryCode != null) {
match = countryLocales[userLocale.countryCode];
if (match != null) {
matchesCountryCode = match;
}
}
}
// When there is no languageCode-only match. Fallback to matching countryCode only. Country
// fallback only applies on iOS. When there is no countryCode-only match, we return first
// supported locale.
final Locale resolvedLocale = matchesLanguageCode ?? matchesCountryCode ?? supportedLocales.first;
return resolvedLocale;
}
@override
void didChangeLocales(List<Locale>? locales) {
final Locale newLocale = _resolveLocales(locales, widget.supportedLocales);
......
......@@ -328,6 +328,133 @@ void main() {
);
expect(ScrollConfiguration.of(capturedContext).runtimeType, ScrollBehavior);
});
test('basicLocaleListResolution', () {
// Matches exactly for language code.
expect(
basicLocaleListResolution(
<Locale>[
const Locale('zh'),
const Locale('un'),
const Locale('en'),
],
<Locale>[
const Locale('en'),
],
),
const Locale('en'),
);
// Matches exactly for language code and country code.
expect(
basicLocaleListResolution(
<Locale>[
const Locale('en'),
const Locale('en', 'US'),
],
<Locale>[
const Locale('en', 'US'),
],
),
const Locale('en', 'US'),
);
// Matches language+script over language+country
expect(
basicLocaleListResolution(
<Locale>[
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
countryCode: 'HK',
),
],
<Locale>[
const Locale.fromSubtags(
languageCode: 'zh',
countryCode: 'HK',
),
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
),
],
),
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
),
);
// Matches exactly for language code, script code and country code.
expect(
basicLocaleListResolution(
<Locale>[
const Locale.fromSubtags(
languageCode: 'zh',
),
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
countryCode: 'TW',
),
],
<Locale>[
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
countryCode: 'TW',
),
],
),
const Locale.fromSubtags(
languageCode: 'zh',
scriptCode: 'Hant',
countryCode: 'TW',
),
);
// Selects for country code if the language code is not found in the
// preferred locales list.
expect(
basicLocaleListResolution(
<Locale>[
const Locale.fromSubtags(
languageCode: 'en',
),
const Locale.fromSubtags(
languageCode: 'ar',
countryCode: 'tn',
),
],
<Locale>[
const Locale.fromSubtags(
languageCode: 'fr',
countryCode: 'tn',
),
],
),
const Locale.fromSubtags(
languageCode: 'fr',
countryCode: 'tn',
),
);
// Selects first (default) locale when no match at all is found.
expect(
basicLocaleListResolution(
<Locale>[
const Locale('tn'),
],
<Locale>[
const Locale('zh'),
const Locale('un'),
const Locale('en'),
],
),
const Locale('zh'),
);
});
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment