smartloc 1.2.1 → 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.
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.vscode/launch.json +65 -0
- package/.vscode/settings.json +14 -0
- package/.vscode/tasks.json +27 -0
- package/README.md +225 -0
- package/build/README.md +225 -0
- package/build/package.json +60 -0
- package/bun.lock +310 -0
- package/index.js +1 -26
- package/package.json +17 -18
- package/samples/express/main.ts +37 -0
- package/samples/express/package.json +10 -0
- package/samples/graphql/main.ts +73 -0
- package/samples/graphql/package.json +15 -0
- package/src/adapters/adapter-base.ts +58 -0
- package/src/adapters/index.ts +31 -0
- package/src/adapters/json.ts +18 -0
- package/src/adapters/xliff.ts +30 -0
- package/src/cli/cli.ts +137 -0
- package/src/cli/collect.ts +196 -0
- package/src/cli/interfaces.ts +7 -0
- package/src/cli/reconciliate.ts +119 -0
- package/src/cli/utils.ts +36 -0
- package/src/core/default-locale.tsx +54 -0
- package/src/core/interfaces.ts +49 -0
- package/{core/json-utils.js → src/core/json-utils.ts} +45 -34
- package/src/core/literal.ts +66 -0
- package/src/core/load.ts +82 -0
- package/src/core/locale-list.ts +128 -0
- package/src/core/locale.tsx +75 -0
- package/src/core/smartloc.tsx +338 -0
- package/src/core/tag.ts +40 -0
- package/src/core/utils.tsx +24 -0
- package/src/express.ts +93 -0
- package/{graphql.js → src/graphql.ts} +80 -68
- package/src/index.ts +7 -0
- package/src/node/index.ts +1 -0
- package/src/node/load.ts +32 -0
- package/test/collect.spec.ts +45 -0
- package/test/reconciliation.spec.ts +132 -0
- package/test/serialization.spec.ts +162 -0
- package/test/translation.spec.ts +61 -0
- package/tsconfig.json +37 -0
- package/tslint.json +103 -0
- package/adapters/adapter-base.d.ts +0 -10
- package/adapters/adapter-base.js +0 -46
- package/adapters/adapter-base.js.map +0 -1
- package/adapters/index.d.ts +0 -3
- package/adapters/index.js +0 -33
- package/adapters/index.js.map +0 -1
- package/adapters/json.d.ts +0 -6
- package/adapters/json.js +0 -22
- package/adapters/json.js.map +0 -1
- package/adapters/xliff.d.ts +0 -6
- package/adapters/xliff.js +0 -32
- package/adapters/xliff.js.map +0 -1
- package/cli/cli.d.ts +0 -2
- package/cli/cli.js +0 -128
- package/cli/cli.js.map +0 -1
- package/cli/collect.d.ts +0 -12
- package/cli/collect.js +0 -151
- package/cli/collect.js.map +0 -1
- package/cli/interfaces.d.ts +0 -25
- package/cli/interfaces.js +0 -3
- package/cli/interfaces.js.map +0 -1
- package/cli/reconciliate.d.ts +0 -6
- package/cli/reconciliate.js +0 -108
- package/cli/reconciliate.js.map +0 -1
- package/cli/utils.d.ts +0 -7
- package/cli/utils.js +0 -54
- package/cli/utils.js.map +0 -1
- package/core/base-loc.d.ts +0 -3
- package/core/base-loc.js +0 -20
- package/core/base-loc.js.map +0 -1
- package/core/default-locale.d.ts +0 -8
- package/core/default-locale.js +0 -42
- package/core/default-locale.js.map +0 -1
- package/core/interfaces.d.ts +0 -32
- package/core/interfaces.js +0 -3
- package/core/interfaces.js.map +0 -1
- package/core/json-utils.d.ts +0 -19
- package/core/json-utils.js.map +0 -1
- package/core/literal.d.ts +0 -9
- package/core/literal.js +0 -52
- package/core/literal.js.map +0 -1
- package/core/locale-list.d.ts +0 -19
- package/core/locale-list.js +0 -84
- package/core/locale-list.js.map +0 -1
- package/core/locale.d.ts +0 -13
- package/core/locale.js +0 -34
- package/core/locale.js.map +0 -1
- package/core/locstr-array.d.ts +0 -10
- package/core/locstr-array.js +0 -48
- package/core/locstr-array.js.map +0 -1
- package/core/smartloc.d.ts +0 -64
- package/core/smartloc.js +0 -222
- package/core/smartloc.js.map +0 -1
- package/core/tag.d.ts +0 -5
- package/core/tag.js +0 -17
- package/core/tag.js.map +0 -1
- package/core/utils.d.ts +0 -1
- package/core/utils.js +0 -13
- package/core/utils.js.map +0 -1
- package/express.d.ts +0 -12
- package/express.js +0 -85
- package/express.js.map +0 -1
- package/graphql.d.ts +0 -12
- package/graphql.js.map +0 -1
- package/index.d.ts +0 -7
- package/index.js.map +0 -1
- package/node/index.d.ts +0 -1
- package/node/index.js +0 -14
- package/node/index.js.map +0 -1
- package/node/load.d.ts +0 -12
- package/node/load.js +0 -44
- 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
|
+
});
|
package/src/core/tag.ts
ADDED
|
@@ -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
|
+
}
|