@pie-players/pie-players-shared 1.0.0-beta.6 → 1.0.0-beta.8
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/README.md +3 -1
- package/dist/i18n/catalogs.d.ts +20 -13
- package/dist/i18n/catalogs.js +22 -23
- package/dist/i18n/content-language.d.ts +46 -4
- package/dist/i18n/content-language.js +83 -5
- package/dist/i18n/index.d.ts +3 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/language-tags.d.ts +73 -4
- package/dist/i18n/language-tags.js +176 -6
- package/dist/i18n/messages/en-US.d.ts +23 -0
- package/dist/i18n/messages/en-US.js +25 -0
- package/dist/i18n/provider.d.ts +3 -6
- package/dist/i18n/provider.js +5 -15
- package/dist/i18n/types.d.ts +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/loaders/ElementLoader.d.ts +1 -1
- package/dist/loaders/element-loader.d.ts +4 -1
- package/dist/loaders/element-loader.js +74 -21
- package/dist/loaders/iife-adapter.d.ts +7 -0
- package/dist/loaders/iife-adapter.js +1 -1
- package/dist/security/private-hosts.d.ts +27 -0
- package/dist/security/private-hosts.js +85 -0
- package/dist/security/validate-style-url.js +2 -2
- package/dist/tools/term-lookup.d.ts +15 -0
- package/dist/tools/term-lookup.js +16 -0
- package/dist/types/index.d.ts +47 -27
- package/dist/types/interchange.d.ts +116 -0
- package/dist/types/interchange.js +1 -0
- package/dist/ui/focus-trap.js +3 -1
- package/dist/ui/fontawesome.d.ts +51 -0
- package/dist/ui/fontawesome.js +122 -0
- package/dist/ui/ime.d.ts +6 -0
- package/dist/ui/ime.js +10 -0
- package/package.json +15 -6
- package/dist/i18n/messages/nl-NL.d.ts +0 -636
- package/dist/i18n/messages/nl-NL.js +0 -636
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
20
20
|
| `@pie-players/pie-players-shared` | The root: the loader, PIE, security and object helpers, loader config, instrumentation providers, the item-player strategy helpers, the default bundle host URL (`BUILDER_BUNDLE_URL`), a subset of the shared types, and the UI helpers (attribute coercion, content styles, pointer gestures, overlay placement, focus trap, safe storage, scoped CSS) |
|
|
21
21
|
| `/loader-config` | `LoaderConfig` and its defaults: instrumentation (`trackPageActions`, `instrumentationProvider`), resource retry, and IIFE bundle retry |
|
|
22
22
|
| `/security` | The item-markup sanitizer (`sanitizeItemMarkup`, `createDefaultItemMarkupSanitizer`, `buildAuthoringAllowList`), external-stylesheet URL validation, the SVG icon and `style` attribute sanitizers, and the wrappers that put overwide images and tables in a scrollable region |
|
|
23
|
+
| `/security/private-hosts` | `isPrivateHostname` and `isCloudMetadataHostname`, the host classification for a URL a server fetches on someone else's behalf. Node-safe: it loads none of the markup sanitizers |
|
|
23
24
|
| `/object` | `mergeObjectsIgnoringNullUndefined`, `cloneDeep`, `isPlainRecord` |
|
|
24
25
|
| `/types` | The shared types: item, passage, section and session entities, PIE models and controllers, accessibility catalogs, and the [media vocabulary](#media-validation) |
|
|
25
26
|
| `/formative` | [Formative delivery](#formative-delivery) |
|
|
@@ -33,9 +34,10 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
33
34
|
| `/i18n/provider` | `SimpleI18n`, `getDefaultI18n` and `resolveInterfaceI18n`, with the English catalog only |
|
|
34
35
|
| `/i18n/catalogs` | `BUNDLED_LOCALES` and lazy loaders for the bundled locales other than English |
|
|
35
36
|
| `/i18n/language-tags` | BCP 47 language tag normalization and matching |
|
|
36
|
-
| `/nds-icon-button` | `<nds-icon-button>`, an icon button vendored as a prebuilt bundle with Lit inlined; importing it registers the tag. The assessment toolkit and `@pie-players/pie-tool-tts-inline` render it |
|
|
37
|
+
| `/nds-icon-button` | `<nds-icon-button>`, an icon button vendored as a prebuilt bundle with Lit inlined; importing it registers the tag. The assessment toolkit, the section player and `@pie-players/pie-tool-tts-inline` render it |
|
|
37
38
|
| `/ui/attribute-coercion` | Boolean attribute coercion for custom elements |
|
|
38
39
|
| `/ui/content-styles` | `installContentStyles`, `auditContentStyles` and the opt-out checks behind the item player's content styles |
|
|
40
|
+
| `/ui/fontawesome` | The FontAwesome `<nds-icon-button>` glyphs need: the one FA Free pin, Pro detection, the `<head>` and shadow-root installs, and the glyph weight. The assessment toolkit, the section player and `@pie-players/pie-tool-tts-inline` use it |
|
|
39
41
|
| `/tools/term-lookup` | Term normalization and the lookup session the dictionary tools share; the host supplies the endpoint or a resolver |
|
|
40
42
|
|
|
41
43
|
## Formative Delivery
|
package/dist/i18n/catalogs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The bundled catalog loader: `en-US`, the one statically shipped catalog.
|
|
3
3
|
*
|
|
4
4
|
* Imported by players, never by tools. A tool resolves its provider from the
|
|
5
5
|
* toolkit runtime context and needs only the interface plus the English
|
|
@@ -7,25 +7,32 @@
|
|
|
7
7
|
* into a bundle that never calls `setLocale`, because every tool
|
|
8
8
|
* `vite.config.ts` sets `external: []`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* alongside the file.
|
|
10
|
+
* `en-US` is the source of truth — the fallback for every missing key in every
|
|
11
|
+
* other locale — so the provider imports it statically and it is always
|
|
12
|
+
* resident. It is the only catalog this package ships: every other locale this
|
|
13
|
+
* repository ships lives in the optional generated tier
|
|
14
|
+
* (`@pie-players/pie-i18n-generated`), whose loaders a host composes after
|
|
15
|
+
* this one. The composed shape is that package's README's wiring example:
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
* locale
|
|
17
|
+
* ```ts
|
|
18
|
+
* loadCatalog: (locale) =>
|
|
19
|
+
* loadBundledCatalog(locale).catch(() => loadGeneratedCatalog(locale));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* A request this function cannot serve rejects, and the provider treats that
|
|
23
|
+
* as "resolve through the fallback chain" rather than an error, so a host
|
|
24
|
+
* locale nothing ships still renders English.
|
|
20
25
|
*
|
|
21
26
|
* @module @pie-players/pie-players-shared/i18n/catalogs
|
|
22
27
|
*/
|
|
23
28
|
import type { BundledLocaleCode, MessageCatalog } from "./types.js";
|
|
24
29
|
/**
|
|
25
|
-
* Every locale tag this repository
|
|
30
|
+
* Every locale tag this repository bundles a catalog for. `en-US` alone: the
|
|
31
|
+
* statically resident source of truth.
|
|
26
32
|
*
|
|
27
33
|
* The provider resolves a request against this list through RFC 4647 lookup and
|
|
28
|
-
* primary-subtag widening, so `
|
|
34
|
+
* primary-subtag widening, so `en`, `en_US` and `en-GB`-without-a-catalog all
|
|
35
|
+
* land on `en-US` unless a composed loader serves the region.
|
|
29
36
|
*/
|
|
30
37
|
export declare const BUNDLED_LOCALES: readonly BundledLocaleCode[];
|
|
31
38
|
/**
|
|
@@ -36,7 +43,7 @@ export declare const BUNDLED_LOCALES: readonly BundledLocaleCode[];
|
|
|
36
43
|
* or a pseudo-localization of the English catalog — asks for it by name like
|
|
37
44
|
* any other locale.
|
|
38
45
|
*
|
|
39
|
-
*
|
|
46
|
+
* Rejects for a locale with no bundled catalog; the provider treats that as
|
|
40
47
|
* "resolve through the fallback chain" rather than an error, so a host locale we
|
|
41
48
|
* do not ship still renders.
|
|
42
49
|
*/
|
package/dist/i18n/catalogs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The bundled catalog loader: `en-US`, the one statically shipped catalog.
|
|
3
3
|
*
|
|
4
4
|
* Imported by players, never by tools. A tool resolves its provider from the
|
|
5
5
|
* toolkit runtime context and needs only the interface plus the English
|
|
@@ -7,30 +7,34 @@
|
|
|
7
7
|
* into a bundle that never calls `setLocale`, because every tool
|
|
8
8
|
* `vite.config.ts` sets `external: []`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* alongside the file.
|
|
10
|
+
* `en-US` is the source of truth — the fallback for every missing key in every
|
|
11
|
+
* other locale — so the provider imports it statically and it is always
|
|
12
|
+
* resident. It is the only catalog this package ships: every other locale this
|
|
13
|
+
* repository ships lives in the optional generated tier
|
|
14
|
+
* (`@pie-players/pie-i18n-generated`), whose loaders a host composes after
|
|
15
|
+
* this one. The composed shape is that package's README's wiring example:
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
* locale
|
|
17
|
+
* ```ts
|
|
18
|
+
* loadCatalog: (locale) =>
|
|
19
|
+
* loadBundledCatalog(locale).catch(() => loadGeneratedCatalog(locale));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* A request this function cannot serve rejects, and the provider treats that
|
|
23
|
+
* as "resolve through the fallback chain" rather than an error, so a host
|
|
24
|
+
* locale nothing ships still renders English.
|
|
20
25
|
*
|
|
21
26
|
* @module @pie-players/pie-players-shared/i18n/catalogs
|
|
22
27
|
*/
|
|
23
28
|
import enUS from "./messages/en-US.js";
|
|
24
|
-
const CATALOG_LOADERS = {
|
|
25
|
-
"nl-NL": () => import("./messages/nl-NL.js"),
|
|
26
|
-
};
|
|
27
29
|
/**
|
|
28
|
-
* Every locale tag this repository
|
|
30
|
+
* Every locale tag this repository bundles a catalog for. `en-US` alone: the
|
|
31
|
+
* statically resident source of truth.
|
|
29
32
|
*
|
|
30
33
|
* The provider resolves a request against this list through RFC 4647 lookup and
|
|
31
|
-
* primary-subtag widening, so `
|
|
34
|
+
* primary-subtag widening, so `en`, `en_US` and `en-GB`-without-a-catalog all
|
|
35
|
+
* land on `en-US` unless a composed loader serves the region.
|
|
32
36
|
*/
|
|
33
|
-
export const BUNDLED_LOCALES = ["en-US"
|
|
37
|
+
export const BUNDLED_LOCALES = ["en-US"];
|
|
34
38
|
/**
|
|
35
39
|
* Load one shipped catalog.
|
|
36
40
|
*
|
|
@@ -39,7 +43,7 @@ export const BUNDLED_LOCALES = ["en-US", "nl-NL"];
|
|
|
39
43
|
* or a pseudo-localization of the English catalog — asks for it by name like
|
|
40
44
|
* any other locale.
|
|
41
45
|
*
|
|
42
|
-
*
|
|
46
|
+
* Rejects for a locale with no bundled catalog; the provider treats that as
|
|
43
47
|
* "resolve through the fallback chain" rather than an error, so a host locale we
|
|
44
48
|
* do not ship still renders.
|
|
45
49
|
*/
|
|
@@ -47,10 +51,5 @@ export async function loadBundledCatalog(locale) {
|
|
|
47
51
|
if (locale === "en-US") {
|
|
48
52
|
return enUS;
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
if (!loader) {
|
|
52
|
-
throw new Error(`No bundled i18n catalog for locale: ${locale}`);
|
|
53
|
-
}
|
|
54
|
-
const module = await loader();
|
|
55
|
-
return module.default;
|
|
54
|
+
throw new Error(`No bundled i18n catalog for locale: ${locale}`);
|
|
56
55
|
}
|
|
@@ -12,8 +12,32 @@
|
|
|
12
12
|
* The player reflects the resolved language onto its content subtree's root as
|
|
13
13
|
* `lang` and `dir`, so a screen reader takes the content's voice rules and RTL
|
|
14
14
|
* content lays out right to left, independent of the chrome's locale.
|
|
15
|
+
*
|
|
16
|
+
* Content that declares nothing falls back to a default the container names:
|
|
17
|
+
* a section's `runtime.contentLanguage`, or {@link UNDECLARED_CONTENT_LANGUAGE}
|
|
18
|
+
* once the host sets an interface locale, which stamps `lang=""` and
|
|
19
|
+
* `dir="auto"` so the content takes neither the interface's language nor its
|
|
20
|
+
* direction. With neither, undeclared content inherits its context, which keeps
|
|
21
|
+
* hosts that set no interface locale rendering as they always have.
|
|
15
22
|
*/
|
|
16
23
|
import type { TextDirection } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* The content-language default for content that declares no language under a
|
|
26
|
+
* host-set interface locale: `lang=""` names the language unknown and
|
|
27
|
+
* `dir="auto"` takes direction from the content's first strong character.
|
|
28
|
+
*/
|
|
29
|
+
export declare const UNDECLARED_CONTENT_LANGUAGE = "auto";
|
|
30
|
+
/**
|
|
31
|
+
* The default a player stamps on content whose `env` declares no language: the
|
|
32
|
+
* host's content language, else {@link UNDECLARED_CONTENT_LANGUAGE} when the
|
|
33
|
+
* host set an interface locale, else `undefined`, which leaves undeclared
|
|
34
|
+
* content inheriting its context. Inputs are read defensively, since both
|
|
35
|
+
* arrive from attributes.
|
|
36
|
+
*/
|
|
37
|
+
export declare function defaultContentLanguage(options: {
|
|
38
|
+
contentLanguage?: unknown;
|
|
39
|
+
interfaceLocale?: unknown;
|
|
40
|
+
}): string | undefined;
|
|
17
41
|
/** `lang` and `dir` for declared content, or `null` when nothing is declared. */
|
|
18
42
|
export interface ContentLanguageAttributes {
|
|
19
43
|
lang: string;
|
|
@@ -28,8 +52,26 @@ export interface ContentLanguageAttributes {
|
|
|
28
52
|
*/
|
|
29
53
|
export declare function contentLanguageAttributes(env: unknown): ContentLanguageAttributes | null;
|
|
30
54
|
/**
|
|
31
|
-
* Stamps `contentLanguageAttributes(env)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
55
|
+
* Stamps the content language onto `target`: `contentLanguageAttributes(env)`,
|
|
56
|
+
* else `fallback` — a tag, or {@link UNDECLARED_CONTENT_LANGUAGE} for
|
|
57
|
+
* `lang=""` and `dir="auto"` — else neither attribute, so a model set can drop
|
|
58
|
+
* the language a previous one declared. A fallback that is not a usable tag
|
|
59
|
+
* counts as absent. No-op without a target.
|
|
60
|
+
*/
|
|
61
|
+
export declare function stampContentLanguage(target: Element | null | undefined, env: unknown, fallback?: string | null): void;
|
|
62
|
+
/**
|
|
63
|
+
* The direction of the first letter in `root`'s text, skipping `script`,
|
|
64
|
+
* `style` and `template`, or `null` when it has none. Letters only, which is
|
|
65
|
+
* where the bidi algorithm's strong characters overwhelmingly sit.
|
|
66
|
+
*/
|
|
67
|
+
export declare function firstStrongDirection(root: Node): TextDirection | null;
|
|
68
|
+
/**
|
|
69
|
+
* The mismatch between undeclared content's script and the direction a
|
|
70
|
+
* fallback stamped on `root`, or `null` when there is none to report: the env
|
|
71
|
+
* declares a language, `root` carries no stamped direction or `dir="auto"`, or
|
|
72
|
+
* the content has no letters.
|
|
34
73
|
*/
|
|
35
|
-
export declare function
|
|
74
|
+
export declare function undeclaredDirectionMismatch(root: Element, env: unknown): {
|
|
75
|
+
stamped: TextDirection;
|
|
76
|
+
script: TextDirection;
|
|
77
|
+
} | null;
|
|
@@ -12,9 +12,40 @@
|
|
|
12
12
|
* The player reflects the resolved language onto its content subtree's root as
|
|
13
13
|
* `lang` and `dir`, so a screen reader takes the content's voice rules and RTL
|
|
14
14
|
* content lays out right to left, independent of the chrome's locale.
|
|
15
|
+
*
|
|
16
|
+
* Content that declares nothing falls back to a default the container names:
|
|
17
|
+
* a section's `runtime.contentLanguage`, or {@link UNDECLARED_CONTENT_LANGUAGE}
|
|
18
|
+
* once the host sets an interface locale, which stamps `lang=""` and
|
|
19
|
+
* `dir="auto"` so the content takes neither the interface's language nor its
|
|
20
|
+
* direction. With neither, undeclared content inherits its context, which keeps
|
|
21
|
+
* hosts that set no interface locale rendering as they always have.
|
|
15
22
|
*/
|
|
16
23
|
import { canonicalLanguageTag } from "./language-tags.js";
|
|
17
24
|
import { localeDirection } from "./provider.js";
|
|
25
|
+
/**
|
|
26
|
+
* The content-language default for content that declares no language under a
|
|
27
|
+
* host-set interface locale: `lang=""` names the language unknown and
|
|
28
|
+
* `dir="auto"` takes direction from the content's first strong character.
|
|
29
|
+
*/
|
|
30
|
+
export const UNDECLARED_CONTENT_LANGUAGE = "auto";
|
|
31
|
+
/**
|
|
32
|
+
* The default a player stamps on content whose `env` declares no language: the
|
|
33
|
+
* host's content language, else {@link UNDECLARED_CONTENT_LANGUAGE} when the
|
|
34
|
+
* host set an interface locale, else `undefined`, which leaves undeclared
|
|
35
|
+
* content inheriting its context. Inputs are read defensively, since both
|
|
36
|
+
* arrive from attributes.
|
|
37
|
+
*/
|
|
38
|
+
export function defaultContentLanguage(options) {
|
|
39
|
+
const named = typeof options.contentLanguage === "string"
|
|
40
|
+
? options.contentLanguage.trim()
|
|
41
|
+
: "";
|
|
42
|
+
if (named)
|
|
43
|
+
return named;
|
|
44
|
+
const requested = typeof options.interfaceLocale === "string"
|
|
45
|
+
? options.interfaceLocale.trim()
|
|
46
|
+
: "";
|
|
47
|
+
return requested ? UNDECLARED_CONTENT_LANGUAGE : undefined;
|
|
48
|
+
}
|
|
18
49
|
/**
|
|
19
50
|
* `lang` and `dir` for the content an `env` describes, or `null` when it
|
|
20
51
|
* declares no usable language. Reads `env.locale` defensively — `env` is
|
|
@@ -32,14 +63,19 @@ export function contentLanguageAttributes(env) {
|
|
|
32
63
|
return { lang, dir: localeDirection(lang) };
|
|
33
64
|
}
|
|
34
65
|
/**
|
|
35
|
-
* Stamps `contentLanguageAttributes(env)
|
|
36
|
-
*
|
|
37
|
-
*
|
|
66
|
+
* Stamps the content language onto `target`: `contentLanguageAttributes(env)`,
|
|
67
|
+
* else `fallback` — a tag, or {@link UNDECLARED_CONTENT_LANGUAGE} for
|
|
68
|
+
* `lang=""` and `dir="auto"` — else neither attribute, so a model set can drop
|
|
69
|
+
* the language a previous one declared. A fallback that is not a usable tag
|
|
70
|
+
* counts as absent. No-op without a target.
|
|
38
71
|
*/
|
|
39
|
-
export function stampContentLanguage(target, env) {
|
|
72
|
+
export function stampContentLanguage(target, env, fallback) {
|
|
40
73
|
if (!target)
|
|
41
74
|
return;
|
|
42
|
-
const attributes = contentLanguageAttributes(env)
|
|
75
|
+
const attributes = contentLanguageAttributes(env) ??
|
|
76
|
+
(fallback === UNDECLARED_CONTENT_LANGUAGE
|
|
77
|
+
? { lang: "", dir: "auto" }
|
|
78
|
+
: contentLanguageAttributes({ locale: fallback }));
|
|
43
79
|
if (attributes) {
|
|
44
80
|
target.setAttribute("lang", attributes.lang);
|
|
45
81
|
target.setAttribute("dir", attributes.dir);
|
|
@@ -49,3 +85,45 @@ export function stampContentLanguage(target, env) {
|
|
|
49
85
|
target.removeAttribute("dir");
|
|
50
86
|
}
|
|
51
87
|
}
|
|
88
|
+
/** Scripts written right to left, for {@link firstStrongDirection}. */
|
|
89
|
+
const RTL_LETTER = /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Adlam}\p{Script=Hanifi_Rohingya}\p{Script=Mandaic}\p{Script=Samaritan}]/u;
|
|
90
|
+
const LETTER = /\p{L}/u;
|
|
91
|
+
/**
|
|
92
|
+
* The direction of the first letter in `root`'s text, skipping `script`,
|
|
93
|
+
* `style` and `template`, or `null` when it has none. Letters only, which is
|
|
94
|
+
* where the bidi algorithm's strong characters overwhelmingly sit.
|
|
95
|
+
*/
|
|
96
|
+
export function firstStrongDirection(root) {
|
|
97
|
+
const doc = root.ownerDocument ?? root;
|
|
98
|
+
const walker = doc.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */, {
|
|
99
|
+
acceptNode: (node) => node.parentElement?.closest("script, style, template")
|
|
100
|
+
? 2 /* NodeFilter.FILTER_REJECT */
|
|
101
|
+
: 1 /* NodeFilter.FILTER_ACCEPT */,
|
|
102
|
+
});
|
|
103
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
104
|
+
for (const char of node.nodeValue ?? "") {
|
|
105
|
+
if (RTL_LETTER.test(char))
|
|
106
|
+
return "rtl";
|
|
107
|
+
if (LETTER.test(char))
|
|
108
|
+
return "ltr";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The mismatch between undeclared content's script and the direction a
|
|
115
|
+
* fallback stamped on `root`, or `null` when there is none to report: the env
|
|
116
|
+
* declares a language, `root` carries no stamped direction or `dir="auto"`, or
|
|
117
|
+
* the content has no letters.
|
|
118
|
+
*/
|
|
119
|
+
export function undeclaredDirectionMismatch(root, env) {
|
|
120
|
+
if (contentLanguageAttributes(env))
|
|
121
|
+
return null;
|
|
122
|
+
const stamped = root.getAttribute("dir");
|
|
123
|
+
if (stamped !== "ltr" && stamped !== "rtl")
|
|
124
|
+
return null;
|
|
125
|
+
const script = firstStrongDirection(root);
|
|
126
|
+
if (!script)
|
|
127
|
+
return null;
|
|
128
|
+
return script === stamped ? null : { stamped, script };
|
|
129
|
+
}
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -28,9 +28,10 @@ import type { I18nConfig, I18nServiceApi } from "./types.js";
|
|
|
28
28
|
*/
|
|
29
29
|
export declare function createPieI18n(config?: I18nConfig): I18nServiceApi;
|
|
30
30
|
export { BUNDLED_LOCALES, loadBundledCatalog } from "./catalogs.js";
|
|
31
|
-
export { canonicalLanguageTag, findBestLanguageMatch, languageTagLookupSequence, languageTagsEqual, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
31
|
+
export { canonicalLanguageTag, findBestLanguageMatch, findClosestLanguageMatch, languageTagLookupSequence, languageTagScriptLookupSequence, languageTagsEqual, negotiateContentLanguage, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
32
|
+
export type { ContentLanguageMatch } from "./language-tags.js";
|
|
32
33
|
export { DEFAULT_LOCALE, detectBrowserLocale, dynamicMessageKey, getDefaultI18n, localeDirection, resolveInterfaceI18n, SimpleI18n, stampInterfaceLocale, } from "./provider.js";
|
|
33
|
-
export { type ContentLanguageAttributes, contentLanguageAttributes, stampContentLanguage, } from "./content-language.js";
|
|
34
|
+
export { type ContentLanguageAttributes, contentLanguageAttributes, defaultContentLanguage, firstStrongDirection, stampContentLanguage, UNDECLARED_CONTENT_LANGUAGE, undeclaredDirectionMismatch, } from "./content-language.js";
|
|
34
35
|
export { type InterfaceLocaleLoadErrorModel, reportInterfaceLocaleLoadError, } from "./locale-load-error.js";
|
|
35
36
|
export { PSEUDO_LOCALE_EXPANDED, PSEUDO_LOCALE_RTL, pseudoLocalize, } from "./pseudo.js";
|
|
36
37
|
export type { PseudoLocalizeOptions } from "./pseudo.js";
|
package/dist/i18n/index.js
CHANGED
|
@@ -35,8 +35,8 @@ export function createPieI18n(config = {}) {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
export { BUNDLED_LOCALES, loadBundledCatalog } from "./catalogs.js";
|
|
38
|
-
export { canonicalLanguageTag, findBestLanguageMatch, languageTagLookupSequence, languageTagsEqual, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
38
|
+
export { canonicalLanguageTag, findBestLanguageMatch, findClosestLanguageMatch, languageTagLookupSequence, languageTagScriptLookupSequence, languageTagsEqual, negotiateContentLanguage, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
39
39
|
export { DEFAULT_LOCALE, detectBrowserLocale, dynamicMessageKey, getDefaultI18n, localeDirection, resolveInterfaceI18n, SimpleI18n, stampInterfaceLocale, } from "./provider.js";
|
|
40
|
-
export { contentLanguageAttributes, stampContentLanguage, } from "./content-language.js";
|
|
40
|
+
export { contentLanguageAttributes, defaultContentLanguage, firstStrongDirection, stampContentLanguage, UNDECLARED_CONTENT_LANGUAGE, undeclaredDirectionMismatch, } from "./content-language.js";
|
|
41
41
|
export { reportInterfaceLocaleLoadError, } from "./locale-load-error.js";
|
|
42
42
|
export { PSEUDO_LOCALE_EXPANDED, PSEUDO_LOCALE_RTL, pseudoLocalize, } from "./pseudo.js";
|
|
@@ -51,12 +51,81 @@ export declare function languageTagsEqual(a: string | null | undefined, b: strin
|
|
|
51
51
|
*/
|
|
52
52
|
export declare function languageTagLookupSequence(tag: string | null | undefined): string[];
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
54
|
+
* The script a tag is written in, lowercased: explicit (`sr-Latn`), or the
|
|
55
|
+
* CLDR likely script (`zh-TW` maximizes to Hant, `sr` to Cyrl). `undefined`
|
|
56
|
+
* when the tag has no script data or the runtime has no `Intl.Locale`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function languageTagScript(tag: string | null | undefined): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a catalog reached by widening serves a request in the same script.
|
|
61
|
+
* A tag without script data passes, so a runtime without `Intl.Locale`
|
|
62
|
+
* resolves as plain RFC 4647 lookup.
|
|
63
|
+
*/
|
|
64
|
+
export declare function languageTagScriptsMatch(requested: string | null | undefined, candidate: string | null | undefined): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The script-aware lookup sequence: the RFC 4647 sequence with the language
|
|
67
|
+
* and likely script inserted before the bare language, so `zh-TW` yields
|
|
68
|
+
* `["zh-tw", "zh-hant", "zh"]`. A bare-language step serves the request only
|
|
69
|
+
* in the same script, which `findBestLanguageMatch` checks.
|
|
70
|
+
*/
|
|
71
|
+
export declare function languageTagScriptLookupSequence(tag: string | null | undefined): string[];
|
|
72
|
+
/**
|
|
73
|
+
* Best available tag for a request, or `undefined` when nothing matches.
|
|
74
|
+
* Ranking is by the request's specificity rather than the candidates' order,
|
|
75
|
+
* so `es-MX` prefers an `es-MX` candidate over an `es` one however they were
|
|
76
|
+
* authored, and `zh-TW` reaches `zh-Hant`. A catalog shorter than the request
|
|
77
|
+
* serves it only in the same script: a bare `zh` catalog is Simplified, so
|
|
78
|
+
* `zh-TW` passes it by.
|
|
58
79
|
*/
|
|
59
80
|
export declare function findBestLanguageMatch(requested: string | null | undefined, available: readonly (string | null | undefined)[]): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* A tag without its variant, extension and private-use subtags, casing kept:
|
|
83
|
+
* `en-GB-x-gulf` yields `en-GB`. The form for a speech engine, whose voices
|
|
84
|
+
* are keyed by language and region.
|
|
85
|
+
*/
|
|
86
|
+
export declare function languageTagWithoutVariants(tag: string | null | undefined): string;
|
|
87
|
+
/**
|
|
88
|
+
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
89
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Widening stays
|
|
90
|
+
* within the request's script and keeps its variant subtags: `zh` never
|
|
91
|
+
* reaches `zh-TW`, `sr-Latn` may reach `sr-ME`, `en-GB` never reaches
|
|
92
|
+
* `en-GB-x-gulf`, and `en-US-x-gulf` may reach `en-GB-x-gulf`. Among several
|
|
93
|
+
* siblings the first in `available` wins. The rule that picks an interface
|
|
94
|
+
* catalog and a content variant alike.
|
|
95
|
+
*/
|
|
96
|
+
export declare function findClosestLanguageMatch(requested: string | null | undefined, available: readonly (string | null | undefined)[]): string | undefined;
|
|
97
|
+
/** The content language a backend serves, and whether it is a substitute. */
|
|
98
|
+
export interface ContentLanguageMatch {
|
|
99
|
+
/** The `available` entry served, as authored; `null` for content that declares no language. */
|
|
100
|
+
language: string | null;
|
|
101
|
+
/**
|
|
102
|
+
* `"fallback"` when the served content stands in for the request;
|
|
103
|
+
* `"variety"` when it is another variety of the requested language.
|
|
104
|
+
*/
|
|
105
|
+
substitution?: "fallback" | "variety";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The language to serve content in: the closest `available` match for the
|
|
109
|
+
* first `requested` tag that has one, under `findClosestLanguageMatch`, or with
|
|
110
|
+
* `matching: "exact"` only an equal tag, so `es-MX` then passes an `es`
|
|
111
|
+
* variant by. With no match and `fallback` other than `"none"`, the fallback
|
|
112
|
+
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
113
|
+
* then content that declares no language (a `null` entry), then `source`;
|
|
114
|
+
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
115
|
+
* requested languages run the chain without a substitution. A closest match
|
|
116
|
+
* in another variety, `es-MX` served `es-ES`, records `"variety"`; a host
|
|
117
|
+
* that must not serve one sets `matching: "exact"` and lists the varieties it
|
|
118
|
+
* accepts. Candidates rank
|
|
119
|
+
* `source` first, then by tag, so the order of `available` never decides
|
|
120
|
+
* between two siblings.
|
|
121
|
+
*/
|
|
122
|
+
export declare function negotiateContentLanguage(requested: readonly string[], available: readonly (string | null)[], options?: {
|
|
123
|
+
defaultLanguage?: string;
|
|
124
|
+
fallback?: "default" | "none";
|
|
125
|
+
matching?: "closest" | "exact";
|
|
126
|
+
/** The family source's `available` entry. */
|
|
127
|
+
source?: string | null;
|
|
128
|
+
}): ContentLanguageMatch | undefined;
|
|
60
129
|
/**
|
|
61
130
|
* The canonical form of a usable language tag, or `undefined` for anything a
|
|
62
131
|
* language in use would not be: `es_MX` normalizes to `es-MX`, `NL-nl` to
|
|
@@ -78,20 +78,190 @@ export function languageTagLookupSequence(tag) {
|
|
|
78
78
|
}
|
|
79
79
|
return sequence;
|
|
80
80
|
}
|
|
81
|
+
/** `maximize()` runs on every resolution, for each candidate catalog. */
|
|
82
|
+
const scripts = new Map();
|
|
81
83
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
84
|
+
* The script a tag is written in, lowercased: explicit (`sr-Latn`), or the
|
|
85
|
+
* CLDR likely script (`zh-TW` maximizes to Hant, `sr` to Cyrl). `undefined`
|
|
86
|
+
* when the tag has no script data or the runtime has no `Intl.Locale`.
|
|
87
|
+
*/
|
|
88
|
+
export function languageTagScript(tag) {
|
|
89
|
+
const normalized = normalizeLanguageTag(tag);
|
|
90
|
+
if (!normalized ||
|
|
91
|
+
typeof Intl === "undefined" ||
|
|
92
|
+
typeof Intl.Locale !== "function") {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (scripts.has(normalized))
|
|
96
|
+
return scripts.get(normalized);
|
|
97
|
+
let script;
|
|
98
|
+
try {
|
|
99
|
+
script = new Intl.Locale(normalized).maximize().script?.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Not a tag `Intl.Locale` parses: no script data.
|
|
103
|
+
}
|
|
104
|
+
scripts.set(normalized, script);
|
|
105
|
+
return script;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a catalog reached by widening serves a request in the same script.
|
|
109
|
+
* A tag without script data passes, so a runtime without `Intl.Locale`
|
|
110
|
+
* resolves as plain RFC 4647 lookup.
|
|
111
|
+
*/
|
|
112
|
+
export function languageTagScriptsMatch(requested, candidate) {
|
|
113
|
+
const requestedScript = languageTagScript(requested);
|
|
114
|
+
const candidateScript = languageTagScript(candidate);
|
|
115
|
+
return (!requestedScript || !candidateScript || requestedScript === candidateScript);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The script-aware lookup sequence: the RFC 4647 sequence with the language
|
|
119
|
+
* and likely script inserted before the bare language, so `zh-TW` yields
|
|
120
|
+
* `["zh-tw", "zh-hant", "zh"]`. A bare-language step serves the request only
|
|
121
|
+
* in the same script, which `findBestLanguageMatch` checks.
|
|
122
|
+
*/
|
|
123
|
+
export function languageTagScriptLookupSequence(tag) {
|
|
124
|
+
const sequence = languageTagLookupSequence(tag);
|
|
125
|
+
const script = languageTagScript(tag);
|
|
126
|
+
if (!script || sequence.length < 2)
|
|
127
|
+
return sequence;
|
|
128
|
+
const language = sequence[sequence.length - 1];
|
|
129
|
+
const withScript = `${language}-${script}`;
|
|
130
|
+
if (sequence.includes(withScript))
|
|
131
|
+
return sequence;
|
|
132
|
+
return [...sequence.slice(0, -1), withScript, language];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Best available tag for a request, or `undefined` when nothing matches.
|
|
136
|
+
* Ranking is by the request's specificity rather than the candidates' order,
|
|
137
|
+
* so `es-MX` prefers an `es-MX` candidate over an `es` one however they were
|
|
138
|
+
* authored, and `zh-TW` reaches `zh-Hant`. A catalog shorter than the request
|
|
139
|
+
* serves it only in the same script: a bare `zh` catalog is Simplified, so
|
|
140
|
+
* `zh-TW` passes it by.
|
|
86
141
|
*/
|
|
87
142
|
export function findBestLanguageMatch(requested, available) {
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
const exact = normalizeLanguageTag(requested);
|
|
144
|
+
for (const step of languageTagScriptLookupSequence(requested)) {
|
|
145
|
+
const hit = available.find((candidate) => normalizeLanguageTag(candidate) === step &&
|
|
146
|
+
(step === exact || languageTagScriptsMatch(requested, candidate)));
|
|
90
147
|
if (hit != null)
|
|
91
148
|
return hit;
|
|
92
149
|
}
|
|
93
150
|
return undefined;
|
|
94
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* A tag's subtags split at the end of its language, script and region:
|
|
154
|
+
* `en-GB-x-gulf` yields `[["en", "GB"], ["x", "gulf"]]`. The second part holds
|
|
155
|
+
* variants, extensions and private use, as authored.
|
|
156
|
+
*/
|
|
157
|
+
function splitVariantSubtags(tag) {
|
|
158
|
+
const subtags = toLanguageTag(tag).split(/[.@]/)[0].split("-");
|
|
159
|
+
let length = 1;
|
|
160
|
+
if (/^[a-z]{4}$/i.test(subtags[length] ?? ""))
|
|
161
|
+
length++;
|
|
162
|
+
if (/^([a-z]{2}|\d{3})$/i.test(subtags[length] ?? ""))
|
|
163
|
+
length++;
|
|
164
|
+
return [subtags.slice(0, length), subtags.slice(length)];
|
|
165
|
+
}
|
|
166
|
+
/** The variant, extension and private-use subtags as a comparison key. */
|
|
167
|
+
function variantSubtagsKey(tag) {
|
|
168
|
+
return splitVariantSubtags(tag)[1].join("-").toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether `served` is another variety of `requested`: it names a different
|
|
172
|
+
* region, or different variant or private-use subtags. `es-MX` served `es-ES`
|
|
173
|
+
* or `en-GB-x-gulf` served `en-GB` is; `es-MX` served `es`, or `zh-TW` served
|
|
174
|
+
* `zh-Hant`, is not.
|
|
175
|
+
*/
|
|
176
|
+
function isOtherLanguageVariety(requested, served) {
|
|
177
|
+
const region = (tag) => {
|
|
178
|
+
const [base] = splitVariantSubtags(tag);
|
|
179
|
+
const last = base.length > 1 ? base[base.length - 1] : "";
|
|
180
|
+
return /^([a-z]{2}|\d{3})$/i.test(last) ? last.toLowerCase() : undefined;
|
|
181
|
+
};
|
|
182
|
+
const requestedRegion = region(requested);
|
|
183
|
+
const servedRegion = region(served);
|
|
184
|
+
return ((requestedRegion !== undefined &&
|
|
185
|
+
servedRegion !== undefined &&
|
|
186
|
+
requestedRegion !== servedRegion) ||
|
|
187
|
+
variantSubtagsKey(requested) !== variantSubtagsKey(served));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A tag without its variant, extension and private-use subtags, casing kept:
|
|
191
|
+
* `en-GB-x-gulf` yields `en-GB`. The form for a speech engine, whose voices
|
|
192
|
+
* are keyed by language and region.
|
|
193
|
+
*/
|
|
194
|
+
export function languageTagWithoutVariants(tag) {
|
|
195
|
+
return splitVariantSubtags(tag)[0].join("-");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
199
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Widening stays
|
|
200
|
+
* within the request's script and keeps its variant subtags: `zh` never
|
|
201
|
+
* reaches `zh-TW`, `sr-Latn` may reach `sr-ME`, `en-GB` never reaches
|
|
202
|
+
* `en-GB-x-gulf`, and `en-US-x-gulf` may reach `en-GB-x-gulf`. Among several
|
|
203
|
+
* siblings the first in `available` wins. The rule that picks an interface
|
|
204
|
+
* catalog and a content variant alike.
|
|
205
|
+
*/
|
|
206
|
+
export function findClosestLanguageMatch(requested, available) {
|
|
207
|
+
const best = findBestLanguageMatch(requested, available);
|
|
208
|
+
if (best !== undefined)
|
|
209
|
+
return best;
|
|
210
|
+
const language = normalizeLanguageTag(requested).split("-")[0];
|
|
211
|
+
if (!language)
|
|
212
|
+
return undefined;
|
|
213
|
+
const variants = variantSubtagsKey(requested);
|
|
214
|
+
const sibling = available.find((candidate) => normalizeLanguageTag(candidate).split("-")[0] === language &&
|
|
215
|
+
variantSubtagsKey(candidate) === variants &&
|
|
216
|
+
languageTagScriptsMatch(requested, candidate));
|
|
217
|
+
return sibling ?? undefined;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The language to serve content in: the closest `available` match for the
|
|
221
|
+
* first `requested` tag that has one, under `findClosestLanguageMatch`, or with
|
|
222
|
+
* `matching: "exact"` only an equal tag, so `es-MX` then passes an `es`
|
|
223
|
+
* variant by. With no match and `fallback` other than `"none"`, the fallback
|
|
224
|
+
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
225
|
+
* then content that declares no language (a `null` entry), then `source`;
|
|
226
|
+
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
227
|
+
* requested languages run the chain without a substitution. A closest match
|
|
228
|
+
* in another variety, `es-MX` served `es-ES`, records `"variety"`; a host
|
|
229
|
+
* that must not serve one sets `matching: "exact"` and lists the varieties it
|
|
230
|
+
* accepts. Candidates rank
|
|
231
|
+
* `source` first, then by tag, so the order of `available` never decides
|
|
232
|
+
* between two siblings.
|
|
233
|
+
*/
|
|
234
|
+
export function negotiateContentLanguage(requested, available, options = {}) {
|
|
235
|
+
const { defaultLanguage, fallback = "default", matching = "closest", source } = options;
|
|
236
|
+
const candidates = [...available].sort((a, b) => {
|
|
237
|
+
if ((a === source) !== (b === source))
|
|
238
|
+
return a === source ? -1 : 1;
|
|
239
|
+
const left = normalizeLanguageTag(a);
|
|
240
|
+
const right = normalizeLanguageTag(b);
|
|
241
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
242
|
+
});
|
|
243
|
+
for (const tag of requested) {
|
|
244
|
+
const language = matching === "exact"
|
|
245
|
+
? candidates.find((candidate) => languageTagsEqual(candidate, tag))
|
|
246
|
+
: findClosestLanguageMatch(tag, candidates);
|
|
247
|
+
if (language != null) {
|
|
248
|
+
return isOtherLanguageVariety(tag, language)
|
|
249
|
+
? { language, substitution: "variety" }
|
|
250
|
+
: { language };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (requested.length > 0 && fallback === "none")
|
|
254
|
+
return undefined;
|
|
255
|
+
let language = findClosestLanguageMatch(defaultLanguage, candidates);
|
|
256
|
+
if (language === undefined && available.includes(null))
|
|
257
|
+
language = null;
|
|
258
|
+
if (language === undefined && source !== undefined && available.includes(source)) {
|
|
259
|
+
language = source;
|
|
260
|
+
}
|
|
261
|
+
if (language === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
return requested.length > 0 ? { language, substitution: "fallback" } : { language };
|
|
264
|
+
}
|
|
95
265
|
/**
|
|
96
266
|
* The canonical form of a usable language tag, or `undefined` for anything a
|
|
97
267
|
* language in use would not be: `es_MX` normalizes to `es-MX`, `NL-nl` to
|