From 2b2fbf0f6474c99feca7e31d17e7687a032bd407 Mon Sep 17 00:00:00 2001 From: hugovdm Date: Mon, 29 Oct 2018 22:48:59 +0100 Subject: [PATCH] Add Locale.fromSubtags and support for scriptCode. (#6518) * Add Locale.fromComponents. * Change toString from underscores to dashes. Expand the unit tests. * Rename 'fromComponents' to 'create'. Change variants from String to List. * Use default for language parameter. Use hashCode/hashList. * Have toString() stick with old (underscore) behaviour. * Demonstrate empty-list bug in assert code. * Fix empty-list assert bug. * Add ignores for lint issues. Unsure about 71340 though. * Fix operator== via _listEquals. * Remove length-checking asserts: we're anyway not checking characters in fields. * Documentation update. * Change reasoning for ignore:prefer_initializing_formals. * Try 'fromSubtags' as new constructor name. * Documentation improvements based on Pull Request review. * Assert-fail for invalid-length subtags and drop bad subtags in production code. * Revert "Assert-fail for invalid-length subtags and drop bad subtags in production code." This reverts commit d6f06f5e7b3537d60000c47641580475ef16abbe. * Re-fix Locale.toString() for variants=[]. * Tear out variants, in case we want to have one fewer pointer in the future. * Make named parameters' names consistent with member names. * Also remove _listEquals: no longer in use. * Lint fix. * Fix code review nits. * Lint fix for assert, and a couple more not-zero-length-string asserts. * Code Review: two of three nits addressed... * Review fix: change 'should' to 'must' for subtag prescriptions. * Assert-check that countryCode is never ''. --- lib/ui/window.dart | 120 +++++++++++++++++++++++++++------- testing/dart/locale_test.dart | 28 +++++++- 2 files changed, 121 insertions(+), 27 deletions(-) diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 28f91372460f2..fff5eb46206bc 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -116,9 +116,11 @@ class WindowPadding { } } -/// An identifier used to select a user's language and formatting preferences, -/// consisting of a language and a country. This is a subset of locale -/// identifiers as defined by BCP 47. +/// An identifier used to select a user's language and formatting preferences. +/// +/// This represents a [Unicode Language +/// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) +/// (i.e. without Locale extensions), except variants are not supported. /// /// Locales are canonicalized according to the "preferred value" entries in the /// [IANA Language Subtag @@ -145,16 +147,58 @@ class Locale { /// The primary language subtag must not be null. The region subtag is /// optional. /// - /// The values are _case sensitive_, and should match the case of the relevant - /// subtags in the [IANA Language Subtag - /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry). - /// Typically this means the primary language subtag should be lowercase and - /// the region subtag should be uppercase. - const Locale(this._languageCode, [ this._countryCode ]) : assert(_languageCode != null), assert(_languageCode != ''); + /// The subtag values are _case sensitive_ and must be one of the valid + /// subtags according to CLDR supplemental data: + /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), + /// [region](http://unicode.org/cldr/latest/common/validity/region.xml). The + /// primary language subtag must be at least two and at most eight lowercase + /// letters, but not four letters. The region region subtag must be two + /// uppercase letters or three digits. See the [Unicode Language + /// Identifier](https://www.unicode.org/reports/tr35/#Unicode_language_identifier) + /// specification. + /// + /// Validity is not checked by default, but some methods may throw away + /// invalid data. + /// + /// See also: + /// + /// * [new Locale.fromSubtags], which also allows a [scriptCode] to be + /// specified. + const Locale( + this._languageCode, [ + this._countryCode, + ]) : assert(_languageCode != null), + assert(_languageCode != ''), + scriptCode = null, + assert(_countryCode != ''); + + /// Creates a new Locale object. + /// + /// The keyword arguments specify the subtags of the Locale. + /// + /// The subtag values are _case sensitive_ and must be valid subtags according + /// to CLDR supplemental data: + /// [language](http://unicode.org/cldr/latest/common/validity/language.xml), + /// [script](http://unicode.org/cldr/latest/common/validity/script.xml) and + /// [region](http://unicode.org/cldr/latest/common/validity/region.xml) for + /// each of languageCode, scriptCode and countryCode respectively. + /// + /// Validity is not checked by default, but some methods may throw away + /// invalid data. + const Locale.fromSubtags({ + String languageCode = 'und', + this.scriptCode, + String countryCode, + }) : assert(languageCode != null), + assert(languageCode != ''), + _languageCode = languageCode, + assert(scriptCode != ''), + assert(countryCode != ''), + _countryCode = countryCode; /// The primary language subtag for the locale. /// - /// This must not be null. + /// This must not be null. It may be 'und', representing 'undefined'. /// /// This is expected to be string registered in the [IANA Language Subtag /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) @@ -166,10 +210,19 @@ class Locale { /// Locale('he')` and `const Locale('iw')` are equal, and both have the /// [languageCode] `he`, because `iw` is a deprecated language subtag that was /// replaced by the subtag `he`. - String get languageCode => _canonicalizeLanguageCode(_languageCode); + /// + /// This must be a valid Unicode Language subtag as listed in [Unicode CLDR + /// supplemental + /// data](http://unicode.org/cldr/latest/common/validity/language.xml). + /// + /// See also: + /// + /// * [new Locale.fromSubtags], which describes the conventions for creating + /// [Locale] objects. + String get languageCode => _replaceDeprecatedLanguageSubtag(_languageCode); final String _languageCode; - static String _canonicalizeLanguageCode(String languageCode) { + static String _replaceDeprecatedLanguageSubtag(String languageCode) { // This switch statement is generated by //flutter/tools/gen_locale.dart // Mappings generated for language subtag registry as of 2018-08-08. switch (languageCode) { @@ -255,9 +308,23 @@ class Locale { } } + /// The script subtag for the locale. + /// + /// This may be null, indicating that there is no specified script subtag. + /// + /// This must be a valid Unicode Language Identifier script subtag as listed + /// in [Unicode CLDR supplemental + /// data](http://unicode.org/cldr/latest/common/validity/script.xml). + /// + /// See also: + /// + /// * [new Locale.fromSubtags], which describes the conventions for creating + /// [Locale] objects. + final String scriptCode; + /// The region subtag for the locale. /// - /// This can be null. + /// This may be null, indicating that there is no specified region subtag. /// /// This is expected to be string registered in the [IANA Language Subtag /// Registry](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry) @@ -269,10 +336,15 @@ class Locale { /// 'DE')` and `const Locale('de', 'DD')` are equal, and both have the /// [countryCode] `DE`, because `DD` is a deprecated language subtag that was /// replaced by the subtag `DE`. - String get countryCode => _canonicalizeRegionCode(_countryCode); + /// + /// See also: + /// + /// * [new Locale.fromSubtags], which describes the conventions for creating + /// [Locale] objects. + String get countryCode => _replaceDeprecatedRegionSubtag(_countryCode); final String _countryCode; - static String _canonicalizeRegionCode(String regionCode) { + static String _replaceDeprecatedRegionSubtag(String regionCode) { // This switch statement is generated by //flutter/tools/gen_locale.dart // Mappings generated for language subtag registry as of 2018-08-08. switch (regionCode) { @@ -294,23 +366,21 @@ class Locale { return false; final Locale typedOther = other; return languageCode == typedOther.languageCode + && scriptCode == typedOther.scriptCode && countryCode == typedOther.countryCode; } @override - int get hashCode { - int result = 373; - result = 37 * result + languageCode.hashCode; - if (_countryCode != null) - result = 37 * result + countryCode.hashCode; - return result; - } + int get hashCode => hashValues(languageCode, scriptCode, countryCode); @override String toString() { - if (_countryCode == null) - return languageCode; - return '${languageCode}_$countryCode'; + final StringBuffer out = StringBuffer(languageCode); + if (scriptCode != null) + out.write('_$scriptCode'); + if (_countryCode != null) + out.write('_$countryCode'); + return out.toString(); } } diff --git a/testing/dart/locale_test.dart b/testing/dart/locale_test.dart index 60a7bf99fe2af..4613a707510c4 100644 --- a/testing/dart/locale_test.dart +++ b/testing/dart/locale_test.dart @@ -12,11 +12,35 @@ void main() { expect(const Locale('en').toString(), 'en'); expect(const Locale('en'), new Locale('en', $null)); expect(const Locale('en').hashCode, new Locale('en', $null).hashCode); - expect(const Locale('en'), isNot(new Locale('en', ''))); - expect(const Locale('en').hashCode, isNot(new Locale('en', '').hashCode)); expect(const Locale('en', 'US').toString(), 'en_US'); expect(const Locale('iw').toString(), 'he'); expect(const Locale('iw', 'DD').toString(), 'he_DE'); expect(const Locale('iw', 'DD'), const Locale('he', 'DE')); }); + + test('Locale.fromSubtags', () { + expect(const Locale.fromSubtags().languageCode, 'und'); + expect(const Locale.fromSubtags().scriptCode, null); + expect(const Locale.fromSubtags().countryCode, null); + + expect(const Locale.fromSubtags(languageCode: 'en').toString(), 'en'); + expect(const Locale.fromSubtags(languageCode: 'en').languageCode, 'en'); + expect(const Locale.fromSubtags(scriptCode: 'Latn').toString(), 'und_Latn'); + expect(const Locale.fromSubtags(scriptCode: 'Latn').scriptCode, 'Latn'); + expect(const Locale.fromSubtags(countryCode: 'US').toString(), 'und_US'); + expect(const Locale.fromSubtags(countryCode: 'US').countryCode, 'US'); + + expect(Locale.fromSubtags(languageCode: 'es', countryCode: '419').toString(), 'es_419'); + expect(Locale.fromSubtags(languageCode: 'es', countryCode: '419').languageCode, 'es'); + expect(Locale.fromSubtags(languageCode: 'es', countryCode: '419').countryCode, '419'); + + expect(Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN').toString(), 'zh_Hans_CN'); + }); + + test('Locale equality', () { + expect(Locale.fromSubtags(languageCode: 'en'), + isNot(Locale.fromSubtags(languageCode: 'en', scriptCode: 'Latn'))); + expect(Locale.fromSubtags(languageCode: 'en').hashCode, + isNot(Locale.fromSubtags(languageCode: 'en', scriptCode: 'Latn').hashCode)); + }); }