@pie-players/pie-players-shared 1.0.0-beta.5 → 1.0.0-beta.7
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/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 +56 -4
- package/dist/i18n/language-tags.js +111 -6
- package/dist/i18n/messages/en-US.d.ts +6 -0
- package/dist/i18n/messages/en-US.js +6 -0
- package/dist/i18n/messages/nl-NL.d.ts +3 -0
- package/dist/i18n/messages/nl-NL.js +3 -0
- package/dist/i18n/provider.d.ts +3 -6
- package/dist/i18n/provider.js +5 -15
- 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/validate-style-url.js +2 -2
- package/dist/types/index.d.ts +33 -27
- package/dist/types/interchange.d.ts +54 -0
- package/dist/types/interchange.js +1 -0
- package/dist/ui/focus-trap.js +3 -1
- package/dist/ui/ime.d.ts +6 -0
- package/dist/ui/ime.js +10 -0
- package/package.json +5 -5
|
@@ -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,64 @@ 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
|
+
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
83
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Both stay within
|
|
84
|
+
* the request's script: `zh` never reaches `zh-TW`, and `sr-Latn` may reach
|
|
85
|
+
* `sr-ME`. The rule that picks an interface catalog and a content variant alike.
|
|
86
|
+
*/
|
|
87
|
+
export declare function findClosestLanguageMatch(requested: string | null | undefined, available: readonly (string | null | undefined)[]): string | undefined;
|
|
88
|
+
/** The content language a backend serves, and whether it is a substitute. */
|
|
89
|
+
export interface ContentLanguageMatch {
|
|
90
|
+
/** The `available` entry served, as authored; `null` for content that declares no language. */
|
|
91
|
+
language: string | null;
|
|
92
|
+
/** `"fallback"` when the served content stands in for the request. */
|
|
93
|
+
substitution?: "fallback";
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The language to serve content in: the closest `available` match for the
|
|
97
|
+
* first `requested` tag that has one, under `findClosestLanguageMatch`, or with
|
|
98
|
+
* `matching: "exact"` only an equal tag, so `es-MX` then passes an `es`
|
|
99
|
+
* variant by. With no match and `fallback` other than `"none"`, the fallback
|
|
100
|
+
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
101
|
+
* then content that declares no language (a `null` entry), then `source`;
|
|
102
|
+
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
103
|
+
* requested languages run the chain without a substitution.
|
|
104
|
+
*/
|
|
105
|
+
export declare function negotiateContentLanguage(requested: readonly string[], available: readonly (string | null)[], options?: {
|
|
106
|
+
defaultLanguage?: string;
|
|
107
|
+
fallback?: "default" | "none";
|
|
108
|
+
matching?: "closest" | "exact";
|
|
109
|
+
/** The family source's `available` entry. */
|
|
110
|
+
source?: string | null;
|
|
111
|
+
}): ContentLanguageMatch | undefined;
|
|
60
112
|
/**
|
|
61
113
|
* The canonical form of a usable language tag, or `undefined` for anything a
|
|
62
114
|
* language in use would not be: `es_MX` normalizes to `es-MX`, `NL-nl` to
|
|
@@ -78,20 +78,125 @@ 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
|
+
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
154
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Both stay within
|
|
155
|
+
* the request's script: `zh` never reaches `zh-TW`, and `sr-Latn` may reach
|
|
156
|
+
* `sr-ME`. The rule that picks an interface catalog and a content variant alike.
|
|
157
|
+
*/
|
|
158
|
+
export function findClosestLanguageMatch(requested, available) {
|
|
159
|
+
const best = findBestLanguageMatch(requested, available);
|
|
160
|
+
if (best !== undefined)
|
|
161
|
+
return best;
|
|
162
|
+
const language = normalizeLanguageTag(requested).split("-")[0];
|
|
163
|
+
if (!language)
|
|
164
|
+
return undefined;
|
|
165
|
+
const sibling = available.find((candidate) => normalizeLanguageTag(candidate).split("-")[0] === language &&
|
|
166
|
+
languageTagScriptsMatch(requested, candidate));
|
|
167
|
+
return sibling ?? undefined;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The language to serve content in: the closest `available` match for the
|
|
171
|
+
* first `requested` tag that has one, under `findClosestLanguageMatch`, or with
|
|
172
|
+
* `matching: "exact"` only an equal tag, so `es-MX` then passes an `es`
|
|
173
|
+
* variant by. With no match and `fallback` other than `"none"`, the fallback
|
|
174
|
+
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
175
|
+
* then content that declares no language (a `null` entry), then `source`;
|
|
176
|
+
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
177
|
+
* requested languages run the chain without a substitution.
|
|
178
|
+
*/
|
|
179
|
+
export function negotiateContentLanguage(requested, available, options = {}) {
|
|
180
|
+
const { defaultLanguage, fallback = "default", matching = "closest", source } = options;
|
|
181
|
+
for (const tag of requested) {
|
|
182
|
+
const language = matching === "exact"
|
|
183
|
+
? available.find((candidate) => languageTagsEqual(candidate, tag))
|
|
184
|
+
: findClosestLanguageMatch(tag, available);
|
|
185
|
+
if (language != null)
|
|
186
|
+
return { language };
|
|
187
|
+
}
|
|
188
|
+
if (requested.length > 0 && fallback === "none")
|
|
189
|
+
return undefined;
|
|
190
|
+
let language = findClosestLanguageMatch(defaultLanguage, available);
|
|
191
|
+
if (language === undefined && available.includes(null))
|
|
192
|
+
language = null;
|
|
193
|
+
if (language === undefined && source !== undefined && available.includes(source)) {
|
|
194
|
+
language = source;
|
|
195
|
+
}
|
|
196
|
+
if (language === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
return requested.length > 0 ? { language, substitution: "fallback" } : { language };
|
|
199
|
+
}
|
|
95
200
|
/**
|
|
96
201
|
* The canonical form of a usable language tag, or `undefined` for anything a
|
|
97
202
|
* language in use would not be: `es_MX` normalizes to `es-MX`, `NL-nl` to
|
|
@@ -431,6 +431,12 @@ declare const enUS: {
|
|
|
431
431
|
description: string;
|
|
432
432
|
buttonA11y: string;
|
|
433
433
|
tooltip: string;
|
|
434
|
+
/** A choice's toggle; `{choice}` is the choice's own accessible name. */
|
|
435
|
+
toggleA11y: string;
|
|
436
|
+
/** Appended to an eliminated choice's label for assistive technology. */
|
|
437
|
+
eliminatedA11y: string;
|
|
438
|
+
/** Stands in for `{choice}` when a choice has no text of its own. */
|
|
439
|
+
unlabeledChoice: string;
|
|
434
440
|
};
|
|
435
441
|
highlighter: {
|
|
436
442
|
name: string;
|
|
@@ -448,6 +448,12 @@ const enUS = {
|
|
|
448
448
|
description: "Strike through answer choices",
|
|
449
449
|
buttonA11y: "Strike Through, eliminate answer choices",
|
|
450
450
|
tooltip: "Strike Through",
|
|
451
|
+
/** A choice's toggle; `{choice}` is the choice's own accessible name. */
|
|
452
|
+
toggleA11y: "Toggle elimination for {choice}",
|
|
453
|
+
/** Appended to an eliminated choice's label for assistive technology. */
|
|
454
|
+
eliminatedA11y: "(eliminated)",
|
|
455
|
+
/** Stands in for `{choice}` when a choice has no text of its own. */
|
|
456
|
+
unlabeledChoice: "Unlabeled choice",
|
|
451
457
|
},
|
|
452
458
|
highlighter: {
|
|
453
459
|
name: "Highlighter",
|
|
@@ -345,6 +345,9 @@ const nlNL = {
|
|
|
345
345
|
description: "Antwoordopties doorstrepen",
|
|
346
346
|
buttonA11y: "Doorstrepen, antwoordopties wegstrepen",
|
|
347
347
|
tooltip: "Doorstrepen",
|
|
348
|
+
toggleA11y: "Wegstrepen aan/uit voor {choice}",
|
|
349
|
+
eliminatedA11y: "(weggestreept)",
|
|
350
|
+
unlabeledChoice: "Keuze zonder label",
|
|
348
351
|
},
|
|
349
352
|
highlighter: {
|
|
350
353
|
name: "Markeerstift",
|
package/dist/i18n/provider.d.ts
CHANGED
|
@@ -122,12 +122,9 @@ export declare class SimpleI18n implements I18nServiceApi {
|
|
|
122
122
|
/** The resolved catalog for a locale, for a host that wants to inspect it. */
|
|
123
123
|
getBundle(locale?: LocaleCode): TranslationBundle | undefined;
|
|
124
124
|
/**
|
|
125
|
-
* Which shipped catalog serves a requested tag
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* primary-subtag widening, so a bare `nl` reaches the regional `nl-NL`
|
|
129
|
-
* catalog we actually ship. POSIX `nl_NL` normalizes on the way in, which is
|
|
130
|
-
* the form the Learnosity transform emits.
|
|
125
|
+
* Which shipped catalog serves a requested tag, under
|
|
126
|
+
* `findClosestLanguageMatch`. POSIX `nl_NL` normalizes on the way in, which
|
|
127
|
+
* is the form the Learnosity transform emits.
|
|
131
128
|
*/
|
|
132
129
|
private resolveLocaleTag;
|
|
133
130
|
private loadCatalog;
|
package/dist/i18n/provider.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module @pie-players/pie-players-shared/i18n/provider
|
|
11
11
|
*/
|
|
12
|
-
import { canonicalLanguageTag,
|
|
12
|
+
import { canonicalLanguageTag, findClosestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
13
13
|
import enUS from "./messages/en-US.js";
|
|
14
14
|
/** The locale every fallback chain terminates at. */
|
|
15
15
|
export const DEFAULT_LOCALE = "en-US";
|
|
@@ -302,24 +302,14 @@ export class SimpleI18n {
|
|
|
302
302
|
return { locale: tag, translations, direction: localeDirection(tag) };
|
|
303
303
|
}
|
|
304
304
|
/**
|
|
305
|
-
* Which shipped catalog serves a requested tag
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* primary-subtag widening, so a bare `nl` reaches the regional `nl-NL`
|
|
309
|
-
* catalog we actually ship. POSIX `nl_NL` normalizes on the way in, which is
|
|
310
|
-
* the form the Learnosity transform emits.
|
|
305
|
+
* Which shipped catalog serves a requested tag, under
|
|
306
|
+
* `findClosestLanguageMatch`. POSIX `nl_NL` normalizes on the way in, which
|
|
307
|
+
* is the form the Learnosity transform emits.
|
|
311
308
|
*/
|
|
312
309
|
resolveLocaleTag(locale) {
|
|
313
310
|
if (!locale)
|
|
314
311
|
return undefined;
|
|
315
|
-
|
|
316
|
-
const exactOrPrefix = findBestLanguageMatch(locale, available);
|
|
317
|
-
if (exactOrPrefix)
|
|
318
|
-
return exactOrPrefix;
|
|
319
|
-
const requestedLanguage = normalizeLanguageTag(locale).split("-")[0];
|
|
320
|
-
if (!requestedLanguage)
|
|
321
|
-
return undefined;
|
|
322
|
-
return available.find((candidate) => normalizeLanguageTag(candidate).split("-")[0] === requestedLanguage);
|
|
312
|
+
return findClosestLanguageMatch(locale, this.getAvailableLocales());
|
|
323
313
|
}
|
|
324
314
|
async loadCatalog(locale) {
|
|
325
315
|
const load = this.config.loadCatalog;
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./ui/pointer-rotate.js";
|
|
|
16
16
|
export * from "./ui/overlay-containment.js";
|
|
17
17
|
export * from "./ui/overlay-placement.js";
|
|
18
18
|
export * from "./ui/focus-trap.js";
|
|
19
|
+
export * from "./ui/ime.js";
|
|
19
20
|
export * from "./ui/first-focusable.js";
|
|
20
21
|
export * from "./ui/safe-storage.js";
|
|
21
22
|
export * from "./ui/scope-css.js";
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from "./ui/pointer-rotate.js";
|
|
|
15
15
|
export * from "./ui/overlay-containment.js";
|
|
16
16
|
export * from "./ui/overlay-placement.js";
|
|
17
17
|
export * from "./ui/focus-trap.js";
|
|
18
|
+
export * from "./ui/ime.js";
|
|
18
19
|
export * from "./ui/first-focusable.js";
|
|
19
20
|
export * from "./ui/safe-storage.js";
|
|
20
21
|
export * from "./ui/scope-css.js";
|
|
@@ -88,7 +88,10 @@ export type EnsureRegisteredOptions = {
|
|
|
88
88
|
*
|
|
89
89
|
* The primitive is the sole authority on registration state:
|
|
90
90
|
* - Empty element map resolves immediately (no backend call).
|
|
91
|
-
* - Already-registered tags resolve immediately (no backend call).
|
|
91
|
+
* - Already-registered tags resolve immediately (no backend call). A request
|
|
92
|
+
* that loads other tags through an IIFE or ESM backend that loads controllers
|
|
93
|
+
* also loads its registered tags with no controller, so their controllers
|
|
94
|
+
* fill in.
|
|
92
95
|
* - Concurrent identical requests share one backend call.
|
|
93
96
|
* - After the adapter's `load` settles, a post-load verification pass
|
|
94
97
|
* (bounded `customElements.whenDefined`) checks every tag. Any missing
|
|
@@ -26,7 +26,7 @@ import { parsePackageName } from "../pie/utils.js";
|
|
|
26
26
|
import { parseVersionedTagName } from "../pie/versioned-tag.js";
|
|
27
27
|
import { AdapterFailure, } from "./element-loader-types.js";
|
|
28
28
|
import { clearPackageMetadataRequests, createEsmBackend, } from "./esm-adapter.js";
|
|
29
|
-
import { createIifeBackend } from "./iife-adapter.js";
|
|
29
|
+
import { createIifeBackend, findIifeElementMapConflict, } from "./iife-adapter.js";
|
|
30
30
|
import { assertElementPackagesAllowed, } from "./element-package-policy.js";
|
|
31
31
|
export { AdapterFailure } from "./element-loader-types.js";
|
|
32
32
|
/**
|
|
@@ -125,9 +125,10 @@ function registeredPackageName(tag) {
|
|
|
125
125
|
}
|
|
126
126
|
const DEFAULT_WHEN_DEFINED_TIMEOUT_MS = 5000;
|
|
127
127
|
const DEFAULT_LOAD_TIMEOUT_MS = DEFAULT_IIFE_BUNDLE_RETRY_CONFIG.timeoutMs;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
128
|
+
// Requests share a backend call by both the original map and the unresolved
|
|
129
|
+
// subset, so incremental registration does not change an in-flight identity.
|
|
130
130
|
const inFlightRequests = new Map();
|
|
131
|
+
const inFlightKeysByPromise = new Map();
|
|
131
132
|
// An ESM backend keeps page state that later loads build on (the import maps it
|
|
132
133
|
// injected, the shared dependency versions they fixed), so every load with the
|
|
133
134
|
// same config reuses one backend. IIFE backends are created per call.
|
|
@@ -139,7 +140,10 @@ const resolvedBackendOverrides = new Map();
|
|
|
139
140
|
*
|
|
140
141
|
* The primitive is the sole authority on registration state:
|
|
141
142
|
* - Empty element map resolves immediately (no backend call).
|
|
142
|
-
* - Already-registered tags resolve immediately (no backend call).
|
|
143
|
+
* - Already-registered tags resolve immediately (no backend call). A request
|
|
144
|
+
* that loads other tags through an IIFE or ESM backend that loads controllers
|
|
145
|
+
* also loads its registered tags with no controller, so their controllers
|
|
146
|
+
* fill in.
|
|
143
147
|
* - Concurrent identical requests share one backend call.
|
|
144
148
|
* - After the adapter's `load` settles, a post-load verification pass
|
|
145
149
|
* (bounded `customElements.whenDefined`) checks every tag. Any missing
|
|
@@ -153,30 +157,54 @@ export async function ensureRegistered(elements, options) {
|
|
|
153
157
|
// Enforce policy before the already-registered fast path: a matching tag in
|
|
154
158
|
// the registry must not turn a disallowed authored package into valid input.
|
|
155
159
|
assertElementPackagesAllowed(elements, options.elementPackagePolicy);
|
|
156
|
-
|
|
160
|
+
// Preserve the original request identity while tags register mid-flight.
|
|
161
|
+
const originalDedupKey = buildDedupKey(options.backend, elements);
|
|
162
|
+
const originalRequest = inFlightRequests.get(originalDedupKey);
|
|
163
|
+
if (originalRequest)
|
|
164
|
+
return originalRequest;
|
|
165
|
+
if (Object.keys(elements).every(isRegistered))
|
|
166
|
+
return;
|
|
167
|
+
const loadsControllers = backendLoadsControllers(options.backend);
|
|
168
|
+
const unresolvedElements = {};
|
|
169
|
+
for (const [tag, packageSpec] of Object.entries(elements)) {
|
|
170
|
+
if (!isRegistered(tag) || (loadsControllers && awaitsController(tag))) {
|
|
171
|
+
unresolvedElements[tag] = packageSpec;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const tags = Object.keys(unresolvedElements);
|
|
157
175
|
const timeoutMs = options.whenDefinedTimeoutMs ?? DEFAULT_WHEN_DEFINED_TIMEOUT_MS;
|
|
158
176
|
const loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS;
|
|
159
|
-
|
|
160
|
-
// bookkeeping entirely.
|
|
161
|
-
if (allAlreadyRegistered(tags))
|
|
162
|
-
return;
|
|
163
|
-
const dedupKey = buildDedupKey(options.backend, elements);
|
|
177
|
+
const dedupKey = buildDedupKey(options.backend, unresolvedElements);
|
|
164
178
|
const existing = inFlightRequests.get(dedupKey);
|
|
165
|
-
if (existing)
|
|
179
|
+
if (existing) {
|
|
180
|
+
aliasInFlightRequest(originalDedupKey, existing);
|
|
166
181
|
return existing;
|
|
182
|
+
}
|
|
167
183
|
const backend = resolveBackend(options.backend);
|
|
168
184
|
const doc = resolveDoc(options.doc);
|
|
169
|
-
const
|
|
170
|
-
|
|
185
|
+
const loadElements = isIifeConfig(options.backend)
|
|
186
|
+
? iifeLoadElements(elements, unresolvedElements)
|
|
187
|
+
: unresolvedElements;
|
|
188
|
+
const promise = runEnsureRegistered(loadElements, tags, backend, { doc, whenDefinedTimeoutMs: timeoutMs }, timeoutMs, loadTimeoutMs);
|
|
189
|
+
aliasInFlightRequest(originalDedupKey, promise);
|
|
190
|
+
aliasInFlightRequest(dedupKey, promise);
|
|
171
191
|
try {
|
|
172
192
|
await promise;
|
|
173
193
|
}
|
|
174
194
|
finally {
|
|
175
|
-
|
|
176
|
-
inFlightRequests.
|
|
195
|
+
for (const key of inFlightKeysByPromise.get(promise) ?? []) {
|
|
196
|
+
if (inFlightRequests.get(key) === promise)
|
|
197
|
+
inFlightRequests.delete(key);
|
|
177
198
|
}
|
|
199
|
+
inFlightKeysByPromise.delete(promise);
|
|
178
200
|
}
|
|
179
201
|
}
|
|
202
|
+
function aliasInFlightRequest(key, promise) {
|
|
203
|
+
inFlightRequests.set(key, promise);
|
|
204
|
+
const keys = inFlightKeysByPromise.get(promise) ?? new Set();
|
|
205
|
+
keys.add(key);
|
|
206
|
+
inFlightKeysByPromise.set(promise, keys);
|
|
207
|
+
}
|
|
180
208
|
async function runEnsureRegistered(elements, tags, backend, context, timeoutMs, loadTimeoutMs) {
|
|
181
209
|
let adapterError;
|
|
182
210
|
try {
|
|
@@ -273,13 +301,13 @@ function resolveBackend(backend) {
|
|
|
273
301
|
if (isDirectBackend(backend))
|
|
274
302
|
return backend;
|
|
275
303
|
if ("kind" in backend) {
|
|
304
|
+
const backendKey = backendKeyOf(backend);
|
|
305
|
+
const override = resolvedBackendOverrides.get(backendKey);
|
|
306
|
+
if (override)
|
|
307
|
+
return override;
|
|
276
308
|
if (backend.kind === "iife")
|
|
277
309
|
return createIifeBackend(backend);
|
|
278
310
|
if (backend.kind === "esm") {
|
|
279
|
-
const backendKey = backendKeyOf(backend);
|
|
280
|
-
const override = resolvedBackendOverrides.get(backendKey);
|
|
281
|
-
if (override)
|
|
282
|
-
return override;
|
|
283
311
|
const existing = resolvedEsmBackends.get(backendKey);
|
|
284
312
|
if (existing)
|
|
285
313
|
return existing;
|
|
@@ -415,8 +443,32 @@ function getOpaqueObjectRefId(value) {
|
|
|
415
443
|
}
|
|
416
444
|
return id;
|
|
417
445
|
}
|
|
418
|
-
function
|
|
419
|
-
return
|
|
446
|
+
function isIifeConfig(backend) {
|
|
447
|
+
return !isDirectBackend(backend) && "kind" in backend && backend.kind === "iife";
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* An IIFE bundle URL names its packages, so the full map keeps it the URL a
|
|
451
|
+
* host already fetched. Two specs of one package cannot share a bundle, so a
|
|
452
|
+
* conflict narrows the map: first to the unresolved tags, then to the undefined.
|
|
453
|
+
*/
|
|
454
|
+
function iifeLoadElements(elements, unresolvedElements) {
|
|
455
|
+
if (!findIifeElementMapConflict(elements))
|
|
456
|
+
return elements;
|
|
457
|
+
if (!findIifeElementMapConflict(unresolvedElements))
|
|
458
|
+
return unresolvedElements;
|
|
459
|
+
return Object.fromEntries(Object.entries(unresolvedElements).filter(([tag]) => !isRegistered(tag)));
|
|
460
|
+
}
|
|
461
|
+
function backendLoadsControllers(backend) {
|
|
462
|
+
if (isDirectBackend(backend) || !("kind" in backend))
|
|
463
|
+
return false;
|
|
464
|
+
if (backend.kind === "iife")
|
|
465
|
+
return backend.needsControllers ?? true;
|
|
466
|
+
if (backend.kind === "esm")
|
|
467
|
+
return backend.loadControllers ?? true;
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
function awaitsController(tag) {
|
|
471
|
+
return typeof window !== "undefined" && !pieRegistry()[tag]?.controller;
|
|
420
472
|
}
|
|
421
473
|
function isRegistered(tag) {
|
|
422
474
|
if (typeof customElements === "undefined")
|
|
@@ -471,6 +523,7 @@ function extractAdapterReason(err, tag) {
|
|
|
471
523
|
export const __testing = {
|
|
472
524
|
resetDedupState() {
|
|
473
525
|
inFlightRequests.clear();
|
|
526
|
+
inFlightKeysByPromise.clear();
|
|
474
527
|
resolvedEsmBackends.clear();
|
|
475
528
|
resolvedBackendOverrides.clear();
|
|
476
529
|
clearPackageMetadataRequests();
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { InstrumentationProvider } from "../instrumentation/types.js";
|
|
22
22
|
import { type IifeBundleRetryConfig } from "../loader-config.js";
|
|
23
23
|
import { BundleType } from "../pie/types.js";
|
|
24
|
+
import type { ElementMap } from "./ElementLoader.js";
|
|
24
25
|
import { type ElementLoaderBackend } from "./element-loader-types.js";
|
|
25
26
|
/**
|
|
26
27
|
* Bundle-build retry status emitted while a bundle is being built or after
|
|
@@ -77,4 +78,10 @@ export type IifeBackendConfig = {
|
|
|
77
78
|
instrumentationProvider?: InstrumentationProvider;
|
|
78
79
|
};
|
|
79
80
|
export type IifeBackend = ElementLoaderBackend & {};
|
|
81
|
+
/**
|
|
82
|
+
* The legacy IIFE bundle global is keyed by unversioned package name. Two
|
|
83
|
+
* distinct specs for one package therefore cannot coexist in a single bundle:
|
|
84
|
+
* one constructor would silently be reused for both requested versioned tags.
|
|
85
|
+
*/
|
|
86
|
+
export declare function findIifeElementMapConflict(elements: ElementMap): string | undefined;
|
|
80
87
|
export declare function createIifeBackend(config: IifeBackendConfig): IifeBackend;
|
|
@@ -31,7 +31,7 @@ import { AdapterFailure, } from "./element-loader-types.js";
|
|
|
31
31
|
* distinct specs for one package therefore cannot coexist in a single bundle:
|
|
32
32
|
* one constructor would silently be reused for both requested versioned tags.
|
|
33
33
|
*/
|
|
34
|
-
function findIifeElementMapConflict(elements) {
|
|
34
|
+
export function findIifeElementMapConflict(elements) {
|
|
35
35
|
const specsByPackageName = new Map();
|
|
36
36
|
for (const packageSpecValue of Object.values(elements)) {
|
|
37
37
|
const packageSpec = String(packageSpecValue);
|
|
@@ -30,11 +30,11 @@ export function validateExternalStyleUrl(url, options = {}) {
|
|
|
30
30
|
? new URL(url, options.baseUrl)
|
|
31
31
|
: new URL(url);
|
|
32
32
|
}
|
|
33
|
-
catch
|
|
33
|
+
catch {
|
|
34
34
|
return {
|
|
35
35
|
ok: false,
|
|
36
36
|
reason: "invalid-url",
|
|
37
|
-
message:
|
|
37
|
+
message: "External stylesheet URL could not be parsed.",
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
if (resolvedUrl.protocol !== "http:" && resolvedUrl.protocol !== "https:") {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { FormativeDeliveryPolicy, FormativeItemPolicy, FormativeSectionSlice } from "../formative/types.js";
|
|
12
12
|
import type { TimedMediaSectionData, TimedMediaSectionSessionSlice } from "../timed-media/types.js";
|
|
13
|
+
import type { AdaptationLink, LanguagePolicy } from "./interchange.js";
|
|
13
14
|
export type { FormativeDeliveryPolicy, FormativeItemPolicy };
|
|
14
15
|
export type { TimedMediaSectionData };
|
|
16
|
+
export type { AdaptationLink, ContentSubstitution, LanguagePolicy, ServedContentIdentity, } from "./interchange.js";
|
|
15
17
|
export interface BaseEntity {
|
|
16
18
|
id?: string;
|
|
17
19
|
createdAt?: Date;
|
|
@@ -32,12 +34,6 @@ export interface VersionEntity extends BaseEntity {
|
|
|
32
34
|
signature?: string;
|
|
33
35
|
version: SemVer;
|
|
34
36
|
}
|
|
35
|
-
export interface SearchMetaData {
|
|
36
|
-
[key: string]: any;
|
|
37
|
-
}
|
|
38
|
-
export interface SearchMetaDataEntity extends BaseEntity {
|
|
39
|
-
searchMetaData?: SearchMetaData;
|
|
40
|
-
}
|
|
41
37
|
export interface ConfigElements {
|
|
42
38
|
[key: string]: string;
|
|
43
39
|
}
|
|
@@ -69,16 +65,29 @@ export interface ConfigEntity {
|
|
|
69
65
|
export interface ConfigContainerEntity extends BaseEntity {
|
|
70
66
|
config: ConfigEntity;
|
|
71
67
|
}
|
|
72
|
-
export interface PassageEntity extends VersionEntity, ConfigContainerEntity
|
|
68
|
+
export interface PassageEntity extends VersionEntity, ConfigContainerEntity {
|
|
73
69
|
name: string;
|
|
70
|
+
/**
|
|
71
|
+
* BCP 47 tag of the content's language (QTI `xml:lang`). The players render
|
|
72
|
+
* and score it under this language; absent, the section's `env.locale` holds.
|
|
73
|
+
*/
|
|
74
|
+
language?: string;
|
|
75
|
+
/** The source a translated version was adapted from. No player reads it. */
|
|
76
|
+
isAdaptationOf?: AdaptationLink;
|
|
74
77
|
/** QTI/APIP-style accessibility catalogs owned by this passage content. */
|
|
75
78
|
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
76
79
|
retired?: boolean;
|
|
77
80
|
published?: boolean;
|
|
78
|
-
highlights?: Record<string, string[]>;
|
|
79
81
|
}
|
|
80
|
-
export interface ItemEntity extends VersionEntity, ConfigContainerEntity
|
|
82
|
+
export interface ItemEntity extends VersionEntity, ConfigContainerEntity {
|
|
81
83
|
name?: string;
|
|
84
|
+
/**
|
|
85
|
+
* BCP 47 tag of the content's language (QTI `xml:lang`). The players render
|
|
86
|
+
* and score it under this language; absent, the section's `env.locale` holds.
|
|
87
|
+
*/
|
|
88
|
+
language?: string;
|
|
89
|
+
/** The source a translated version was adapted from. No player reads it. */
|
|
90
|
+
isAdaptationOf?: AdaptationLink;
|
|
82
91
|
/**
|
|
83
92
|
* `null` as well as absent, because importers write it: JSON has no
|
|
84
93
|
* `undefined`, so an item transformed from another format carries an
|
|
@@ -90,7 +99,6 @@ export interface ItemEntity extends VersionEntity, ConfigContainerEntity, Search
|
|
|
90
99
|
retired?: boolean;
|
|
91
100
|
published?: boolean;
|
|
92
101
|
configuration?: any;
|
|
93
|
-
highlights?: Record<string, string[]>;
|
|
94
102
|
}
|
|
95
103
|
/**
|
|
96
104
|
* `baseId` and `version` are optional on an entity a section references. They
|
|
@@ -109,20 +117,6 @@ export type ReferencedPassageEntity = Omit<WithOptionalVersion<PassageEntity>, "
|
|
|
109
117
|
export type ReferencedItemEntity = Omit<WithOptionalVersion<ItemEntity>, "passage"> & {
|
|
110
118
|
passage?: string | ReferencedPassageEntity | null;
|
|
111
119
|
};
|
|
112
|
-
/**
|
|
113
|
-
* Metadata specifically for interpretation by clients, typically containing
|
|
114
|
-
* options that are relevant for the user interface. This is not indexed for search.
|
|
115
|
-
*/
|
|
116
|
-
export interface SettingsMetaData {
|
|
117
|
-
[key: string]: any;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Objects that contain metadata in the settings property, which is NOT indexed for search
|
|
121
|
-
* but typically only relevant for particular client cases.
|
|
122
|
-
*/
|
|
123
|
-
export interface SettingsMetaDataEntity {
|
|
124
|
-
settings?: SettingsMetaData;
|
|
125
|
-
}
|
|
126
120
|
export type RubricBlockView = "author" | "candidate" | "proctor" | "scorer" | "testConstructor" | "tutor";
|
|
127
121
|
export interface RubricBlock {
|
|
128
122
|
identifier?: string;
|
|
@@ -150,7 +144,7 @@ export interface RubricBlock {
|
|
|
150
144
|
* References an item within a section. The item property contains the resolved
|
|
151
145
|
* ItemEntity with its PIE config.
|
|
152
146
|
*/
|
|
153
|
-
export interface AssessmentItemRef extends
|
|
147
|
+
export interface AssessmentItemRef extends BaseEntity {
|
|
154
148
|
id?: string;
|
|
155
149
|
identifier: string;
|
|
156
150
|
title?: string;
|
|
@@ -175,7 +169,7 @@ export interface AssessmentItemRef extends SearchMetaDataEntity {
|
|
|
175
169
|
*/
|
|
176
170
|
formative?: FormativeItemPolicy;
|
|
177
171
|
}
|
|
178
|
-
export interface AssessmentSection extends
|
|
172
|
+
export interface AssessmentSection extends BaseEntity {
|
|
179
173
|
id?: string;
|
|
180
174
|
identifier: string;
|
|
181
175
|
title?: string;
|
|
@@ -484,7 +478,7 @@ export interface PersonalNeedsProfile {
|
|
|
484
478
|
*/
|
|
485
479
|
toolParameters?: ToolParameters;
|
|
486
480
|
}
|
|
487
|
-
export interface AssessmentEntity extends BaseEntity
|
|
481
|
+
export interface AssessmentEntity extends BaseEntity {
|
|
488
482
|
name?: string;
|
|
489
483
|
title?: string;
|
|
490
484
|
identifier?: string;
|
|
@@ -499,6 +493,11 @@ export interface AssessmentEntity extends BaseEntity, SearchMetaDataEntity {
|
|
|
499
493
|
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
500
494
|
/** QTI 3.0: Personal Needs Profile (PNP 3.0) */
|
|
501
495
|
personalNeedsProfile?: PersonalNeedsProfile;
|
|
496
|
+
/**
|
|
497
|
+
* The languages the assessment is offered in, read by a backend that
|
|
498
|
+
* resolves language variants. No player reads it.
|
|
499
|
+
*/
|
|
500
|
+
languagePolicy?: LanguagePolicy;
|
|
502
501
|
/**
|
|
503
502
|
* QTI 3.0: testParts structure (authoritative for QTI format).
|
|
504
503
|
*/
|
|
@@ -548,6 +547,13 @@ export interface ToolParameterMap {
|
|
|
548
547
|
signLanguage: {
|
|
549
548
|
signLang?: string;
|
|
550
549
|
};
|
|
550
|
+
/**
|
|
551
|
+
* `language` is the BCP 47 content language the learner reads items in. A
|
|
552
|
+
* host passes it to delivery as `requestedLanguages`.
|
|
553
|
+
*/
|
|
554
|
+
itemTranslation: {
|
|
555
|
+
language?: string;
|
|
556
|
+
};
|
|
551
557
|
/**
|
|
552
558
|
* `rate` multiplies every playback speed, 1 being the provider's normal
|
|
553
559
|
* speed; `pitch` runs 0–2 with 1 normal; `volume` runs 0–1.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-variant interchange types (PIE-1393). Backends and authoring tools
|
|
3
|
+
* exchange them; no player reads `AdaptationLink` or `LanguagePolicy`.
|
|
4
|
+
*
|
|
5
|
+
* A language variant is its own item or passage version that declares
|
|
6
|
+
* `language` and, when it is a translation, `isAdaptationOf`. Its family is
|
|
7
|
+
* `isAdaptationOf.baseId ?? baseId`.
|
|
8
|
+
*/
|
|
9
|
+
import type { SemVer } from "./index.js";
|
|
10
|
+
/** The source a translated item or passage was adapted from. */
|
|
11
|
+
export interface AdaptationLink {
|
|
12
|
+
baseId: string;
|
|
13
|
+
/** The source version translated. Absent when provenance is unknown. */
|
|
14
|
+
version?: SemVer;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The languages an assessment offers and what a backend serves when a family
|
|
18
|
+
* has no variant in the requested language.
|
|
19
|
+
*/
|
|
20
|
+
export interface LanguagePolicy {
|
|
21
|
+
/** BCP 47 tags the assessment may be served in. */
|
|
22
|
+
languages: string[];
|
|
23
|
+
/**
|
|
24
|
+
* `"default"` serves an item whose family lacks the requested language in
|
|
25
|
+
* the closest match for the platform default, `en-US` (so `en` or `en-GB`),
|
|
26
|
+
* then its content that declares no language, then its source. Fallback is
|
|
27
|
+
* per item; items sharing a passage fall back together. `"none"` fails the
|
|
28
|
+
* load.
|
|
29
|
+
*/
|
|
30
|
+
fallback: "default" | "none";
|
|
31
|
+
/**
|
|
32
|
+
* `"closest"`, the default, serves the nearest variant in the requested
|
|
33
|
+
* language, so `es-MX` reaches `es` or `es-ES`; `"exact"` serves only an
|
|
34
|
+
* equal tag and leaves anything else to `fallback`.
|
|
35
|
+
*/
|
|
36
|
+
matching?: "closest" | "exact";
|
|
37
|
+
/**
|
|
38
|
+
* `"serve"` serves a variant whose source has a newer release and records
|
|
39
|
+
* it as stale; `"require-current"` treats that variant as missing.
|
|
40
|
+
*/
|
|
41
|
+
staleness: "serve" | "require-current";
|
|
42
|
+
}
|
|
43
|
+
/** Why the served content differs from what was requested. */
|
|
44
|
+
export type ContentSubstitution = "fallback" | "stale";
|
|
45
|
+
/** The content version a backend served, and the language it served it in. */
|
|
46
|
+
export interface ServedContentIdentity {
|
|
47
|
+
baseId: string;
|
|
48
|
+
version?: SemVer;
|
|
49
|
+
/** The family key, when the served version belongs to a variant family. */
|
|
50
|
+
family?: string;
|
|
51
|
+
/** BCP 47 tag of the served content. */
|
|
52
|
+
servedLanguage?: string;
|
|
53
|
+
substitution?: ContentSubstitution;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/focus-trap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectFocusable } from "./first-focusable.js";
|
|
2
|
+
import { isComposingKeyEvent } from "./ime.js";
|
|
2
3
|
/**
|
|
3
4
|
* Shadow-aware: a trapped panel almost always hosts a custom element rendering into
|
|
4
5
|
* `shadow: "open"`, and a flat `querySelectorAll` would collect only the panel's own
|
|
@@ -44,7 +45,8 @@ export function createFocusTrap(container, options = {}) {
|
|
|
44
45
|
const wrap = options.wrap ?? true;
|
|
45
46
|
const onKeydown = (event) => {
|
|
46
47
|
if (event.key === "Escape") {
|
|
47
|
-
|
|
48
|
+
// An Escape that cancels an IME composition belongs to the IME.
|
|
49
|
+
if (!options.onEscape || isComposingKeyEvent(event))
|
|
48
50
|
return;
|
|
49
51
|
// Claimed, so a page-wide Escape listener does not also act on it.
|
|
50
52
|
event.preventDefault();
|
package/dist/ui/ime.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a keydown belongs to an IME composition, so it must not act as a
|
|
3
|
+
* shortcut. Safari reports `isComposing` false on the Enter that commits a
|
|
4
|
+
* composition and marks it only with `keyCode` 229.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isComposingKeyEvent(event: Pick<KeyboardEvent, "isComposing" | "keyCode">): boolean;
|
package/dist/ui/ime.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The legacy `keyCode` a browser reports for a keydown the IME consumes. */
|
|
2
|
+
const IME_PROCESS_KEY_CODE = 229;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a keydown belongs to an IME composition, so it must not act as a
|
|
5
|
+
* shortcut. Safari reports `isComposing` false on the Enter that commits a
|
|
6
|
+
* composition and marks it only with `keyCode` 229.
|
|
7
|
+
*/
|
|
8
|
+
export function isComposingKeyEvent(event) {
|
|
9
|
+
return event.isComposing || event.keyCode === IME_PROCESS_KEY_CODE;
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-players-shared",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared runtime + UI utilities for PIE players",
|
|
6
6
|
"license": "MIT",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"dist"
|
|
107
107
|
],
|
|
108
108
|
"dependencies": {
|
|
109
|
-
"@pie-element/shared-utils": "0.1.
|
|
109
|
+
"@pie-element/shared-utils": "0.1.6",
|
|
110
110
|
"@pie-lib/math-rendering-module": "6.0.0",
|
|
111
111
|
"dompurify": "^3.4.17",
|
|
112
112
|
"semver": "^7.8.5"
|
|
@@ -122,9 +122,9 @@
|
|
|
122
122
|
"scan-hardcoded": "bun run src/i18n/scripts/scan-hardcoded.ts"
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
|
-
"@biomejs/biome": "^2.
|
|
126
|
-
"@happy-dom/global-registrator": "^
|
|
127
|
-
"@pie-element/shared-math-rendering-mathjax": "0.1.
|
|
125
|
+
"@biomejs/biome": "^2.6.2",
|
|
126
|
+
"@happy-dom/global-registrator": "^21.0.4",
|
|
127
|
+
"@pie-element/shared-math-rendering-mathjax": "0.1.8",
|
|
128
128
|
"@playwright/test": "^1.64.1",
|
|
129
129
|
"@types/semver": "^7.8.0",
|
|
130
130
|
"es-module-shims": "^2.8.4",
|