@l10nmonster/helpers-json 1.0.0 → 1.0.3
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/README.md +3 -2
- package/i18next.js +46 -17
- package/package.json +1 -1
- package/utils.js +114 -0
package/README.md
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
### JSON Filter
|
|
4
4
|
|
|
5
|
-
A filter for JSON files. It supports annotations as defined by the [ARB spec](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification). In addition it supports nested keys and plurals as defined by the [i18next JSON v4](https://www.i18next.com/misc/json-format) format.
|
|
5
|
+
A filter for JSON files. It supports annotations as defined by the [ARB spec](https://github.com/google/app-resource-bundle/wiki/ApplicationResourceBundleSpecification). In addition it supports arrays, nested keys and plurals as defined by the [i18next JSON v4](https://www.i18next.com/misc/json-format) format.
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
8
|
this.resourceFilter = new filters.JsonFilter({
|
|
9
9
|
enableArbAnnotations: true,
|
|
10
10
|
enablePluralSuffixes: true,
|
|
11
|
-
emitArbAnnotations: true
|
|
11
|
+
emitArbAnnotations: true,
|
|
12
|
+
enableArrays: true
|
|
12
13
|
});
|
|
13
14
|
```
|
package/i18next.js
CHANGED
|
@@ -1,40 +1,69 @@
|
|
|
1
|
+
// Intentionally uses `== null` to handle undefined and null
|
|
2
|
+
/* eslint-disable no-eq-null, eqeqeq */
|
|
3
|
+
|
|
1
4
|
// i18next v4 json format defined at https://www.i18next.com/misc/json-format
|
|
2
5
|
const flat = require('flat');
|
|
3
6
|
const { regex } = require('@l10nmonster/helpers');
|
|
7
|
+
const { flattenAndSplitResources, ARB_ANNOTATION_MARKER, arbPlaceholderHandler } = require('./utils');
|
|
4
8
|
|
|
5
|
-
const isArbAnnotations = e => e[0].split('.').slice(-2)[0].startsWith(
|
|
6
|
-
const validArbAnnotations = new Set(['description', 'type', 'context', 'placeholders', 'screenshot', 'video', 'source_text']);
|
|
9
|
+
const isArbAnnotations = e => e[0].split('.').slice(-2)[0].startsWith(ARB_ANNOTATION_MARKER);
|
|
7
10
|
const validPluralSuffixes = new Set(['one', 'other', 'zero', 'two', 'few', 'many']);
|
|
8
11
|
const extractArbGroupsRegex = /(?<prefix>.+?\.)?@(?<key>\S+)\.(?<attribute>\S+)/;
|
|
12
|
+
const defaultArbAnnotationHandlers = {
|
|
13
|
+
description: (_, data) => (data == null ? undefined : data),
|
|
14
|
+
placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
15
|
+
DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
19
|
+
if (!enableArbAnnotations) {
|
|
20
|
+
return [ Object.entries(flat.flatten(resource)), {} ]
|
|
21
|
+
}
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const notes
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const { res, notes } = flattenAndSplitResources([], resource)
|
|
24
|
+
const parsedNotes = {}
|
|
25
|
+
for (const [key, arbAnnotations] of Object.entries(notes)) {
|
|
26
|
+
if (typeof arbAnnotations === "object") {
|
|
27
|
+
const notes = []
|
|
28
|
+
for (const [annotation, data] of Object.entries(arbAnnotations)) {
|
|
29
|
+
const handler = arbAnnotationHandlers[annotation] ?? arbAnnotationHandlers.DEFAULT
|
|
30
|
+
if (handler != null) {
|
|
31
|
+
const val = handler(annotation, data)
|
|
32
|
+
if (val !== undefined) {
|
|
33
|
+
notes.push(val)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
}
|
|
37
|
+
parsedNotes[key] = notes.join("\n")
|
|
38
|
+
} else {
|
|
39
|
+
parsedNotes[key] = arbAnnotations
|
|
22
40
|
}
|
|
23
41
|
}
|
|
24
|
-
|
|
25
|
-
return [ parsedResource, notes ];
|
|
42
|
+
return [ Object.entries(res), parsedNotes ];
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
exports.Filter = class I18nextFilter {
|
|
29
46
|
constructor(params) {
|
|
30
47
|
this.enableArbAnnotations = params?.enableArbAnnotations || false;
|
|
31
48
|
this.enablePluralSuffixes = params?.enablePluralSuffixes || false;
|
|
49
|
+
this.enableArrays = params?.enableArrays || false;
|
|
32
50
|
this.emitArbAnnotations = params?.emitArbAnnotations || false;
|
|
51
|
+
this.arbAnnotationHandlers = {
|
|
52
|
+
...defaultArbAnnotationHandlers,
|
|
53
|
+
...(params?.arbAnnotationHandlers ?? {})
|
|
54
|
+
}
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
async parseResource({ resource }) {
|
|
36
58
|
const segments = [];
|
|
37
|
-
|
|
59
|
+
if (!resource) {
|
|
60
|
+
return { segments };
|
|
61
|
+
}
|
|
62
|
+
const [ parsedResource, notes ] = parseResourceAnnotations(
|
|
63
|
+
JSON.parse(resource),
|
|
64
|
+
this.enableArbAnnotations,
|
|
65
|
+
this.arbAnnotationHandlers,
|
|
66
|
+
);
|
|
38
67
|
for (const [key, value] of parsedResource) {
|
|
39
68
|
let seg = { sid: key, str: value };
|
|
40
69
|
notes[key] && (seg.notes = notes[key]);
|
|
@@ -70,7 +99,7 @@ exports.Filter = class I18nextFilter {
|
|
|
70
99
|
}
|
|
71
100
|
}
|
|
72
101
|
}
|
|
73
|
-
return JSON.stringify(flat.unflatten(flatResource), null, 2);
|
|
102
|
+
return JSON.stringify(flat.unflatten(flatResource, { object: !this.enableArrays }), null, 2) + '\n';
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
|
package/package.json
CHANGED
package/utils.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const ARB_ANNOTATION_MARKER = "@";
|
|
2
|
+
const FLATTEN_SEPARATOR = ".";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively flatten the resources object while splitting it into resources and notes
|
|
6
|
+
* Keys that start with the ARB annotation marker are separated into notes
|
|
7
|
+
*
|
|
8
|
+
* @param {string[]} keys Stack of keys seen. Used to create a flattened key
|
|
9
|
+
* @param {object} resource Object to parse
|
|
10
|
+
* @returns {{resource: object, notes: object}}
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* const obj = {
|
|
14
|
+
* str: "string",
|
|
15
|
+
* "@str": {
|
|
16
|
+
* description: "string",
|
|
17
|
+
* },
|
|
18
|
+
* ns1: {
|
|
19
|
+
* str: "string, {{foo}}",
|
|
20
|
+
* "@str": {
|
|
21
|
+
* description: "string",
|
|
22
|
+
* placeholders: { foo: { example: "foo example", description: "foo description" } }
|
|
23
|
+
* },
|
|
24
|
+
* ns2: {
|
|
25
|
+
* str: "string",
|
|
26
|
+
* "@str": {
|
|
27
|
+
* description: "string",
|
|
28
|
+
* },
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
* const {res, notes} = flattenAndSplitResources([], obj)
|
|
33
|
+
* assert(
|
|
34
|
+
* JSON.stringify(res) === JSON.stringify({
|
|
35
|
+
* str: 'string',
|
|
36
|
+
* 'ns1.str': 'string, {{foo}}',
|
|
37
|
+
* 'ns1.ns2.str': 'string'
|
|
38
|
+
* })
|
|
39
|
+
* )
|
|
40
|
+
* assert(
|
|
41
|
+
* JSON.stringify(notes) === JSON.stringify({
|
|
42
|
+
* str: {
|
|
43
|
+
* description: 'string'
|
|
44
|
+
* },
|
|
45
|
+
* 'ns1.str': {
|
|
46
|
+
* description: 'string',
|
|
47
|
+
* placeholders: {
|
|
48
|
+
* foo: {
|
|
49
|
+
* example: "foo example",
|
|
50
|
+
* description: "foo description"
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* },
|
|
54
|
+
* 'ns1.ns2.str': {
|
|
55
|
+
* description: 'string'
|
|
56
|
+
* }
|
|
57
|
+
* })
|
|
58
|
+
* )
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
function flattenAndSplitResources(keys, obj) {
|
|
62
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
63
|
+
if (typeof value === "object" && key.startsWith(ARB_ANNOTATION_MARKER)) {
|
|
64
|
+
// If the key is `@key` and the value is an object, it is likely an ARB annotation.
|
|
65
|
+
// Put it in the `notes` object.
|
|
66
|
+
const k = keys.slice().concat(key.slice(1))
|
|
67
|
+
acc.notes[k.join(FLATTEN_SEPARATOR)] = value
|
|
68
|
+
} else if (typeof value === "object") {
|
|
69
|
+
// If the key is _not_ `@key` and the value is an object, it is a namespace.
|
|
70
|
+
// Recursively flatten and split the value.
|
|
71
|
+
const { res, notes } = flattenAndSplitResources([...keys, key], value)
|
|
72
|
+
Object.assign(acc.res, res)
|
|
73
|
+
Object.assign(acc.notes, notes)
|
|
74
|
+
} else {
|
|
75
|
+
// If the value is _not_ an object, it is a key-value pair of resources.
|
|
76
|
+
// Put it in the `res` object.
|
|
77
|
+
const k = keys.concat(key)
|
|
78
|
+
acc.res[k.join(FLATTEN_SEPARATOR)] = value
|
|
79
|
+
}
|
|
80
|
+
return acc
|
|
81
|
+
}, { res: {}, notes: {} })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Accepts ARB placeholders and returns them in PH() format
|
|
86
|
+
*
|
|
87
|
+
* ```
|
|
88
|
+
* const placeholders = {
|
|
89
|
+
* count: {
|
|
90
|
+
* example: "1",
|
|
91
|
+
* description: "number of tickets"
|
|
92
|
+
* }
|
|
93
|
+
* }
|
|
94
|
+
* const ph = arbPlaceholderHandler(placeholders)
|
|
95
|
+
* assert(ph === "PH({{count}}|1|number of tickets)")
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @param {{ [key: string]: value }} ARB placeholders
|
|
99
|
+
* @returns {string} placeholders formatted in PH() and separated by "\n"
|
|
100
|
+
*/
|
|
101
|
+
function arbPlaceholderHandler(placeholders) {
|
|
102
|
+
const phs = []
|
|
103
|
+
for (const [key, val] of Object.entries(placeholders)) {
|
|
104
|
+
phs.push(`PH({{${key}}}|${val.example}|${val.description})`)
|
|
105
|
+
}
|
|
106
|
+
return phs.join("\n")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
ARB_ANNOTATION_MARKER,
|
|
111
|
+
FLATTEN_SEPARATOR,
|
|
112
|
+
flattenAndSplitResources,
|
|
113
|
+
arbPlaceholderHandler,
|
|
114
|
+
}
|