smartloc 1.2.2 → 2.0.1
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
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const accept_language_parser_1 = __importDefault(require("accept-language-parser"));
|
|
11
|
-
const smartloc_1 = require("./core/smartloc");
|
|
12
|
-
const json_utils_1 = require("./core/json-utils");
|
|
13
|
-
const language_1 = require("graphql/language");
|
|
1
|
+
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
|
|
2
|
+
import { GraphQLScalarType, GraphQLList, GraphQLUnionType, GraphQLInterfaceType, GraphQLField, GraphQLNonNull, } from 'graphql';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { isLocStr } from '.';
|
|
5
|
+
import parser from 'accept-language-parser';
|
|
6
|
+
import { withLocales, multiLoc, singleLoc } from './core/smartloc';
|
|
7
|
+
import { translateInContext } from './core/json-utils';
|
|
8
|
+
import { Kind } from 'graphql/language';
|
|
9
|
+
|
|
14
10
|
const INVALID_MSG = `Invalid localizable string input. Expecting either a string in the default locale, or an object like {"en": "English translation", "fr": "Traduction française"}`;
|
|
15
|
-
function parseValue(value) {
|
|
11
|
+
function parseValue(value: any) {
|
|
16
12
|
if (!value) {
|
|
17
13
|
return null;
|
|
18
14
|
}
|
|
19
15
|
if (typeof value === 'string') {
|
|
20
|
-
return
|
|
16
|
+
return singleLoc(value);
|
|
21
17
|
}
|
|
22
18
|
if (typeof value === 'object') {
|
|
23
19
|
for (const v of Object.values(value)) {
|
|
@@ -25,21 +21,22 @@ function parseValue(value) {
|
|
|
25
21
|
throw new TypeError(INVALID_MSG);
|
|
26
22
|
}
|
|
27
23
|
}
|
|
28
|
-
return
|
|
24
|
+
return multiLoc(value);
|
|
29
25
|
}
|
|
30
26
|
throw new TypeError(INVALID_MSG);
|
|
31
27
|
}
|
|
32
|
-
|
|
28
|
+
|
|
29
|
+
function parseObject(ast: any, variables: any) {
|
|
33
30
|
const value = Object.create(null);
|
|
34
31
|
let hasValue = false;
|
|
35
|
-
ast.fields.forEach(field => {
|
|
36
|
-
let val;
|
|
32
|
+
ast.fields.forEach((field: any) => {
|
|
33
|
+
let val: string;
|
|
37
34
|
switch (field.value.kind) {
|
|
38
|
-
case
|
|
35
|
+
case Kind.STRING:
|
|
39
36
|
val = field.value.value;
|
|
40
37
|
break;
|
|
41
|
-
case
|
|
42
|
-
val = variables
|
|
38
|
+
case Kind.VARIABLE: {
|
|
39
|
+
val = variables?.[field.value.name.value];
|
|
43
40
|
break;
|
|
44
41
|
}
|
|
45
42
|
default:
|
|
@@ -57,17 +54,18 @@ function parseObject(ast, variables) {
|
|
|
57
54
|
if (!hasValue) {
|
|
58
55
|
return null;
|
|
59
56
|
}
|
|
60
|
-
return
|
|
57
|
+
return multiLoc(value);
|
|
61
58
|
}
|
|
62
|
-
|
|
59
|
+
|
|
60
|
+
function parseLiteral(ast: any, variables: any) {
|
|
63
61
|
switch (ast.kind) {
|
|
64
|
-
case
|
|
65
|
-
return
|
|
66
|
-
case
|
|
62
|
+
case Kind.STRING:
|
|
63
|
+
return singleLoc(ast.value);
|
|
64
|
+
case Kind.OBJECT:
|
|
67
65
|
return parseObject(ast, variables);
|
|
68
|
-
case
|
|
66
|
+
case Kind.NULL:
|
|
69
67
|
return null;
|
|
70
|
-
case
|
|
68
|
+
case Kind.VARIABLE: {
|
|
71
69
|
const name = ast.name.value;
|
|
72
70
|
return variables ? parseValue(variables[name]) : undefined;
|
|
73
71
|
}
|
|
@@ -75,27 +73,32 @@ function parseLiteral(ast, variables) {
|
|
|
75
73
|
throw new TypeError(`Invalid localizable string input (${ast.kind}). Expecting either a string in the default locale, or an object like {"en": "English translation", "fr": "Traduction française"}`);
|
|
76
74
|
}
|
|
77
75
|
}
|
|
76
|
+
|
|
78
77
|
const localeTag = Symbol('_smartloc_locale');
|
|
79
|
-
|
|
78
|
+
export const GLocString = new GraphQLScalarType({
|
|
80
79
|
name: 'LocalizedString',
|
|
81
80
|
description: 'A string which value depends on Accept-Language request headers.',
|
|
82
81
|
serialize(value) {
|
|
83
|
-
if (
|
|
82
|
+
if (isLocStr(value)) {
|
|
84
83
|
throw new TypeError(`You must apply the localizeSchema() patch to fix your schema when using LocalizedString`);
|
|
85
84
|
}
|
|
85
|
+
|
|
86
86
|
if (value === null || value === undefined || typeof value === 'string') {
|
|
87
87
|
return value;
|
|
88
88
|
}
|
|
89
|
+
|
|
89
90
|
throw new TypeError(`Graphql field of type LocalizedString expecting a localizable string`);
|
|
90
91
|
},
|
|
91
92
|
parseValue,
|
|
92
93
|
parseLiteral,
|
|
93
94
|
});
|
|
95
|
+
|
|
96
|
+
|
|
94
97
|
const patchedTag = Symbol('_smartloc_patched');
|
|
95
|
-
function localizeSchema(schema) {
|
|
98
|
+
export function localizeSchema(schema: GraphQLSchema) {
|
|
96
99
|
const patched = new Set();
|
|
97
|
-
function patchObject(type) {
|
|
98
|
-
if (!(type instanceof
|
|
100
|
+
function patchObject(type: GraphQLObjectType) {
|
|
101
|
+
if (!(type instanceof GraphQLObjectType)) {
|
|
99
102
|
return;
|
|
100
103
|
}
|
|
101
104
|
const fields = type.getFields();
|
|
@@ -104,91 +107,100 @@ function localizeSchema(schema) {
|
|
|
104
107
|
}
|
|
105
108
|
patched.add(type);
|
|
106
109
|
for (const [id, field] of Object.entries(fields)) {
|
|
107
|
-
let ft = field.type;
|
|
110
|
+
let ft = (field as any).type;
|
|
108
111
|
if (!ft) {
|
|
109
112
|
continue;
|
|
110
113
|
}
|
|
114
|
+
|
|
111
115
|
while (true) {
|
|
112
|
-
if (ft instanceof
|
|
116
|
+
if (ft instanceof GraphQLList || ft instanceof GraphQLNonNull) {
|
|
113
117
|
ft = ft.ofType;
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
118
|
+
} else {
|
|
116
119
|
break;
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
|
-
if (ft instanceof
|
|
122
|
+
if (ft instanceof GraphQLUnionType) {
|
|
120
123
|
for (const t of ft.getTypes()) {
|
|
121
124
|
patchObject(t);
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
if (ft instanceof
|
|
127
|
+
if (ft instanceof GraphQLInterfaceType) {
|
|
125
128
|
for (const t of schema.getPossibleTypes(ft)) {
|
|
126
129
|
patchObject(t);
|
|
127
130
|
}
|
|
128
|
-
}
|
|
129
|
-
else if (ft instanceof graphql_1.GraphQLObjectType) {
|
|
131
|
+
} else if (ft instanceof GraphQLObjectType) {
|
|
130
132
|
patchObject(ft);
|
|
131
|
-
}
|
|
132
|
-
else if (ft === exports.GLocString) {
|
|
133
|
+
} else if (ft === GLocString) {
|
|
133
134
|
patchField(id, field);
|
|
134
|
-
}
|
|
135
|
-
else if (ft instanceof graphql_2.GraphQLScalarType && (ft.name === 'JSON' || ft.name === 'JSONObject')) {
|
|
135
|
+
} else if (ft instanceof GraphQLScalarType && (ft.name === 'JSON' || ft.name === 'JSONObject')) {
|
|
136
136
|
patchField(id, field);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
patchObject(schema.
|
|
140
|
+
|
|
141
|
+
patchObject(schema.getQueryType() as GraphQLObjectType);
|
|
142
|
+
patchObject(schema.getMutationType() as GraphQLObjectType);
|
|
142
143
|
return schema;
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
-
function patchField(id, field) {
|
|
146
|
-
|
|
147
|
-
if ((_a = field.resolve) === null || _a === void 0 ? void 0 : _a[patchedTag]) {
|
|
145
|
+
|
|
146
|
+
function patchField(id: string, field: GraphQLField<any, any>) {
|
|
147
|
+
if ((field.resolve as any)?.[patchedTag]) {
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
const or = field.resolve;
|
|
151
151
|
field.resolve = async function (source, args, ctx, info) {
|
|
152
152
|
const value = or
|
|
153
153
|
? await or.apply(this, [source, args, ctx, info])
|
|
154
|
-
: source
|
|
154
|
+
: source?.[id];
|
|
155
155
|
const locales = ctx && typeof ctx === 'object'
|
|
156
|
-
? ctx
|
|
157
|
-
|
|
156
|
+
? ctx?.[localeTag]
|
|
157
|
+
: null;
|
|
158
|
+
return withLocales(locales, () => translateInContext(value));
|
|
158
159
|
};
|
|
159
|
-
field.resolve[patchedTag] = true;
|
|
160
|
+
(field.resolve as any)[patchedTag] = true;
|
|
160
161
|
}
|
|
161
|
-
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
export interface ExpressContext {
|
|
166
|
+
req: express.Request;
|
|
167
|
+
res: express.Response;
|
|
168
|
+
}
|
|
169
|
+
export type Context<T = object> = T;
|
|
170
|
+
export type ContextFunction<FunctionParams = any, ProducedContext = object> = (
|
|
171
|
+
context: FunctionParams,
|
|
172
|
+
) => Context<ProducedContext> | Promise<Context<ProducedContext>>;
|
|
173
|
+
|
|
174
|
+
export function localizedContext(context: ContextFunction<ExpressContext, Context>): ContextFunction<ExpressContext, Context> {
|
|
162
175
|
return app => {
|
|
163
176
|
const ctx = context(app);
|
|
164
177
|
if ('then' in ctx && typeof ctx.then === 'function') {
|
|
165
178
|
return ctx.then(x => patchContextObj(app.req, x));
|
|
166
179
|
}
|
|
167
180
|
return patchContextObj(app.req, ctx);
|
|
168
|
-
}
|
|
181
|
+
}
|
|
169
182
|
}
|
|
170
|
-
|
|
171
|
-
function localizedContextObject(ctx, locales) {
|
|
183
|
+
|
|
184
|
+
export function localizedContextObject<T>(ctx: T, locales: string[]): T {
|
|
172
185
|
if (!ctx || typeof ctx !== 'object') {
|
|
173
186
|
return ctx;
|
|
174
187
|
}
|
|
175
|
-
ctx[localeTag] = locales;
|
|
188
|
+
(ctx as any)[localeTag] = locales;
|
|
176
189
|
return ctx;
|
|
177
190
|
}
|
|
178
|
-
|
|
179
|
-
function patchContextObj(req, ctx) {
|
|
180
|
-
var _a, _b;
|
|
191
|
+
|
|
192
|
+
function patchContextObj(req: express.Request, ctx: any) {
|
|
181
193
|
if (!ctx || typeof ctx !== 'object') {
|
|
182
194
|
return ctx;
|
|
183
195
|
}
|
|
184
196
|
if (ctx[localeTag]) {
|
|
185
197
|
return ctx;
|
|
186
198
|
}
|
|
187
|
-
const lh =
|
|
199
|
+
const lh = req?.headers?.['accept-language'];
|
|
188
200
|
if (!lh) {
|
|
189
201
|
return ctx;
|
|
190
202
|
}
|
|
191
|
-
ctx[localeTag] =
|
|
203
|
+
ctx[localeTag] = parser.parse(lh)
|
|
204
|
+
?.map(x => x.region ? `${x.code}-${x.region}` : x.code);
|
|
192
205
|
return ctx;
|
|
193
|
-
}
|
|
194
|
-
//# sourceMappingURL=graphql.js.map
|
|
206
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { changeLocale, addLocale, clearLocales, removeLocale, setDefaultLocale, getDefaultLocale } from './core/locale-list';
|
|
2
|
+
export { loc } from './core/tag';
|
|
3
|
+
export { withLocales, singleLoc, multiLoc, joinArray, withSerializationContext, useAsString } from './core/smartloc';
|
|
4
|
+
export { isLocStr } from './core/literal';
|
|
5
|
+
export type { LocStr, TranslationOf, StorableOf } from './core/interfaces';
|
|
6
|
+
export { translateInContext, jsonParseLocalized, toJsonStorable, translateObject, toLocalizable } from './core/json-utils';
|
|
7
|
+
export { loadJsonLocale } from './core/load';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './load';
|
package/src/node/load.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { loadLocaleFrom } from '../adapters';
|
|
4
|
+
import { addLocale } from '../core/locale-list';
|
|
5
|
+
import { convertLocale } from 'src/core/load';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loads all locales generated from CLI
|
|
9
|
+
* @param dirPath The file directory where to look for translation
|
|
10
|
+
* @param merge If true, then the translations will be merged with already existing translations.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadAllLocales(dirPath: string, merge?: boolean) {
|
|
13
|
+
for (const f of await fs.readdir(dirPath)) {
|
|
14
|
+
await loadLocale(path.join(dirPath, f), merge);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads a locale generated from CLI
|
|
21
|
+
* @param filePath The file path to load
|
|
22
|
+
* @param merge If true, then the translations will be merged with already existing translations.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadLocale(filePath: string, merge?: boolean) {
|
|
25
|
+
const locale = await loadLocaleFrom(filePath);
|
|
26
|
+
if (!locale) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const transformed = convertLocale(locale);
|
|
30
|
+
addLocale(locale.targetLanguage, transformed, merge);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { collectFromSource, Loc } from '../src/cli/collect';
|
|
3
|
+
import { autoGenerateId } from '../src/core/utils';
|
|
4
|
+
|
|
5
|
+
describe('Collection', () => {
|
|
6
|
+
function col(content: string) {
|
|
7
|
+
const all: Loc[] = [];
|
|
8
|
+
const ids = new Map();
|
|
9
|
+
collectFromSource(content, ids, all);
|
|
10
|
+
return all.map(x => {
|
|
11
|
+
const ret: { id: string; val: string; plural?: boolean } = {
|
|
12
|
+
id: x.id,
|
|
13
|
+
val: x.source,
|
|
14
|
+
};
|
|
15
|
+
if (x.plural) {
|
|
16
|
+
ret.plural = x.plural;
|
|
17
|
+
}
|
|
18
|
+
return ret;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('collect with id', () => {
|
|
23
|
+
const all = col('blah`` loc("myId") `String ${with} some ${params}`');
|
|
24
|
+
expect(all).toEqual([
|
|
25
|
+
{ id: 'myId', val: 'String {0} some {1}' }
|
|
26
|
+
])
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
it('collect without id', () => {
|
|
31
|
+
const all = col('blah`` loc `String ${with} some ${params}`');
|
|
32
|
+
const id = autoGenerateId(['String ', ' some ', '']);
|
|
33
|
+
expect(all).toEqual([
|
|
34
|
+
{ id, val: 'String {0} some {1}' }
|
|
35
|
+
])
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('collects plural', () => {
|
|
39
|
+
const all = col('blah`` loc.plural(42) `String ${with} some ${params}`');
|
|
40
|
+
const id = autoGenerateId(['String ', ' some ', '']);
|
|
41
|
+
expect(all).toEqual([
|
|
42
|
+
{ id, val: 'String {0} some {1}', plural: true }
|
|
43
|
+
])
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { reconciliate } from '../src/cli/reconciliate';
|
|
3
|
+
import { CollectResult } from '../src/cli/interfaces';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('Reconciliation', () => {
|
|
7
|
+
|
|
8
|
+
function recon(translated: CollectResult, orig: CollectResult) {
|
|
9
|
+
return reconciliate({
|
|
10
|
+
resources: translated,
|
|
11
|
+
sourceLanguage: 'en',
|
|
12
|
+
targetLanguage: 'fr',
|
|
13
|
+
}, {
|
|
14
|
+
resources: orig,
|
|
15
|
+
targetLanguage: 'en',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setupAB() {
|
|
20
|
+
const orig: CollectResult = {
|
|
21
|
+
a: { target: 'Sentence A' },
|
|
22
|
+
b: { target: 'Sentence B' },
|
|
23
|
+
};
|
|
24
|
+
const trans: CollectResult = {
|
|
25
|
+
a: { source: 'Sentence A', target: 'Phrase A' },
|
|
26
|
+
b: { source: 'Sentence B', target: 'Phrase B' },
|
|
27
|
+
};
|
|
28
|
+
return { orig, trans };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
it('does nothing when ok', () => {
|
|
32
|
+
const { orig, trans } = setupAB();
|
|
33
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
34
|
+
expect(dirty).toEqual(0);
|
|
35
|
+
expect(missing).toEqual(0);
|
|
36
|
+
expect(same).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
it('marks as dirty', () => {
|
|
41
|
+
const { orig, trans } = setupAB();
|
|
42
|
+
|
|
43
|
+
// change 'in code' sentence
|
|
44
|
+
orig.a.target = 'New sentence A';
|
|
45
|
+
|
|
46
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
47
|
+
expect(dirty).toEqual(1);
|
|
48
|
+
expect(missing).toEqual(0);
|
|
49
|
+
expect(same).toEqual([]);
|
|
50
|
+
expect(trans.a.dirty).toBe(true);
|
|
51
|
+
expect(trans.a.source).toEqual('New sentence A');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
it('adds new sentence', () => {
|
|
56
|
+
const { orig, trans } = setupAB();
|
|
57
|
+
|
|
58
|
+
// add new 'in code' sentence
|
|
59
|
+
orig.c = { target: 'Sentence C' };
|
|
60
|
+
|
|
61
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
62
|
+
expect(dirty).toEqual(0);
|
|
63
|
+
expect(missing).toEqual(1);
|
|
64
|
+
expect(same).toEqual([]);
|
|
65
|
+
expect(trans.c.source).toEqual('Sentence C');
|
|
66
|
+
expect(trans.c.target).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
it('finds on ID change', () => {
|
|
72
|
+
const { orig, trans } = setupAB();
|
|
73
|
+
|
|
74
|
+
// add new 'in code' sentence
|
|
75
|
+
orig.newb = orig.b;
|
|
76
|
+
delete orig.b;
|
|
77
|
+
|
|
78
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
79
|
+
expect(dirty).toEqual(0);
|
|
80
|
+
expect(missing).toEqual(0);
|
|
81
|
+
expect(same).toEqual([]);
|
|
82
|
+
expect(trans.b).toBeUndefined();
|
|
83
|
+
expect(trans.newb).toBeDefined();
|
|
84
|
+
expect(trans.newb.source).toEqual('Sentence B');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
it('finds on ID and content change change', () => {
|
|
89
|
+
const { orig, trans } = setupAB();
|
|
90
|
+
|
|
91
|
+
// add new 'in code' sentence
|
|
92
|
+
orig.newb = orig.b;
|
|
93
|
+
orig.newb.target = 'Sentence B bis';
|
|
94
|
+
delete orig.b;
|
|
95
|
+
|
|
96
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
97
|
+
expect(dirty).toEqual(1); // source changed => dirty
|
|
98
|
+
expect(missing).toEqual(0);
|
|
99
|
+
expect(same).toEqual([]);
|
|
100
|
+
expect(trans.b).toBeUndefined();
|
|
101
|
+
expect(trans.newb).toBeDefined();
|
|
102
|
+
expect(trans.newb.source).toEqual('Sentence B bis');
|
|
103
|
+
expect(trans.newb.target).toEqual('Phrase B');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
it('finds multiple ID and content change', () => {
|
|
109
|
+
const { orig, trans } = setupAB();
|
|
110
|
+
|
|
111
|
+
// add new 'in code' sentence
|
|
112
|
+
orig.newa = orig.a;
|
|
113
|
+
orig.newb = orig.b;
|
|
114
|
+
orig.newa.target = 'Sentence A bis';
|
|
115
|
+
orig.newb.target = 'Sentence B bis';
|
|
116
|
+
delete orig.b;
|
|
117
|
+
delete orig.a;
|
|
118
|
+
|
|
119
|
+
const { dirty, missing, same } = recon(trans, orig);
|
|
120
|
+
expect(dirty).toEqual(2); // source changed => dirty
|
|
121
|
+
expect(missing).toEqual(0);
|
|
122
|
+
expect(same).toEqual([]);
|
|
123
|
+
expect(trans.a).toBeUndefined();
|
|
124
|
+
expect(trans.b).toBeUndefined();
|
|
125
|
+
expect(trans.newa).toBeDefined();
|
|
126
|
+
expect(trans.newb).toBeDefined();
|
|
127
|
+
expect(trans.newa.source).toEqual('Sentence A bis');
|
|
128
|
+
expect(trans.newb.source).toEqual('Sentence B bis');
|
|
129
|
+
expect(trans.newa.target).toEqual('Phrase A');
|
|
130
|
+
expect(trans.newb.target).toEqual('Phrase B');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
2
|
+
import { loc, setDefaultLocale, addLocale, withLocales, clearLocales, toJsonStorable, SingleLoc, MultiLoc, jsonParseLocalized, toLocalizable } from '../src';
|
|
3
|
+
import { withSerializationContext, SmartLoc } from '../src/core/smartloc';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('Json serialization', () => {
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
clearLocales();
|
|
10
|
+
setDefaultLocale('en-US');
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('toJsonStorable() handles singleloc values', () => {
|
|
14
|
+
const str = new SingleLoc(`a translation`);
|
|
15
|
+
expect(toJsonStorable(str)).toEqual('i18n/single:a translation')
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('toJsonStorable() handles singleloc props', () => {
|
|
19
|
+
const str = { value: new SingleLoc(`a translation`) };
|
|
20
|
+
expect(toJsonStorable(str)).toEqual({ value: 'i18n/single:a translation' })
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
it('toJsonStorable() handles singleloc arrays', () => {
|
|
25
|
+
const str = { value: [new SingleLoc(`first`), new SingleLoc(`second`)] };
|
|
26
|
+
expect(toJsonStorable(str)).toEqual({ value: ['i18n/single:first', 'i18n/single:second'] })
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
it('toJsonStorable() handles multiloc', () => {
|
|
31
|
+
const str = { value: new MultiLoc({ fr: 'FR value', en: 'EN value' }) };
|
|
32
|
+
expect(toJsonStorable(str)).toEqual({ value: { 'i18n:fr': 'FR value', 'i18n:en': 'EN value', } })
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
it('toJsonStorable() handles smartloc with translate option to "id"', () => {
|
|
37
|
+
const str = { value: loc('something')`Something` };
|
|
38
|
+
const received = toJsonStorable(str, { nonSelfDescriptive: 'id' });
|
|
39
|
+
expect(received).toEqual({ value: 'i18n/id:something' })
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('toJsonStorable() handles smartloc with countable translate', () => {
|
|
43
|
+
const str = { value: loc.plural(42)('something')`Something ${42}` };
|
|
44
|
+
expect(toJsonStorable(str, { nonSelfDescriptive: 'id' })).toEqual({ value: {
|
|
45
|
+
i18n: 'something',
|
|
46
|
+
data: [42],
|
|
47
|
+
count: 42
|
|
48
|
+
} })
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
it('toJsonStorable() handles smartloc with inclusions', () => {
|
|
53
|
+
const str = { value: loc('something')`Answer: ${42} with ${'str'}` };
|
|
54
|
+
expect(toJsonStorable(str, { nonSelfDescriptive: 'id' })).toEqual({
|
|
55
|
+
value: {
|
|
56
|
+
i18n: 'something',
|
|
57
|
+
data: [42, 'str']
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('toJsonStorable() handles smartloc with translate option to "toMulti"', () => {
|
|
63
|
+
const str = { value: loc('something')`Something` };
|
|
64
|
+
addLocale('fr', { something: 'Quelque chose' })
|
|
65
|
+
expect(toJsonStorable(str, { nonSelfDescriptive: 'toMulti' })).toEqual({ value: { 'i18n:fr': 'Quelque chose', 'i18n:en-US': 'Something', } })
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
it('toJsonStorable() handles smartloc with translate option to "skip/"', () => {
|
|
70
|
+
const str = { value: loc('something')`Something` };
|
|
71
|
+
addLocale('fr', { something: 'Quelque chose' })
|
|
72
|
+
const got = toJsonStorable(str, { nonSelfDescriptive: 'skip' });
|
|
73
|
+
expect(got).toEqual(str);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('can parse a multiloc', () => {
|
|
77
|
+
const str = { value: new MultiLoc({ fr: 'FR value', en: 'EN value' }) };
|
|
78
|
+
const serial = withSerializationContext(() => JSON.stringify(str));
|
|
79
|
+
const deser = jsonParseLocalized(serial);
|
|
80
|
+
expect(deser.value).toBeInstanceOf(MultiLoc);
|
|
81
|
+
// check translation
|
|
82
|
+
addLocale('fr-FR', {});
|
|
83
|
+
const translated = withLocales(['fr'], () => JSON.stringify(deser));
|
|
84
|
+
expect(translated).toEqual('{"value":"FR value"}');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
it('can parse a singleloc', () => {
|
|
89
|
+
const str = { value: [new SingleLoc(`first`), new SingleLoc(`second`)] };
|
|
90
|
+
const serial = withSerializationContext(() => JSON.stringify(str));
|
|
91
|
+
const deser = jsonParseLocalized(serial);
|
|
92
|
+
expect(deser.value).toBeInstanceOf(Array);
|
|
93
|
+
for (const k of deser.value) {
|
|
94
|
+
expect(k).toBeInstanceOf(SingleLoc);
|
|
95
|
+
}
|
|
96
|
+
// check translation
|
|
97
|
+
addLocale('fr-FR', {});
|
|
98
|
+
const translated = withLocales(['fr'], () => JSON.stringify(deser));
|
|
99
|
+
expect(translated).toEqual('{"value":["first","second"]}');
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
it('can parse a smartloc', () => {
|
|
104
|
+
// must be registered to deserialize ids
|
|
105
|
+
addLocale('fr', { something: 'Quelque chose' });
|
|
106
|
+
addLocale('en-US', { something: 'Something' });
|
|
107
|
+
|
|
108
|
+
const str = { value: [loc('something')`Something`] };
|
|
109
|
+
const serial = withSerializationContext(() => JSON.stringify(str));
|
|
110
|
+
const deser = jsonParseLocalized(serial);
|
|
111
|
+
expect(deser.value).toBeInstanceOf(Array);
|
|
112
|
+
for (const k of deser.value) {
|
|
113
|
+
expect(k).toBeInstanceOf(SmartLoc);
|
|
114
|
+
}
|
|
115
|
+
// check translation
|
|
116
|
+
let translated = withLocales(['fr'], () => JSON.stringify(deser));
|
|
117
|
+
expect(translated).toEqual('{"value":["Quelque chose"]}');
|
|
118
|
+
translated = withLocales(['en'], () => JSON.stringify(deser));
|
|
119
|
+
expect(translated).toEqual('{"value":["Something"]}');
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
it('can parse a smartloc with arguments', () => {
|
|
124
|
+
// must be registered to deserialize ids
|
|
125
|
+
addLocale('fr', { something: 'Quelque chose {0} {1}' });
|
|
126
|
+
addLocale('en-US', { something: 'Something {0} {1}' });
|
|
127
|
+
|
|
128
|
+
const str = { value: [loc('something')`Something ${'str'} ${42}`] };
|
|
129
|
+
const serial = withSerializationContext(() => JSON.stringify(str));
|
|
130
|
+
const deser = jsonParseLocalized(serial);
|
|
131
|
+
expect(deser.value).toBeInstanceOf(Array);
|
|
132
|
+
for (const k of deser.value) {
|
|
133
|
+
expect(k).toBeInstanceOf(SmartLoc);
|
|
134
|
+
}
|
|
135
|
+
// check translation
|
|
136
|
+
let translated = withLocales(['fr'], () => JSON.stringify(deser));
|
|
137
|
+
expect(translated).toEqual('{"value":["Quelque chose str 42"]}');
|
|
138
|
+
translated = withLocales(['en'], () => JSON.stringify(deser));
|
|
139
|
+
expect(translated).toEqual('{"value":["Something str 42"]}');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
it('should return the same object instance if no smartloc instance', async () => {
|
|
144
|
+
const obj = {
|
|
145
|
+
a: { b: [42, { c: 51 }] }
|
|
146
|
+
};
|
|
147
|
+
const ret = toJsonStorable(obj);
|
|
148
|
+
expect(obj === ret).toBe(true);
|
|
149
|
+
const deser = toLocalizable(ret);
|
|
150
|
+
expect(obj === deser).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return other instance if no smartloc instance', async () => {
|
|
154
|
+
const obj = {
|
|
155
|
+
a: { b: [42, { c: loc`Whatever` }] }
|
|
156
|
+
};
|
|
157
|
+
const ret = toJsonStorable(obj);
|
|
158
|
+
expect(obj === ret).toBe(false);
|
|
159
|
+
const deser = toLocalizable(ret);
|
|
160
|
+
expect(ret === deser).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|