@worthy-ventures/metaglotta-runtime 1.0.0

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.
Files changed (57) hide show
  1. package/README.md +44 -0
  2. package/dist/backend.d.ts +19 -0
  3. package/dist/backend.js +77 -0
  4. package/dist/backend.js.map +1 -0
  5. package/dist/cache.d.ts +36 -0
  6. package/dist/cache.js +77 -0
  7. package/dist/cache.js.map +1 -0
  8. package/dist/events.d.ts +6 -0
  9. package/dist/events.js +74 -0
  10. package/dist/events.js.map +1 -0
  11. package/dist/format.d.ts +9 -0
  12. package/dist/format.js +60 -0
  13. package/dist/format.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.js +19 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/instance.d.ts +38 -0
  18. package/dist/instance.js +223 -0
  19. package/dist/instance.js.map +1 -0
  20. package/dist/props.d.ts +5 -0
  21. package/dist/props.js +60 -0
  22. package/dist/props.js.map +1 -0
  23. package/dist/types.d.ts +168 -0
  24. package/dist/types.js +9 -0
  25. package/dist/types.js.map +1 -0
  26. package/dist-esm/backend.js +72 -0
  27. package/dist-esm/backend.js.map +1 -0
  28. package/dist-esm/cache.js +72 -0
  29. package/dist-esm/cache.js.map +1 -0
  30. package/dist-esm/events.js +71 -0
  31. package/dist-esm/events.js.map +1 -0
  32. package/dist-esm/format.js +55 -0
  33. package/dist-esm/format.js.map +1 -0
  34. package/dist-esm/index.js +6 -0
  35. package/dist-esm/index.js.map +1 -0
  36. package/dist-esm/instance.js +219 -0
  37. package/dist-esm/instance.js.map +1 -0
  38. package/dist-esm/package.json +1 -0
  39. package/dist-esm/props.js +55 -0
  40. package/dist-esm/props.js.map +1 -0
  41. package/dist-esm/types.js +8 -0
  42. package/dist-esm/types.js.map +1 -0
  43. package/package.json +37 -0
  44. package/src/backend.test.ts +117 -0
  45. package/src/backend.ts +81 -0
  46. package/src/cache.test.ts +99 -0
  47. package/src/cache.ts +80 -0
  48. package/src/events.test.ts +118 -0
  49. package/src/events.ts +77 -0
  50. package/src/format.test.ts +92 -0
  51. package/src/format.ts +59 -0
  52. package/src/index.ts +25 -0
  53. package/src/instance.test.ts +645 -0
  54. package/src/instance.ts +257 -0
  55. package/src/props.test.ts +79 -0
  56. package/src/props.ts +62 -0
  57. package/src/types.ts +165 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Which events mean "what is on screen may now be wrong".
3
+ *
4
+ * Cache writes are batched. A load writes one record per (language, namespace), and emitting
5
+ * an update for each would re-translate the page six times for one load, so they are queued
6
+ * and flushed on the next tick. A language change is not batched: it is a single event and
7
+ * the page should not wait a tick to reflect it. An error is not an update at all - nothing
8
+ * on screen changed because a file failed to load.
9
+ */
10
+ const FEEDS_UPDATE = {
11
+ language: 'now',
12
+ initialLoad: 'now',
13
+ cache: 'batched',
14
+ };
15
+ export function createEvents() {
16
+ const handlers = new Map();
17
+ let queued = [];
18
+ let flushScheduled = false;
19
+ function listeners(name) {
20
+ const existing = handlers.get(name);
21
+ if (existing)
22
+ return existing;
23
+ const created = [];
24
+ handlers.set(name, created);
25
+ return created;
26
+ }
27
+ function deliver(name, payload) {
28
+ // Copied before iterating: a handler that unsubscribes itself would otherwise make
29
+ // the loop skip the handler after it.
30
+ for (const handler of [...listeners(name)])
31
+ handler(payload);
32
+ }
33
+ function flush() {
34
+ flushScheduled = false;
35
+ if (!queued.length)
36
+ return;
37
+ const batch = queued;
38
+ queued = [];
39
+ deliver('update', batch);
40
+ }
41
+ return {
42
+ on(name, handler) {
43
+ const list = listeners(name);
44
+ list.push(handler);
45
+ return {
46
+ unsubscribe() {
47
+ const at = list.indexOf(handler);
48
+ if (at >= 0)
49
+ list.splice(at, 1);
50
+ },
51
+ };
52
+ },
53
+ emit(name, value) {
54
+ const event = { type: name, value };
55
+ deliver(name, event);
56
+ const update = FEEDS_UPDATE[name];
57
+ if (!update)
58
+ return;
59
+ queued.push(event);
60
+ if (update === 'now') {
61
+ flush();
62
+ return;
63
+ }
64
+ if (!flushScheduled) {
65
+ flushScheduled = true;
66
+ setTimeout(flush, 0);
67
+ }
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,YAAY,GAAkD;IAChE,QAAQ,EAAE,KAAK;IACf,WAAW,EAAE,KAAK;IAClB,KAAK,EAAE,SAAS;CACnB,CAAC;AAEF,MAAM,UAAU,YAAY;IACxB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAsC,CAAC;IAC/D,IAAI,MAAM,GAAgB,EAAE,CAAC;IAC7B,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,SAAS,SAAS,CAAC,IAAY;QAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,OAAO,GAA+B,EAAE,CAAC;QAC/C,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5B,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,SAAS,OAAO,CAAC,IAAY,EAAE,OAAgB;QAC3C,mFAAmF;QACnF,sCAAsC;QACtC,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAAG,OAAoC,CAAC,OAAO,CAAC,CAAC;IAC/F,CAAC;IAED,SAAS,KAAK;QACV,cAAc,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO;QAC3B,MAAM,KAAK,GAAG,MAAM,CAAC;QACrB,MAAM,GAAG,EAAE,CAAC;QACZ,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO;QACH,EAAE,CAAC,IAA0B,EAAE,OAA+B;YAC1D,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO;gBACH,WAAW;oBACP,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;oBACjC,IAAI,EAAE,IAAI,CAAC;wBAAE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;gBACpC,CAAC;aACJ,CAAC;QACN,CAAC;QAED,IAAI,CAAC,IAAe,EAAE,KAAc;YAChC,MAAM,KAAK,GAAc,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;YAC/C,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAErB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM;gBAAE,OAAO;YAEpB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACnB,KAAK,EAAE,CAAC;gBACR,OAAO;YACX,CAAC;YACD,IAAI,CAAC,cAAc,EAAE,CAAC;gBAClB,cAAc,GAAG,IAAI,CAAC;gBACtB,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACzB,CAAC;QACL,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The NAMED import, not the default one.
3
+ *
4
+ * intl-messageformat ships CommonJS with an ESM wrapper, and under Node's ESM interop the
5
+ * default export resolves to the whole module namespace rather than to the class - so
6
+ * `new IntlMessageFormat(...)` throws "is not a constructor", and every string with a
7
+ * placeholder in it renders as the format-error text. Bundlers and jest both take the
8
+ * CommonJS path, where the default DOES work, so nothing catches this until it is running
9
+ * somewhere real. The named export is the class in both.
10
+ */
11
+ import { IntlMessageFormat } from 'intl-messageformat';
12
+ /**
13
+ * ICU MessageFormat, over FormatJS directly rather than through a wrapper.
14
+ *
15
+ * Compiled formatters are cached by (language, message). Parsing an ICU message is the
16
+ * expensive half, and the message text is what varies least: the same string is formatted
17
+ * once per render of every component that shows it, with different parameters each time.
18
+ */
19
+ const compiled = new Map();
20
+ /**
21
+ * Strings that cannot mean anything but themselves.
22
+ *
23
+ * Most translations are plain text, and running the parser over them is pure cost. Only two
24
+ * characters can make a string non-literal: `{` opens a placeholder, and `'` is ICU's escape
25
+ * character - `'{'` renders a literal brace and `''` renders one apostrophe, so `It''s` would
26
+ * render wrongly if this shortcut took it. `#` is special only inside a plural, which needs a
27
+ * `{` to open.
28
+ */
29
+ const LITERAL = /^[^{']*$/;
30
+ export function formatIcu({ translation, language, params }) {
31
+ if (LITERAL.test(translation))
32
+ return translation;
33
+ const cacheKey = `${language} ${translation}`;
34
+ let formatter = compiled.get(cacheKey);
35
+ if (!formatter) {
36
+ formatter = new IntlMessageFormat(translation, language);
37
+ compiled.set(cacheKey, formatter);
38
+ }
39
+ const formatted = formatter.format(params);
40
+ // format() returns an array for messages with rich-text tags. Nothing here uses those,
41
+ // but joining beats rendering "[object Object]" if one ever appears.
42
+ return Array.isArray(formatted) ? formatted.join('') : String(formatted);
43
+ }
44
+ /** Test seam: the cache is a module global by design, and lives as long as the page. */
45
+ export function forgetCompiled() {
46
+ compiled.clear();
47
+ }
48
+ export function resolveFormat(format) {
49
+ if (typeof format === 'function')
50
+ return format;
51
+ if (format === 'none')
52
+ return ({ translation }) => translation;
53
+ return formatIcu;
54
+ }
55
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;;;GAMG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;AAEtD;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG,UAAU,CAAC;AAE3B,MAAM,UAAU,SAAS,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAuE;IAC5H,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;QAAE,OAAO,WAAW,CAAC;IAElD,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,WAAW,EAAE,CAAC;IAC9C,IAAI,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC,SAAS,EAAE,CAAC;QACb,SAAS,GAAG,IAAI,iBAAiB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACzD,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,MAAqD,CAAC,CAAC;IAC1F,uFAAuF;IACvF,qEAAqE;IACrE,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,cAAc;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAA6C;IACvE,IAAI,OAAO,MAAM,KAAK,UAAU;QAAE,OAAO,MAAM,CAAC;IAChD,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,WAAW,CAAC;IAC/D,OAAO,SAAS,CAAC;AACrB,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { Metaglotta } from './instance.js';
2
+ export { RecordLoadError } from './backend.js';
3
+ export { getTranslateProps, asList, unique } from './props.js';
4
+ export { flatten, recordKey } from './cache.js';
5
+ export { formatIcu, forgetCompiled, resolveFormat } from './format.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,219 @@
1
+ import { createBackend, RecordLoadError } from './backend.js';
2
+ import { createCache } from './cache.js';
3
+ import { createEvents } from './events.js';
4
+ import { resolveFormat } from './format.js';
5
+ import { asList, getTranslateProps, unique } from './props.js';
6
+ const DEFAULT_FORMAT_ERROR = 'invalid';
7
+ /**
8
+ * The Metaglotta translation runtime.
9
+ *
10
+ * Everything the eleven applications do in production: hold translations per language and
11
+ * namespace, fetch the ones they need from the CDN, fall back through namespaces and
12
+ * languages when a key is missing, format ICU, and say when any of that changed.
13
+ *
14
+ * What it deliberately does not do is in-context editing. That needs the page's rendered text
15
+ * traced back to its key, which means wrapping every string and watching the DOM - a
16
+ * different job, only wanted while authoring, and it stays where it is until it is rewritten
17
+ * too. Both runtimes satisfy I18nInstance, so a bootstrap picks one per environment.
18
+ */
19
+ export function Metaglotta(options) {
20
+ const events = createEvents();
21
+ const cache = createCache();
22
+ const format = resolveFormat(options.format);
23
+ const backend = options.backend ? createBackend(options.backend) : undefined;
24
+ const onMissing = options.onMissing ?? ((props) => props.key);
25
+ // Identity in production: there is nothing to decorate a string with, and a call per
26
+ // rendered string is worth not making.
27
+ const decorate = options.decorate ?? ((result) => result);
28
+ /** Namespaces a subscriber is currently holding open, by how many hold each. */
29
+ const active = new Map();
30
+ /**
31
+ * Loads in flight, by record.
32
+ *
33
+ * Every pipe instance activates its namespace on subscribe - some thousands of them on a
34
+ * first render - and without this each would start its own fetch of the same file.
35
+ */
36
+ const inFlight = new Map();
37
+ let language = options.language;
38
+ /** The language a change is on its way to, so a slower earlier change cannot win. */
39
+ let pending = options.language;
40
+ let running = false;
41
+ let runPromise;
42
+ for (const [key, data] of Object.entries(options.staticData ?? {})) {
43
+ const [first, ...rest] = key.split(':');
44
+ cache.set({ language: first, namespace: rest.join(':') }, data);
45
+ }
46
+ const defaultNamespace = () => options.defaultNs ?? options.ns?.[0] ?? '';
47
+ /** Where a lookup looks, in order: the one asked for, then the configured fallbacks. */
48
+ const lookupNamespaces = (ns) => unique([...asList(ns ?? defaultNamespace()), ...asList(options.fallbackNs)]);
49
+ /** Everything that must be in memory: the fallbacks plus whatever is being held open. */
50
+ const requiredNamespaces = (ns) => unique([...asList(ns ?? defaultNamespace()), defaultNamespace(), ...(options.ns ?? []), ...asList(options.fallbackNs), ...active.keys()]);
51
+ const languagesFor = (requested) => {
52
+ const from = requested || language;
53
+ if (!from)
54
+ return [];
55
+ return unique([from, ...fallbacksOf(from, options.fallbackLanguage)]);
56
+ };
57
+ const descriptors = (forLanguage, ns) => languagesFor(forLanguage).flatMap(lang => requiredNamespaces(ns).map(namespace => ({ language: lang, namespace })));
58
+ async function loadOne(descriptor) {
59
+ if (!backend || cache.has(descriptor))
60
+ return;
61
+ const key = `${descriptor.language}:${descriptor.namespace}`;
62
+ const existing = inFlight.get(key);
63
+ if (existing)
64
+ return existing;
65
+ const attempt = backend
66
+ .load(descriptor)
67
+ .then(data => {
68
+ // A backend that returns nothing has nothing to say about this record, which
69
+ // is different from failing: leave the record absent so a later load retries.
70
+ if (data) {
71
+ cache.set(descriptor, data);
72
+ events.emit('cache', descriptor);
73
+ }
74
+ })
75
+ .catch((error) => {
76
+ events.emit('error', error);
77
+ console.error(error);
78
+ if (!backend.fallbackOnFail)
79
+ throw error;
80
+ /**
81
+ * Remembered as empty rather than left absent.
82
+ *
83
+ * Absent means "not loaded", and every one of the thousands of pipes on a
84
+ * page activates its namespace on subscribe - so a namespace whose file 404s
85
+ * would be re-fetched once per pipe, for as long as the page is open. One
86
+ * bad path is a deploy mistake; it should not also be a thundering herd.
87
+ */
88
+ cache.set(descriptor, {});
89
+ })
90
+ .finally(() => inFlight.delete(key));
91
+ inFlight.set(key, attempt);
92
+ return attempt;
93
+ }
94
+ const loadAll = async (records) => {
95
+ await Promise.all(records.map(loadOne));
96
+ };
97
+ function formatted(props, text, forLanguage) {
98
+ try {
99
+ return format({ translation: text, language: forLanguage, params: props.params });
100
+ }
101
+ catch (error) {
102
+ console.error(error);
103
+ const handler = options.onFormatError;
104
+ if (typeof handler === 'string')
105
+ return handler;
106
+ if (typeof handler === 'function')
107
+ return handler(messageOf(error), props);
108
+ return DEFAULT_FORMAT_ERROR;
109
+ }
110
+ }
111
+ const self = {
112
+ t(keyOrProps, ...rest) {
113
+ const props = getTranslateProps(keyOrProps, ...rest);
114
+ const namespaces = lookupNamespaces(props.ns);
115
+ const stored = cache.find(namespaces, languagesFor(props.language), props.key);
116
+ /**
117
+ * Where the string came from, or - with nothing stored - where a new one would go.
118
+ *
119
+ * Only interesting to the decorate pass, and only in the authoring build, but it
120
+ * has to be worked out here: this is the one place that knows which namespace
121
+ * answered, and by the time anybody clicks the rendered text it is unknowable.
122
+ */
123
+ const namespace = stored?.namespace ?? namespaces[0] ?? '';
124
+ /**
125
+ * The missing case, which two call sites depend on being exactly this.
126
+ *
127
+ * With nothing stored and no default, t() ECHOES THE KEY - that is what makes a
128
+ * missing string visible on the page instead of a silent gap, and callers write
129
+ * `instant(k) || fallback` against it. The pipe passes orEmpty and so renders ''.
130
+ */
131
+ const value = stored?.value ?? props.defaultValue;
132
+ if (value === undefined)
133
+ return decorate(props.orEmpty ? '' : onMissing(props), props, namespace);
134
+ const forLanguage = props.language || language;
135
+ if (!value || !forLanguage)
136
+ return decorate(value, props, namespace);
137
+ return decorate(formatted(props, value, forLanguage), props, namespace);
138
+ },
139
+ /**
140
+ * Rewrites one translation in memory.
141
+ *
142
+ * For the in-context dialog: the page shows the new text the moment it is saved,
143
+ * rather than after a reload. Refuses a namespace nothing has loaded, because writing
144
+ * into one would invent a record no element reads and hide the mistake.
145
+ */
146
+ changeTranslation(descriptor, key, value) {
147
+ const written = cache.write({ language: descriptor.language ?? language, namespace: descriptor.namespace ?? defaultNamespace() }, key, value);
148
+ if (written)
149
+ events.emit('cache', descriptor);
150
+ return written;
151
+ },
152
+ on: events.on,
153
+ getLanguage: () => language,
154
+ async changeLanguage(next) {
155
+ if (pending === next && language === next)
156
+ return;
157
+ pending = next;
158
+ if (running)
159
+ await loadAll(descriptors(next));
160
+ // Two changes can be in flight at once, and the one that finishes last is not
161
+ // necessarily the one that was asked for last. Only the latest may apply.
162
+ if (pending !== next)
163
+ return;
164
+ if (language !== next) {
165
+ language = next;
166
+ events.emit('language', next);
167
+ }
168
+ },
169
+ async addActiveNs(ns, forget) {
170
+ if (!forget) {
171
+ // Naming no namespace holds nothing open: the default one is always required
172
+ // anyway, so there is nothing for a reference count to keep alive.
173
+ for (const namespace of asList(ns)) {
174
+ active.set(namespace, (active.get(namespace) ?? 0) + 1);
175
+ }
176
+ }
177
+ if (running)
178
+ await loadAll(descriptors(undefined, ns));
179
+ },
180
+ removeActiveNs(ns) {
181
+ for (const namespace of asList(ns)) {
182
+ const held = active.get(namespace);
183
+ if (held !== undefined && held > 1)
184
+ active.set(namespace, held - 1);
185
+ else
186
+ active.delete(namespace);
187
+ }
188
+ },
189
+ run() {
190
+ if (!running) {
191
+ running = true;
192
+ runPromise = loadAll(descriptors()).then(() => {
193
+ events.emit('initialLoad', undefined);
194
+ });
195
+ }
196
+ return runPromise ?? Promise.resolve();
197
+ },
198
+ stop() {
199
+ running = false;
200
+ },
201
+ };
202
+ return self;
203
+ }
204
+ /** A fallback language list, which may be given per language or for all of them. */
205
+ function fallbacksOf(forLanguage, fallback) {
206
+ if (fallback && typeof fallback === 'object' && !Array.isArray(fallback)) {
207
+ return asList(fallback[forLanguage]);
208
+ }
209
+ return asList(fallback);
210
+ }
211
+ function messageOf(error) {
212
+ if (typeof error === 'string')
213
+ return error;
214
+ if (error instanceof Error)
215
+ return error.message;
216
+ return DEFAULT_FORMAT_ERROR;
217
+ }
218
+ export { RecordLoadError };
219
+ //# sourceMappingURL=instance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance.js","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The four ways t() can be called, reduced to one shape.
3
+ *
4
+ * t('key')
5
+ * t('key', 'Default value')
6
+ * t('key', { name: 'Ada' })
7
+ * t('key', 'Default value', { ns: 'login' })
8
+ * t({ key: 'key', ns: 'login', params: { name: 'Ada' } })
9
+ *
10
+ * The third form is the awkward one: options and parameters share a single object, so
11
+ * anything that is not a known option is a parameter. That is why `ns`, `noWrap`, `orEmpty`
12
+ * and `language` cannot be used as parameter names - a limitation of the calling convention,
13
+ * not of the formatter.
14
+ */
15
+ const OPTION_NAMES = ['ns', 'noWrap', 'orEmpty', 'language'];
16
+ function split(combined) {
17
+ const options = {};
18
+ const params = {};
19
+ for (const [name, value] of Object.entries(combined)) {
20
+ if (OPTION_NAMES.includes(name)) {
21
+ options[name] = value;
22
+ }
23
+ else {
24
+ params[name] = value;
25
+ }
26
+ }
27
+ return { ...options, params };
28
+ }
29
+ export function getTranslateProps(keyOrProps, ...rest) {
30
+ // Already the full shape: nothing to work out.
31
+ if (typeof keyOrProps === 'object' && keyOrProps !== null)
32
+ return keyOrProps;
33
+ const props = { key: keyOrProps };
34
+ let combined;
35
+ if (typeof rest[0] === 'string') {
36
+ props.defaultValue = rest[0];
37
+ combined = rest[1];
38
+ }
39
+ else if (typeof rest[0] === 'object' && rest[0] !== null) {
40
+ combined = rest[0];
41
+ }
42
+ return combined ? { ...split(combined), ...props } : props;
43
+ }
44
+ /** A fallback list, however it was written, as an array. */
45
+ export function asList(value) {
46
+ if (typeof value === 'string')
47
+ return [value];
48
+ if (Array.isArray(value))
49
+ return value;
50
+ return [];
51
+ }
52
+ export function unique(values) {
53
+ return [...new Set(values)];
54
+ }
55
+ //# sourceMappingURL=props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.js","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAItE,SAAS,KAAK,CAAC,QAAkB;IAC7B,MAAM,OAAO,GAA4B,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAoB,EAAE,CAAC;IAEnC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnD,IAAK,YAAkC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACpD,OAAmC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACvD,CAAC;aAAM,CAAC;YACJ,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,CAAC;IACL,CAAC;IAED,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAmC,EAAE,GAAG,IAAe;IACrF,+CAA+C;IAC/C,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAE7E,MAAM,KAAK,GAAmB,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;IAClD,IAAI,QAA8B,CAAC;IAEnC,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC9B,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAyB,CAAC;IAC/C,CAAC;SAAM,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACzD,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAa,CAAC;IACnC,CAAC;IAED,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/D,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,MAAM,CAAC,KAAwB;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,OAAO,EAAE,CAAC;AACd,CAAC;AAED,MAAM,UAAU,MAAM,CAAI,MAAW;IACjC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What a caller can pass to t(), and what the runtime hands back.
3
+ *
4
+ * Deliberately small: this is the whole surface the Angular glue and the eleven applications
5
+ * use, verified by counting call sites rather than by reading a feature list.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@worthy-ventures/metaglotta-runtime",
3
+ "version": "1.0.0",
4
+ "description": "The Metaglotta translation runtime: cache, namespace and language fallback, CDN loading and ICU formatting. No framework, no DOM.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "module": "dist-esm/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist-esm/index.js",
13
+ "require": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist/**/*",
19
+ "dist-esm/**/*",
20
+ "src/**/*",
21
+ "README.md"
22
+ ],
23
+ "sideEffects": false,
24
+ "scripts": {
25
+ "build": "npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.esm.json && node -e \"require('fs').writeFileSync('dist-esm/package.json', JSON.stringify({type:'module'}))\"",
26
+ "test": "jest -c jest.config.js",
27
+ "clean": "rimraf dist dist-esm coverage",
28
+ "verify:esm": "node scripts/verify-esm.mjs",
29
+ "verify": "npm run dev:lint && npm test && npm run build && npm run verify:partial-ivy && npm run verify:esm"
30
+ },
31
+ "dependencies": {
32
+ "intl-messageformat": "10.7.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,117 @@
1
+ import { createBackend, RecordLoadError } from './backend.js';
2
+
3
+ const ok = (body: unknown) => ({ ok: true, status: 200, json: async () => body }) as unknown as Response;
4
+ const notFound = () => ({ ok: false, status: 404, json: async () => ({}) }) as unknown as Response;
5
+
6
+ describe('the backend', () => {
7
+ it('builds the path the published layout uses', async () => {
8
+ const asked: string[] = [];
9
+ const backend = createBackend({
10
+ prefix: 'https://cdn.example/langs/lis',
11
+ fetch: (async (url: string) => {
12
+ asked.push(url);
13
+ return ok({ greeting: 'Hello' });
14
+ }) as unknown as typeof fetch,
15
+ });
16
+
17
+ await backend.load({ language: 'en', namespace: 'login' });
18
+
19
+ expect(asked).toEqual(['https://cdn.example/langs/lis/login/en.json']);
20
+ });
21
+
22
+ /** The default namespace has no folder of its own; the file sits at the prefix. */
23
+ it('leaves the default namespace out of the path', async () => {
24
+ const asked: string[] = [];
25
+ const backend = createBackend({
26
+ prefix: 'https://cdn.example/langs/lis/',
27
+ fetch: (async (url: string) => {
28
+ asked.push(url);
29
+ return ok({});
30
+ }) as unknown as typeof fetch,
31
+ });
32
+
33
+ await backend.load({ language: 'el', namespace: '' });
34
+
35
+ expect(asked).toEqual(['https://cdn.example/langs/lis/el.json']);
36
+ });
37
+
38
+ it('takes a path builder of your own, which is how a cache-buster gets in', async () => {
39
+ const asked: string[] = [];
40
+ const backend = createBackend({
41
+ prefix: '/langs',
42
+ getPath: ({ namespace, language, prefix }) => `${prefix}/${namespace}/${language}.json?v=7`,
43
+ fetch: (async (url: string) => {
44
+ asked.push(url);
45
+ return ok({});
46
+ }) as unknown as typeof fetch,
47
+ });
48
+
49
+ await backend.load({ language: 'en', namespace: 'login' });
50
+
51
+ expect(asked).toEqual(['/langs/login/en.json?v=7']);
52
+ });
53
+
54
+ it('returns what the file holds', async () => {
55
+ const backend = createBackend({ fetch: (async () => ok({ greeting: 'Hello' })) as unknown as typeof fetch });
56
+
57
+ expect(await backend.load({ language: 'en', namespace: '' })).toEqual({ greeting: 'Hello' });
58
+ });
59
+
60
+ /**
61
+ * Which record failed has to travel with the failure. The caller decides whether one
62
+ * unreadable file costs a namespace or the whole load, and it cannot decide that from a
63
+ * bare fetch error.
64
+ */
65
+ it('says which record failed, not just that something did', async () => {
66
+ const backend = createBackend({ prefix: '/langs', fetch: (async () => notFound()) as unknown as typeof fetch });
67
+
68
+ const error = await backend.load({ language: 'el', namespace: 'reception' }).catch((e: unknown) => e);
69
+
70
+ expect(error).toBeInstanceOf(RecordLoadError);
71
+ expect((error as RecordLoadError).descriptor).toEqual({ language: 'el', namespace: 'reception' });
72
+ expect((error as RecordLoadError).url).toBe('/langs/reception/el.json');
73
+ expect(String(error)).toContain('reception/el');
74
+ });
75
+
76
+ it('names the default namespace readably when it is the one that failed', async () => {
77
+ const backend = createBackend({ fetch: (async () => notFound()) as unknown as typeof fetch });
78
+
79
+ const error = await backend.load({ language: 'en', namespace: '' }).catch((e: unknown) => e);
80
+
81
+ expect(String(error)).toContain('(default)/en');
82
+ });
83
+
84
+ it('treats a network failure the same as a bad status', async () => {
85
+ const backend = createBackend({
86
+ fetch: (async () => {
87
+ throw new Error('offline');
88
+ }) as unknown as typeof fetch,
89
+ });
90
+
91
+ await expect(backend.load({ language: 'en', namespace: '' })).rejects.toBeInstanceOf(RecordLoadError);
92
+ });
93
+
94
+ /**
95
+ * Without a timeout a hung connection holds the initial load open for as long as the
96
+ * browser's own, which is minutes - and an application that awaits run() waits with it.
97
+ */
98
+ it('gives up on a request that never answers', async () => {
99
+ jest.useFakeTimers();
100
+ try {
101
+ const backend = createBackend({ timeout: 50, fetch: (() => new Promise<Response>(() => undefined)) as unknown as typeof fetch });
102
+ const result = backend.load({ language: 'en', namespace: '' });
103
+ const settled = result.catch((e: unknown) => e);
104
+
105
+ jest.advanceTimersByTime(60);
106
+
107
+ expect(await settled).toBeInstanceOf(RecordLoadError);
108
+ } finally {
109
+ jest.useRealTimers();
110
+ }
111
+ });
112
+
113
+ it('survivability is the caller’s to read, and defaults to survivable', () => {
114
+ expect(createBackend({}).fallbackOnFail).toBe(true);
115
+ expect(createBackend({ fallbackOnFail: false }).fallbackOnFail).toBe(false);
116
+ });
117
+ });