From 7173e297a229254dd9faed4abe25db17fb96dc68 Mon Sep 17 00:00:00 2001 From: Batjaa Batbold Date: Wed, 27 Apr 2022 07:15:24 -0700 Subject: [PATCH] feat(@formatjs/icu-messageformat-parser): preprocess 'j' date time skeleton before parsing as suggested in the Unicode guide (#3544) feat(intl-messageformat): add support for 'j' time skeleton format Co-authored-by: Batjaa Batbold --- CONTRIBUTING.md | 22 + packages/icu-messageformat-parser/BUILD | 9 + .../date-time-pattern-generator.ts | 96 ++ packages/icu-messageformat-parser/parser.ts | 21 +- .../scripts/time-data-gen.ts | 30 + .../tests/__snapshots__/parser.test.ts.snap | 358 +++++ .../tests/date-time-pattern-generator.test.ts | 60 + .../tests/parser.test.ts | 22 + .../time-data.generated.ts | 1339 +++++++++++++++++ packages/intl-messageformat/src/core.ts | 19 +- .../intl-messageformat/tests/index.test.ts | 29 + 11 files changed, 1998 insertions(+), 7 deletions(-) create mode 100644 packages/icu-messageformat-parser/date-time-pattern-generator.ts create mode 100644 packages/icu-messageformat-parser/scripts/time-data-gen.ts create mode 100644 packages/icu-messageformat-parser/tests/date-time-pattern-generator.test.ts create mode 100644 packages/icu-messageformat-parser/time-data.generated.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca0cbfacf8..957db8458d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,3 +73,25 @@ npm run release:next ``` bazel run //packages/intl-datetimeformat:tz_data.update ``` + +### Generating CLDR data + +1. Check out `./BUILD` file for generatable data — which are identifiable via `generate_src_file()` call + ```BUILD + generate_src_file( + name = "regex", + ... + ) + ``` +2. Create an empty file with the given `src` attribute — path is relative to module root + ```shell + touch packages/icu-messageformat-parser/regex.generated.ts + ``` +3. Run update script + ```shell + bazel run //packages/icu-messageformat-parser:regex.update + ``` +4. Verify + ```shell + bazel run //packages/icu-messageformat-parser:regex + ``` diff --git a/packages/icu-messageformat-parser/BUILD b/packages/icu-messageformat-parser/BUILD index 6f0c380a68..7f6b0af0ad 100644 --- a/packages/icu-messageformat-parser/BUILD +++ b/packages/icu-messageformat-parser/BUILD @@ -106,6 +106,15 @@ generate_src_file( entry_point = "scripts/regex-gen.ts", ) +generate_src_file( + name = "time-data", + src = "time-data.generated.ts", + data = [ + "@npm//cldr-core", + ], + entry_point = "scripts/time-data-gen.ts", +) + package_json_test( name = "package_json_test", deps = SRC_DEPS, diff --git a/packages/icu-messageformat-parser/date-time-pattern-generator.ts b/packages/icu-messageformat-parser/date-time-pattern-generator.ts new file mode 100644 index 0000000000..65fc3b671f --- /dev/null +++ b/packages/icu-messageformat-parser/date-time-pattern-generator.ts @@ -0,0 +1,96 @@ +import {timeData} from './time-data.generated' + +/** + * Returns the best matching date time pattern if a date time skeleton + * pattern is provided with a locale. Follows the Unicode specification: + * https://www.unicode.org/reports/tr35/tr35-dates.html#table-mapping-requested-time-skeletons-to-patterns + * @param skeleton date time skeleton pattern that possibly includes j, J or C + * @param locale + */ +export function getBestPattern(skeleton: string, locale: Intl.Locale) { + let skeletonCopy = '' + for (let patternPos = 0; patternPos < skeleton.length; patternPos++) { + const patternChar = skeleton.charAt(patternPos) + + if (patternChar === 'j') { + let extraLength = 0 + while ( + patternPos + 1 < skeleton.length && + skeleton.charAt(patternPos + 1) === patternChar + ) { + extraLength++ + patternPos++ + } + + let hourLen = 1 + (extraLength & 1) + let dayPeriodLen = extraLength < 2 ? 1 : 3 + (extraLength >> 1) + let dayPeriodChar = 'a' + let hourChar = getDefaultHourSymbolFromLocale(locale) + + if (hourChar == 'H' || hourChar == 'k') { + dayPeriodLen = 0 + } + + while (dayPeriodLen-- > 0) { + skeletonCopy += dayPeriodChar + } + while (hourLen-- > 0) { + skeletonCopy = hourChar + skeletonCopy + } + } else if (patternChar === 'J') { + skeletonCopy += 'H' + } else { + skeletonCopy += patternChar + } + } + + return skeletonCopy +} + +/** + * Maps the [hour cycle type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle) + * of the given `locale` to the corresponding time pattern. + * @param locale + */ +function getDefaultHourSymbolFromLocale(locale: Intl.Locale): string { + let hourCycle = locale.hourCycle + + if ( + hourCycle === undefined && + // @ts-ignore hourCycle(s) is not identified yet + locale.hourCycles && + // @ts-ignore + locale.hourCycles.length + ) { + // @ts-ignore + hourCycle = locale.hourCycles[0] + } + + if (hourCycle) { + switch (hourCycle) { + case 'h24': + return 'k' + case 'h23': + return 'H' + case 'h12': + return 'h' + case 'h11': + return 'K' + default: + throw new Error('Invalid hourCycle') + } + } + + // TODO: Once hourCycle is fully supported remove the following with data generation + const languageTag = locale.language + let regionTag: string | undefined + if (languageTag !== 'root') { + regionTag = locale.maximize().region + } + const hourCycles = + timeData[regionTag || ''] || + timeData[languageTag || ''] || + timeData[`${languageTag}-001`] || + timeData['001'] + return hourCycles[0] +} diff --git a/packages/icu-messageformat-parser/parser.ts b/packages/icu-messageformat-parser/parser.ts index dbde0cf1f6..8e770bf757 100644 --- a/packages/icu-messageformat-parser/parser.ts +++ b/packages/icu-messageformat-parser/parser.ts @@ -17,6 +17,7 @@ import { parseNumberSkeletonFromString, parseDateTimeSkeleton, } from '@formatjs/icu-skeleton-parser' +import {getBestPattern} from './date-time-pattern-generator' const SPACE_SEPARATOR_START_REGEX = new RegExp( `^${SPACE_SEPARATOR_REGEX.source}*` @@ -56,6 +57,8 @@ export interface ParserOptions { * Default is false */ captureLocation?: boolean + + locale?: Intl.Locale } export type Result = {val: T; err: null} | {val: null; err: E} @@ -236,6 +239,7 @@ if (REGEX_SUPPORTS_U_AND_Y) { export class Parser { private message: string private position: Position + private locale?: Intl.Locale private ignoreTag: boolean private requiresOtherClause: boolean @@ -245,6 +249,7 @@ export class Parser { this.message = message this.position = {offset: 0, line: 1, column: 1} this.ignoreTag = !!options.ignoreTag + this.locale = options.locale this.requiresOtherClause = !!options.requiresOtherClause this.shouldParseSkeletons = !!options.shouldParseSkeletons } @@ -739,7 +744,7 @@ export class Parser { // Extract style or skeleton if (styleAndLocation && startsWith(styleAndLocation?.style, '::', 0)) { // Skeleton starts with `::`. - const skeleton = trimStart(styleAndLocation.style.slice(2)) + let skeleton = trimStart(styleAndLocation.style.slice(2)) if (argType === 'number') { const result = this.parseNumberSkeletonFromString( @@ -757,12 +762,22 @@ export class Parser { if (skeleton.length === 0) { return this.error(ErrorKind.EXPECT_DATE_TIME_SKELETON, location) } + + let dateTimePattern = skeleton + + // Get "best match" pattern only if locale is passed, if not, let it + // pass as-is where `parseDateTimeSkeleton()` will throw an error + // for unsupported patterns. + if (this.locale) { + dateTimePattern = getBestPattern(skeleton, this.locale) + } + const style: DateTimeSkeleton = { type: SKELETON_TYPE.dateTime, - pattern: skeleton, + pattern: dateTimePattern, location: styleAndLocation.styleLocation, parsedOptions: this.shouldParseSkeletons - ? parseDateTimeSkeleton(skeleton) + ? parseDateTimeSkeleton(dateTimePattern) : {}, } diff --git a/packages/icu-messageformat-parser/scripts/time-data-gen.ts b/packages/icu-messageformat-parser/scripts/time-data-gen.ts new file mode 100644 index 0000000000..6ae46c5d5e --- /dev/null +++ b/packages/icu-messageformat-parser/scripts/time-data-gen.ts @@ -0,0 +1,30 @@ +import * as rawTimeData from 'cldr-core/supplemental/timeData.json' +import {outputFileSync} from 'fs-extra' +import minimist from 'minimist' + +function main(args: minimist.ParsedArgs) { + const {timeData} = rawTimeData.supplemental + const data = Object.keys(timeData).reduce( + (all: Record, k) => { + all[k.replace('_', '-')] = + timeData[k as keyof typeof timeData]._allowed.split(' ') + return all + }, + {} + ) + outputFileSync( + args.out, + `// @generated from time-data-gen.ts +// prettier-ignore +export const timeData: Record = ${JSON.stringify( + data, + undefined, + 2 + )}; +` + ) +} + +if (require.main === module) { + main(minimist(process.argv)) +} diff --git a/packages/icu-messageformat-parser/tests/__snapshots__/parser.test.ts.snap b/packages/icu-messageformat-parser/tests/__snapshots__/parser.test.ts.snap index 2817598990..25d7e5dee6 100644 --- a/packages/icu-messageformat-parser/tests/__snapshots__/parser.test.ts.snap +++ b/packages/icu-messageformat-parser/tests/__snapshots__/parser.test.ts.snap @@ -434,6 +434,364 @@ Object { } `; +exports[`date_skeleton date_arg_skeleton_with_jJ 1`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 15, + "line": 1, + "offset": 14, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 14, + "line": 1, + "offset": 13, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "numeric", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "ha", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 2`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 15, + "line": 1, + "offset": 14, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "2-digit", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "hha", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 3`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 17, + "line": 1, + "offset": 16, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "numeric", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "haaaa", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 4`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 18, + "line": 1, + "offset": 17, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 17, + "line": 1, + "offset": 16, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "2-digit", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "hhaaaa", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 5`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 19, + "line": 1, + "offset": 18, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 18, + "line": 1, + "offset": 17, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "numeric", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "haaaaa", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 6`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 20, + "line": 1, + "offset": 19, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 19, + "line": 1, + "offset": 18, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "2-digit", + "hour12": true, + "hourCycle": "h12", + }, + "pattern": "hhaaaaa", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 7`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 15, + "line": 1, + "offset": 14, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 14, + "line": 1, + "offset": 13, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "numeric", + "hourCycle": "h23", + }, + "pattern": "H", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + +exports[`date_skeleton date_arg_skeleton_with_jJ 8`] = ` +Object { + "err": null, + "val": Array [ + Object { + "location": Object { + "end": Object { + "column": 16, + "line": 1, + "offset": 15, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "style": Object { + "location": Object { + "end": Object { + "column": 15, + "line": 1, + "offset": 14, + }, + "start": Object { + "column": 11, + "line": 1, + "offset": 10, + }, + }, + "parsedOptions": Object { + "hour": "2-digit", + "hourCycle": "h23", + }, + "pattern": "HH", + "type": 1, + }, + "type": 3, + "value": "0", + }, + ], +} +`; + exports[`double_apostrophes_1 1`] = ` Object { "err": null, diff --git a/packages/icu-messageformat-parser/tests/date-time-pattern-generator.test.ts b/packages/icu-messageformat-parser/tests/date-time-pattern-generator.test.ts new file mode 100644 index 0000000000..ed087912c4 --- /dev/null +++ b/packages/icu-messageformat-parser/tests/date-time-pattern-generator.test.ts @@ -0,0 +1,60 @@ +import {getBestPattern} from '../date-time-pattern-generator' + +describe('date-time-pattern-generator', () => { + // Test most commong 2 patterns + const testDatah12 = [ + {skeleton: '', expectedTimePattern: ''}, + + // h12 + {skeleton: 'j', expectedTimePattern: 'ha'}, + {skeleton: 'jj', expectedTimePattern: 'hha'}, + {skeleton: 'jjj', expectedTimePattern: 'haaaa'}, + {skeleton: 'jjjj', expectedTimePattern: 'hhaaaa'}, + {skeleton: 'jjjjj', expectedTimePattern: 'haaaaa'}, + {skeleton: 'jjjjjj', expectedTimePattern: 'hhaaaaa'}, + ] + const testDatah23 = [ + // h23 + {skeleton: 'j', expectedTimePattern: 'H'}, + {skeleton: 'jj', expectedTimePattern: 'HH'}, + {skeleton: 'jjj', expectedTimePattern: 'H'}, + {skeleton: 'jjjj', expectedTimePattern: 'HH'}, + {skeleton: 'jjjjj', expectedTimePattern: 'H'}, + {skeleton: 'jjjjjj', expectedTimePattern: 'HH'}, + ] + describe('when locale has hourCycle', () => { + it('returns desired time patterns', function () { + let locale = new Intl.Locale('und', {hourCycle: 'h12'}) + testDatah12.forEach(data => { + expect(getBestPattern(data.skeleton, locale)).toBe( + data.expectedTimePattern + ) + }) + + locale = new Intl.Locale('und', {hourCycle: 'h23'}) + testDatah23.forEach(data => { + expect(getBestPattern(data.skeleton, locale)).toBe( + data.expectedTimePattern + ) + }) + }) + }) + + describe('when locale has no hourCycle', () => { + it('returns desired time patterns', function () { + let locale = new Intl.Locale('en-US') + testDatah12.forEach(data => { + expect(getBestPattern(data.skeleton, locale)).toBe( + data.expectedTimePattern + ) + }) + + locale = new Intl.Locale('de-DE') + testDatah23.forEach(data => { + expect(getBestPattern(data.skeleton, locale)).toBe( + data.expectedTimePattern + ) + }) + }) + }) +}) diff --git a/packages/icu-messageformat-parser/tests/parser.test.ts b/packages/icu-messageformat-parser/tests/parser.test.ts index 70d1ed48a0..2b5f79e2a9 100644 --- a/packages/icu-messageformat-parser/tests/parser.test.ts +++ b/packages/icu-messageformat-parser/tests/parser.test.ts @@ -141,6 +141,28 @@ test('date_arg_skeleton_2', () => test('date_arg_skeleton_3', () => testParser('{0, date, ::h:mm a}', {})) +describe('date_skeleton', function () { + const locale = new Intl.Locale('und', {hourCycle: 'h12'}) + const options = { + locale, + shouldParseSkeletons: true, + requiresOtherClause: true, + } + + test.each([ + {skeleton: '{0, date, ::j}'}, + {skeleton: '{0, date, ::jj}'}, + {skeleton: '{0, date, ::jjj}'}, + {skeleton: '{0, date, ::jjjj}'}, + {skeleton: '{0, date, ::jjjjj}'}, + {skeleton: '{0, date, ::jjjjjj}'}, + {skeleton: '{0, date, ::J}'}, + {skeleton: '{0, date, ::JJ}'}, + ])('date_arg_skeleton_with_jJ', ({skeleton}) => { + testParser(skeleton, options) + }) +}) + test('duplicate_plural_selectors', () => testParser( 'You have {count, plural, one {# hot dog} one {# hamburger} one {# sandwich} other {# snacks}} in your lunch bag.' diff --git a/packages/icu-messageformat-parser/time-data.generated.ts b/packages/icu-messageformat-parser/time-data.generated.ts new file mode 100644 index 0000000000..bf1c8b9478 --- /dev/null +++ b/packages/icu-messageformat-parser/time-data.generated.ts @@ -0,0 +1,1339 @@ +// @generated from time-data-gen.ts +// prettier-ignore +export const timeData: Record = { + "AX": [ + "H" + ], + "BQ": [ + "H" + ], + "CP": [ + "H" + ], + "CZ": [ + "H" + ], + "DK": [ + "H" + ], + "FI": [ + "H" + ], + "ID": [ + "H" + ], + "IS": [ + "H" + ], + "ML": [ + "H" + ], + "NE": [ + "H" + ], + "RU": [ + "H" + ], + "SE": [ + "H" + ], + "SJ": [ + "H" + ], + "SK": [ + "H" + ], + "AS": [ + "h", + "H" + ], + "BT": [ + "h", + "H" + ], + "DJ": [ + "h", + "H" + ], + "ER": [ + "h", + "H" + ], + "GH": [ + "h", + "H" + ], + "IN": [ + "h", + "H" + ], + "LS": [ + "h", + "H" + ], + "PG": [ + "h", + "H" + ], + "PW": [ + "h", + "H" + ], + "SO": [ + "h", + "H" + ], + "TO": [ + "h", + "H" + ], + "VU": [ + "h", + "H" + ], + "WS": [ + "h", + "H" + ], + "001": [ + "H", + "h" + ], + "AL": [ + "h", + "H", + "hB" + ], + "TD": [ + "h", + "H", + "hB" + ], + "ca-ES": [ + "H", + "h", + "hB" + ], + "CF": [ + "H", + "h", + "hB" + ], + "CM": [ + "H", + "h", + "hB" + ], + "fr-CA": [ + "H", + "h", + "hB" + ], + "gl-ES": [ + "H", + "h", + "hB" + ], + "it-CH": [ + "H", + "h", + "hB" + ], + "it-IT": [ + "H", + "h", + "hB" + ], + "LU": [ + "H", + "h", + "hB" + ], + "NP": [ + "H", + "h", + "hB" + ], + "PF": [ + "H", + "h", + "hB" + ], + "SC": [ + "H", + "h", + "hB" + ], + "SM": [ + "H", + "h", + "hB" + ], + "SN": [ + "H", + "h", + "hB" + ], + "TF": [ + "H", + "h", + "hB" + ], + "VA": [ + "H", + "h", + "hB" + ], + "CY": [ + "h", + "H", + "hb", + "hB" + ], + "GR": [ + "h", + "H", + "hb", + "hB" + ], + "CO": [ + "h", + "H", + "hB", + "hb" + ], + "DO": [ + "h", + "H", + "hB", + "hb" + ], + "KP": [ + "h", + "H", + "hB", + "hb" + ], + "KR": [ + "h", + "H", + "hB", + "hb" + ], + "NA": [ + "h", + "H", + "hB", + "hb" + ], + "PA": [ + "h", + "H", + "hB", + "hb" + ], + "PR": [ + "h", + "H", + "hB", + "hb" + ], + "VE": [ + "h", + "H", + "hB", + "hb" + ], + "AC": [ + "H", + "h", + "hb", + "hB" + ], + "AI": [ + "H", + "h", + "hb", + "hB" + ], + "BW": [ + "H", + "h", + "hb", + "hB" + ], + "BZ": [ + "H", + "h", + "hb", + "hB" + ], + "CC": [ + "H", + "h", + "hb", + "hB" + ], + "CK": [ + "H", + "h", + "hb", + "hB" + ], + "CX": [ + "H", + "h", + "hb", + "hB" + ], + "DG": [ + "H", + "h", + "hb", + "hB" + ], + "FK": [ + "H", + "h", + "hb", + "hB" + ], + "GB": [ + "H", + "h", + "hb", + "hB" + ], + "GG": [ + "H", + "h", + "hb", + "hB" + ], + "GI": [ + "H", + "h", + "hb", + "hB" + ], + "IE": [ + "H", + "h", + "hb", + "hB" + ], + "IM": [ + "H", + "h", + "hb", + "hB" + ], + "IO": [ + "H", + "h", + "hb", + "hB" + ], + "JE": [ + "H", + "h", + "hb", + "hB" + ], + "LT": [ + "H", + "h", + "hb", + "hB" + ], + "MK": [ + "H", + "h", + "hb", + "hB" + ], + "MN": [ + "H", + "h", + "hb", + "hB" + ], + "MS": [ + "H", + "h", + "hb", + "hB" + ], + "NF": [ + "H", + "h", + "hb", + "hB" + ], + "NG": [ + "H", + "h", + "hb", + "hB" + ], + "NR": [ + "H", + "h", + "hb", + "hB" + ], + "NU": [ + "H", + "h", + "hb", + "hB" + ], + "PN": [ + "H", + "h", + "hb", + "hB" + ], + "SH": [ + "H", + "h", + "hb", + "hB" + ], + "SX": [ + "H", + "h", + "hb", + "hB" + ], + "TA": [ + "H", + "h", + "hb", + "hB" + ], + "ZA": [ + "H", + "h", + "hb", + "hB" + ], + "af-ZA": [ + "H", + "h", + "hB", + "hb" + ], + "AR": [ + "H", + "h", + "hB", + "hb" + ], + "CL": [ + "H", + "h", + "hB", + "hb" + ], + "CR": [ + "H", + "h", + "hB", + "hb" + ], + "CU": [ + "H", + "h", + "hB", + "hb" + ], + "EA": [ + "H", + "h", + "hB", + "hb" + ], + "es-BO": [ + "H", + "h", + "hB", + "hb" + ], + "es-BR": [ + "H", + "h", + "hB", + "hb" + ], + "es-EC": [ + "H", + "h", + "hB", + "hb" + ], + "es-ES": [ + "H", + "h", + "hB", + "hb" + ], + "es-GQ": [ + "H", + "h", + "hB", + "hb" + ], + "es-PE": [ + "H", + "h", + "hB", + "hb" + ], + "GT": [ + "H", + "h", + "hB", + "hb" + ], + "HN": [ + "H", + "h", + "hB", + "hb" + ], + "IC": [ + "H", + "h", + "hB", + "hb" + ], + "KG": [ + "H", + "h", + "hB", + "hb" + ], + "KM": [ + "H", + "h", + "hB", + "hb" + ], + "LK": [ + "H", + "h", + "hB", + "hb" + ], + "MA": [ + "H", + "h", + "hB", + "hb" + ], + "MX": [ + "H", + "h", + "hB", + "hb" + ], + "NI": [ + "H", + "h", + "hB", + "hb" + ], + "PY": [ + "H", + "h", + "hB", + "hb" + ], + "SV": [ + "H", + "h", + "hB", + "hb" + ], + "UY": [ + "H", + "h", + "hB", + "hb" + ], + "JP": [ + "H", + "h", + "K" + ], + "AD": [ + "H", + "hB" + ], + "AM": [ + "H", + "hB" + ], + "AO": [ + "H", + "hB" + ], + "AT": [ + "H", + "hB" + ], + "AW": [ + "H", + "hB" + ], + "BE": [ + "H", + "hB" + ], + "BF": [ + "H", + "hB" + ], + "BJ": [ + "H", + "hB" + ], + "BL": [ + "H", + "hB" + ], + "BR": [ + "H", + "hB" + ], + "CG": [ + "H", + "hB" + ], + "CI": [ + "H", + "hB" + ], + "CV": [ + "H", + "hB" + ], + "DE": [ + "H", + "hB" + ], + "EE": [ + "H", + "hB" + ], + "FR": [ + "H", + "hB" + ], + "GA": [ + "H", + "hB" + ], + "GF": [ + "H", + "hB" + ], + "GN": [ + "H", + "hB" + ], + "GP": [ + "H", + "hB" + ], + "GW": [ + "H", + "hB" + ], + "HR": [ + "H", + "hB" + ], + "IL": [ + "H", + "hB" + ], + "IT": [ + "H", + "hB" + ], + "KZ": [ + "H", + "hB" + ], + "MC": [ + "H", + "hB" + ], + "MD": [ + "H", + "hB" + ], + "MF": [ + "H", + "hB" + ], + "MQ": [ + "H", + "hB" + ], + "MZ": [ + "H", + "hB" + ], + "NC": [ + "H", + "hB" + ], + "NL": [ + "H", + "hB" + ], + "PM": [ + "H", + "hB" + ], + "PT": [ + "H", + "hB" + ], + "RE": [ + "H", + "hB" + ], + "RO": [ + "H", + "hB" + ], + "SI": [ + "H", + "hB" + ], + "SR": [ + "H", + "hB" + ], + "ST": [ + "H", + "hB" + ], + "TG": [ + "H", + "hB" + ], + "TR": [ + "H", + "hB" + ], + "WF": [ + "H", + "hB" + ], + "YT": [ + "H", + "hB" + ], + "BD": [ + "h", + "hB", + "H" + ], + "PK": [ + "h", + "hB", + "H" + ], + "AZ": [ + "H", + "hB", + "h" + ], + "BA": [ + "H", + "hB", + "h" + ], + "BG": [ + "H", + "hB", + "h" + ], + "CH": [ + "H", + "hB", + "h" + ], + "GE": [ + "H", + "hB", + "h" + ], + "LI": [ + "H", + "hB", + "h" + ], + "ME": [ + "H", + "hB", + "h" + ], + "RS": [ + "H", + "hB", + "h" + ], + "UA": [ + "H", + "hB", + "h" + ], + "UZ": [ + "H", + "hB", + "h" + ], + "XK": [ + "H", + "hB", + "h" + ], + "AG": [ + "h", + "hb", + "H", + "hB" + ], + "AU": [ + "h", + "hb", + "H", + "hB" + ], + "BB": [ + "h", + "hb", + "H", + "hB" + ], + "BM": [ + "h", + "hb", + "H", + "hB" + ], + "BS": [ + "h", + "hb", + "H", + "hB" + ], + "CA": [ + "h", + "hb", + "H", + "hB" + ], + "DM": [ + "h", + "hb", + "H", + "hB" + ], + "en-001": [ + "h", + "hb", + "H", + "hB" + ], + "FJ": [ + "h", + "hb", + "H", + "hB" + ], + "FM": [ + "h", + "hb", + "H", + "hB" + ], + "GD": [ + "h", + "hb", + "H", + "hB" + ], + "GM": [ + "h", + "hb", + "H", + "hB" + ], + "GU": [ + "h", + "hb", + "H", + "hB" + ], + "GY": [ + "h", + "hb", + "H", + "hB" + ], + "JM": [ + "h", + "hb", + "H", + "hB" + ], + "KI": [ + "h", + "hb", + "H", + "hB" + ], + "KN": [ + "h", + "hb", + "H", + "hB" + ], + "KY": [ + "h", + "hb", + "H", + "hB" + ], + "LC": [ + "h", + "hb", + "H", + "hB" + ], + "LR": [ + "h", + "hb", + "H", + "hB" + ], + "MH": [ + "h", + "hb", + "H", + "hB" + ], + "MP": [ + "h", + "hb", + "H", + "hB" + ], + "MW": [ + "h", + "hb", + "H", + "hB" + ], + "NZ": [ + "h", + "hb", + "H", + "hB" + ], + "SB": [ + "h", + "hb", + "H", + "hB" + ], + "SG": [ + "h", + "hb", + "H", + "hB" + ], + "SL": [ + "h", + "hb", + "H", + "hB" + ], + "SS": [ + "h", + "hb", + "H", + "hB" + ], + "SZ": [ + "h", + "hb", + "H", + "hB" + ], + "TC": [ + "h", + "hb", + "H", + "hB" + ], + "TT": [ + "h", + "hb", + "H", + "hB" + ], + "UM": [ + "h", + "hb", + "H", + "hB" + ], + "US": [ + "h", + "hb", + "H", + "hB" + ], + "VC": [ + "h", + "hb", + "H", + "hB" + ], + "VG": [ + "h", + "hb", + "H", + "hB" + ], + "VI": [ + "h", + "hb", + "H", + "hB" + ], + "ZM": [ + "h", + "hb", + "H", + "hB" + ], + "BO": [ + "H", + "hB", + "h", + "hb" + ], + "EC": [ + "H", + "hB", + "h", + "hb" + ], + "ES": [ + "H", + "hB", + "h", + "hb" + ], + "GQ": [ + "H", + "hB", + "h", + "hb" + ], + "PE": [ + "H", + "hB", + "h", + "hb" + ], + "AE": [ + "h", + "hB", + "hb", + "H" + ], + "ar-001": [ + "h", + "hB", + "hb", + "H" + ], + "BH": [ + "h", + "hB", + "hb", + "H" + ], + "DZ": [ + "h", + "hB", + "hb", + "H" + ], + "EG": [ + "h", + "hB", + "hb", + "H" + ], + "EH": [ + "h", + "hB", + "hb", + "H" + ], + "HK": [ + "h", + "hB", + "hb", + "H" + ], + "IQ": [ + "h", + "hB", + "hb", + "H" + ], + "JO": [ + "h", + "hB", + "hb", + "H" + ], + "KW": [ + "h", + "hB", + "hb", + "H" + ], + "LB": [ + "h", + "hB", + "hb", + "H" + ], + "LY": [ + "h", + "hB", + "hb", + "H" + ], + "MO": [ + "h", + "hB", + "hb", + "H" + ], + "MR": [ + "h", + "hB", + "hb", + "H" + ], + "OM": [ + "h", + "hB", + "hb", + "H" + ], + "PH": [ + "h", + "hB", + "hb", + "H" + ], + "PS": [ + "h", + "hB", + "hb", + "H" + ], + "QA": [ + "h", + "hB", + "hb", + "H" + ], + "SA": [ + "h", + "hB", + "hb", + "H" + ], + "SD": [ + "h", + "hB", + "hb", + "H" + ], + "SY": [ + "h", + "hB", + "hb", + "H" + ], + "TN": [ + "h", + "hB", + "hb", + "H" + ], + "YE": [ + "h", + "hB", + "hb", + "H" + ], + "AF": [ + "H", + "hb", + "hB", + "h" + ], + "LA": [ + "H", + "hb", + "hB", + "h" + ], + "CN": [ + "H", + "hB", + "hb", + "h" + ], + "LV": [ + "H", + "hB", + "hb", + "h" + ], + "TL": [ + "H", + "hB", + "hb", + "h" + ], + "zu-ZA": [ + "H", + "hB", + "hb", + "h" + ], + "CD": [ + "hB", + "H" + ], + "IR": [ + "hB", + "H" + ], + "hi-IN": [ + "hB", + "h", + "H" + ], + "kn-IN": [ + "hB", + "h", + "H" + ], + "ml-IN": [ + "hB", + "h", + "H" + ], + "te-IN": [ + "hB", + "h", + "H" + ], + "KH": [ + "hB", + "h", + "H", + "hb" + ], + "ta-IN": [ + "hB", + "h", + "hb", + "H" + ], + "BN": [ + "hb", + "hB", + "h", + "H" + ], + "MY": [ + "hb", + "hB", + "h", + "H" + ], + "ET": [ + "hB", + "hb", + "h", + "H" + ], + "gu-IN": [ + "hB", + "hb", + "h", + "H" + ], + "mr-IN": [ + "hB", + "hb", + "h", + "H" + ], + "pa-IN": [ + "hB", + "hb", + "h", + "H" + ], + "TW": [ + "hB", + "hb", + "h", + "H" + ], + "KE": [ + "hB", + "hb", + "H", + "h" + ], + "MM": [ + "hB", + "hb", + "H", + "h" + ], + "TZ": [ + "hB", + "hb", + "H", + "h" + ], + "UG": [ + "hB", + "hb", + "H", + "h" + ] +}; diff --git a/packages/intl-messageformat/src/core.ts b/packages/intl-messageformat/src/core.ts index bb93099fe3..b52915ce84 100644 --- a/packages/intl-messageformat/src/core.ts +++ b/packages/intl-messageformat/src/core.ts @@ -105,6 +105,7 @@ function createDefaultFormatters( export class IntlMessageFormat { private readonly ast: MessageFormatElement[] private readonly locales: string | string[] + private readonly resolvedLocale: Intl.Locale private readonly formatters: Formatters private readonly formats: Formats private readonly message: string | undefined @@ -119,6 +120,10 @@ export class IntlMessageFormat { overrideFormats?: Partial, opts?: Options ) { + // Defined first because it's used to build the format pattern. + this.locales = locales + this.resolvedLocale = IntlMessageFormat.resolveLocale(locales) + if (typeof message === 'string') { this.message = message if (!IntlMessageFormat.__parse) { @@ -129,6 +134,7 @@ export class IntlMessageFormat { // Parse string messages into an AST. this.ast = IntlMessageFormat.__parse(message, { ignoreTag: opts?.ignoreTag, + locale: this.resolvedLocale, }) } else { this.ast = message @@ -142,9 +148,6 @@ export class IntlMessageFormat { // formats. this.formats = mergeConfigs(IntlMessageFormat.formats, overrideFormats) - // Defined first because it's used to build the format pattern. - this.locales = locales - this.formatters = (opts && opts.formatters) || createDefaultFormatters(this.formatterCache) } @@ -188,7 +191,7 @@ export class IntlMessageFormat { this.message ) resolvedOptions = () => ({ - locale: Intl.NumberFormat.supportedLocalesOf(this.locales)[0], + locale: this.resolvedLocale.toString(), }) getAst = () => this.ast private static memoizedDefaultLocale: string | null = null @@ -201,6 +204,14 @@ export class IntlMessageFormat { return IntlMessageFormat.memoizedDefaultLocale } + static resolveLocale = (locales: string | string[]): Intl.Locale => { + const supportedLocales = Intl.NumberFormat.supportedLocalesOf(locales) + if (supportedLocales.length > 0) { + return new Intl.Locale(supportedLocales[0]) + } + + return new Intl.Locale(typeof locales === 'string' ? locales : locales[0]) + } static __parse: typeof parse | undefined = parse // Default format options used as the prototype of the `formats` provided to the // constructor. These are used when constructing the internal Intl.NumberFormat diff --git a/packages/intl-messageformat/tests/index.test.ts b/packages/intl-messageformat/tests/index.test.ts index 8cea9d7e43..cce916dc0a 100644 --- a/packages/intl-messageformat/tests/index.test.ts +++ b/packages/intl-messageformat/tests/index.test.ts @@ -892,6 +892,35 @@ describe('IntlMessageFormat', function () { d: new Date(0), }) ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}(.*?)[AP]M/) // Deal w/ IE11 + expect( + new IntlMessageFormat('{d, time, ::hhmmssz}', 'en-US').format({ + d: new Date(0), + }) + ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}(.*?)[AP]M/) // Deal w/ IE11 + + expect( + new IntlMessageFormat('{d, time, ::jjmmss}', 'de-DE').format({ + d: new Date(0), + }) + ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}$/) // Deal w/ IE11 + + expect( + new IntlMessageFormat('{d, time, ::jjmmss}', 'en-US').format({ + d: new Date(0), + }) + ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}(.*?)[AP]M$/) // Deal w/ IE11 + + expect( + new IntlMessageFormat('{d, time, ::jjmmssz}', 'de-DE').format({ + d: new Date(0), + }) + ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}(.*?)[A-Z]{3}/) // Deal w/ IE11 + + expect( + new IntlMessageFormat('{d, time, ::jjmmssz}', 'en-US').format({ + d: new Date(0), + }) + ).toMatch(/\d{2}(.*?):(.*?)\d{2}(.*?):(.*?)\d{2}(.*?)[AP]M(.*?)[A-Z]{3}/) // Deal w/ IE11 }) }