@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.
Files changed (4) hide show
  1. package/README.md +3 -2
  2. package/i18next.js +46 -17
  3. package/package.json +1 -1
  4. 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
- function parseResourceAnnotations(resource, enableArbAnnotations) {
11
- let parsedResource = Object.entries(flat.flatten(resource));
12
- const notes = {};
13
- if (enableArbAnnotations) {
14
- for (const [key, value] of parsedResource.filter(isArbAnnotations)) {
15
- const arbGroups = extractArbGroupsRegex.exec(key).groups;
16
- const sid = `${arbGroups.prefix ?? ''}${arbGroups.key}`;
17
- if (validArbAnnotations.has(arbGroups.attribute)) {
18
- notes[sid] = `${notes[sid] ? `${notes[sid]}\n` : ''}${arbGroups.attribute === 'description' ? '' : `${arbGroups.attribute}: `}${arbGroups.attribute === 'placeholders' ? JSON.stringify(value) : value}`;
19
- } else {
20
- l10nmonster.logger.verbose(`Unexpected ${arbGroups.attribute} annotation for SID ${sid}`);
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
- enableArbAnnotations && (parsedResource = parsedResource.filter(e => !isArbAnnotations(e)));
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
- const [ parsedResource, notes ] = parseResourceAnnotations(JSON.parse(resource), this.enableArbAnnotations);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l10nmonster/helpers-json",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Helpers to deal with JSON file formats",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
+ }