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,54 @@
|
|
|
1
|
+
import { ILocaleDef, LocLiteral, ILiteralLocalizer } from './interfaces';
|
|
2
|
+
import { LiteralLocalizer } from './literal';
|
|
3
|
+
import { getLocale } from './locale-list';
|
|
4
|
+
import { asFragment, autoGenerateId, getLocaleCode } from './utils';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
const emptyStringAutoId = autoGenerateId([]);
|
|
8
|
+
export class DefaultLocale implements ILocaleDef {
|
|
9
|
+
|
|
10
|
+
readonly literals: ILiteralLocalizer;
|
|
11
|
+
readonly code: string | undefined;
|
|
12
|
+
|
|
13
|
+
constructor(readonly id: string) {
|
|
14
|
+
this.literals = new LiteralLocalizer(this);
|
|
15
|
+
this.code = getLocaleCode(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: false): string;
|
|
19
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode;
|
|
20
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: boolean): string | React.ReactNode {
|
|
21
|
+
const existing = getLocale(this.id);
|
|
22
|
+
if (existing) {
|
|
23
|
+
const exist = existing.localize(id, count, parts, placeholders, react);
|
|
24
|
+
if (exist) {
|
|
25
|
+
return exist;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!parts || !placeholders) {
|
|
30
|
+
// special case for empty string, which could not be translated.
|
|
31
|
+
if (id === emptyStringAutoId) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`The smartloc string "${id}" cannot be translated to your default locale. It is likely that is has been deserialized. In order to support deserialized strings, you MUST load your default locale as a classic locale. nb: To generate your default locale translation file through smartloc CLI, use --generateDefault option`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// fallback to the in-code translation
|
|
38
|
+
const result: React.ReactNode[] = [];
|
|
39
|
+
|
|
40
|
+
// interleave the literals with the placeholders
|
|
41
|
+
for (let i = 0; i < placeholders.length; i++) {
|
|
42
|
+
result.push(parts[i]);
|
|
43
|
+
result.push(this.literals.localize(placeholders[i], react));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// add the last literal
|
|
47
|
+
result.push(parts[parts.length - 1]);
|
|
48
|
+
if (react) {
|
|
49
|
+
// return a fragment having result as children
|
|
50
|
+
return asFragment(result);
|
|
51
|
+
}
|
|
52
|
+
return result.join('');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type LocStr = InternalLocStr & React.ReactElement;
|
|
2
|
+
|
|
3
|
+
export interface InternalLocStr {
|
|
4
|
+
/**
|
|
5
|
+
* Localizes this string. To be used within a @see withLocales scope to have an actual translation.
|
|
6
|
+
*/
|
|
7
|
+
toString(locale?: string | ILocaleDef): string;
|
|
8
|
+
toJSON(): any;
|
|
9
|
+
|
|
10
|
+
/** Apply a transformation to the final translated text */
|
|
11
|
+
transform(transformer: (x: string) => string): LocStr;
|
|
12
|
+
|
|
13
|
+
/** React hook equivalent of .toString(), but which will updated when local changes */
|
|
14
|
+
useAsString(): string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LocLiteral = string | number | Date | LocStr | React.ReactNode;
|
|
18
|
+
export interface ILiteralLocalizer {
|
|
19
|
+
localize(data: LocLiteral, react: false): string;
|
|
20
|
+
localize(data: LocLiteral, react: true): React.ReactNode;
|
|
21
|
+
localize(data: LocLiteral, react: boolean): string | React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ILocaleDef {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly code: string | undefined;
|
|
27
|
+
readonly literals: ILiteralLocalizer;
|
|
28
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: false): string | null;
|
|
29
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode | null;
|
|
30
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: boolean): string | React.ReactNode | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TranslationTarget = string | {
|
|
34
|
+
singular: string | null;
|
|
35
|
+
plural: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type TranslationOf<T> = T extends LocStr ? string
|
|
39
|
+
: T extends Object ? { [K in keyof T]: TranslationOf<T[K]> }
|
|
40
|
+
: T extends (infer E)[] ? TranslationOf<E>[]
|
|
41
|
+
: T;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
export type StorableOf<T> = T extends LocStr ? Stored
|
|
45
|
+
: T extends Object ? { [K in keyof T]: StorableOf<T[K]> }
|
|
46
|
+
: T extends (infer E)[] ? StorableOf<E>[]
|
|
47
|
+
: T;
|
|
48
|
+
|
|
49
|
+
type Stored = string | { [key: string]: string } | { i18n: string; data: any[]; count?: number } | LocStr; // LocStr is there in case nonDescriptive is 'skip'
|
|
@@ -1,46 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.translateInContext = exports.translateObject = exports.toJsonStorable = exports.toLocalizable = exports.jsonParseLocalized = void 0;
|
|
7
|
-
const smartloc_1 = require("./smartloc");
|
|
8
|
-
const moment_1 = __importDefault(require("moment"));
|
|
9
|
-
const literal_1 = require("./literal");
|
|
1
|
+
import { singleLoc, multiLoc, withSerializationContext, withLocales, SerializationContextOptions, smartLoc } from './smartloc';
|
|
2
|
+
import { isLocStr } from './literal';
|
|
3
|
+
import type { TranslationOf, StorableOf } from './interfaces';
|
|
4
|
+
|
|
10
5
|
/**
|
|
11
6
|
* Parse a JSON string where localized strings have been serialized in their non translated form
|
|
12
7
|
*/
|
|
13
|
-
function jsonParseLocalized(value) {
|
|
8
|
+
export function jsonParseLocalized(value: string) {
|
|
14
9
|
const parsed = JSON.parse(value);
|
|
15
10
|
return _toLocalizable(parsed, 50);
|
|
16
11
|
}
|
|
17
|
-
|
|
12
|
+
|
|
18
13
|
/**
|
|
19
14
|
* Transforms an object that has been deserialized from a non translated from to an object that is translatable
|
|
20
15
|
*/
|
|
21
|
-
function toLocalizable(value) {
|
|
16
|
+
export function toLocalizable(value: any) {
|
|
22
17
|
return _toLocalizable(value, 50);
|
|
23
18
|
}
|
|
24
|
-
|
|
25
|
-
function _toLocalizable(value, depth) {
|
|
19
|
+
function _toLocalizable(value: any, depth: number): any {
|
|
26
20
|
if (depth < 0) {
|
|
27
21
|
throw new Error('This object is either too nested, or has cycles');
|
|
28
22
|
}
|
|
29
23
|
if (!value) {
|
|
30
24
|
return value;
|
|
31
25
|
}
|
|
26
|
+
|
|
32
27
|
if (typeof value === 'string') {
|
|
33
28
|
if (value.startsWith('i18n/single:')) {
|
|
34
|
-
return
|
|
29
|
+
return singleLoc(value.substr('i18n/single:'.length));
|
|
35
30
|
}
|
|
36
31
|
if (value.startsWith('i18n/id:')) {
|
|
37
|
-
return
|
|
32
|
+
return smartLoc({
|
|
33
|
+
id: value.substr('i18n/id:'.length),
|
|
34
|
+
count: undefined,
|
|
35
|
+
literals: null,
|
|
36
|
+
placeholders: null
|
|
37
|
+
});
|
|
38
38
|
}
|
|
39
39
|
return value;
|
|
40
40
|
}
|
|
41
|
+
|
|
41
42
|
if (typeof value !== 'object' || value instanceof Date) {
|
|
42
43
|
return value;
|
|
43
44
|
}
|
|
45
|
+
|
|
44
46
|
// === handle arrays
|
|
45
47
|
if (value instanceof Array) {
|
|
46
48
|
const retArray = [];
|
|
@@ -58,18 +60,26 @@ function _toLocalizable(value, depth) {
|
|
|
58
60
|
if (!keys.length) {
|
|
59
61
|
return value;
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
// === handle smartloc with args
|
|
62
65
|
if (keys.length === 2 && keys.includes('i18n') && keys.includes('data') && typeof value.i18n === 'string' && value.data instanceof Array) {
|
|
63
|
-
return
|
|
64
|
-
|
|
66
|
+
return smartLoc({
|
|
67
|
+
id: value.i18n,
|
|
68
|
+
literals: null,
|
|
69
|
+
placeholders: value.data,
|
|
70
|
+
count: value.count
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
// === handle multi
|
|
66
|
-
const ret = {};
|
|
75
|
+
const ret: Record<string, string> = {};
|
|
67
76
|
if (!keys.some(x => !x.startsWith('i18n:'))) {
|
|
68
77
|
for (const k of keys) {
|
|
69
78
|
ret[k.substr('i18n:'.length)] = value[k];
|
|
70
79
|
}
|
|
71
|
-
return
|
|
80
|
+
return multiLoc(ret);
|
|
72
81
|
}
|
|
82
|
+
|
|
73
83
|
// === handle plain object
|
|
74
84
|
let changed = false;
|
|
75
85
|
for (const k of keys) {
|
|
@@ -80,28 +90,29 @@ function _toLocalizable(value, depth) {
|
|
|
80
90
|
? ret
|
|
81
91
|
: value;
|
|
82
92
|
}
|
|
93
|
+
|
|
83
94
|
/**
|
|
84
95
|
* Transforms an object so it can be stored with localized strings in their non translated form
|
|
85
96
|
* @returns Either a new copy of your object, or your object (if had no smarloc strings in it)
|
|
86
97
|
*/
|
|
87
|
-
function toJsonStorable(value, options) {
|
|
88
|
-
return
|
|
98
|
+
export function toJsonStorable<T>(value: T, options?: SerializationContextOptions): StorableOf<T> {
|
|
99
|
+
return withSerializationContext(() => _toJsonStorable(value, 50), options);
|
|
89
100
|
}
|
|
90
|
-
|
|
101
|
+
|
|
91
102
|
/** Translates an object to the given language */
|
|
92
|
-
function translateObject(locales, object) {
|
|
103
|
+
export function translateObject<T>(locales: string | string[], object: T): TranslationOf<T> {
|
|
93
104
|
if (typeof locales === 'string') {
|
|
94
105
|
locales = [locales];
|
|
95
106
|
}
|
|
96
|
-
return
|
|
107
|
+
return withLocales(locales, () => _toJsonStorable(object, 50));
|
|
97
108
|
}
|
|
98
|
-
|
|
109
|
+
|
|
99
110
|
/** Translates an object according to the current context (call this when wrapped in withSerializationContext() or withLocales()) */
|
|
100
|
-
function translateInContext(object) {
|
|
111
|
+
export function translateInContext(object: any) {
|
|
101
112
|
return _toJsonStorable(object, 50);
|
|
102
113
|
}
|
|
103
|
-
|
|
104
|
-
function _toJsonStorable(v, depth) {
|
|
114
|
+
|
|
115
|
+
function _toJsonStorable(v: any, depth: number) {
|
|
105
116
|
if (depth < 0) {
|
|
106
117
|
throw new Error('This object is either too nested, or has cycles');
|
|
107
118
|
}
|
|
@@ -111,11 +122,11 @@ function _toJsonStorable(v, depth) {
|
|
|
111
122
|
if (typeof v !== 'object') {
|
|
112
123
|
return v;
|
|
113
124
|
}
|
|
114
|
-
if (
|
|
125
|
+
if (v instanceof Date) {
|
|
115
126
|
return v;
|
|
116
127
|
}
|
|
117
128
|
if (v instanceof Array) {
|
|
118
|
-
let ret;
|
|
129
|
+
let ret: any[] | undefined = undefined;
|
|
119
130
|
for (let i = 0; i < v.length; i++) {
|
|
120
131
|
const p = _toJsonStorable(v[i], depth - 1);
|
|
121
132
|
if (p !== v[i] && !ret) {
|
|
@@ -130,10 +141,11 @@ function _toJsonStorable(v, depth) {
|
|
|
130
141
|
}
|
|
131
142
|
return ret || v;
|
|
132
143
|
}
|
|
133
|
-
if (
|
|
144
|
+
if (isLocStr(v)) {
|
|
134
145
|
return v.toJSON();
|
|
135
146
|
}
|
|
136
|
-
|
|
147
|
+
|
|
148
|
+
const obj: Record<string, any> = {};
|
|
137
149
|
let diff = false;
|
|
138
150
|
for (const [k, p] of Object.entries(v)) {
|
|
139
151
|
const val = _toJsonStorable(p, depth - 1);
|
|
@@ -148,4 +160,3 @@ function _toJsonStorable(v, depth) {
|
|
|
148
160
|
Object.setPrototypeOf(obj, Object.getPrototypeOf(v));
|
|
149
161
|
return obj;
|
|
150
162
|
}
|
|
151
|
-
//# sourceMappingURL=json-utils.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LocLiteral, LocStr, ILocaleDef, ILiteralLocalizer } from './interfaces';
|
|
3
|
+
|
|
4
|
+
const locTag = Symbol('_localizable');
|
|
5
|
+
export function isLocStr(data: any): data is LocStr {
|
|
6
|
+
return data && typeof data === 'object' && data[locTag];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setIsLoc(data: any) {
|
|
10
|
+
data[locTag] = true;
|
|
11
|
+
Object.freeze(data);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class LiteralLocalizer implements ILiteralLocalizer {
|
|
15
|
+
private numbers: Intl.NumberFormat;
|
|
16
|
+
private dateTimeFormat: Intl.DateTimeFormat;
|
|
17
|
+
private dateFormat: Intl.DateTimeFormat;
|
|
18
|
+
|
|
19
|
+
constructor(private locale: ILocaleDef) {
|
|
20
|
+
this.numbers = new Intl.NumberFormat(locale.id);
|
|
21
|
+
// Format for dates with time (equivalent to moment's 'LLL')
|
|
22
|
+
this.dateTimeFormat = new Intl.DateTimeFormat(locale.id, {
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: 'long',
|
|
25
|
+
day: 'numeric',
|
|
26
|
+
hour: 'numeric',
|
|
27
|
+
minute: 'numeric'
|
|
28
|
+
});
|
|
29
|
+
// Format for dates without time (equivalent to moment's 'LL')
|
|
30
|
+
this.dateFormat = new Intl.DateTimeFormat(locale.id, {
|
|
31
|
+
year: 'numeric',
|
|
32
|
+
month: 'long',
|
|
33
|
+
day: 'numeric'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
localize(data: LocLiteral, react: false): string;
|
|
38
|
+
localize(data: LocLiteral, react: true): React.ReactNode;
|
|
39
|
+
localize(data: LocLiteral, react: boolean): string | React.ReactNode {
|
|
40
|
+
if (typeof data === 'string') {
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
if (typeof data === 'number') {
|
|
44
|
+
return this.numbers.format(data);
|
|
45
|
+
}
|
|
46
|
+
if (data instanceof Date) {
|
|
47
|
+
// Check if the date has a time component (non-zero hours or minutes)
|
|
48
|
+
if (data.getHours() || data.getMinutes()) {
|
|
49
|
+
return this.dateTimeFormat.format(data);
|
|
50
|
+
}
|
|
51
|
+
return this.dateFormat.format(data);
|
|
52
|
+
}
|
|
53
|
+
if (isLocStr(data)) {
|
|
54
|
+
return data.toString(this.locale);
|
|
55
|
+
}
|
|
56
|
+
if (react) {
|
|
57
|
+
return data as React.ReactNode;
|
|
58
|
+
} else {
|
|
59
|
+
if (isLocStr(data)) {
|
|
60
|
+
return data.toString(this.locale);
|
|
61
|
+
}
|
|
62
|
+
console.error(new Error('Cannot use React elements in LocStr elements when not used in a React renderer'));
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/core/load.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { addLocale } from './locale-list';
|
|
2
|
+
import type { TranslationTarget } from './interfaces';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads a locale generated from CLI
|
|
6
|
+
* @param filePath The file path to load
|
|
7
|
+
* @param merge If true, then the translations will be merged with already existing translations.
|
|
8
|
+
*/
|
|
9
|
+
export function loadJsonLocale(obj: GroupedTranslation, merge?: boolean): void
|
|
10
|
+
export function loadJsonLocale(url: string, merge?: boolean): Promise<void>
|
|
11
|
+
export function loadJsonLocale(urlOrObj: string | GroupedTranslation, merge?: boolean) {
|
|
12
|
+
if (typeof urlOrObj === 'string') {
|
|
13
|
+
return fetch(urlOrObj).then(async response => {
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(`Failed to fetch ${urlOrObj}: ${response.statusText}`);
|
|
16
|
+
}
|
|
17
|
+
const text = await response.json();
|
|
18
|
+
if (typeof text !== 'object') {
|
|
19
|
+
throw new Error(`Expecting ${urlOrObj} to be a JSON object`);
|
|
20
|
+
}
|
|
21
|
+
return loadJsonLocale(text, merge);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const transformed = convertLocale(ungroupTranslation(urlOrObj));
|
|
26
|
+
addLocale(urlOrObj.targetLanguage, transformed, merge);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export interface CollectItem {
|
|
31
|
+
source?: TranslationTarget;
|
|
32
|
+
target?: TranslationTarget | null;
|
|
33
|
+
dirty?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CollectResult {
|
|
37
|
+
[id: string]: CollectItem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Translation {
|
|
41
|
+
resources: CollectResult;
|
|
42
|
+
sourceLanguage?: string;
|
|
43
|
+
targetLanguage: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export interface GroupedTranslation {
|
|
48
|
+
resources: { [namespace: string]: CollectResult };
|
|
49
|
+
sourceLanguage?: string;
|
|
50
|
+
targetLanguage: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
export function ungroupTranslation(translation: GroupedTranslation): Translation {
|
|
55
|
+
const result: Translation = {
|
|
56
|
+
targetLanguage: translation.targetLanguage,
|
|
57
|
+
sourceLanguage: translation.sourceLanguage,
|
|
58
|
+
resources: {},
|
|
59
|
+
};
|
|
60
|
+
for (const [namespace, data] of Object.entries(translation.resources)) {
|
|
61
|
+
for (const [id, val] of Object.entries(data ?? {})) {
|
|
62
|
+
if (namespace === '$default') {
|
|
63
|
+
result.resources[id] = val;
|
|
64
|
+
} else {
|
|
65
|
+
result.resources[`${namespace}.${id}`] = val;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
export function convertLocale(locale: Translation): { [key: string]: TranslationTarget } {
|
|
75
|
+
const ret: { [key: string]: TranslationTarget } = {};
|
|
76
|
+
for (const [k, v] of Object.entries(locale.resources)) {
|
|
77
|
+
if (v.target) {
|
|
78
|
+
ret[k] = v.target;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return ret;
|
|
82
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { ILocaleDef, TranslationTarget } from './interfaces';
|
|
2
|
+
import { DefaultLocale } from './default-locale';
|
|
3
|
+
import { Locale } from './locale';
|
|
4
|
+
|
|
5
|
+
let locales: { [key: string]: Locale } = {};
|
|
6
|
+
|
|
7
|
+
let defaultLocale: ILocaleDef | null = null;
|
|
8
|
+
let currentLocale: ILocaleDef | null = null;
|
|
9
|
+
|
|
10
|
+
/** Defines the locale used in code */
|
|
11
|
+
export function setDefaultLocale(localeId: string) {
|
|
12
|
+
defaultLocale = new DefaultLocale(localeId);
|
|
13
|
+
currentLocale = defaultLocale;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function changeLocale(locale: ILocaleDef | string, throwIfNotFound = true) {
|
|
17
|
+
const loc = typeof locale === 'string' ? getLocale(locale) : locale;
|
|
18
|
+
if (!loc) {
|
|
19
|
+
if (!throwIfNotFound) {
|
|
20
|
+
return 'not found';
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Locale '${locale}' not found. Please load it first`);
|
|
23
|
+
}
|
|
24
|
+
if (currentLocale === loc) {
|
|
25
|
+
return 'unchanged';
|
|
26
|
+
}
|
|
27
|
+
currentLocale = loc;
|
|
28
|
+
subs.forEach(sub => sub());
|
|
29
|
+
return 'changed';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const subs: Set<() => void> = new Set();
|
|
33
|
+
export function subscribeToLocaleChange(callback: () => void) {
|
|
34
|
+
subs.add(callback);
|
|
35
|
+
return () => { subs.delete(callback); };
|
|
36
|
+
}
|
|
37
|
+
export function getCurrentLocale(noDefault?: false): ILocaleDef;
|
|
38
|
+
export function getCurrentLocale(noDefault: true): ILocaleDef | null;
|
|
39
|
+
export function getCurrentLocale(noDefault = false) {
|
|
40
|
+
if (currentLocale || noDefault) {
|
|
41
|
+
return currentLocale ?? null;
|
|
42
|
+
}
|
|
43
|
+
return getDefaultLocale();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDefaultLocale() {
|
|
47
|
+
if (!defaultLocale) {
|
|
48
|
+
throw new Error('You must specify default locale via setDefaultLocale() before using localization');
|
|
49
|
+
}
|
|
50
|
+
return defaultLocale;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resets all locales */
|
|
54
|
+
export function clearLocales() {
|
|
55
|
+
locales = {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Remove a locale */
|
|
59
|
+
export function removeLocale(id: string) {
|
|
60
|
+
id = id.toLowerCase();
|
|
61
|
+
const existing = locales[id];
|
|
62
|
+
if (!existing) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
delete locales[id];
|
|
66
|
+
const bl = baseLocale(id);
|
|
67
|
+
if (bl && locales[bl] === existing) {
|
|
68
|
+
delete locales[bl];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Adds translations
|
|
74
|
+
* @param id Locale ID (ex: 'en', or 'en-US')
|
|
75
|
+
* @param localeDef Translations (string id as key, formatted string as values - ex : {myId: 'Translated {0} text {1}'})
|
|
76
|
+
* @param merge If true, then the translations will be merged with already existing translations.
|
|
77
|
+
*/
|
|
78
|
+
export function addLocale(id: string, localeDef: { [id: string]: TranslationTarget }, merge?: boolean) {
|
|
79
|
+
id = id.toLowerCase();
|
|
80
|
+
let loc = locales[id];
|
|
81
|
+
if (loc) {
|
|
82
|
+
if (!merge) {
|
|
83
|
+
throw new Error(`There already is a locale '${id}' defined. Set the 'merge' argument to 'true' if you want to add translations`);
|
|
84
|
+
}
|
|
85
|
+
loc.localeDef = {
|
|
86
|
+
...loc.localeDef,
|
|
87
|
+
...localeDef,
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
loc = locales[id] = new Locale(id, localeDef);
|
|
92
|
+
const bl = baseLocale(id);
|
|
93
|
+
if (bl && !locales[bl]) {
|
|
94
|
+
locales[bl] = loc;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getLocale(id: string): ILocaleDef | null {
|
|
99
|
+
if (!id) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const found = locales[id];
|
|
103
|
+
if (found) {
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
// if given "en-US", then try to downgrade to a non specific language.
|
|
107
|
+
const bl = baseLocale(id);
|
|
108
|
+
return bl && locales[bl] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function baseLocale(l: string) {
|
|
112
|
+
const m = /^([a-zA-Z]+)\-[a-zA-Z]+$/.exec(l);
|
|
113
|
+
return m?.[1] || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function* listLocales() {
|
|
117
|
+
yield* Object.keys(locales);
|
|
118
|
+
if (defaultLocale) {
|
|
119
|
+
yield defaultLocale.id;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function* listLocaleDefs(): IterableIterator<ILocaleDef> {
|
|
124
|
+
yield* Object.values(locales);
|
|
125
|
+
if (defaultLocale) {
|
|
126
|
+
yield defaultLocale;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ILocaleDef, ILiteralLocalizer, LocLiteral, TranslationTarget } from './interfaces';
|
|
2
|
+
import { LiteralLocalizer } from './literal';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { asFragment, getLocaleCode } from './utils';
|
|
5
|
+
|
|
6
|
+
export class Locale implements ILocaleDef {
|
|
7
|
+
readonly literals: ILiteralLocalizer;
|
|
8
|
+
readonly code: string | undefined;
|
|
9
|
+
|
|
10
|
+
constructor(readonly id: string, public localeDef: { [id: string]: TranslationTarget }) {
|
|
11
|
+
this.literals = new LiteralLocalizer(this);
|
|
12
|
+
this.code = getLocaleCode(id);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react?: false): string | null;
|
|
16
|
+
localize(id: string, count: number | null, parts: TemplateStringsArray | null, placeholders: LocLiteral[] | null, react: true): React.ReactNode | null;
|
|
17
|
+
localize(id: string, count: number | null, _parts: TemplateStringsArray, placeholders: LocLiteral[], react?: boolean): string | React.ReactNode | null {
|
|
18
|
+
const templateObj = this.localeDef[id];
|
|
19
|
+
if (!templateObj) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let template: string | undefined = undefined;
|
|
23
|
+
if (typeof templateObj === 'string') {
|
|
24
|
+
template = templateObj;
|
|
25
|
+
} else {
|
|
26
|
+
if (count === 1 && templateObj.singular) {
|
|
27
|
+
template = templateObj.singular;
|
|
28
|
+
} else {
|
|
29
|
+
template = templateObj.plural;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!react) {
|
|
33
|
+
// same, but faster
|
|
34
|
+
const literals = placeholders?.map(x => this.literals.localize(x, false));
|
|
35
|
+
return format(template, literals);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const ret: React.ReactNode[] = [];
|
|
40
|
+
// match all {N} in the template and replace them with the corresponding literal
|
|
41
|
+
const matches = template.matchAll(/\{(\d+)\}/g);
|
|
42
|
+
let currentIndex = 0;
|
|
43
|
+
for (const match of matches) {
|
|
44
|
+
const fromLast = template.substring(currentIndex, match.index);
|
|
45
|
+
if (fromLast) {
|
|
46
|
+
ret.push(fromLast);
|
|
47
|
+
}
|
|
48
|
+
const i = parseInt(match[1]);
|
|
49
|
+
const p = placeholders[i];
|
|
50
|
+
if (p) {
|
|
51
|
+
ret.push(this.literals.localize(p, true));
|
|
52
|
+
} else {
|
|
53
|
+
console.warn(`Placeholder {${i}} in template of ${id} for locale ${this.id} does not exist`);
|
|
54
|
+
}
|
|
55
|
+
currentIndex = match.index + match[0].length;
|
|
56
|
+
}
|
|
57
|
+
const fromLast = template.substring(currentIndex);
|
|
58
|
+
if (fromLast) {
|
|
59
|
+
ret.push(fromLast);
|
|
60
|
+
}
|
|
61
|
+
return asFragment(ret);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function format(str: string, args: string[]) {
|
|
66
|
+
if (!args || !args.length) {
|
|
67
|
+
return str;
|
|
68
|
+
}
|
|
69
|
+
return str.replace(/{(\d+)}/g, function (match, number) {
|
|
70
|
+
return typeof args[number] !== 'undefined'
|
|
71
|
+
? args[number]
|
|
72
|
+
: match
|
|
73
|
+
;
|
|
74
|
+
});
|
|
75
|
+
}
|