@openmrs/esm-utils 10.0.1-pre.5136 → 10.0.1-pre.5147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/get-locale.d.ts.map +1 -1
- package/dist/get-locale.js +5 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/language-tag.d.ts +44 -0
- package/dist/language-tag.d.ts.map +1 -0
- package/dist/language-tag.js +67 -0
- package/dist/match-locale.d.ts.map +1 -1
- package/dist/match-locale.js +3 -18
- package/package.json +3 -3
- package/src/get-locale.ts +6 -2
- package/src/index.ts +1 -0
- package/src/language-tag.test.ts +62 -0
- package/src/language-tag.ts +69 -0
- package/src/match-locale.test.ts +6 -0
- package/src/match-locale.ts +3 -21
package/.turbo/turbo-build.log
CHANGED
package/dist/get-locale.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../src/get-locale.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../src/get-locale.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,SAAS,WAWxB"}
|
package/dist/get-locale.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { toLanguageTag } from "./language-tag.js";
|
|
1
2
|
/**
|
|
2
3
|
* Returns the current locale of the application.
|
|
3
4
|
* @returns string
|
|
4
5
|
*/ export function getLocale() {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
// i18next detects the language from the `lang` attribute, a `?lang=` parameter, localStorage or
|
|
7
|
+
// the browser, none of which is guaranteed to be a BCP 47 tag, so normalize before returning.
|
|
8
|
+
const detected = window.i18next.language;
|
|
9
|
+
let language = toLanguageTag(detected) ?? detected;
|
|
7
10
|
// Hack for `ht` until all browsers update their unicode support with ht to fr mapping.
|
|
8
11
|
// See https://unicode-org.atlassian.net/browse/CLDR-14956
|
|
9
12
|
if (language === 'ht') {
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./age-helpers.js";
|
|
|
3
3
|
export * from "./dates/index.js";
|
|
4
4
|
export * from "./get-locale.js";
|
|
5
5
|
export * from "./is-online.js";
|
|
6
|
+
export * from "./language-tag.js";
|
|
6
7
|
export * from "./match-locale.js";
|
|
7
8
|
export * from "./patient-helpers.js";
|
|
8
9
|
export * from "./shallowEqual.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an OpenMRS locale identifier into a canonical BCP 47 language tag.
|
|
3
|
+
*
|
|
4
|
+
* OpenMRS locale identifiers are not always language tags. User properties such as
|
|
5
|
+
* `defaultLocale` store Java's `Locale#toString()` form (`en_US`, `sw_KE`) verbatim, some backends
|
|
6
|
+
* serialize session locales the same way, and some installs configure POSIX-style variants
|
|
7
|
+
* (`uz@Latn`). The `Intl` constructors throw `RangeError` rather than degrading when handed any of
|
|
8
|
+
* them, so convert before passing an OpenMRS locale to `Intl`.
|
|
9
|
+
*
|
|
10
|
+
* Both `_` and `@` are treated as subtag separators, which covers the `@Latn` script modifier.
|
|
11
|
+
* Other POSIX modifiers have no BCP 47 equivalent and parse as whatever their length implies
|
|
12
|
+
* (`de@euro` becomes the script subtag `de-Euro`), so they are converted rather than rejected.
|
|
13
|
+
*
|
|
14
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`. Locale data reaching this function
|
|
15
|
+
* comes from the REST session and from configuration, so anything that is not a string is
|
|
16
|
+
* treated as an unresolvable locale rather than an error.
|
|
17
|
+
* @returns The canonical language tag, or `undefined` if `locale` is not a structurally valid tag.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* toLanguageTag('sw_KE'); // => 'sw-KE'
|
|
21
|
+
* toLanguageTag('uz@Latn'); // => 'uz-Latn'
|
|
22
|
+
* toLanguageTag('en_us'); // => 'en-US'
|
|
23
|
+
* toLanguageTag('not a locale'); // => undefined
|
|
24
|
+
*/
|
|
25
|
+
export declare function toLanguageTag(locale: string | null | undefined): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the name of a locale as written in that locale, so `fr` becomes "français" and `sw_KE`
|
|
28
|
+
* becomes "Kiswahili (Kenya)". Falls back to the identifier itself for anything `Intl` cannot
|
|
29
|
+
* resolve, so the result is always safe to render.
|
|
30
|
+
*
|
|
31
|
+
* The name is CLDR's middle-of-sentence form. When presenting it as a standalone label, capitalize
|
|
32
|
+
* only the first character: lowercasing the remainder corrupts names carrying internal capitals,
|
|
33
|
+
* turning "American English" into "American english".
|
|
34
|
+
*
|
|
35
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`.
|
|
36
|
+
* @returns The locale's name in its own language, `locale` itself if it cannot be resolved, or an
|
|
37
|
+
* empty string if no locale was given.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* getLocaleDisplayName('fr'); // => 'français'
|
|
41
|
+
* getLocaleDisplayName('sw_KE'); // => 'Kiswahili (Kenya)'
|
|
42
|
+
*/
|
|
43
|
+
export declare function getLocaleDisplayName(locale: string | null | undefined): string;
|
|
44
|
+
//# sourceMappingURL=language-tag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-tag.d.ts","sourceRoot":"","sources":["../src/language-tag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUnF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAe9E"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an OpenMRS locale identifier into a canonical BCP 47 language tag.
|
|
3
|
+
*
|
|
4
|
+
* OpenMRS locale identifiers are not always language tags. User properties such as
|
|
5
|
+
* `defaultLocale` store Java's `Locale#toString()` form (`en_US`, `sw_KE`) verbatim, some backends
|
|
6
|
+
* serialize session locales the same way, and some installs configure POSIX-style variants
|
|
7
|
+
* (`uz@Latn`). The `Intl` constructors throw `RangeError` rather than degrading when handed any of
|
|
8
|
+
* them, so convert before passing an OpenMRS locale to `Intl`.
|
|
9
|
+
*
|
|
10
|
+
* Both `_` and `@` are treated as subtag separators, which covers the `@Latn` script modifier.
|
|
11
|
+
* Other POSIX modifiers have no BCP 47 equivalent and parse as whatever their length implies
|
|
12
|
+
* (`de@euro` becomes the script subtag `de-Euro`), so they are converted rather than rejected.
|
|
13
|
+
*
|
|
14
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`. Locale data reaching this function
|
|
15
|
+
* comes from the REST session and from configuration, so anything that is not a string is
|
|
16
|
+
* treated as an unresolvable locale rather than an error.
|
|
17
|
+
* @returns The canonical language tag, or `undefined` if `locale` is not a structurally valid tag.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* toLanguageTag('sw_KE'); // => 'sw-KE'
|
|
21
|
+
* toLanguageTag('uz@Latn'); // => 'uz-Latn'
|
|
22
|
+
* toLanguageTag('en_us'); // => 'en-US'
|
|
23
|
+
* toLanguageTag('not a locale'); // => undefined
|
|
24
|
+
*/ export function toLanguageTag(locale) {
|
|
25
|
+
if (typeof locale !== 'string' || locale.trim().length === 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return new Intl.Locale(locale.replace(/[_@]/g, '-')).toString();
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns the name of a locale as written in that locale, so `fr` becomes "français" and `sw_KE`
|
|
36
|
+
* becomes "Kiswahili (Kenya)". Falls back to the identifier itself for anything `Intl` cannot
|
|
37
|
+
* resolve, so the result is always safe to render.
|
|
38
|
+
*
|
|
39
|
+
* The name is CLDR's middle-of-sentence form. When presenting it as a standalone label, capitalize
|
|
40
|
+
* only the first character: lowercasing the remainder corrupts names carrying internal capitals,
|
|
41
|
+
* turning "American English" into "American english".
|
|
42
|
+
*
|
|
43
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`.
|
|
44
|
+
* @returns The locale's name in its own language, `locale` itself if it cannot be resolved, or an
|
|
45
|
+
* empty string if no locale was given.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* getLocaleDisplayName('fr'); // => 'français'
|
|
49
|
+
* getLocaleDisplayName('sw_KE'); // => 'Kiswahili (Kenya)'
|
|
50
|
+
*/ export function getLocaleDisplayName(locale) {
|
|
51
|
+
const identifier = locale ?? '';
|
|
52
|
+
const languageTag = toLanguageTag(identifier);
|
|
53
|
+
if (!languageTag) {
|
|
54
|
+
return identifier;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return new Intl.DisplayNames([
|
|
58
|
+
languageTag
|
|
59
|
+
], {
|
|
60
|
+
type: 'language'
|
|
61
|
+
}).of(languageTag) ?? identifier;
|
|
62
|
+
} catch {
|
|
63
|
+
// `Intl.DisplayNames` validates independently of `Intl.Locale` and rejects tags carrying
|
|
64
|
+
// extension or private-use subtags, such as `en-u-ca-gregory`.
|
|
65
|
+
return identifier;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match-locale.d.ts","sourceRoot":"","sources":["../src/match-locale.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"match-locale.d.ts","sourceRoot":"","sources":["../src/match-locale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CA6DpB"}
|
package/dist/match-locale.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Returns a canonical BCP 47 representation of `tag`, or `undefined` if it is not a
|
|
3
|
-
* structurally valid tag. Underscores are accepted as a separator and normalized to
|
|
4
|
-
* hyphens before canonicalization to accommodate locale strings that originate from
|
|
5
|
-
* Java-style identifiers (e.g. `en_GB`).
|
|
6
|
-
*/ function canonicalize(tag) {
|
|
7
|
-
if (typeof tag !== 'string' || tag.trim().length === 0) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
const normalized = tag.replaceAll('_', '-');
|
|
11
|
-
try {
|
|
12
|
-
return new Intl.Locale(normalized).toString();
|
|
13
|
-
} catch {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
1
|
+
import { toLanguageTag } from "./language-tag.js";
|
|
17
2
|
/**
|
|
18
3
|
* Resolves a requested locale against a list of available locales using the
|
|
19
4
|
* BCP 47 lookup algorithm (RFC 4647, §3.4).
|
|
@@ -49,14 +34,14 @@
|
|
|
49
34
|
if (requested == null) {
|
|
50
35
|
return fallback;
|
|
51
36
|
}
|
|
52
|
-
const requestedCanonical =
|
|
37
|
+
const requestedCanonical = toLanguageTag(requested);
|
|
53
38
|
if (!requestedCanonical) {
|
|
54
39
|
console.warn(`matchLocale: invalid requested locale tag: ${JSON.stringify(requested)}`);
|
|
55
40
|
return fallback;
|
|
56
41
|
}
|
|
57
42
|
const pool = [];
|
|
58
43
|
for (const entry of available){
|
|
59
|
-
const canonical =
|
|
44
|
+
const canonical = toLanguageTag(entry);
|
|
60
45
|
if (!canonical) {
|
|
61
46
|
console.warn(`matchLocale: skipping invalid available locale tag: ${JSON.stringify(entry)}`);
|
|
62
47
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-utils",
|
|
3
|
-
"version": "10.0.1-pre.
|
|
3
|
+
"version": "10.0.1-pre.5147",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Helper utilities for OpenMRS",
|
|
6
6
|
"type": "module",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"semver": "^7.7.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"@openmrs/esm-globals": "^10.0.1-pre.
|
|
60
|
+
"@openmrs/esm-globals": "^10.0.1-pre.5147",
|
|
61
61
|
"dayjs": "1.x",
|
|
62
62
|
"i18next": "25.x",
|
|
63
63
|
"rxjs": "6.x"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@openmrs/esm-globals": "10.0.1-pre.
|
|
66
|
+
"@openmrs/esm-globals": "10.0.1-pre.5147",
|
|
67
67
|
"@swc/cli": "0.8.1",
|
|
68
68
|
"@swc/core": "1.15.21",
|
|
69
69
|
"@types/lodash-es": "^4.17.12",
|
package/src/get-locale.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { toLanguageTag } from './language-tag';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Returns the current locale of the application.
|
|
3
5
|
* @returns string
|
|
4
6
|
*/
|
|
5
7
|
export function getLocale() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
// i18next detects the language from the `lang` attribute, a `?lang=` parameter, localStorage or
|
|
9
|
+
// the browser, none of which is guaranteed to be a BCP 47 tag, so normalize before returning.
|
|
10
|
+
const detected = window.i18next.language;
|
|
11
|
+
let language = toLanguageTag(detected) ?? detected;
|
|
8
12
|
// Hack for `ht` until all browsers update their unicode support with ht to fr mapping.
|
|
9
13
|
// See https://unicode-org.atlassian.net/browse/CLDR-14956
|
|
10
14
|
if (language === 'ht') {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getLocaleDisplayName, toLanguageTag } from './language-tag';
|
|
3
|
+
|
|
4
|
+
describe('toLanguageTag', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
['en', 'en'],
|
|
7
|
+
// The REST session reports Java `Locale#toString()` values, which are not language tags.
|
|
8
|
+
['en_US', 'en-US'],
|
|
9
|
+
['sw_KE', 'sw-KE'],
|
|
10
|
+
['uz@Latn', 'uz-Latn'],
|
|
11
|
+
// Canonicalization also fixes subtag casing.
|
|
12
|
+
['en_us', 'en-US'],
|
|
13
|
+
['ZH-hant-tw', 'zh-Hant-TW'],
|
|
14
|
+
])('converts %s to %s', (locale, expected) => {
|
|
15
|
+
expect(toLanguageTag(locale)).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it.each([['not a locale'], ['!!!'], ['']])('returns undefined for %j', (locale) => {
|
|
19
|
+
expect(toLanguageTag(locale)).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns undefined for a missing locale rather than throwing', () => {
|
|
23
|
+
expect(toLanguageTag(null)).toBeUndefined();
|
|
24
|
+
expect(toLanguageTag(undefined)).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getLocaleDisplayName', () => {
|
|
29
|
+
it.each([
|
|
30
|
+
['en', 'English'],
|
|
31
|
+
['fr', 'français'],
|
|
32
|
+
['en_US', 'American English'],
|
|
33
|
+
['sw_KE', 'Kiswahili (Kenya)'],
|
|
34
|
+
['pt_BR', 'português (Brasil)'],
|
|
35
|
+
])('renders %s as %s', (locale, expected) => {
|
|
36
|
+
expect(getLocaleDisplayName(locale)).toBe(expected);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('resolves a POSIX script modifier to a name rather than the raw identifier', () => {
|
|
40
|
+
// Asserted loosely on purpose. The script name is engine-specific: Node renders `uz@Latn` as
|
|
41
|
+
// "o‘zbek (lotin)" and Chrome as "O‘zbek (Latn)", so pinning either would assert a string half
|
|
42
|
+
// the runtimes never produce. `toLanguageTag` covers the conversion itself exactly.
|
|
43
|
+
const name = getLocaleDisplayName('uz@Latn');
|
|
44
|
+
|
|
45
|
+
expect(name).not.toBe('uz@Latn');
|
|
46
|
+
expect(name.toLowerCase()).toContain('zbek');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('falls back to the identifier when the locale is not a valid tag', () => {
|
|
50
|
+
expect(getLocaleDisplayName('not a locale at all')).toBe('not a locale at all');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to the identifier for a tag `Intl.DisplayNames` rejects', () => {
|
|
54
|
+
// `Intl.Locale` accepts extension subtags but `Intl.DisplayNames#of` throws on them.
|
|
55
|
+
expect(getLocaleDisplayName('en-u-ca-gregory')).toBe('en-u-ca-gregory');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns an empty string when no locale is given', () => {
|
|
59
|
+
expect(getLocaleDisplayName(null)).toBe('');
|
|
60
|
+
expect(getLocaleDisplayName(undefined)).toBe('');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an OpenMRS locale identifier into a canonical BCP 47 language tag.
|
|
3
|
+
*
|
|
4
|
+
* OpenMRS locale identifiers are not always language tags. User properties such as
|
|
5
|
+
* `defaultLocale` store Java's `Locale#toString()` form (`en_US`, `sw_KE`) verbatim, some backends
|
|
6
|
+
* serialize session locales the same way, and some installs configure POSIX-style variants
|
|
7
|
+
* (`uz@Latn`). The `Intl` constructors throw `RangeError` rather than degrading when handed any of
|
|
8
|
+
* them, so convert before passing an OpenMRS locale to `Intl`.
|
|
9
|
+
*
|
|
10
|
+
* Both `_` and `@` are treated as subtag separators, which covers the `@Latn` script modifier.
|
|
11
|
+
* Other POSIX modifiers have no BCP 47 equivalent and parse as whatever their length implies
|
|
12
|
+
* (`de@euro` becomes the script subtag `de-Euro`), so they are converted rather than rejected.
|
|
13
|
+
*
|
|
14
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`. Locale data reaching this function
|
|
15
|
+
* comes from the REST session and from configuration, so anything that is not a string is
|
|
16
|
+
* treated as an unresolvable locale rather than an error.
|
|
17
|
+
* @returns The canonical language tag, or `undefined` if `locale` is not a structurally valid tag.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* toLanguageTag('sw_KE'); // => 'sw-KE'
|
|
21
|
+
* toLanguageTag('uz@Latn'); // => 'uz-Latn'
|
|
22
|
+
* toLanguageTag('en_us'); // => 'en-US'
|
|
23
|
+
* toLanguageTag('not a locale'); // => undefined
|
|
24
|
+
*/
|
|
25
|
+
export function toLanguageTag(locale: string | null | undefined): string | undefined {
|
|
26
|
+
if (typeof locale !== 'string' || locale.trim().length === 0) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return new Intl.Locale(locale.replace(/[_@]/g, '-')).toString();
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the name of a locale as written in that locale, so `fr` becomes "français" and `sw_KE`
|
|
39
|
+
* becomes "Kiswahili (Kenya)". Falls back to the identifier itself for anything `Intl` cannot
|
|
40
|
+
* resolve, so the result is always safe to render.
|
|
41
|
+
*
|
|
42
|
+
* The name is CLDR's middle-of-sentence form. When presenting it as a standalone label, capitalize
|
|
43
|
+
* only the first character: lowercasing the remainder corrupts names carrying internal capitals,
|
|
44
|
+
* turning "American English" into "American english".
|
|
45
|
+
*
|
|
46
|
+
* @param locale An OpenMRS locale identifier, e.g. `sw_KE`.
|
|
47
|
+
* @returns The locale's name in its own language, `locale` itself if it cannot be resolved, or an
|
|
48
|
+
* empty string if no locale was given.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* getLocaleDisplayName('fr'); // => 'français'
|
|
52
|
+
* getLocaleDisplayName('sw_KE'); // => 'Kiswahili (Kenya)'
|
|
53
|
+
*/
|
|
54
|
+
export function getLocaleDisplayName(locale: string | null | undefined): string {
|
|
55
|
+
const identifier = locale ?? '';
|
|
56
|
+
const languageTag = toLanguageTag(identifier);
|
|
57
|
+
|
|
58
|
+
if (!languageTag) {
|
|
59
|
+
return identifier;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return new Intl.DisplayNames([languageTag], { type: 'language' }).of(languageTag) ?? identifier;
|
|
64
|
+
} catch {
|
|
65
|
+
// `Intl.DisplayNames` validates independently of `Intl.Locale` and rejects tags carrying
|
|
66
|
+
// extension or private-use subtags, such as `en-u-ca-gregory`.
|
|
67
|
+
return identifier;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/match-locale.test.ts
CHANGED
|
@@ -54,6 +54,12 @@ describe('matchLocale', () => {
|
|
|
54
54
|
it('canonicalizes script casing for matching (Latn vs latn)', () => {
|
|
55
55
|
expect(matchLocale('sr-latn-rs', ['sr-Latn'])).toBe('sr-Latn');
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
it('treats a POSIX script modifier as a script subtag', () => {
|
|
59
|
+
// `uz@Latn` is the form OpenMRS ships translations under, on both sides of the comparison.
|
|
60
|
+
expect(matchLocale('uz@Latn', ['uz@Latn'])).toBe('uz@Latn');
|
|
61
|
+
expect(matchLocale('uz@Latn', ['uz-Latn'])).toBe('uz-Latn');
|
|
62
|
+
});
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
describe('truncation', () => {
|
package/src/match-locale.ts
CHANGED
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Returns a canonical BCP 47 representation of `tag`, or `undefined` if it is not a
|
|
3
|
-
* structurally valid tag. Underscores are accepted as a separator and normalized to
|
|
4
|
-
* hyphens before canonicalization to accommodate locale strings that originate from
|
|
5
|
-
* Java-style identifiers (e.g. `en_GB`).
|
|
6
|
-
*/
|
|
7
|
-
function canonicalize(tag: string): string | undefined {
|
|
8
|
-
if (typeof tag !== 'string' || tag.trim().length === 0) {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const normalized = tag.replaceAll('_', '-');
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
return new Intl.Locale(normalized).toString();
|
|
16
|
-
} catch {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
import { toLanguageTag } from './language-tag';
|
|
20
2
|
|
|
21
3
|
/**
|
|
22
4
|
* Resolves a requested locale against a list of available locales using the
|
|
@@ -59,7 +41,7 @@ export function matchLocale(
|
|
|
59
41
|
return fallback;
|
|
60
42
|
}
|
|
61
43
|
|
|
62
|
-
const requestedCanonical =
|
|
44
|
+
const requestedCanonical = toLanguageTag(requested);
|
|
63
45
|
if (!requestedCanonical) {
|
|
64
46
|
console.warn(`matchLocale: invalid requested locale tag: ${JSON.stringify(requested)}`);
|
|
65
47
|
return fallback;
|
|
@@ -67,7 +49,7 @@ export function matchLocale(
|
|
|
67
49
|
|
|
68
50
|
const pool: Array<{ canonical: string; original: string }> = [];
|
|
69
51
|
for (const entry of available) {
|
|
70
|
-
const canonical =
|
|
52
|
+
const canonical = toLanguageTag(entry);
|
|
71
53
|
if (!canonical) {
|
|
72
54
|
console.warn(`matchLocale: skipping invalid available locale tag: ${JSON.stringify(entry)}`);
|
|
73
55
|
continue;
|