// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// Tests for the Locale class. /// /// Currently, the primary intention of these tests is to exercise and /// demonstrate the API: full test coverage is a non-goal for the prototype. /// /// For production code, use of ICU would influence what needs and doesn't need /// to be tested. library; import 'package:intl/locale.dart'; import 'package:test/test.dart'; import 'locale_test_data.dart'; void main() { group('Construction and properties:', () { // Simple with normalization: testFromSubtags('Zh', null, null, 'zh', null, null, 'zh'); testFromSubtags('zH', null, 'cn', 'zh', null, 'CN', 'zh-CN'); testFromSubtags('ZH', null, 'Cn', 'zh', null, 'CN', 'zh-CN'); testFromSubtags('zh', null, 'cN', 'zh', null, 'CN', 'zh-CN'); testFromSubtags('zh', 'hans', null, 'zh', 'Hans', null, 'zh-Hans'); testFromSubtags('ZH', 'HANS', 'CN', 'zh', 'Hans', 'CN', 'zh-Hans-CN'); // Region codes can be three digits. testFromSubtags('es', null, '419', 'es', null, '419', 'es-419'); // While language is usually 2 characters, it can also be 3. testFromSubtags('CKB', 'arab', null, 'ckb', 'Arab', null, 'ckb-Arab'); // With canonicalization: testFromSubtags('Iw', null, null, 'he', null, null, 'he'); testFromSubtags('iW', null, null, 'he', null, null, 'he'); testFromSubtags('My', null, 'Bu', 'my', null, 'MM', 'my-MM'); }); group('Locale.fromSubtags() FormatExceptions:', () { void testExceptionForSubtags( String language, String? script, String? region) { test('fromSubtags: "$language / $script / $region"', () { expect( () => Locale.fromSubtags( languageCode: language, scriptCode: script, countryCode: region), throwsFormatException); }); } testExceptionForSubtags('a', null, null); testExceptionForSubtags('en', 'ZA', null); testExceptionForSubtags('en', null, 'Latn'); }); group('Locale normalization matching ICU.', () { localeParsingTestData.forEach((unnormalized, normalized) { test('Locale normalization: $unnormalized -> $normalized', () { expect(Locale.parse(unnormalized).toLanguageTag(), normalized); }); }); }); group('Unicode LDML Locale Identifier support', () { // 'root' is a valid Unicode Locale Identifier, but should be taken as // 'und'[1]. ICU's toLanguageTag still returns 'root'. // [1]: // http://unicode.org/reports/tr35/#Unicode_Locale_Identifier_CLDR_to_BCP_47 testParse('root', 'und', null, null, [], 'und'); testParse('Root', 'und', null, null, [], 'und'); testParse('ROOT', 'und', null, null, [], 'und'); // We support underscores, whereas ICU's `forLanguageTag` does // not. testParse('CKB_arab', 'ckb', 'Arab', null, [], 'ckb-Arab'); testParse('My_Bu', 'my', null, 'MM', [], 'my-MM'); // Normalises tags, sorts subtags alphabetically, including variants[1]: // ICU is currently not sorting variants. // [1]: http://unicode.org/reports/tr35/#Unicode_locale_identifier testParse('en-scouse-fonipa', 'en', null, null, ['fonipa', 'scouse'], 'en-fonipa-scouse'); // Normalises tags, sorts subtags alphabetically and suppresses unneeded // "true" in u extension (ICU is currently not dropping -true): // http://unicode.org/reports/tr35/#u_Extension testParse('en-u-Foo-bar-nu-thai-ca-buddhist-kk-true', 'en', null, null, [], 'en-u-bar-foo-ca-buddhist-kk-nu-thai'); // The specification does permit empty extensions for extensions other than // u- and t-. testParse('en-a', 'en', null, null, [], 'en'); testParse('en-x', 'en', null, null, [], 'en'); testParse('en-z', 'en', null, null, [], 'en'); // Normalization of `tlang` - ICU still returns -t-iw-bu. testParse('en-t-iw-Bu', 'en', null, null, [], 'en-t-he-mm'); test('en-u-ca is equivalent to en-u-ca-true', () { expect(Locale.parse('en-u-ca').toLanguageTag(), Locale.parse('en-u-ca-true').toLanguageTag()); }); }); // Normalization: sorting of extension subtags: testParse('en-z-abc-001-foo-fii-bar-u-cu-usd-co-phonebk', 'en', null, null, [], 'en-u-co-phonebk-cu-usd-z-abc-001-foo-fii-bar'); group('Locale.parse() throws FormatException:', () { void testExceptionForId(String x) { test('"$x"', () { expect(() => Locale.parse(x), throwsFormatException); }); } for (var badLocaleIdentifier in invalidLocales) { testExceptionForId(badLocaleIdentifier); } // ICU permits '', taking it as 'und', but it is not a valid Unicode Locale // Identifier: We reject it. testExceptionForId(''); // abcd-Latn throws exceptions in our Dart implementation, whereas // ECMAScript's Intl.Locale permits it. This is because the BCP47 spec // still allows for the possible addition of 4-character languages in // the future, whereas the Unicode Locale Identifiers spec bans it // outright. testExceptionForId('abcd-Latn'); // ICU permits 'root-Latn' since it conforms to pure BCP47, but it is an // invalid Unicode BCP47 Locale Identifier. testExceptionForId('root-Latn'); // ICU permits empty tkeys. testExceptionForId('en-t-a0'); // ICU permits duplicate tkeys, returning the content of -t- verbatim. testExceptionForId('en-t-a0-one-a0-two'); // ICU permits duplicate keys, in this case dropping -ca-buddhist. testExceptionForId('en-u-ca-islamic-ca-buddhist'); }); group('Locale.tryParse() returns null:', () { for (var badLocaleIdentifier in invalidLocales) { test('"$badLocaleIdentifier"', () { expect(Locale.tryParse(badLocaleIdentifier), isNull); }); } }); // TODO: determine appropriate behaviour for the following examples. // // 'mo' is deprecated, and is a tag that ought to be replaced by *two* // // subtags (ro-MD), although Chrome Unstable also doesn't presently do // // that (replaces it by 'ro' only). // // TODO: check up on the Chrome implementation. // testParse('mo', 'ro', null, 'MD', [], 'ro-MD'); // // Script deprecation. // testParse('en-Qaai', 'en', 'Zinh', null, [], 'en-Zinh'); // // Variant deprecation. // testParse('sv-aaland', 'sv', null, 'AX', [], 'sv-AX'); // // Variant deprecation. // testParse('en-heploc', 'en', null, null, ['alalc97'], 'en-alalc97'); // // Variant deprecation. // testParse('en-polytoni', 'en', null, null, ['polyton'], 'en-polyton'); test('Locale cannot be modified via the variants field', () { var l = Locale.parse('en-scotland'); var v = l.variants as List; var good = false; try { v.add('basiceng'); } on Error { good = true; } expect(l.toLanguageTag(), 'en-scotland'); expect(good, isTrue); }); test('operator== and hashCode', () { Locale l1, l2; l1 = Locale.parse('en-Shaw-ZA'); l2 = Locale.fromSubtags( languageCode: 'en', scriptCode: 'Shaw', countryCode: 'ZA'); expect(l1, l2); expect(l1.hashCode, l2.hashCode); l1 = Locale.parse('en'); l2 = Locale.fromSubtags( languageCode: 'en', scriptCode: null, countryCode: null); expect(l1, l2); expect(l1.hashCode, l2.hashCode); }); } void testFromSubtags( String language, String? script, String? region, String? expectedLanguage, String? expectedScript, String? expectedRegion, String? expectedTag) { test('Locale.fromSubtags(...) with $language, $script, $region', () { var l = Locale.fromSubtags( languageCode: language, scriptCode: script, countryCode: region); expect(l.languageCode, expectedLanguage); expect(l.scriptCode, expectedScript); expect(l.countryCode, expectedRegion); expect(l.toLanguageTag(), expectedTag); expect(l.toString(), expectedTag); }); } void testParse( String bcp47Tag, String expectedLanguage, String? expectedScript, String? expectedRegion, Iterable expectedVariants, String? expectedTag) { test('Locale.parse("$bcp47Tag");', () { var l = Locale.parse(bcp47Tag); expect(l.languageCode, expectedLanguage); expect(l.scriptCode, expectedScript); expect(l.countryCode, expectedRegion); expect(l.toLanguageTag(), expectedTag); expect(l.variants, orderedEquals(expectedVariants)); }); }