@l10nmonster/helpers-json 3.0.0-alpha.15 → 3.0.0-alpha.17
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/i18next.js +103 -117
- package/package.json +1 -1
- package/utils.js +65 -0
package/i18next.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { unflatten } from 'flat';
|
|
6
6
|
import { regex } from '@l10nmonster/core';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
arbPlaceholderHandler,
|
|
9
|
+
parseResourceAnnotations,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
const ALL_PLURAL_FORMS = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
|
13
|
+
const DEFAULT_SOURCE_PLURAL_FORMS = ['one', 'other'];
|
|
8
14
|
|
|
9
|
-
const isArbAnnotations = e => e[0].split('.').some(segment => segment.startsWith(ARB_ANNOTATION_MARKER));
|
|
10
|
-
const validPluralSuffixes = new Set(['one', 'other', 'zero', 'two', 'few', 'many']);
|
|
11
|
-
const extractArbGroupsRegex = /(?<prefix>.+?\.)?@(?<key>[^.]+)\.(?<attribute>.+)/;
|
|
12
15
|
const defaultArbAnnotationHandlers = {
|
|
13
16
|
description: (_, data) => (data == null ? undefined : data),
|
|
14
17
|
placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
@@ -16,139 +19,122 @@ const defaultArbAnnotationHandlers = {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
*
|
|
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" }
|
|
22
|
+
* Filter for i18next v4 JSON format.
|
|
23
|
+
* @see https://www.i18next.com/misc/json-format
|
|
50
24
|
*/
|
|
51
|
-
function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
52
|
-
if (!enableArbAnnotations) {
|
|
53
|
-
return [ Object.entries(flatten(resource)), {} ]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { res, notes } = flattenAndSplitResources([], resource)
|
|
57
|
-
const parsedNotes = {}
|
|
58
|
-
for (const [key, arbAnnotations] of Object.entries(notes)) {
|
|
59
|
-
if (typeof arbAnnotations === "object") {
|
|
60
|
-
const notes = []
|
|
61
|
-
for (const [annotation, data] of Object.entries(arbAnnotations)) {
|
|
62
|
-
const handler = arbAnnotationHandlers[annotation] ?? arbAnnotationHandlers.DEFAULT
|
|
63
|
-
if (handler != null) {
|
|
64
|
-
const val = handler(annotation, data)
|
|
65
|
-
if (val !== undefined) {
|
|
66
|
-
notes.push(val)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
parsedNotes[key] = notes.join("\n")
|
|
71
|
-
} else {
|
|
72
|
-
parsedNotes[key] = arbAnnotations
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [ Object.entries(res), parsedNotes ];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
25
|
export class I18nextFilter {
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} [params] - Configuration options
|
|
29
|
+
* @param {boolean} [params.enableArbAnnotations=false] - Enable parsing of ARB-style annotations (@key objects)
|
|
30
|
+
* @param {boolean} [params.enableArrays=false] - Enable array syntax in generated output (vs object notation)
|
|
31
|
+
* @param {boolean} [params.emitArbAnnotations=false] - Emit ARB annotations in generated output
|
|
32
|
+
* @param {Object} [params.arbAnnotationHandlers] - Custom handlers for ARB annotation types.
|
|
33
|
+
* Each handler is a function(name, data) returning a string or undefined.
|
|
34
|
+
* Built-in handlers: description, placeholders, DEFAULT.
|
|
35
|
+
*/
|
|
79
36
|
constructor(params) {
|
|
80
37
|
this.enableArbAnnotations = params?.enableArbAnnotations || false;
|
|
81
|
-
this.enablePluralSuffixes = params?.enablePluralSuffixes || false;
|
|
82
38
|
this.enableArrays = params?.enableArrays || false;
|
|
83
39
|
this.emitArbAnnotations = params?.emitArbAnnotations || false;
|
|
84
40
|
this.arbAnnotationHandlers = {
|
|
85
41
|
...defaultArbAnnotationHandlers,
|
|
86
42
|
...(params?.arbAnnotationHandlers ?? {})
|
|
87
|
-
}
|
|
43
|
+
};
|
|
88
44
|
}
|
|
89
45
|
|
|
90
|
-
async parseResource({ resource }) {
|
|
91
|
-
const response = {
|
|
92
|
-
|
|
46
|
+
async parseResource({ resource, sourcePluralForms, targetPluralForms }) {
|
|
47
|
+
const response = { segments: [] };
|
|
48
|
+
if (!resource) return response;
|
|
49
|
+
|
|
50
|
+
const unParsedResource = JSON.parse(resource);
|
|
51
|
+
const targetLangs = unParsedResource['@@targetLocales'];
|
|
52
|
+
Array.isArray(targetLangs) && (response.targetLangs = targetLangs);
|
|
53
|
+
|
|
54
|
+
const [parsedResource, notes] = parseResourceAnnotations(
|
|
55
|
+
unParsedResource,
|
|
56
|
+
this.enableArbAnnotations,
|
|
57
|
+
this.arbAnnotationHandlers,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Use provided plural forms or defaults
|
|
61
|
+
const requiredSourceForms = sourcePluralForms ?? DEFAULT_SOURCE_PLURAL_FORMS;
|
|
62
|
+
const requiredTargetForms = targetPluralForms ?? ALL_PLURAL_FORMS;
|
|
63
|
+
const targetFormsSet = new Set(requiredTargetForms);
|
|
64
|
+
|
|
65
|
+
// Collect all segments and group potential plurals by baseKey
|
|
66
|
+
const potentialPluralGroups = new Map(); // baseKey -> Map(suffix -> segment)
|
|
67
|
+
|
|
68
|
+
for (const [key, value] of parsedResource) {
|
|
69
|
+
const seg = {
|
|
70
|
+
sid: key,
|
|
71
|
+
str: value,
|
|
72
|
+
...(notes[key] && { notes: notes[key] }),
|
|
73
|
+
};
|
|
74
|
+
response.segments.push(seg);
|
|
75
|
+
|
|
76
|
+
// Check if key is a plural form (e.g., "key_one", "key_other")
|
|
77
|
+
const underscoreIdx = key.lastIndexOf('_');
|
|
78
|
+
if (underscoreIdx !== -1) {
|
|
79
|
+
const suffix = key.slice(underscoreIdx + 1);
|
|
80
|
+
if (targetFormsSet.has(suffix)) {
|
|
81
|
+
const baseKey = key.slice(0, underscoreIdx);
|
|
82
|
+
if (!potentialPluralGroups.has(baseKey)) {
|
|
83
|
+
potentialPluralGroups.set(baseKey, new Map());
|
|
84
|
+
}
|
|
85
|
+
potentialPluralGroups.get(baseKey).set(suffix, seg);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
93
88
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
|
|
90
|
+
// For groups with all required forms: set pluralForm and generate missing forms
|
|
91
|
+
for (const [baseKey, forms] of potentialPluralGroups) {
|
|
92
|
+
const hasAllForms = requiredSourceForms.every(form => forms.has(form));
|
|
93
|
+
if (!hasAllForms) continue;
|
|
94
|
+
|
|
95
|
+
// Set pluralForm on existing segments
|
|
96
|
+
for (const [suffix, seg] of forms) {
|
|
97
|
+
seg.pluralForm = suffix;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate missing forms from _other
|
|
101
|
+
const other = forms.get('other');
|
|
102
|
+
for (const suffix of requiredTargetForms) {
|
|
103
|
+
if (!forms.has(suffix)) {
|
|
104
|
+
response.segments.push({
|
|
105
|
+
sid: `${baseKey}_${suffix}`,
|
|
106
|
+
str: other.str,
|
|
107
|
+
pluralForm: suffix,
|
|
108
|
+
...(other.notes && { notes: other.notes })
|
|
109
|
+
});
|
|
108
110
|
}
|
|
109
|
-
response.segments.push(seg);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
return response;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Generate a resource file from segments
|
|
118
|
+
* @param {Object} params
|
|
119
|
+
* @param {Array} params.segments - Array of segment objects with sid, str, etc.
|
|
120
|
+
* @param {Function} params.translator - Translator function(seg) that returns translated string
|
|
121
|
+
* @param {Array} [params.targetPluralForms] - Array of plural forms required for target language
|
|
122
|
+
* @returns {Promise<string>} JSON string of the generated resource
|
|
123
|
+
*/
|
|
124
|
+
async generateResource({ segments, translator, targetPluralForms }) {
|
|
125
|
+
const targetFormsSet = targetPluralForms ? new Set(targetPluralForms) : null;
|
|
126
|
+
const flatResource = {};
|
|
127
|
+
for (const seg of segments) {
|
|
128
|
+
// Skip plural forms not needed for target language
|
|
129
|
+
if (seg.pluralForm && targetFormsSet && !targetFormsSet.has(seg.pluralForm)) {
|
|
130
|
+
continue;
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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];
|
|
149
|
-
}
|
|
132
|
+
const translatedStr = await translator(seg);
|
|
133
|
+
if (translatedStr != null) {
|
|
134
|
+
flatResource[seg.sid] = translatedStr.str;
|
|
150
135
|
}
|
|
151
136
|
}
|
|
137
|
+
|
|
152
138
|
return `${JSON.stringify(unflatten(flatResource, { object: !this.enableArrays }), null, 2)}\n`;
|
|
153
139
|
}
|
|
154
140
|
}
|
package/package.json
CHANGED
package/utils.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Intentionally uses `== null` to handle undefined and null
|
|
2
|
+
/* eslint-disable no-eq-null, eqeqeq */
|
|
3
|
+
|
|
4
|
+
import { flatten } from 'flat';
|
|
5
|
+
|
|
1
6
|
export const ARB_ANNOTATION_MARKER = "@";
|
|
2
7
|
export const FLATTEN_SEPARATOR = ".";
|
|
3
8
|
|
|
@@ -105,3 +110,63 @@ export function arbPlaceholderHandler(placeholders) {
|
|
|
105
110
|
}
|
|
106
111
|
return phs.join("\n")
|
|
107
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @function parseResourceAnnotations
|
|
116
|
+
*
|
|
117
|
+
* @description
|
|
118
|
+
* Parse resource annotations according to the given configuration.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} resource - The resource to parse.
|
|
121
|
+
* @param {boolean} enableArbAnnotations - Whether to enable annotations
|
|
122
|
+
* @param {object} arbAnnotationHandlers - An object mapping annotation names to a function which takes an annotation name and its value and returns a string.
|
|
123
|
+
*
|
|
124
|
+
* @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.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const resource = {
|
|
128
|
+
* "key": "value",
|
|
129
|
+
* "@key": {
|
|
130
|
+
* "description": "description for key",
|
|
131
|
+
* "placeholders": {
|
|
132
|
+
* "placeholder": {
|
|
133
|
+
* "example": "example for placeholder",
|
|
134
|
+
* "description": "description for placeholder",
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* };
|
|
139
|
+
* const [segments, notes] = parseResourceAnnotations(resource, true, {
|
|
140
|
+
* description: (_, data) => (data == null ? undefined : data),
|
|
141
|
+
* placeholders: (_, data) => (data == null ? undefined : arbPlaceholderHandler(data)),
|
|
142
|
+
* DEFAULT: (name, data) => (data == null ? undefined : `${name}: ${data}`),
|
|
143
|
+
* });
|
|
144
|
+
* // segments is [["key", "value"]]
|
|
145
|
+
* // notes is { "key": "description for key\nplaceholder: example for placeholder - description for placeholder" }
|
|
146
|
+
*/
|
|
147
|
+
export function parseResourceAnnotations(resource, enableArbAnnotations, arbAnnotationHandlers) {
|
|
148
|
+
if (!enableArbAnnotations) {
|
|
149
|
+
return [ Object.entries(flatten(resource)), {} ]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { res, notes } = flattenAndSplitResources([], resource)
|
|
153
|
+
const parsedNotes = {}
|
|
154
|
+
for (const [key, arbAnnotations] of Object.entries(notes)) {
|
|
155
|
+
if (typeof arbAnnotations === "object") {
|
|
156
|
+
const notesList = []
|
|
157
|
+
for (const [annotation, data] of Object.entries(arbAnnotations)) {
|
|
158
|
+
const handler = arbAnnotationHandlers[annotation] ?? arbAnnotationHandlers.DEFAULT
|
|
159
|
+
if (handler != null) {
|
|
160
|
+
const val = handler(annotation, data)
|
|
161
|
+
if (val !== undefined) {
|
|
162
|
+
notesList.push(val)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
parsedNotes[key] = notesList.join("\n")
|
|
167
|
+
} else {
|
|
168
|
+
parsedNotes[key] = arbAnnotations
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return [ Object.entries(res), parsedNotes ];
|
|
172
|
+
}
|