[web] Add Intl.Locale to parse browser languages. (#172964)

Closes https://github.com/flutter/flutter/issues/130174

### Description
- Adds `DomLocale` extension type for
[`Intl.Locale`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale)
- Replaces manual browser language parsing with `DomLocale` usage
- Adds tests to cover new functionality

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Kostia Sokolovskyi 2025-08-04 16:28:20 +02:00 committed by GitHub
parent fe07188bf3
commit a128ccbe94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 157 additions and 7 deletions

View File

@ -2444,6 +2444,50 @@ extension type DomSegments._(JSObject _) implements JSObject {
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale
@JS('Intl.Locale')
extension type DomLocale._(JSObject _) implements JSObject {
external DomLocale(String tag, [DomLocaleOptions? options]);
external String get language;
external String? get script;
external String? get region;
external String? get calendar;
external String? get caseFirst;
external String? get collation;
external String? get hourCycle;
external String? get numberingSystem;
external bool? get numeric;
@JS('toString')
external String toJSString();
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/Locale#options
extension type DomLocaleOptions._(JSObject _) implements JSObject {
external DomLocaleOptions({
String? language,
String? script,
String? region,
String? calendar,
String? caseFirst,
String? collation,
String? hourCycle,
String? numberingSystem,
bool? numeric,
});
external String? get language;
external String? get script;
external String? get region;
external String? get calendar;
external String? get caseFirst;
external String? get collation;
external String? get hourCycle;
external String? get numberingSystem;
external bool? get numeric;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
@JS('Iterator')
extension type DomIterator._(JSObject _) implements JSObject {

View File

@ -901,9 +901,22 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
configuration = configuration.copyWith(locales: parseBrowserLanguages());
}
/// Overrides the browser languages list.
///
/// If [value] is null, resets the browser languages back to the real value.
///
/// This is intended for tests only.
@visibleForTesting
static void debugOverrideBrowserLanguages(List<String>? value) {
_browserLanguagesOverride = value;
}
static List<String>? _browserLanguagesOverride;
@visibleForTesting
static List<ui.Locale> parseBrowserLanguages() {
// TODO(yjbanov): find a solution for IE
final List<String>? languages = domWindow.navigator.languages;
final List<String>? languages = _browserLanguagesOverride ?? domWindow.navigator.languages;
if (languages == null || languages.isEmpty) {
// To make it easier for the app code, let's not leave the locales list
// empty. This way there's fewer corner cases for apps to handle.
@ -912,12 +925,14 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
final List<ui.Locale> locales = <ui.Locale>[];
for (final String language in languages) {
final List<String> parts = language.split('-');
if (parts.length > 1) {
locales.add(ui.Locale(parts.first, parts.last));
} else {
locales.add(ui.Locale(language));
}
final DomLocale domLocale = DomLocale(language);
locales.add(
ui.Locale.fromSubtags(
languageCode: domLocale.language,
scriptCode: domLocale.script,
countryCode: domLocale.region,
),
);
}
assert(locales.isNotEmpty);

View File

@ -4,6 +4,7 @@
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
void main() {
@ -60,4 +61,66 @@ void testMain() {
isNot(const Locale.fromSubtags(languageCode: 'en', scriptCode: 'Latn').hashCode),
);
});
test('DomLocale', () {
final locale1 = DomLocale('uk-UA');
expect(locale1.language, 'uk');
expect(locale1.script, isNull);
expect(locale1.region, 'UA');
expect(locale1.calendar, isNull);
expect(locale1.caseFirst, isNull);
expect(locale1.collation, isNull);
expect(locale1.hourCycle, isNull);
expect(locale1.numberingSystem, isNull);
expect(locale1.numeric, false);
final locale2 = DomLocale('en-Latn-US');
expect(locale2.language, 'en');
expect(locale2.script, 'Latn');
expect(locale2.region, 'US');
expect(locale2.calendar, isNull);
expect(locale2.caseFirst, isNull);
expect(locale2.collation, isNull);
expect(locale2.hourCycle, isNull);
expect(locale2.numberingSystem, isNull);
expect(locale2.numeric, false);
final locale3 = DomLocale('de-Latn-DE-u-ca-gregory-kf-upper-co-dict-hc-h24-nu-latn-kn-true');
expect(locale3.language, 'de');
expect(locale3.script, 'Latn');
expect(locale3.region, 'DE');
expect(locale3.calendar, 'gregory');
expect(locale3.caseFirst, 'upper');
expect(locale3.collation, 'dict');
expect(locale3.hourCycle, 'h24');
expect(locale3.numberingSystem, 'latn');
expect(locale3.numeric, isTrue);
final locale4 = DomLocale(
'th',
DomLocaleOptions(
script: 'Thai',
region: 'TH',
calendar: 'buddhist',
caseFirst: 'lower',
collation: 'dict',
hourCycle: 'h12',
numberingSystem: 'thai',
numeric: true,
),
);
expect(locale4.language, 'th');
expect(locale4.script, 'Thai');
expect(locale4.region, 'TH');
expect(locale4.calendar, 'buddhist');
expect(locale4.caseFirst, 'lower');
expect(locale4.collation, 'dict');
expect(locale4.hourCycle, 'h12');
expect(locale4.numberingSystem, 'thai');
expect(locale4.numeric, true);
});
}

View File

@ -430,6 +430,34 @@ void testMain() {
});
});
group('parseBrowserLanguages', () {
test('returns the default locale when no browser languages are present', () {
EnginePlatformDispatcher.debugOverrideBrowserLanguages([]);
addTearDown(() => EnginePlatformDispatcher.debugOverrideBrowserLanguages(null));
expect(EnginePlatformDispatcher.parseBrowserLanguages(), const [ui.Locale('en', 'US')]);
});
test('returns locales list parsed from browser languages', () {
EnginePlatformDispatcher.debugOverrideBrowserLanguages([
'uk-UA',
'en',
'ar-Arab-SA',
'zh-Hant-HK',
'de-DE',
]);
addTearDown(() => EnginePlatformDispatcher.debugOverrideBrowserLanguages(null));
expect(EnginePlatformDispatcher.parseBrowserLanguages(), const [
ui.Locale('uk', 'UA'),
ui.Locale('en'),
ui.Locale.fromSubtags(languageCode: 'ar', scriptCode: 'Arab', countryCode: 'SA'),
ui.Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'),
ui.Locale('de', 'DE'),
]);
});
});
group('AT Focus Handler Integration', () {
test('navigation focus handler is registered during initialization', () {
final DomElement navButton = createDomHTMLButtonElement();