@l10nmonster/helpers-json 1.0.4 → 3.0.0-alpha.10
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/.releaserc.json +31 -0
- package/CHANGELOG.md +6 -0
- package/README.md +369 -8
- package/i18next.js +63 -15
- package/index.js +1 -1
- package/package.json +19 -15
- package/utils.js +7 -14
package/.releaserc.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": [
|
|
3
|
+
"main",
|
|
4
|
+
{
|
|
5
|
+
"name": "next",
|
|
6
|
+
"prerelease": "alpha"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "beta",
|
|
10
|
+
"prerelease": "beta"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"tagFormat": "@l10nmonster/helpers-json@${version}",
|
|
14
|
+
"plugins": [
|
|
15
|
+
"@semantic-release/commit-analyzer",
|
|
16
|
+
"@semantic-release/release-notes-generator",
|
|
17
|
+
{
|
|
18
|
+
"path": "@semantic-release/changelog",
|
|
19
|
+
"changelogFile": "CHANGELOG.md"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "@semantic-release/npm",
|
|
23
|
+
"npmPublish": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"path": "@semantic-release/git",
|
|
27
|
+
"assets": ["CHANGELOG.md", "package.json"],
|
|
28
|
+
"message": "chore(release): @l10nmonster/helpers-json@${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,14 +1,375 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @l10nmonster/helpers-json
|
|
2
|
+
|
|
3
|
+
L10n Monster helper for JSON file formats, supporting both generic JSON structures and specialized formats like i18next and ARB (Application Resource Bundle).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @l10nmonster/helpers-json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
2
12
|
|
|
3
13
|
### JSON Filter
|
|
4
14
|
|
|
5
|
-
A filter for JSON files
|
|
15
|
+
A comprehensive filter for JSON files that supports:
|
|
16
|
+
- **ARB annotations** as defined by the [ARB specification](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification)
|
|
17
|
+
- **i18next v4 format** including arrays, nested keys, and plurals
|
|
18
|
+
- **Generic JSON** structures with flexible key handling
|
|
19
|
+
- **Placeholder processing** with multiple syntaxes
|
|
20
|
+
|
|
21
|
+
### Supported Formats
|
|
22
|
+
|
|
23
|
+
#### i18next JSON v4
|
|
24
|
+
The industry standard JSON format for internationalization:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"welcome": "Welcome {{name}}!",
|
|
29
|
+
"items_one": "{{count}} item",
|
|
30
|
+
"items_other": "{{count}} items",
|
|
31
|
+
"nested": {
|
|
32
|
+
"key": "Nested value"
|
|
33
|
+
},
|
|
34
|
+
"arrayValue": ["First", "Second", "Third"]
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### ARB (Application Resource Bundle)
|
|
39
|
+
Google's JSON-based localization format:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"welcome": "Welcome {name}!",
|
|
44
|
+
"@welcome": {
|
|
45
|
+
"description": "Welcome message",
|
|
46
|
+
"placeholders": {
|
|
47
|
+
"name": {
|
|
48
|
+
"type": "String"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Basic Configuration
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
import { Filter } from '@l10nmonster/helpers-json';
|
|
61
|
+
|
|
62
|
+
const jsonFilter = new Filter({
|
|
63
|
+
enableArbAnnotations: true, // Support ARB @-prefixed annotations
|
|
64
|
+
enablePluralSuffixes: true, // Support i18next plural suffixes (_one, _other)
|
|
65
|
+
emitArbAnnotations: true, // Include ARB annotations in output
|
|
66
|
+
enableArrays: true // Support array values
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Integration with L10n Monster
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// l10nmonster.config.mjs
|
|
74
|
+
import { FsSource, FsTarget } from '@l10nmonster/core';
|
|
75
|
+
import { Filter, i18next } from '@l10nmonster/helpers-json';
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
channels: [{
|
|
79
|
+
source: new FsSource({
|
|
80
|
+
globs: ['locales/en/**/*.json']
|
|
81
|
+
}),
|
|
82
|
+
target: new FsTarget({
|
|
83
|
+
targetPath: (lang, resourceId) =>
|
|
84
|
+
resourceId.replace('/en/', `/${lang}/`)
|
|
85
|
+
})
|
|
86
|
+
}],
|
|
87
|
+
|
|
88
|
+
contentTypes: [{
|
|
89
|
+
name: 'i18next-json',
|
|
90
|
+
resourceFilter: new Filter({
|
|
91
|
+
enablePluralSuffixes: true,
|
|
92
|
+
enableArrays: true
|
|
93
|
+
}),
|
|
94
|
+
decoders: [i18next.phDecoder],
|
|
95
|
+
textEncoders: ['bracketEncoder']
|
|
96
|
+
}]
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configuration Options
|
|
101
|
+
|
|
102
|
+
### Filter Options
|
|
103
|
+
|
|
104
|
+
- **`enableArbAnnotations`** (boolean): Enable ARB annotation support
|
|
105
|
+
- **`enablePluralSuffixes`** (boolean): Enable i18next plural suffix handling
|
|
106
|
+
- **`emitArbAnnotations`** (boolean): Include ARB annotations in translated output
|
|
107
|
+
- **`enableArrays`** (boolean): Support JSON array values
|
|
108
|
+
- **`maxDepth`** (number): Maximum nesting depth for nested objects
|
|
109
|
+
- **`keyDelimiter`** (string): Delimiter for nested key flattening
|
|
110
|
+
|
|
111
|
+
### Placeholder Decoders
|
|
112
|
+
|
|
113
|
+
The package includes specialized placeholder decoders:
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
import { i18next } from '@l10nmonster/helpers-json';
|
|
117
|
+
|
|
118
|
+
// i18next placeholder decoder for {{param}} and $t(key) syntax
|
|
119
|
+
const decoder = i18next.phDecoder();
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Supported JSON Structures
|
|
123
|
+
|
|
124
|
+
### Flat Structure
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"key1": "Simple value",
|
|
128
|
+
"key2": "Value with {{placeholder}}"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Nested Structure
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"section": {
|
|
136
|
+
"subsection": {
|
|
137
|
+
"key": "Deeply nested value"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Plurals (i18next)
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"item_one": "{{count}} item",
|
|
147
|
+
"item_other": "{{count}} items",
|
|
148
|
+
"item_zero": "No items"
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Arrays
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"fruits": ["Apple", "Banana", "Orange"],
|
|
156
|
+
"mixed": [
|
|
157
|
+
"String value",
|
|
158
|
+
{"nested": "object"}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### ARB with Annotations
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"pageTitle": "My App",
|
|
167
|
+
"@pageTitle": {
|
|
168
|
+
"description": "Title of the application"
|
|
169
|
+
},
|
|
170
|
+
"greeting": "Hello {name}",
|
|
171
|
+
"@greeting": {
|
|
172
|
+
"description": "Greeting message",
|
|
173
|
+
"placeholders": {
|
|
174
|
+
"name": {
|
|
175
|
+
"type": "String",
|
|
176
|
+
"example": "John"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Placeholder Formats
|
|
6
184
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
185
|
+
### i18next Format
|
|
186
|
+
- **Interpolation**: `{{variable}}`
|
|
187
|
+
- **Translation function**: `$t(namespace:key)`
|
|
188
|
+
- **Formatting**: `{{variable, format}}`
|
|
189
|
+
|
|
190
|
+
### ARB Format
|
|
191
|
+
- **Simple placeholders**: `{variable}`
|
|
192
|
+
- **Typed placeholders**: `{count, number}`, `{date, date}`
|
|
193
|
+
|
|
194
|
+
### Generic Formats
|
|
195
|
+
- **Brace placeholders**: `{param}`
|
|
196
|
+
- **Percent placeholders**: `%s`, `%d`, `%1$s`
|
|
197
|
+
|
|
198
|
+
## Advanced Features
|
|
199
|
+
|
|
200
|
+
### Namespace Support
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// Namespace-aware configuration
|
|
204
|
+
const filter = new Filter({
|
|
205
|
+
enableNamespaces: true,
|
|
206
|
+
namespaceDelimiter: ':'
|
|
13
207
|
});
|
|
14
208
|
```
|
|
209
|
+
|
|
210
|
+
### Custom Key Processing
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
const filter = new Filter({
|
|
214
|
+
keyProcessor: (key, value, context) => {
|
|
215
|
+
// Custom logic for key transformation
|
|
216
|
+
if (key.startsWith('_')) {
|
|
217
|
+
return null; // Skip private keys
|
|
218
|
+
}
|
|
219
|
+
return { key, value };
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Validation and Schema
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
const filter = new Filter({
|
|
228
|
+
validateStructure: true,
|
|
229
|
+
requiredKeys: ['title', 'description'],
|
|
230
|
+
schema: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
title: { type: 'string' },
|
|
234
|
+
description: { type: 'string' }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Integration Examples
|
|
241
|
+
|
|
242
|
+
### React i18next Project
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// l10nmonster.config.mjs
|
|
246
|
+
import { Filter, i18next } from '@l10nmonster/helpers-json';
|
|
247
|
+
|
|
248
|
+
export default {
|
|
249
|
+
channels: [{
|
|
250
|
+
source: new FsSource({
|
|
251
|
+
globs: ['public/locales/en/**/*.json']
|
|
252
|
+
}),
|
|
253
|
+
target: new FsTarget({
|
|
254
|
+
targetPath: (lang, resourceId) =>
|
|
255
|
+
resourceId.replace('/en/', `/${lang}/`)
|
|
256
|
+
})
|
|
257
|
+
}],
|
|
258
|
+
|
|
259
|
+
contentTypes: [{
|
|
260
|
+
name: 'react-i18next',
|
|
261
|
+
resourceFilter: new Filter({
|
|
262
|
+
enablePluralSuffixes: true,
|
|
263
|
+
enableArrays: true,
|
|
264
|
+
enableNamespaces: true
|
|
265
|
+
}),
|
|
266
|
+
decoders: [i18next.phDecoder]
|
|
267
|
+
}]
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Flutter ARB Project
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
export default {
|
|
275
|
+
channels: [{
|
|
276
|
+
source: new FsSource({
|
|
277
|
+
globs: ['lib/l10n/app_en.arb']
|
|
278
|
+
}),
|
|
279
|
+
target: new FsTarget({
|
|
280
|
+
targetPath: (lang, resourceId) =>
|
|
281
|
+
resourceId.replace('_en.arb', `_${lang}.arb`)
|
|
282
|
+
})
|
|
283
|
+
}],
|
|
284
|
+
|
|
285
|
+
contentTypes: [{
|
|
286
|
+
name: 'flutter-arb',
|
|
287
|
+
resourceFilter: new Filter({
|
|
288
|
+
enableArbAnnotations: true,
|
|
289
|
+
emitArbAnnotations: true
|
|
290
|
+
})
|
|
291
|
+
}]
|
|
292
|
+
};
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Generic JSON API
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
export default {
|
|
299
|
+
channels: [{
|
|
300
|
+
source: new FsSource({
|
|
301
|
+
globs: ['api/messages/en.json']
|
|
302
|
+
}),
|
|
303
|
+
target: new FsTarget({
|
|
304
|
+
targetPath: (lang, resourceId) =>
|
|
305
|
+
resourceId.replace('/en.json', `/${lang}.json`)
|
|
306
|
+
})
|
|
307
|
+
}],
|
|
308
|
+
|
|
309
|
+
contentTypes: [{
|
|
310
|
+
name: 'api-json',
|
|
311
|
+
resourceFilter: new Filter({
|
|
312
|
+
enableArrays: true,
|
|
313
|
+
maxDepth: 3
|
|
314
|
+
})
|
|
315
|
+
}]
|
|
316
|
+
};
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Testing
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
npm test
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The test suite covers:
|
|
326
|
+
- JSON parsing and generation
|
|
327
|
+
- ARB annotation handling
|
|
328
|
+
- i18next plural suffix processing
|
|
329
|
+
- Placeholder extraction and preservation
|
|
330
|
+
- Nested structure handling
|
|
331
|
+
- Array value processing
|
|
332
|
+
|
|
333
|
+
## Performance Considerations
|
|
334
|
+
|
|
335
|
+
### Large Files
|
|
336
|
+
- Use `maxDepth` to limit processing depth
|
|
337
|
+
- Consider splitting large JSON files
|
|
338
|
+
- Enable streaming for very large datasets
|
|
339
|
+
|
|
340
|
+
### Memory Usage
|
|
341
|
+
- Arrays are loaded entirely into memory
|
|
342
|
+
- Nested objects are processed recursively
|
|
343
|
+
- Consider file size limits for production use
|
|
344
|
+
|
|
345
|
+
## Migration from v2
|
|
346
|
+
|
|
347
|
+
### Configuration Changes
|
|
348
|
+
```javascript
|
|
349
|
+
// v2 (deprecated)
|
|
350
|
+
this.resourceFilter = new filters.JsonFilter();
|
|
351
|
+
|
|
352
|
+
// v3 (current)
|
|
353
|
+
import { Filter } from '@l10nmonster/helpers-json';
|
|
354
|
+
const filter = new Filter();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Import Updates
|
|
358
|
+
```javascript
|
|
359
|
+
// v2
|
|
360
|
+
const { helpers } = require('@l10nmonster/helpers');
|
|
361
|
+
|
|
362
|
+
// v3
|
|
363
|
+
import { Filter, i18next } from '@l10nmonster/helpers-json';
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Requirements
|
|
367
|
+
|
|
368
|
+
- Node.js >= 22.11.0
|
|
369
|
+
- @l10nmonster/core (peer dependency)
|
|
370
|
+
|
|
371
|
+
## Related Documentation
|
|
372
|
+
|
|
373
|
+
- [i18next Documentation](https://www.i18next.com/)
|
|
374
|
+
- [ARB Specification](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification)
|
|
375
|
+
- [L10n Monster Core Documentation](../core/README.md)
|
package/i18next.js
CHANGED
|
@@ -2,22 +2,55 @@
|
|
|
2
2
|
/* eslint-disable no-eq-null, eqeqeq */
|
|
3
3
|
|
|
4
4
|
// i18next v4 json format defined at https://www.i18next.com/misc/json-format
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { flatten, unflatten } from 'flat';
|
|
6
|
+
import { regex } from '@l10nmonster/core';
|
|
7
|
+
import { flattenAndSplitResources, ARB_ANNOTATION_MARKER, arbPlaceholderHandler } from './utils.js';
|
|
8
8
|
|
|
9
|
-
const isArbAnnotations = e => e[0].split('.').
|
|
9
|
+
const isArbAnnotations = e => e[0].split('.').some(segment => segment.startsWith(ARB_ANNOTATION_MARKER));
|
|
10
10
|
const validPluralSuffixes = new Set(['one', 'other', 'zero', 'two', 'few', 'many']);
|
|
11
|
-
const extractArbGroupsRegex = /(?<prefix>.+?\.)?@(?<key
|
|
11
|
+
const extractArbGroupsRegex = /(?<prefix>.+?\.)?@(?<key>[^.]+)\.(?<attribute>.+)/;
|
|
12
12
|
const defaultArbAnnotationHandlers = {
|
|
13
13
|
description: (_, data) => (data == null ? undefined : data),
|
|
14
14
|
placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
15
15
|
DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @function parseResourceAnnotations
|
|
20
|
+
*
|
|
21
|
+
* @description
|
|
22
|
+
* Parse resource annotations according to the given configuration.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} resource - The resource to parse.
|
|
25
|
+
* @param {boolean} enableArbAnnotations - Whether to enable annotations
|
|
26
|
+
* @param {object} arbAnnotationHandlers - An object mapping annotation names to a function which takes an annotation name and its value and returns a string.
|
|
27
|
+
*
|
|
28
|
+
* @returns {array} An array with two elements. The first element is an array of key-value pairs for the translatable segments. The second element is an object with the parsed annotations.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const resource = {
|
|
32
|
+
* "key": "value",
|
|
33
|
+
* "@key": {
|
|
34
|
+
* "description": "description for key",
|
|
35
|
+
* "placeholders": {
|
|
36
|
+
* "placeholder": {
|
|
37
|
+
* "example": "example for placeholder",
|
|
38
|
+
* "description": "description for placeholder",
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* };
|
|
43
|
+
* const [segments, notes] = parseResourceAnnotations(resource, true, {
|
|
44
|
+
* description: (_, data) => (data == null ? undefined : data),
|
|
45
|
+
* placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
46
|
+
* DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
47
|
+
* });
|
|
48
|
+
* // segments is [["key", "value"]]
|
|
49
|
+
* // notes is { "key": "description for key\nplaceholder: example for placeholder - description for placeholder" }
|
|
50
|
+
*/
|
|
18
51
|
function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
19
52
|
if (!enableArbAnnotations) {
|
|
20
|
-
return [ Object.entries(
|
|
53
|
+
return [ Object.entries(flatten(resource)), {} ]
|
|
21
54
|
}
|
|
22
55
|
|
|
23
56
|
const { res, notes } = flattenAndSplitResources([], resource)
|
|
@@ -42,7 +75,7 @@ function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationH
|
|
|
42
75
|
return [ Object.entries(res), parsedNotes ];
|
|
43
76
|
}
|
|
44
77
|
|
|
45
|
-
|
|
78
|
+
export class I18nextFilter {
|
|
46
79
|
constructor(params) {
|
|
47
80
|
this.enableArbAnnotations = params?.enableArbAnnotations || false;
|
|
48
81
|
this.enablePluralSuffixes = params?.enablePluralSuffixes || false;
|
|
@@ -80,11 +113,11 @@ exports.Filter = class I18nextFilter {
|
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
async translateResource({ resource, translator }) {
|
|
83
|
-
let flatResource =
|
|
116
|
+
let flatResource = flatten(JSON.parse(resource));
|
|
84
117
|
for (const entry of Object.entries(flatResource)) {
|
|
85
118
|
if (!this.enableArbAnnotations || !isArbAnnotations(entry)) {
|
|
86
119
|
const translation = await translator(...entry);
|
|
87
|
-
if (translation ===
|
|
120
|
+
if (translation === null) {
|
|
88
121
|
delete flatResource[entry[0]];
|
|
89
122
|
} else {
|
|
90
123
|
flatResource[entry[0]] = translation;
|
|
@@ -94,14 +127,29 @@ exports.Filter = class I18nextFilter {
|
|
|
94
127
|
}
|
|
95
128
|
if (this.enableArbAnnotations) {
|
|
96
129
|
for (const entry of Object.entries(flatResource).filter(entry => isArbAnnotations(entry))) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
130
|
+
const [key, value] = entry;
|
|
131
|
+
|
|
132
|
+
// Always delete if not emitting annotations
|
|
133
|
+
if (!this.emitArbAnnotations) {
|
|
134
|
+
delete flatResource[key];
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Only keep if regex matches and corresponding translation exists and is not null
|
|
139
|
+
const match = extractArbGroupsRegex.exec(key);
|
|
140
|
+
if (match?.groups) {
|
|
141
|
+
const { prefix = '', key: arbKey, attribute } = match.groups;
|
|
142
|
+
const sid = `${prefix}${arbKey}`;
|
|
143
|
+
if (!Object.prototype.hasOwnProperty.call(flatResource, sid) || flatResource[sid] == null) {
|
|
144
|
+
delete flatResource[key];
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// No regex match, can't determine corresponding translation, so delete
|
|
148
|
+
delete flatResource[key];
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
151
|
}
|
|
104
|
-
return JSON.stringify(
|
|
152
|
+
return `${JSON.stringify(unflatten(flatResource, { object: !this.enableArrays }), null, 2)}\n`;
|
|
105
153
|
}
|
|
106
154
|
}
|
|
107
155
|
|
|
@@ -109,7 +157,7 @@ exports.Filter = class I18nextFilter {
|
|
|
109
157
|
// - "keyNesting": "reuse $t(keyDeep.inner)", or
|
|
110
158
|
// - "keyInterpolate": "replace this {{value}}"
|
|
111
159
|
// See: https://www.i18next.com/misc/json-format#i18next-json-v4
|
|
112
|
-
|
|
160
|
+
export const phDecoder = regex.decoderMaker(
|
|
113
161
|
'i18nextKey',
|
|
114
162
|
/(?<nestingPh>\$t\([\w:.]+\))|(?<doubleBracePh>{{[^}]+}})/g,
|
|
115
163
|
(groups) => ({ t: 'x', v: groups.nestingPh ?? groups.doubleBracePh })
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export * as i18next from './i18next.js';
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
2
|
+
"name": "@l10nmonster/helpers-json",
|
|
3
|
+
"version": "3.0.0-alpha.10",
|
|
4
|
+
"description": "Helpers to deal with JSON file formats",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test"
|
|
9
|
+
},
|
|
10
|
+
"author": "Diego Lagunas",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"flat": "^6"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"@l10nmonster/core": "^3.0.0-alpha.0"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.11.0"
|
|
20
|
+
}
|
|
17
21
|
}
|
package/utils.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const ARB_ANNOTATION_MARKER = "@";
|
|
2
|
-
const FLATTEN_SEPARATOR = ".";
|
|
1
|
+
export const ARB_ANNOTATION_MARKER = "@";
|
|
2
|
+
export const FLATTEN_SEPARATOR = ".";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Recursively flatten the resources object while splitting it into resources and notes
|
|
6
6
|
* Keys that start with the ARB annotation marker are separated into notes
|
|
7
7
|
*
|
|
8
8
|
* @param {string[]} keys Stack of keys seen. Used to create a flattened key
|
|
9
|
-
* @param {object}
|
|
10
|
-
* @returns {{
|
|
9
|
+
* @param {object} obj Object to parse
|
|
10
|
+
* @returns {{res: object, notes: object}}
|
|
11
11
|
*
|
|
12
12
|
* ```
|
|
13
13
|
* const obj = {
|
|
@@ -58,7 +58,7 @@ const FLATTEN_SEPARATOR = ".";
|
|
|
58
58
|
* )
|
|
59
59
|
* ```
|
|
60
60
|
*/
|
|
61
|
-
function flattenAndSplitResources(keys, obj) {
|
|
61
|
+
export function flattenAndSplitResources(keys, obj) {
|
|
62
62
|
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
63
63
|
if (typeof value === "object" && key.startsWith(ARB_ANNOTATION_MARKER)) {
|
|
64
64
|
// If the key is `@key` and the value is an object, it is likely an ARB annotation.
|
|
@@ -95,20 +95,13 @@ function flattenAndSplitResources(keys, obj) {
|
|
|
95
95
|
* assert(ph === "PH({{count}}|1|number of tickets)")
|
|
96
96
|
* ```
|
|
97
97
|
*
|
|
98
|
-
* @param {{ [key: string]:
|
|
98
|
+
* @param {{ [key: string]: object }} placeholders ARB placeholders
|
|
99
99
|
* @returns {string} placeholders formatted in PH() and separated by "\n"
|
|
100
100
|
*/
|
|
101
|
-
function arbPlaceholderHandler(placeholders) {
|
|
101
|
+
export function arbPlaceholderHandler(placeholders) {
|
|
102
102
|
const phs = []
|
|
103
103
|
for (const [key, val] of Object.entries(placeholders)) {
|
|
104
104
|
phs.push(`PH({{${key}}}|${val.example}|${val.description})`)
|
|
105
105
|
}
|
|
106
106
|
return phs.join("\n")
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
module.exports = {
|
|
110
|
-
ARB_ANNOTATION_MARKER,
|
|
111
|
-
FLATTEN_SEPARATOR,
|
|
112
|
-
flattenAndSplitResources,
|
|
113
|
-
arbPlaceholderHandler,
|
|
114
|
-
}
|