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
@@ -1,23 +1,19 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.localizedContextObject = exports.localizedContext = exports.localizeSchema = exports.GLocString = void 0;
7
- const graphql_1 = require("graphql");
8
- const graphql_2 = require("graphql");
9
- const _1 = require(".");
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 new smartloc_1.SingleLoc(value);
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 new smartloc_1.MultiLoc(value);
24
+ return multiLoc(value);
29
25
  }
30
26
  throw new TypeError(INVALID_MSG);
31
27
  }
32
- function parseObject(ast, variables) {
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 language_1.Kind.STRING:
35
+ case Kind.STRING:
39
36
  val = field.value.value;
40
37
  break;
41
- case language_1.Kind.VARIABLE: {
42
- val = variables === null || variables === void 0 ? void 0 : variables[field.value.name.value];
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 new smartloc_1.MultiLoc(value);
57
+ return multiLoc(value);
61
58
  }
62
- function parseLiteral(ast, variables) {
59
+
60
+ function parseLiteral(ast: any, variables: any) {
63
61
  switch (ast.kind) {
64
- case language_1.Kind.STRING:
65
- return new smartloc_1.SingleLoc(ast.value);
66
- case language_1.Kind.OBJECT:
62
+ case Kind.STRING:
63
+ return singleLoc(ast.value);
64
+ case Kind.OBJECT:
67
65
  return parseObject(ast, variables);
68
- case language_1.Kind.NULL:
66
+ case Kind.NULL:
69
67
  return null;
70
- case language_1.Kind.VARIABLE: {
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
- exports.GLocString = new graphql_2.GraphQLScalarType({
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 (_1.isLocStr(value)) {
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 graphql_1.GraphQLObjectType)) {
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 graphql_2.GraphQLList || ft instanceof graphql_2.GraphQLNonNull) {
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 graphql_2.GraphQLUnionType) {
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 graphql_2.GraphQLInterfaceType) {
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
- patchObject(schema.getQueryType());
141
- patchObject(schema.getMutationType());
140
+
141
+ patchObject(schema.getQueryType() as GraphQLObjectType);
142
+ patchObject(schema.getMutationType() as GraphQLObjectType);
142
143
  return schema;
143
144
  }
144
- exports.localizeSchema = localizeSchema;
145
- function patchField(id, field) {
146
- var _a;
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 === null || source === void 0 ? void 0 : source[id];
154
+ : source?.[id];
155
155
  const locales = ctx && typeof ctx === 'object'
156
- ? ctx === null || ctx === void 0 ? void 0 : ctx[localeTag] : null;
157
- return smartloc_1.withLocales(locales, () => json_utils_1.translateInContext(value));
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
- function localizedContext(context) {
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
- exports.localizedContext = localizedContext;
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
- exports.localizedContextObject = localizedContextObject;
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 = (_a = req === null || req === void 0 ? void 0 : req.headers) === null || _a === void 0 ? void 0 : _a['accept-language'];
199
+ const lh = req?.headers?.['accept-language'];
188
200
  if (!lh) {
189
201
  return ctx;
190
202
  }
191
- ctx[localeTag] = (_b = accept_language_parser_1.default.parse(lh)) === null || _b === void 0 ? void 0 : _b.map(x => x.region ? `${x.code}-${x.region}` : x.code);
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';
@@ -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
+ });