@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,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RecordLoadError = void 0;
4
+ exports.Metaglotta = Metaglotta;
5
+ const backend_js_1 = require("./backend.js");
6
+ Object.defineProperty(exports, "RecordLoadError", { enumerable: true, get: function () { return backend_js_1.RecordLoadError; } });
7
+ const cache_js_1 = require("./cache.js");
8
+ const events_js_1 = require("./events.js");
9
+ const format_js_1 = require("./format.js");
10
+ const props_js_1 = require("./props.js");
11
+ const DEFAULT_FORMAT_ERROR = 'invalid';
12
+ /**
13
+ * The Metaglotta translation runtime.
14
+ *
15
+ * Everything the eleven applications do in production: hold translations per language and
16
+ * namespace, fetch the ones they need from the CDN, fall back through namespaces and
17
+ * languages when a key is missing, format ICU, and say when any of that changed.
18
+ *
19
+ * What it deliberately does not do is in-context editing. That needs the page's rendered text
20
+ * traced back to its key, which means wrapping every string and watching the DOM - a
21
+ * different job, only wanted while authoring, and it stays where it is until it is rewritten
22
+ * too. Both runtimes satisfy I18nInstance, so a bootstrap picks one per environment.
23
+ */
24
+ function Metaglotta(options) {
25
+ const events = (0, events_js_1.createEvents)();
26
+ const cache = (0, cache_js_1.createCache)();
27
+ const format = (0, format_js_1.resolveFormat)(options.format);
28
+ const backend = options.backend ? (0, backend_js_1.createBackend)(options.backend) : undefined;
29
+ const onMissing = options.onMissing ?? ((props) => props.key);
30
+ // Identity in production: there is nothing to decorate a string with, and a call per
31
+ // rendered string is worth not making.
32
+ const decorate = options.decorate ?? ((result) => result);
33
+ /** Namespaces a subscriber is currently holding open, by how many hold each. */
34
+ const active = new Map();
35
+ /**
36
+ * Loads in flight, by record.
37
+ *
38
+ * Every pipe instance activates its namespace on subscribe - some thousands of them on a
39
+ * first render - and without this each would start its own fetch of the same file.
40
+ */
41
+ const inFlight = new Map();
42
+ let language = options.language;
43
+ /** The language a change is on its way to, so a slower earlier change cannot win. */
44
+ let pending = options.language;
45
+ let running = false;
46
+ let runPromise;
47
+ for (const [key, data] of Object.entries(options.staticData ?? {})) {
48
+ const [first, ...rest] = key.split(':');
49
+ cache.set({ language: first, namespace: rest.join(':') }, data);
50
+ }
51
+ const defaultNamespace = () => options.defaultNs ?? options.ns?.[0] ?? '';
52
+ /** Where a lookup looks, in order: the one asked for, then the configured fallbacks. */
53
+ const lookupNamespaces = (ns) => (0, props_js_1.unique)([...(0, props_js_1.asList)(ns ?? defaultNamespace()), ...(0, props_js_1.asList)(options.fallbackNs)]);
54
+ /** Everything that must be in memory: the fallbacks plus whatever is being held open. */
55
+ const requiredNamespaces = (ns) => (0, props_js_1.unique)([...(0, props_js_1.asList)(ns ?? defaultNamespace()), defaultNamespace(), ...(options.ns ?? []), ...(0, props_js_1.asList)(options.fallbackNs), ...active.keys()]);
56
+ const languagesFor = (requested) => {
57
+ const from = requested || language;
58
+ if (!from)
59
+ return [];
60
+ return (0, props_js_1.unique)([from, ...fallbacksOf(from, options.fallbackLanguage)]);
61
+ };
62
+ const descriptors = (forLanguage, ns) => languagesFor(forLanguage).flatMap(lang => requiredNamespaces(ns).map(namespace => ({ language: lang, namespace })));
63
+ async function loadOne(descriptor) {
64
+ if (!backend || cache.has(descriptor))
65
+ return;
66
+ const key = `${descriptor.language}:${descriptor.namespace}`;
67
+ const existing = inFlight.get(key);
68
+ if (existing)
69
+ return existing;
70
+ const attempt = backend
71
+ .load(descriptor)
72
+ .then(data => {
73
+ // A backend that returns nothing has nothing to say about this record, which
74
+ // is different from failing: leave the record absent so a later load retries.
75
+ if (data) {
76
+ cache.set(descriptor, data);
77
+ events.emit('cache', descriptor);
78
+ }
79
+ })
80
+ .catch((error) => {
81
+ events.emit('error', error);
82
+ console.error(error);
83
+ if (!backend.fallbackOnFail)
84
+ throw error;
85
+ /**
86
+ * Remembered as empty rather than left absent.
87
+ *
88
+ * Absent means "not loaded", and every one of the thousands of pipes on a
89
+ * page activates its namespace on subscribe - so a namespace whose file 404s
90
+ * would be re-fetched once per pipe, for as long as the page is open. One
91
+ * bad path is a deploy mistake; it should not also be a thundering herd.
92
+ */
93
+ cache.set(descriptor, {});
94
+ })
95
+ .finally(() => inFlight.delete(key));
96
+ inFlight.set(key, attempt);
97
+ return attempt;
98
+ }
99
+ const loadAll = async (records) => {
100
+ await Promise.all(records.map(loadOne));
101
+ };
102
+ function formatted(props, text, forLanguage) {
103
+ try {
104
+ return format({ translation: text, language: forLanguage, params: props.params });
105
+ }
106
+ catch (error) {
107
+ console.error(error);
108
+ const handler = options.onFormatError;
109
+ if (typeof handler === 'string')
110
+ return handler;
111
+ if (typeof handler === 'function')
112
+ return handler(messageOf(error), props);
113
+ return DEFAULT_FORMAT_ERROR;
114
+ }
115
+ }
116
+ const self = {
117
+ t(keyOrProps, ...rest) {
118
+ const props = (0, props_js_1.getTranslateProps)(keyOrProps, ...rest);
119
+ const namespaces = lookupNamespaces(props.ns);
120
+ const stored = cache.find(namespaces, languagesFor(props.language), props.key);
121
+ /**
122
+ * Where the string came from, or - with nothing stored - where a new one would go.
123
+ *
124
+ * Only interesting to the decorate pass, and only in the authoring build, but it
125
+ * has to be worked out here: this is the one place that knows which namespace
126
+ * answered, and by the time anybody clicks the rendered text it is unknowable.
127
+ */
128
+ const namespace = stored?.namespace ?? namespaces[0] ?? '';
129
+ /**
130
+ * The missing case, which two call sites depend on being exactly this.
131
+ *
132
+ * With nothing stored and no default, t() ECHOES THE KEY - that is what makes a
133
+ * missing string visible on the page instead of a silent gap, and callers write
134
+ * `instant(k) || fallback` against it. The pipe passes orEmpty and so renders ''.
135
+ */
136
+ const value = stored?.value ?? props.defaultValue;
137
+ if (value === undefined)
138
+ return decorate(props.orEmpty ? '' : onMissing(props), props, namespace);
139
+ const forLanguage = props.language || language;
140
+ if (!value || !forLanguage)
141
+ return decorate(value, props, namespace);
142
+ return decorate(formatted(props, value, forLanguage), props, namespace);
143
+ },
144
+ /**
145
+ * Rewrites one translation in memory.
146
+ *
147
+ * For the in-context dialog: the page shows the new text the moment it is saved,
148
+ * rather than after a reload. Refuses a namespace nothing has loaded, because writing
149
+ * into one would invent a record no element reads and hide the mistake.
150
+ */
151
+ changeTranslation(descriptor, key, value) {
152
+ const written = cache.write({ language: descriptor.language ?? language, namespace: descriptor.namespace ?? defaultNamespace() }, key, value);
153
+ if (written)
154
+ events.emit('cache', descriptor);
155
+ return written;
156
+ },
157
+ on: events.on,
158
+ getLanguage: () => language,
159
+ async changeLanguage(next) {
160
+ if (pending === next && language === next)
161
+ return;
162
+ pending = next;
163
+ if (running)
164
+ await loadAll(descriptors(next));
165
+ // Two changes can be in flight at once, and the one that finishes last is not
166
+ // necessarily the one that was asked for last. Only the latest may apply.
167
+ if (pending !== next)
168
+ return;
169
+ if (language !== next) {
170
+ language = next;
171
+ events.emit('language', next);
172
+ }
173
+ },
174
+ async addActiveNs(ns, forget) {
175
+ if (!forget) {
176
+ // Naming no namespace holds nothing open: the default one is always required
177
+ // anyway, so there is nothing for a reference count to keep alive.
178
+ for (const namespace of (0, props_js_1.asList)(ns)) {
179
+ active.set(namespace, (active.get(namespace) ?? 0) + 1);
180
+ }
181
+ }
182
+ if (running)
183
+ await loadAll(descriptors(undefined, ns));
184
+ },
185
+ removeActiveNs(ns) {
186
+ for (const namespace of (0, props_js_1.asList)(ns)) {
187
+ const held = active.get(namespace);
188
+ if (held !== undefined && held > 1)
189
+ active.set(namespace, held - 1);
190
+ else
191
+ active.delete(namespace);
192
+ }
193
+ },
194
+ run() {
195
+ if (!running) {
196
+ running = true;
197
+ runPromise = loadAll(descriptors()).then(() => {
198
+ events.emit('initialLoad', undefined);
199
+ });
200
+ }
201
+ return runPromise ?? Promise.resolve();
202
+ },
203
+ stop() {
204
+ running = false;
205
+ },
206
+ };
207
+ return self;
208
+ }
209
+ /** A fallback language list, which may be given per language or for all of them. */
210
+ function fallbacksOf(forLanguage, fallback) {
211
+ if (fallback && typeof fallback === 'object' && !Array.isArray(fallback)) {
212
+ return (0, props_js_1.asList)(fallback[forLanguage]);
213
+ }
214
+ return (0, props_js_1.asList)(fallback);
215
+ }
216
+ function messageOf(error) {
217
+ if (typeof error === 'string')
218
+ return error;
219
+ if (error instanceof Error)
220
+ return error.message;
221
+ return DEFAULT_FORMAT_ERROR;
222
+ }
223
+ //# 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,5 @@
1
+ import type { NamespaceFallback, TranslateProps } from './types.js';
2
+ export declare function getTranslateProps(keyOrProps: string | TranslateProps, ...rest: unknown[]): TranslateProps;
3
+ /** A fallback list, however it was written, as an array. */
4
+ export declare function asList(value: NamespaceFallback): string[];
5
+ export declare function unique<T>(values: T[]): T[];
package/dist/props.js ADDED
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTranslateProps = getTranslateProps;
4
+ exports.asList = asList;
5
+ exports.unique = unique;
6
+ /**
7
+ * The four ways t() can be called, reduced to one shape.
8
+ *
9
+ * t('key')
10
+ * t('key', 'Default value')
11
+ * t('key', { name: 'Ada' })
12
+ * t('key', 'Default value', { ns: 'login' })
13
+ * t({ key: 'key', ns: 'login', params: { name: 'Ada' } })
14
+ *
15
+ * The third form is the awkward one: options and parameters share a single object, so
16
+ * anything that is not a known option is a parameter. That is why `ns`, `noWrap`, `orEmpty`
17
+ * and `language` cannot be used as parameter names - a limitation of the calling convention,
18
+ * not of the formatter.
19
+ */
20
+ const OPTION_NAMES = ['ns', 'noWrap', 'orEmpty', 'language'];
21
+ function split(combined) {
22
+ const options = {};
23
+ const params = {};
24
+ for (const [name, value] of Object.entries(combined)) {
25
+ if (OPTION_NAMES.includes(name)) {
26
+ options[name] = value;
27
+ }
28
+ else {
29
+ params[name] = value;
30
+ }
31
+ }
32
+ return { ...options, params };
33
+ }
34
+ function getTranslateProps(keyOrProps, ...rest) {
35
+ // Already the full shape: nothing to work out.
36
+ if (typeof keyOrProps === 'object' && keyOrProps !== null)
37
+ return keyOrProps;
38
+ const props = { key: keyOrProps };
39
+ let combined;
40
+ if (typeof rest[0] === 'string') {
41
+ props.defaultValue = rest[0];
42
+ combined = rest[1];
43
+ }
44
+ else if (typeof rest[0] === 'object' && rest[0] !== null) {
45
+ combined = rest[0];
46
+ }
47
+ return combined ? { ...split(combined), ...props } : props;
48
+ }
49
+ /** A fallback list, however it was written, as an array. */
50
+ function asList(value) {
51
+ if (typeof value === 'string')
52
+ return [value];
53
+ if (Array.isArray(value))
54
+ return value;
55
+ return [];
56
+ }
57
+ function unique(values) {
58
+ return [...new Set(values)];
59
+ }
60
+ //# sourceMappingURL=props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"props.js","sourceRoot":"","sources":["../src/props.ts"],"names":[],"mappings":";;AAmCA,8CAeC;AAGD,wBAIC;AAED,wBAEC;AA3DD;;;;;;;;;;;;;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,SAAgB,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,SAAgB,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,SAAgB,MAAM,CAAI,MAAW;IACjC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAChC,CAAC"}
@@ -0,0 +1,168 @@
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 type TranslateParams = Record<string, unknown>;
8
+ export type TranslateProps = {
9
+ key: string;
10
+ /** Used when nothing is stored, in place of echoing the key back. */
11
+ defaultValue?: string;
12
+ params?: TranslateParams;
13
+ /**
14
+ * Where to look. Undefined means the default namespace; null means the same, because
15
+ * callers that model "no namespace" as null are common enough to be worth accepting.
16
+ */
17
+ ns?: NamespaceFallback;
18
+ /** Return '' rather than the key when nothing is stored. */
19
+ orEmpty?: boolean;
20
+ /** Read a language other than the current one. */
21
+ language?: string;
22
+ /**
23
+ * Accepted and ignored.
24
+ *
25
+ * In-context editing wraps every rendered string so a click can be traced back to its
26
+ * key; noWrap opts out of that. This runtime does no wrapping at all - it is the
27
+ * production path - so there is nothing to opt out of. Accepted rather than rejected
28
+ * because the same templates render under both runtimes.
29
+ */
30
+ noWrap?: boolean;
31
+ };
32
+ /** A single namespace, several to try in order, or nothing for the default one. */
33
+ export type NamespaceFallback = string | string[] | undefined | null;
34
+ /** A language, several to fall back through, or a per-language map of those. */
35
+ export type LanguageFallback = string | string[] | Record<string, string | string[]> | undefined;
36
+ export type I18nEvent<Value = unknown> = {
37
+ type: string;
38
+ value: Value;
39
+ };
40
+ export type Subscription = {
41
+ unsubscribe(): void;
42
+ };
43
+ export type EventName = 'language' | 'update' | 'running' | 'initialLoad' | 'cache' | 'error';
44
+ /** Nested json is flattened on the way in, so a record is always one level deep. */
45
+ export type TranslationsRecord = Record<string, string>;
46
+ export type TranslationsInput = {
47
+ [key: string]: string | number | boolean | null | undefined | TranslationsInput;
48
+ };
49
+ export type RecordDescriptor = {
50
+ language: string;
51
+ namespace: string;
52
+ };
53
+ export type GetPath = (options: {
54
+ namespace: string;
55
+ language: string;
56
+ prefix: string;
57
+ }) => string;
58
+ export type BackendOptions = {
59
+ /** Where the files live. A namespace and language are appended by getPath. */
60
+ prefix?: string;
61
+ getPath?: GetPath;
62
+ /**
63
+ * Reads the body. Given the record it was asked for, because a source may not answer in
64
+ * the shape it was asked in - the authoring API returns `{ [language]: {...} }` where the
65
+ * CDN returns the translations directly, and unwrapping that needs to know the language.
66
+ */
67
+ getData?: (response: Response, descriptor: RecordDescriptor) => Promise<unknown>;
68
+ headers?: Record<string, string>;
69
+ /**
70
+ * Leave a record empty when its file cannot be read, rather than failing the whole load
71
+ * (default true).
72
+ *
73
+ * The opposite is what the previous runtime defaulted to, and it means one missing
74
+ * namespace file empties every namespace: the load is one operation and it rejects as
75
+ * one. A missing `reception/el.json` should cost you the reception strings, not the
76
+ * application. Failures are reported on the 'error' event and to the console either way.
77
+ */
78
+ fallbackOnFail?: boolean;
79
+ fetch?: typeof fetch;
80
+ /** Abort a file that has not arrived in this many milliseconds. */
81
+ timeout?: number;
82
+ };
83
+ export type FormatFn = (options: {
84
+ translation: string;
85
+ language: string;
86
+ params?: TranslateParams;
87
+ }) => string;
88
+ /**
89
+ * A last pass over the finished string, for in-context editing.
90
+ *
91
+ * The authoring build appends invisible characters naming the key, so a click on the rendered
92
+ * text can be traced back to what produced it. Production leaves this unset and the string
93
+ * goes out exactly as formatted.
94
+ *
95
+ * `namespace` is where the string was RESOLVED FROM, which is not what the caller asked with:
96
+ * a template writing `{{ 'greeting' | translate }}` names no namespace at all, and one
97
+ * writing `ns: 'login'` may still have been answered out of `common` by the fallback. Passing
98
+ * the caller's own answer through here instead sends anything editing the string to the wrong
99
+ * place - and for the default case, to a namespace nothing is in. When there is nothing stored
100
+ * it is the namespace a new key would be created in.
101
+ */
102
+ export type DecorateFn = (result: string, props: TranslateProps, namespace: string) => string;
103
+ export type MetaglottaOptions = {
104
+ /** The language to start in. */
105
+ language: string;
106
+ /** Tried in order when the current language has nothing for a key. */
107
+ fallbackLanguage?: LanguageFallback;
108
+ /** Fetched up front, and always kept loaded. */
109
+ ns?: string[];
110
+ /** Where to look when a call names no namespace of its own. */
111
+ fallbackNs?: NamespaceFallback;
112
+ /** The namespace a call with no namespace means (default: the first of `ns`). */
113
+ defaultNs?: string;
114
+ /**
115
+ * Accepted and ignored.
116
+ *
117
+ * It exists for language detection and for loading every language at once, neither of
118
+ * which this runtime does. Kept in the type so a bootstrap can pass the same options
119
+ * object to either runtime.
120
+ */
121
+ availableLanguages?: string[];
122
+ /** Where translations are loaded from. Omitted, nothing is loaded. */
123
+ backend?: BackendOptions;
124
+ /**
125
+ * How a stored string becomes the rendered one. 'icu' is the ICU MessageFormat used
126
+ * across these projects; 'none' returns it unchanged.
127
+ */
128
+ format?: 'icu' | 'none' | FormatFn;
129
+ /** Translations to start with, keyed `language` or `language:namespace`. */
130
+ staticData?: Record<string, TranslationsInput>;
131
+ /**
132
+ * What a missing translation renders as, when there is no defaultValue and orEmpty was
133
+ * not asked for. The default echoes the key, which is what makes a missing string
134
+ * visible on the page rather than a silent gap.
135
+ */
136
+ onMissing?: (props: TranslateProps) => string;
137
+ /** What an unformattable string renders as. Default 'invalid'. */
138
+ onFormatError?: string | ((error: string, props: TranslateProps) => string);
139
+ /**
140
+ * A last pass over every finished string. Used by in-context editing and by nothing else.
141
+ *
142
+ * Applied to the MISSING case too - to the echoed key, and to the empty string orEmpty
143
+ * asks for - because a key with no translation yet is exactly the one somebody wants to
144
+ * click on.
145
+ */
146
+ decorate?: DecorateFn;
147
+ };
148
+ /** The options half of the combined bag `t(key, { ns, name })` accepts. */
149
+ export type TranslateOptions = {
150
+ ns?: NamespaceFallback;
151
+ noWrap?: boolean;
152
+ orEmpty?: boolean;
153
+ language?: string;
154
+ };
155
+ /**
156
+ * How t(), instant(), translate() and the pipe are all called.
157
+ *
158
+ * Written out as overloads rather than a loose signature because these are the shapes 3,709
159
+ * template uses and 938 call sites already have, and the compiler is the only thing that
160
+ * would notice if one of them stopped being accepted.
161
+ */
162
+ export type TranslateFn<Result> = {
163
+ (key: string): Result;
164
+ (key: string, defaultValue: string): Result;
165
+ (key: string, options: TranslateParams & TranslateOptions): Result;
166
+ (key: string, defaultValue: string, options: TranslateParams & TranslateOptions): Result;
167
+ (props: TranslateProps): Result;
168
+ };
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * What a caller can pass to t(), and what the runtime hands back.
4
+ *
5
+ * Deliberately small: this is the whole surface the Angular glue and the eleven applications
6
+ * use, verified by counting call sites rather than by reading a feature list.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;GAKG"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Loading one `{namespace}/{language}.json` from wherever the translations are published.
3
+ *
4
+ * Deliberately not a plugin. There is exactly one backend in these applications - a
5
+ * CloudFront prefix - and a plugin system for a single implementation is machinery with
6
+ * nothing to choose between.
7
+ */
8
+ const defaultGetPath = ({ namespace, language, prefix }) => {
9
+ const base = prefix.replace(/\/+$/, '');
10
+ return namespace ? `${base}/${namespace}/${language}.json` : `${base}/${language}.json`;
11
+ };
12
+ /**
13
+ * Carries which record failed, because the url alone does not say it.
14
+ *
15
+ * The caller decides whether one unreadable file costs a namespace or the whole load, and it
16
+ * cannot decide that without knowing which namespace it was.
17
+ */
18
+ export class RecordLoadError extends Error {
19
+ constructor(descriptor, url, cause) {
20
+ super(`Metaglotta: could not load ${descriptor.namespace || '(default)'}/${descriptor.language} from ${url}`);
21
+ this.descriptor = descriptor;
22
+ this.url = url;
23
+ this.cause = cause;
24
+ this.name = 'RecordLoadError';
25
+ }
26
+ }
27
+ export function createBackend(options) {
28
+ const prefix = options.prefix ?? '/i18n';
29
+ const getPath = options.getPath ?? defaultGetPath;
30
+ const getData = options.getData ?? ((response) => response.json());
31
+ const headers = { Accept: 'application/json', ...options.headers };
32
+ const fetchFn = options.fetch ?? ((input, init) => fetch(input, init));
33
+ return {
34
+ fallbackOnFail: options.fallbackOnFail !== false,
35
+ async load(descriptor) {
36
+ const url = getPath({ ...descriptor, prefix });
37
+ try {
38
+ const response = await withTimeout(() => fetchFn(url, { headers }), options.timeout, url);
39
+ if (!response.ok)
40
+ throw new Error(`responded ${response.status}`);
41
+ return (await getData(response, descriptor));
42
+ }
43
+ catch (error) {
44
+ throw new RecordLoadError(descriptor, url, error);
45
+ }
46
+ },
47
+ };
48
+ }
49
+ /**
50
+ * A fetch that gives up.
51
+ *
52
+ * Without this a hung connection holds the initial load open for as long as the browser's own
53
+ * timeout, which is minutes - and an application that awaits run() waits with it.
54
+ */
55
+ async function withTimeout(run, ms, url) {
56
+ if (ms === undefined)
57
+ return run();
58
+ let timer;
59
+ try {
60
+ return await Promise.race([
61
+ run(),
62
+ new Promise((_, reject) => {
63
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms: ${url}`)), ms);
64
+ }),
65
+ ]);
66
+ }
67
+ finally {
68
+ if (timer)
69
+ clearTimeout(timer);
70
+ }
71
+ }
72
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Translations, keyed by language and namespace.
3
+ *
4
+ * Flat on the way in. A file may nest - `{ "menu": { "save": "Save" } }` - and callers ask
5
+ * for `menu.save`, so the nesting is resolved once at load rather than walked on every
6
+ * lookup. Empty values are dropped rather than stored: a null in the file means the string
7
+ * has no translation, and storing it would stop the language fallback from ever running.
8
+ */
9
+ export function flatten(data, prefix = '') {
10
+ const flat = {};
11
+ for (const [key, value] of Object.entries(data)) {
12
+ if (value === undefined || value === null)
13
+ continue;
14
+ const path = prefix ? `${prefix}.${key}` : key;
15
+ if (typeof value === 'object') {
16
+ Object.assign(flat, flatten(value, path));
17
+ }
18
+ else {
19
+ flat[path] = String(value);
20
+ }
21
+ }
22
+ return flat;
23
+ }
24
+ /** `en` for the default namespace, `en:login` otherwise - the shape staticData is keyed by. */
25
+ export function recordKey({ language, namespace }) {
26
+ return namespace ? `${language}:${namespace}` : language;
27
+ }
28
+ export function createCache() {
29
+ const records = new Map();
30
+ return {
31
+ has(descriptor) {
32
+ return records.has(recordKey(descriptor));
33
+ },
34
+ set(descriptor, data) {
35
+ records.set(recordKey(descriptor), flatten(data));
36
+ },
37
+ /**
38
+ * The first stored value, trying every namespace against every language.
39
+ *
40
+ * Namespace is the OUTER loop, and that is not arbitrary: a key present in both the
41
+ * asked-for namespace's fallback language and a fallback namespace's primary
42
+ * language resolves to the former. Reversing the loops changes which translation the
43
+ * page shows, silently, and only for keys that exist in two namespaces.
44
+ *
45
+ * WHERE it was found comes back with it. A template that names no namespace still
46
+ * resolves to a real one, and anything that wants to edit the string afterwards needs
47
+ * that name rather than the nothing the caller asked with.
48
+ */
49
+ find(namespaces, languages, key) {
50
+ for (const namespace of namespaces) {
51
+ for (const language of languages) {
52
+ const value = records.get(recordKey({ language, namespace }))?.[key];
53
+ if (value !== undefined)
54
+ return { value, namespace };
55
+ }
56
+ }
57
+ return undefined;
58
+ },
59
+ /** In-memory only, for a page whose text was just edited elsewhere. */
60
+ write(descriptor, key, value) {
61
+ const record = records.get(recordKey(descriptor));
62
+ if (!record)
63
+ return false;
64
+ record[key] = value;
65
+ return true;
66
+ },
67
+ clear() {
68
+ records.clear();
69
+ },
70
+ };
71
+ }
72
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,IAAuB,EAAE,MAAM,GAAG,EAAE;IACxD,MAAM,IAAI,GAAuB,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAoB;IAC/D,OAAO,SAAS,CAAC,CAAC,CAAC,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,WAAW;IACvB,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8B,CAAC;IAEtD,OAAO;QACH,GAAG,CAAC,UAA4B;YAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;QAC9C,CAAC;QAED,GAAG,CAAC,UAA4B,EAAE,IAAuB;YACrD,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,CAAC;QAED;;;;;;;;;;;WAWG;QACH,IAAI,CAAC,UAAoB,EAAE,SAAmB,EAAE,GAAW;YACvD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACjC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;oBAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;oBACrE,IAAI,KAAK,KAAK,SAAS;wBAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;gBACzD,CAAC;YACL,CAAC;YACD,OAAO,SAAS,CAAC;QACrB,CAAC;QAED,uEAAuE;QACvE,KAAK,CAAC,UAA4B,EAAE,GAAW,EAAE,KAAa;YAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;YAClD,IAAI,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACpB,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,KAAK;YACD,OAAO,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;KACJ,CAAC;AACN,CAAC"}