smartloc 1.2.2 → 2.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 (116) hide show
  1. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  2. package/.vscode/launch.json +65 -0
  3. package/.vscode/settings.json +14 -0
  4. package/.vscode/tasks.json +27 -0
  5. package/README.md +225 -0
  6. package/build/README.md +225 -0
  7. package/build/package.json +60 -0
  8. package/bun.lock +310 -0
  9. package/index.js +1 -26
  10. package/package.json +17 -18
  11. package/samples/express/main.ts +37 -0
  12. package/samples/express/package.json +10 -0
  13. package/samples/graphql/main.ts +73 -0
  14. package/samples/graphql/package.json +15 -0
  15. package/src/adapters/adapter-base.ts +58 -0
  16. package/src/adapters/index.ts +31 -0
  17. package/src/adapters/json.ts +18 -0
  18. package/src/adapters/xliff.ts +30 -0
  19. package/src/cli/cli.ts +137 -0
  20. package/src/cli/collect.ts +196 -0
  21. package/src/cli/interfaces.ts +7 -0
  22. package/src/cli/reconciliate.ts +119 -0
  23. package/src/cli/utils.ts +36 -0
  24. package/src/core/default-locale.tsx +54 -0
  25. package/src/core/interfaces.ts +49 -0
  26. package/{core/json-utils.js → src/core/json-utils.ts} +45 -34
  27. package/src/core/literal.ts +66 -0
  28. package/src/core/load.ts +82 -0
  29. package/src/core/locale-list.ts +128 -0
  30. package/src/core/locale.tsx +75 -0
  31. package/src/core/smartloc.tsx +338 -0
  32. package/src/core/tag.ts +40 -0
  33. package/src/core/utils.tsx +24 -0
  34. package/src/express.ts +93 -0
  35. package/{graphql.js → src/graphql.ts} +80 -68
  36. package/src/index.ts +7 -0
  37. package/src/node/index.ts +1 -0
  38. package/src/node/load.ts +32 -0
  39. package/test/collect.spec.ts +45 -0
  40. package/test/reconciliation.spec.ts +132 -0
  41. package/test/serialization.spec.ts +162 -0
  42. package/test/translation.spec.ts +61 -0
  43. package/tsconfig.json +37 -0
  44. package/tslint.json +103 -0
  45. package/adapters/adapter-base.d.ts +0 -10
  46. package/adapters/adapter-base.js +0 -46
  47. package/adapters/adapter-base.js.map +0 -1
  48. package/adapters/index.d.ts +0 -3
  49. package/adapters/index.js +0 -33
  50. package/adapters/index.js.map +0 -1
  51. package/adapters/json.d.ts +0 -6
  52. package/adapters/json.js +0 -22
  53. package/adapters/json.js.map +0 -1
  54. package/adapters/xliff.d.ts +0 -6
  55. package/adapters/xliff.js +0 -32
  56. package/adapters/xliff.js.map +0 -1
  57. package/cli/cli.d.ts +0 -2
  58. package/cli/cli.js +0 -128
  59. package/cli/cli.js.map +0 -1
  60. package/cli/collect.d.ts +0 -12
  61. package/cli/collect.js +0 -151
  62. package/cli/collect.js.map +0 -1
  63. package/cli/interfaces.d.ts +0 -25
  64. package/cli/interfaces.js +0 -3
  65. package/cli/interfaces.js.map +0 -1
  66. package/cli/reconciliate.d.ts +0 -6
  67. package/cli/reconciliate.js +0 -108
  68. package/cli/reconciliate.js.map +0 -1
  69. package/cli/utils.d.ts +0 -7
  70. package/cli/utils.js +0 -54
  71. package/cli/utils.js.map +0 -1
  72. package/core/base-loc.d.ts +0 -3
  73. package/core/base-loc.js +0 -20
  74. package/core/base-loc.js.map +0 -1
  75. package/core/default-locale.d.ts +0 -8
  76. package/core/default-locale.js +0 -42
  77. package/core/default-locale.js.map +0 -1
  78. package/core/interfaces.d.ts +0 -32
  79. package/core/interfaces.js +0 -3
  80. package/core/interfaces.js.map +0 -1
  81. package/core/json-utils.d.ts +0 -19
  82. package/core/json-utils.js.map +0 -1
  83. package/core/literal.d.ts +0 -9
  84. package/core/literal.js +0 -52
  85. package/core/literal.js.map +0 -1
  86. package/core/locale-list.d.ts +0 -19
  87. package/core/locale-list.js +0 -84
  88. package/core/locale-list.js.map +0 -1
  89. package/core/locale.d.ts +0 -13
  90. package/core/locale.js +0 -34
  91. package/core/locale.js.map +0 -1
  92. package/core/locstr-array.d.ts +0 -10
  93. package/core/locstr-array.js +0 -48
  94. package/core/locstr-array.js.map +0 -1
  95. package/core/smartloc.d.ts +0 -64
  96. package/core/smartloc.js +0 -222
  97. package/core/smartloc.js.map +0 -1
  98. package/core/tag.d.ts +0 -5
  99. package/core/tag.js +0 -17
  100. package/core/tag.js.map +0 -1
  101. package/core/utils.d.ts +0 -1
  102. package/core/utils.js +0 -13
  103. package/core/utils.js.map +0 -1
  104. package/express.d.ts +0 -12
  105. package/express.js +0 -85
  106. package/express.js.map +0 -1
  107. package/graphql.d.ts +0 -12
  108. package/graphql.js.map +0 -1
  109. package/index.d.ts +0 -7
  110. package/index.js.map +0 -1
  111. package/node/index.d.ts +0 -1
  112. package/node/index.js +0 -14
  113. package/node/index.js.map +0 -1
  114. package/node/load.d.ts +0 -12
  115. package/node/load.js +0 -44
  116. package/node/load.js.map +0 -1
@@ -0,0 +1,338 @@
1
+ import { getLocale, getDefaultLocale, getCurrentLocale, subscribeToLocaleChange, listLocaleDefs } from './locale-list';
2
+ import { ILocaleDef, InternalLocStr, LocLiteral, LocStr } from './interfaces';
3
+ import { setIsLoc } from './literal';
4
+ import React, { useState } from 'react';
5
+ import { asFragment } from './utils';
6
+
7
+ let langCtx: ILocaleDef[] | null = null;
8
+ let serialContext: SerializationContextOptions | null = null;
9
+
10
+ export function useAsString(loc: LocStr | string | null): string | null;
11
+ export function useAsString(loc: LocStr | string | undefined): string | undefined;
12
+ export function useAsString(loc: LocStr | string | undefined): string | undefined;
13
+ export function useAsString(loc: LocStr | string | null | undefined): string | null | undefined {
14
+ const [, setCount] = useState(0);
15
+ React.useEffect(() => {
16
+ const unsub = subscribeToLocaleChange(() => setCount(c => c + 1));
17
+ return unsub;
18
+ }, []);
19
+ return loc?.toString();
20
+ }
21
+
22
+
23
+ export interface SerializationContextOptions {
24
+ /** HOw non self-descriptive (MultiLoc, SingleLoc, ...) will be transformed. Defaults to 'id'.
25
+ *
26
+ * id => will serialize its ID ... the context of deserialization will be required to have the given translation
27
+ * skip => will be left as it (=> serialization will translate it)
28
+ * toMulti => translates to all registered languages, and serializes as a 'multi'
29
+ */
30
+ nonSelfDescriptive?: 'skip' | 'toMulti' | 'id';
31
+ }
32
+ /**
33
+ * Execute something in the given locales context.
34
+ * @param acceptLanguages The accepted languages, by order of preference
35
+ * @param action Action to perform (usually a JSON.serialize() or a .toString() operation)
36
+ **/
37
+ export function withLocales<T = void>(acceptLanguages: string[], action: () => T) {
38
+ if (!acceptLanguages || !acceptLanguages.length) {
39
+ return action();
40
+ }
41
+ const oldLangCtx = langCtx;
42
+ try {
43
+ // set accepted languages
44
+ langCtx = acceptLanguages
45
+ .map(getLocale)
46
+ .filter(x => !!x);
47
+
48
+ // perform action
49
+ return action();
50
+ } finally {
51
+ langCtx = oldLangCtx;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute something in a context where .toJson() loc strings
57
+ * will be serialized into a form that is parsable via jsonParseLocalized()
58
+ */
59
+ export function withSerializationContext<T = void>(action: () => T, options?: SerializationContextOptions) {
60
+ const os = serialContext;
61
+ try {
62
+ serialContext = options || {};
63
+ return action();
64
+ } finally {
65
+ serialContext = os;
66
+ }
67
+ }
68
+
69
+
70
+ interface SmartlocProps {
71
+ id: string,
72
+ literals: TemplateStringsArray | null;
73
+ placeholders: LocLiteral[] | null;
74
+ count: number | undefined;
75
+ }
76
+
77
+ function createKind<props extends object>(opts: {
78
+ name: string;
79
+ render: (props: props, locale: ILocaleDef) => React.ReactNode | null;
80
+ toString: (props: props, locale: ILocaleDef) => string | null;
81
+ toJson: (props: props) => any;
82
+ }) {
83
+
84
+ const Renderer = {
85
+ [opts.name]: class extends React.Component<props, { locale: ILocaleDef | null }> {
86
+ private rerender = () => this.setState({
87
+ locale: getCurrentLocale(),
88
+ });
89
+ private unsub?: () => void;
90
+
91
+ override componentDidMount(): void {
92
+ this.unsub = subscribeToLocaleChange(this.rerender);
93
+
94
+ }
95
+
96
+ override componentWillUnmount() {
97
+ this.unsub?.();
98
+ }
99
+
100
+ override render(): React.ReactNode {
101
+ const locale = this.state?.locale;
102
+ if (locale) {
103
+ const rendered = opts.render(this.props, locale);
104
+ if (rendered) {
105
+ return rendered;
106
+ }
107
+ }
108
+ const currentLocale = getCurrentLocale(true);
109
+ if (currentLocale) {
110
+ const rendered = opts.render(this.props, currentLocale);
111
+ if (rendered) {
112
+ return rendered;
113
+ }
114
+ }
115
+ const defaultLocale = getDefaultLocale();
116
+ if (defaultLocale) {
117
+ const rendered = opts.render(this.props, defaultLocale);
118
+ if (rendered) {
119
+ return rendered;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ },
125
+ }[opts.name];
126
+
127
+ const Proto = {
128
+ [opts.name]: class implements InternalLocStr {
129
+ get locKind() {
130
+ return opts.name;
131
+ }
132
+ // will be set the hackky way, see creator below
133
+ props!: props;
134
+
135
+ toJSON() {
136
+ if (!serialContext) {
137
+ return this.toString();
138
+ }
139
+ return opts.toJson(this.props);
140
+ }
141
+
142
+ toString(locale?: string | ILocaleDef | null): string {
143
+ if (typeof locale === 'string') {
144
+ locale = getLocale(locale);
145
+ }
146
+
147
+ if (locale) {
148
+ const localized = opts.toString(this.props, locale)
149
+ if (localized) {
150
+ return localized;
151
+ }
152
+ }
153
+ if (langCtx?.length) {
154
+ for (const l of langCtx) {
155
+ const localized = opts.toString(this.props, l);
156
+ if (localized) {
157
+ return localized;
158
+ }
159
+ }
160
+ }
161
+
162
+ // try current locale
163
+ const currentLocale = getCurrentLocale(true);
164
+ if (currentLocale) {
165
+ const localized = opts.toString(this.props, currentLocale);
166
+ if (localized) {
167
+ return localized;
168
+ }
169
+ }
170
+
171
+ // fallback to default locale
172
+ const defaultLocale = getDefaultLocale();
173
+ if (!defaultLocale) {
174
+ throw new Error('You must specify default locale via setDefaultLocale() before using localization');
175
+ }
176
+ // default locale cannot return null
177
+ const def = opts.toString(this.props, defaultLocale);
178
+ if (!def) {
179
+ throw new Error('Cannot get a translation'); // should not happen
180
+ }
181
+ return def;
182
+ }
183
+
184
+ transform(transformer: (x: string) => string): LocStr {
185
+ return transformed({
186
+ parent: this,
187
+ transformer,
188
+ });
189
+ }
190
+
191
+ useAsString(): string {
192
+ const [, setCount] = useState(0);
193
+ React.useEffect(() => {
194
+ const unsub = subscribeToLocaleChange(() => setCount(c => c + 1));
195
+ return unsub;
196
+ }, []);
197
+ return this.toString();
198
+ }
199
+ }
200
+ }[opts.name];
201
+
202
+
203
+
204
+ // trick to create a function that is named like the given name
205
+ const creator = {
206
+ [opts.name](props: props): LocStr {
207
+ const e = React.createElement(Renderer, props);
208
+ const eProps = Object.getOwnPropertyDescriptors(e);
209
+ const ret = Object.create(Proto.prototype);
210
+ Object.defineProperties(ret, eProps);
211
+ setIsLoc(ret); // this will freeze the object
212
+ return ret;
213
+ }
214
+ }[opts.name];
215
+
216
+ return creator;
217
+ }
218
+
219
+
220
+
221
+ export const smartLoc = createKind<SmartlocProps>({
222
+ name: 'SmartLoc',
223
+ render(props, locale) {
224
+ return locale.localize(props.id, props.count ?? null, props.literals, props.placeholders, true);
225
+ },
226
+ toString(props, locale) {
227
+ return locale.localize(props.id, props.count ?? null, props.literals, props.placeholders, false);
228
+ },
229
+ toJson(props) {
230
+ switch (serialContext!.nonSelfDescriptive) {
231
+ case 'skip':
232
+ return this;
233
+ case 'toMulti':
234
+ const multi: Record<string, string> = {};
235
+ for (const l of listLocaleDefs()) {
236
+ const str = this.toString(props, l);
237
+ if (str) {
238
+ multi[l.id] = str;
239
+ }
240
+ }
241
+ // tslint:disable-next-line: no-use-before-declare
242
+ return multiLoc(multi).toJSON();
243
+ default:
244
+ if (props.placeholders?.length || props.count !== undefined) {
245
+ return {
246
+ i18n: props.id,
247
+ data: props.placeholders,
248
+ count: props.count,
249
+ };
250
+ // throw new Error(`Cannot serialize smartloc instance "${this.id}" which has placeholders ("\${}" in string definition). Please use another SerializationContextOption.nonSelfDescriptive option`);
251
+ }
252
+ return `i18n/id:${props.id}`;
253
+ }
254
+ },
255
+
256
+ });
257
+
258
+ export const transformed = createKind<{ parent: InternalLocStr; transformer: (x: string) => string; }>({
259
+ name: 'TransformedLoc',
260
+ render(props, _) {
261
+ console.warn('Transformed localized string cannot be rendered as React nodes => fallback to parent rendering');
262
+ return <>{props.parent}</>;
263
+ },
264
+ toString(props, locale) {
265
+ const ret = props.parent.toString(locale);
266
+ return props.transformer(ret);
267
+ },
268
+ toJson(props) {
269
+ if (serialContext) {
270
+ throw new Error('Cannot serialize transformed localized strings');
271
+ }
272
+ return props.parent.toJSON();
273
+ },
274
+ });
275
+ export function singleLoc(text: string) {
276
+ return _singleLoc({ text });
277
+ }
278
+
279
+ const _singleLoc = createKind<{ text: string }>({
280
+ name: 'SingleLoc',
281
+ render(props) {
282
+ return props.text;
283
+ },
284
+ toString(props) {
285
+ return props.text;
286
+ },
287
+ toJson(props) {
288
+ return 'i18n/single:' + props.text;
289
+ },
290
+ });
291
+
292
+
293
+ export const multiLoc = createKind<Record<string, string>>({
294
+ name: 'MultiLoc',
295
+ render(props, locale) {
296
+ const t = this.toString(props, locale);
297
+ if (!t) {
298
+ return <></>;
299
+ }
300
+ return t;
301
+ },
302
+ toString(props, locale) {
303
+ return props[locale.id] ?? props[locale.code ?? ''];
304
+ },
305
+ toJson(props) {
306
+ const ret: Record<string, string> = {};
307
+ for (const [k, v] of Object.entries(props)) {
308
+ ret[`i18n:${k}`] = v;
309
+ }
310
+ return ret;
311
+ }
312
+ });
313
+
314
+ export function joinArray(separator: string | LocStr, items: (string | LocStr)[]): LocStr {
315
+ return _joinedArray({ items, separator });
316
+ }
317
+
318
+ const _joinedArray = createKind<{ separator: string | LocStr, items: (string | LocStr)[] }>({
319
+ name: 'LocStringArray',
320
+ render(props) {
321
+ const elts: any[] = [];
322
+ // locstrings are react-elements
323
+ for (let i = 0; i < props.items.length; i++) {
324
+ if (i) {
325
+ elts.push(props.separator);
326
+ }
327
+ elts.push(props.items[i]);
328
+ }
329
+ return asFragment(elts);
330
+ },
331
+ toString(props, locale) {
332
+ const sep = typeof props.separator === 'string' ? props.separator : props.separator.toString(locale);
333
+ return props.items.map(item => item.toString(locale)).join(sep);
334
+ },
335
+ toJson() {
336
+ throw new Error('Cannot transform a joined array to json');
337
+ }
338
+ });
@@ -0,0 +1,40 @@
1
+ import { smartLoc } from './smartloc';
2
+ import { LocLiteral, LocStr } from './interfaces';
3
+ import { autoGenerateId } from './utils';
4
+
5
+
6
+ type Templater = (literals: TemplateStringsArray, ...placeholders: LocLiteral[]) => LocStr;
7
+ // just a signature.
8
+ declare function _tagSig(id: string): Templater
9
+ declare function _tagSig(literals: TemplateStringsArray, ...placeholders: LocLiteral[]): LocStr;
10
+ type TagSignature = typeof _tagSig;
11
+ type LocTag = TagSignature & { plural(count: number): TagSignature };
12
+
13
+
14
+
15
+
16
+ function __loc(count: number | undefined, idOrLiterals: string | TemplateStringsArray, ...ph: LocLiteral[]): Templater | LocStr {
17
+ if (typeof idOrLiterals === 'string') {
18
+ const id = idOrLiterals;
19
+ return (literals: TemplateStringsArray, ...placeholders: LocLiteral[]) => {
20
+ return smartLoc({ id, literals, placeholders, count });
21
+ };
22
+ }
23
+ return smartLoc({ id: autoGenerateId(idOrLiterals), literals: idOrLiterals, placeholders: ph, count });
24
+ }
25
+
26
+ function _loc(id: string): Templater;
27
+ function _loc(idOrLiteralsOrCount: string | TemplateStringsArray, ...ph: LocLiteral[]): LocStr;
28
+ function _loc(idOrLiteralsOrCount: string | TemplateStringsArray, ...ph: LocLiteral[]): Templater | LocStr {
29
+ return __loc(undefined, idOrLiteralsOrCount, ...ph);
30
+ }
31
+ _loc.plural = (count: number) => {
32
+ function templater(id: string): Templater;
33
+ function templater(template: TemplateStringsArray, ...placeholders: LocLiteral[]): Templater;
34
+ function templater(idOrLiterals: string | TemplateStringsArray, ...ph: LocLiteral[]): Templater | LocStr {
35
+ return __loc(count, idOrLiterals, ...ph);
36
+ }
37
+ return templater;
38
+ };
39
+
40
+ export const loc: LocTag = _loc as any;
@@ -0,0 +1,24 @@
1
+ import { sha1 } from 'js-sha1';
2
+ import React from 'react';
3
+
4
+ export function autoGenerateId(str: TemplateStringsArray | string[]): string {
5
+ const sha = sha1(str.join('|'));
6
+ return 'sha1.' + sha;
7
+ }
8
+
9
+ export function getLocaleCode(locale: string) {
10
+ return locale && /^[a-z]+/.exec(locale.toLowerCase())?.[0] || undefined;
11
+ }
12
+
13
+
14
+ export function asFragment(children: React.ReactNode[]): React.ReactNode {
15
+ if (children.length === 1) {
16
+ return children[0];
17
+ }
18
+ return <React.Fragment>{children.map((x, i) => {
19
+ if (React.isValidElement(x) && !x.key) {
20
+ return <React.Fragment key={i}>{x}</React.Fragment>;
21
+ }
22
+ return x;
23
+ })}</React.Fragment>;
24
+ }
package/src/express.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { withLocales, isLocStr } from '.';
2
+ import parser from 'accept-language-parser';
3
+
4
+ const supportedMethods = [
5
+ { key: 'json', },
6
+ { key: 'jsonp', },
7
+ { key: 'send', fn: (v: any) => isLocStr(v) ? v.toString() : v },
8
+ ];
9
+
10
+ export interface SmartlocExpressOptions {
11
+ /** (optional) Waits for this promise before performing a translation. This might be for instance the promise that waits all locales to be declared via addLocale() loaded */
12
+ waitFor?: Promise<any>;
13
+ /** Custom resolution (will fall back on accept-language if this function does not return anything) */
14
+ customLocaleResolver?: (req: any) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>;
15
+ /** Error logger (gives a chance to log resolution errors) */
16
+ errorLogger?: (err: any) => void;
17
+ }
18
+
19
+ /**
20
+ * Express middleware that will translate calls to .send(), .json() (etc...) based on the Accept-Language request header.
21
+ */
22
+ export default function (options?: SmartlocExpressOptions) {
23
+ const { errorLogger, customLocaleResolver, waitFor } = options ?? {};
24
+ let waiter: any = waitFor;
25
+ return function (req: any, res: any, next: any) {
26
+
27
+ let locales: string[];
28
+
29
+ function serve() {
30
+ waiter = null;
31
+ try {
32
+ // === resolve locales from custom resolver
33
+ if (!locales) {
34
+ const cust = customLocaleResolver?.(req);
35
+ if (typeof cust === 'string') {
36
+ locales = [cust];
37
+ } else if (cust instanceof Array) {
38
+ locales = cust;
39
+ } else if (cust) {
40
+ cust.then(resCust => {
41
+ if (typeof resCust === 'string') {
42
+ resCust = [resCust];
43
+ }
44
+ locales = resCust ?? [];
45
+ serve();
46
+ }, e => {
47
+ errorLogger?.(e);
48
+ serve();
49
+ })
50
+ return; // defered => method will be called again later.
51
+ }
52
+ }
53
+
54
+ // === resolve locales from headers
55
+ if (!locales || !locales.length) {
56
+ const lh = req?.headers?.['accept-language'];
57
+ if (!lh) {
58
+ next();
59
+ return; // => no locale... just serve without patching
60
+ }
61
+ locales = parser.parse(lh)
62
+ ?.map(x => x.region ? `${x.code}-${x.region}` : x.code);
63
+ }
64
+
65
+ // === override methods that sends potentially translatable content
66
+ for (const { key, fn } of supportedMethods) {
67
+ const orig: Function = res[key];
68
+ res[key] = function (fst: any, ...args: any[]) {
69
+ return withLocales(locales, () => {
70
+ return orig.call(this
71
+ , fn ? fn(fst) : fst
72
+ , ...args);
73
+ })
74
+ };
75
+ }
76
+ next();
77
+ return;
78
+ } catch (e) {
79
+ errorLogger?.(e);
80
+ next();
81
+ }
82
+ };
83
+
84
+
85
+ // === serve if nothing to wait
86
+ if (!waiter) {
87
+ serve();
88
+ } else {
89
+ waiter.then(serve, serve);
90
+ }
91
+
92
+ }
93
+ }