@orderly.network/i18n 3.0.0-beta.1 → 3.0.0-beta.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 (57) hide show
  1. package/README.md +23 -469
  2. package/dist/{constant-BeXwHrGj.d.mts → constant-DkvDyddr.d.mts} +37 -40
  3. package/dist/{constant-BeXwHrGj.d.ts → constant-DkvDyddr.d.ts} +37 -40
  4. package/dist/constant.d.mts +1 -1
  5. package/dist/constant.d.ts +1 -1
  6. package/dist/constant.js.map +1 -1
  7. package/dist/constant.mjs.map +1 -1
  8. package/dist/index.d.mts +69 -30
  9. package/dist/index.d.ts +69 -30
  10. package/dist/index.js +140 -118
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +132 -115
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/locale.csv +33 -106
  15. package/dist/locales/de.json +33 -38
  16. package/dist/locales/en.json +33 -38
  17. package/dist/locales/es.json +33 -38
  18. package/dist/locales/fr.json +33 -38
  19. package/dist/locales/id.json +33 -38
  20. package/dist/locales/it.json +33 -38
  21. package/dist/locales/ja.json +33 -38
  22. package/dist/locales/ko.json +33 -38
  23. package/dist/locales/nl.json +33 -38
  24. package/dist/locales/pl.json +33 -38
  25. package/dist/locales/pt.json +33 -38
  26. package/dist/locales/ru.json +33 -38
  27. package/dist/locales/tc.json +33 -38
  28. package/dist/locales/tr.json +33 -38
  29. package/dist/locales/uk.json +33 -38
  30. package/dist/locales/vi.json +33 -38
  31. package/dist/locales/zh.json +33 -38
  32. package/dist/utils.d.mts +1 -1
  33. package/dist/utils.d.ts +1 -1
  34. package/dist/utils.js +40 -45
  35. package/dist/utils.js.map +1 -1
  36. package/dist/utils.mjs +40 -45
  37. package/dist/utils.mjs.map +1 -1
  38. package/docs/guide/AGENTS.md +109 -0
  39. package/docs/guide/cli.md +133 -0
  40. package/docs/guide/examples.md +455 -0
  41. package/docs/guide/exports.md +14 -0
  42. package/docs/guide/integration.md +223 -0
  43. package/docs/guide/utils.md +14 -0
  44. package/package.json +8 -6
  45. package/scripts/copyLocales.js +11 -0
  46. package/scripts/csv2json.js +28 -0
  47. package/scripts/diffCsv.js +175 -0
  48. package/scripts/fillJson.js +33 -0
  49. package/scripts/filterLocaleKeys.js +127 -0
  50. package/scripts/generateCsv.js +36 -0
  51. package/scripts/generateEnJson.js +11 -0
  52. package/scripts/generateMissingKeys.js +49 -0
  53. package/scripts/json-csv-converter.js +286 -0
  54. package/scripts/json2csv.js +38 -0
  55. package/scripts/mergeJson.js +67 -0
  56. package/scripts/separateJson.js +50 -0
  57. package/scripts/utils.js +94 -0
@@ -0,0 +1,36 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { checkFileExists } = require("./utils");
4
+ const { multiJson2Csv } = require("./json-csv-converter");
5
+ const { defaultLanguages } = require("../dist");
6
+
7
+ /** Generate a locale CSV file */
8
+ async function generateCsv(inputDir, outputPath) {
9
+ const headers = [""];
10
+ const jsonList = [];
11
+
12
+ for (const item of defaultLanguages) {
13
+ headers.push(item.localCode);
14
+ const json = await fs.readJSON(
15
+ path.resolve(inputDir, `${item.localCode}.json`),
16
+ {
17
+ encoding: "utf8",
18
+ },
19
+ );
20
+ jsonList.push(json);
21
+ }
22
+
23
+ const csv = multiJson2Csv(jsonList, headers);
24
+
25
+ const csvPath = path.resolve(outputPath);
26
+
27
+ await checkFileExists(outputPath);
28
+
29
+ await fs.outputFile(outputPath, csv, { encoding: "utf8" });
30
+
31
+ console.log("generateCsv success =>", csvPath);
32
+ }
33
+
34
+ module.exports = {
35
+ generateCsv,
36
+ };
@@ -0,0 +1,11 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { en } = require("../dist");
4
+
5
+ async function generateEnJson() {
6
+ const outPath = path.resolve(__dirname, "../locales/en.json");
7
+ const jsonData = JSON.stringify(en, null, 2);
8
+ await fs.outputFile(outPath, jsonData, { encoding: "utf8" });
9
+ }
10
+
11
+ generateEnJson();
@@ -0,0 +1,49 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { getMissingKeys } = require("./json-csv-converter");
4
+ const { checkFileExists, findJsonFiles } = require("./utils");
5
+ const { LocaleEnum } = require("../dist");
6
+
7
+ async function generateMissingKeys() {
8
+ const inputDir = path.resolve(__dirname, "../locales");
9
+ const jsonFiles = await findJsonFiles(inputDir);
10
+
11
+ // Sort input files by locale
12
+ jsonFiles.sort((a, b) => (b.startsWith(LocaleEnum.en) ? 1 : -1));
13
+
14
+ const jsonList = [];
15
+ const headers = [""];
16
+
17
+ for (const file of jsonFiles) {
18
+ const jsonPath = path.resolve(inputDir, file);
19
+
20
+ const json = await fs.readJSON(jsonPath, {
21
+ encoding: "utf8",
22
+ });
23
+
24
+ jsonList.push(json);
25
+
26
+ const fileName = path.basename(file, path.extname(file));
27
+ headers.push(fileName);
28
+ }
29
+
30
+ const errors = getMissingKeys(jsonList, headers);
31
+
32
+ const missingJson = {};
33
+ for (const locale of Object.values(errors)) {
34
+ for (const [key, value] of Object.entries(locale)) {
35
+ missingJson[key] = value;
36
+ }
37
+ }
38
+
39
+ const outputPath = path.resolve(inputDir, "extend/en.json");
40
+
41
+ // don't check file exists
42
+ // await checkFileExists(outputPath);
43
+
44
+ await fs.outputFile(outputPath, JSON.stringify(missingJson, null, 2), {
45
+ encoding: "utf8",
46
+ });
47
+ }
48
+
49
+ generateMissingKeys();
@@ -0,0 +1,286 @@
1
+ const { validateI18nValue } = require("./utils");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ const separator = ".";
6
+
7
+ function multiJson2Csv(jsonList, header) {
8
+ if (!Array.isArray(jsonList) || jsonList.length === 0) {
9
+ throw "Object array please.";
10
+ }
11
+
12
+ const ignoreKeys = loadIgnoreKeys();
13
+ const baseJson = jsonList[0];
14
+ const baseKeys = Object.keys(baseJson);
15
+ const errors = {};
16
+ let result = header.length ? [header] : [];
17
+ for (const key of baseKeys) {
18
+ const values = [];
19
+ for (const [index, json] of jsonList.entries()) {
20
+ const val = json[key] || "";
21
+ values.push(val);
22
+ // Skip validation if key is in ignore list
23
+ if (!shouldIgnoreKey(key, ignoreKeys)) {
24
+ const bool = validateI18nValue(val);
25
+ if (!bool.valid) {
26
+ const locale = header[index + 1];
27
+ if (!errors[locale]) {
28
+ errors[locale] = {};
29
+ }
30
+ errors[locale][key] = baseJson[key];
31
+ }
32
+ }
33
+ }
34
+ result.push(stringsToCsvLine([key, ...values]));
35
+ }
36
+ if (Object.keys(errors).length > 0) {
37
+ throw new Error(
38
+ "valid i18n value failed, please check the value of the following values: " +
39
+ JSON.stringify(errors, null, 4),
40
+ );
41
+ }
42
+ return result.join("\n");
43
+ }
44
+
45
+ function csv2multiJson(csv) {
46
+ if (typeof csv !== "string") throw "String please.";
47
+ const json = {};
48
+
49
+ const ignoreKeys = loadIgnoreKeys();
50
+ const lines = csvToLines(csv);
51
+ const csvLines = lines.filter(Boolean).map(parseCsvLine);
52
+
53
+ const headers = csvLines.shift();
54
+
55
+ const errors = {};
56
+ for (const [index, header] of headers.entries()) {
57
+ json[header] = {};
58
+ for (const line of csvLines) {
59
+ const [key, ...values] = line;
60
+ if (!key) {
61
+ continue;
62
+ }
63
+ const val = values[index];
64
+ json[header][key] = val;
65
+ // Skip validation if key is in ignore list
66
+ if (!shouldIgnoreKey(key, ignoreKeys)) {
67
+ const bool = validateI18nValue(val);
68
+ if (!bool.valid) {
69
+ if (!errors[header]) {
70
+ errors[header] = {};
71
+ }
72
+ errors[header][key] = values[0];
73
+ }
74
+ }
75
+ }
76
+
77
+ if (Object.values(json[header]).every((value) => !value)) {
78
+ delete json[header];
79
+ }
80
+ }
81
+
82
+ if (Object.keys(errors).length > 0) {
83
+ throw new Error(
84
+ "valid i18n value failed, please check the value of the following values: " +
85
+ JSON.stringify(errors, null, 4),
86
+ );
87
+ }
88
+
89
+ return json;
90
+ }
91
+
92
+ function json2Csv(json) {
93
+ if (typeof json !== "object" || !json) throw "Object please.";
94
+ return findKeyValues(json).join("\n");
95
+ }
96
+
97
+ function csv2Json(csv) {
98
+ if (typeof csv !== "string") throw "String please.";
99
+ const json = {};
100
+ parseCsvLines(csv).forEach((line) => {
101
+ const [path, value] = line;
102
+ const pathSplit = path.split(separator);
103
+ deepSet(json, pathSplit, value);
104
+ });
105
+ return json;
106
+ }
107
+
108
+ function findKeyValues(o, prefix, l = 0) {
109
+ let result = [];
110
+ for (const key of Object.keys(o)) {
111
+ const fullKey = prefix ? prefix + separator + key : key;
112
+ const value = o[key];
113
+ if (typeof value === "string") {
114
+ result.push(stringsToCsvLine([fullKey, value]));
115
+ } else if (typeof value === "object" && value) {
116
+ result = result.concat(findKeyValues(value, fullKey, l + 1));
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+
122
+ function stringsToCsvLine(line) {
123
+ return line.map((s) => `"${s.replace(/"/g, '""')}"`).join(",");
124
+ }
125
+
126
+ function deepSet(o, path, value) {
127
+ if (path.length > 1) {
128
+ const key = path[0];
129
+ if (!o[key]) {
130
+ o[key] = {};
131
+ }
132
+ deepSet(o[key], path.slice(1), value);
133
+ } else {
134
+ o[path[0]] = value;
135
+ }
136
+ }
137
+
138
+ function parseCsvLines(csv) {
139
+ const lines = csvToLines(csv);
140
+ return lines.filter(Boolean).map(parseCsvLine);
141
+ }
142
+
143
+ function csvToLines(csv) {
144
+ return csv.split(/[\r\n]+/);
145
+ }
146
+
147
+ function parseCsvLine(line) {
148
+ const result = [];
149
+ for (let i = 0; i < line.length; i++) {
150
+ const char = line[i];
151
+ switch (char) {
152
+ case ",":
153
+ continue;
154
+ case '"': {
155
+ i++;
156
+ const [str, i2] = parseCsvString(line, i);
157
+ i = i2;
158
+ result.push(str);
159
+ break;
160
+ }
161
+ default: {
162
+ const [str, i2] = parseUnquotedField(line, i);
163
+ i = i2;
164
+ result.push(str);
165
+ break;
166
+ }
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+
172
+ /** Parses one field until the next comma or end of line (for unquoted CSV). */
173
+ function parseUnquotedField(line, i) {
174
+ let result = "";
175
+ for (; i < line.length; i++) {
176
+ if (line[i] === ",") break;
177
+ result += line[i];
178
+ }
179
+ return [result, i];
180
+ }
181
+
182
+ function parseCsvString(line, i) {
183
+ let result = "";
184
+ loop: for (; i < line.length; i++) {
185
+ const char = line[i];
186
+ switch (char) {
187
+ case '"':
188
+ i++;
189
+ if (line[i] === '"') {
190
+ result += '"';
191
+ } else {
192
+ break loop;
193
+ }
194
+ break;
195
+ default:
196
+ result += char;
197
+ }
198
+ }
199
+ return [result, i];
200
+ }
201
+
202
+ function getMissingKeys(jsonList, header) {
203
+ if (!Array.isArray(jsonList) || jsonList.length === 0) {
204
+ throw "Object array please.";
205
+ }
206
+
207
+ const baseJson = jsonList[0];
208
+ const baseKeys = Object.keys(baseJson);
209
+ const errors = {};
210
+ for (const key of baseKeys) {
211
+ for (const [index, json] of jsonList.entries()) {
212
+ const val = json[key] || "";
213
+ const bool = validateI18nValue(val);
214
+ if (!bool.valid) {
215
+ const locale = header[index + 1];
216
+ if (!errors[locale]) {
217
+ errors[locale] = {};
218
+ }
219
+ errors[locale][key] = baseJson[key];
220
+ }
221
+ }
222
+ }
223
+ return errors;
224
+ }
225
+
226
+ // Load ignore validation keys configuration
227
+ function loadIgnoreKeys() {
228
+ const configPath = path.join(__dirname, "../validation-ignore.json");
229
+ try {
230
+ if (fs.existsSync(configPath)) {
231
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
232
+ const ignoreKeys = config.ignoreKeys || [];
233
+ // Convert config to matcher array: string or regex
234
+ return ignoreKeys
235
+ .map((item) => {
236
+ if (typeof item === "string") {
237
+ // If string starts and ends with /, treat as regex pattern
238
+ if (item.startsWith("/") && item.endsWith("/") && item.length > 2) {
239
+ try {
240
+ const pattern = item.slice(1, -1); // Remove leading and trailing /
241
+ return new RegExp(pattern);
242
+ } catch (error) {
243
+ console.warn(
244
+ `Warning: Invalid regex pattern "${item}": ${error.message}`,
245
+ );
246
+ return item;
247
+ }
248
+ }
249
+ return item;
250
+ }
251
+ return item;
252
+ })
253
+ .filter(Boolean);
254
+ }
255
+ } catch (error) {
256
+ console.warn(
257
+ `Warning: Failed to load validation-ignore.json: ${error.message}`,
258
+ );
259
+ }
260
+ return [];
261
+ }
262
+
263
+ // Check if key should be ignored
264
+ function shouldIgnoreKey(key, ignoreKeys) {
265
+ for (const matcher of ignoreKeys) {
266
+ if (matcher instanceof RegExp) {
267
+ if (matcher.test(key)) {
268
+ return true;
269
+ }
270
+ } else if (typeof matcher === "string") {
271
+ if (matcher === key) {
272
+ return true;
273
+ }
274
+ }
275
+ }
276
+ return false;
277
+ }
278
+
279
+ module.exports = {
280
+ separator,
281
+ json2Csv,
282
+ csv2Json,
283
+ multiJson2Csv,
284
+ csv2multiJson,
285
+ getMissingKeys,
286
+ };
@@ -0,0 +1,38 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { multiJson2Csv } = require("./json-csv-converter");
4
+ const { checkFileExists, findJsonFiles } = require("./utils");
5
+ const { LocaleEnum } = require("../dist");
6
+
7
+ /** Convert multiple locale JSON files to a locale CSV file */
8
+ async function json2csv(inputDir, outputPath) {
9
+ const inputFiles = await findJsonFiles(inputDir);
10
+
11
+ // Sort input files by locale
12
+ inputFiles.sort((a, b) => (b.startsWith(LocaleEnum.en) ? 1 : -1));
13
+
14
+ const jsonList = [];
15
+ const headers = [""];
16
+ for (const filePath of inputFiles) {
17
+ const json = await fs.readJSON(path.resolve(inputDir, filePath), {
18
+ encoding: "utf8",
19
+ });
20
+ jsonList.push(json);
21
+ const fileName = path.basename(filePath, path.extname(filePath));
22
+ headers.push(fileName);
23
+ }
24
+
25
+ const csv = multiJson2Csv(jsonList, headers);
26
+
27
+ const csvPath = path.resolve(outputPath);
28
+
29
+ await checkFileExists(csvPath);
30
+
31
+ await fs.outputFile(csvPath, csv, { encoding: "utf8" });
32
+
33
+ console.log("json2csv success =>", csvPath);
34
+ }
35
+
36
+ module.exports = {
37
+ json2csv,
38
+ };
@@ -0,0 +1,67 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { checkFileExists, findJsonFiles } = require("./utils");
4
+ const { LocaleEnum } = require("../dist");
5
+
6
+ /**
7
+ * Merge default and extend JSON files back into one file
8
+ * @param {string} inputDir - The input directory containing both default and extend JSON files
9
+ * @param {string} outputDir - The output directory for merged JSON files
10
+ */
11
+ async function mergeJson(inputDir, outputDir) {
12
+ const jsonFiles = await findJsonFiles(inputDir);
13
+
14
+ // Sort input files by locale
15
+ jsonFiles.sort((a, b) => (b.startsWith(LocaleEnum.en) ? 1 : -1));
16
+ let baseJson = {};
17
+
18
+ const extendDir = path.resolve(inputDir, "extend");
19
+
20
+ for (const [index, file] of jsonFiles.entries()) {
21
+ const defaultJsonPath = path.resolve(inputDir, file);
22
+ const extendJsonPath = path.resolve(extendDir, file);
23
+
24
+ // Read default JSON file
25
+ const defaultJson = await fs.readJSON(defaultJsonPath, {
26
+ encoding: "utf8",
27
+ });
28
+
29
+ // Read extend JSON file if it exists
30
+ let extendJson = {};
31
+ if (await fs.pathExists(extendJsonPath)) {
32
+ extendJson = await fs.readJSON(extendJsonPath, {
33
+ encoding: "utf8",
34
+ });
35
+ }
36
+
37
+ // Merge the JSON objects
38
+ const mergedJson = { ...defaultJson, ...extendJson };
39
+
40
+ let sortedJson = {};
41
+
42
+ // base json
43
+ if (index === 0) {
44
+ baseJson = mergedJson;
45
+ sortedJson = mergedJson;
46
+ } else {
47
+ for (const key of Object.keys(baseJson)) {
48
+ sortedJson[key] = mergedJson[key];
49
+ }
50
+ }
51
+
52
+ const outputPath = path.resolve(outputDir, file);
53
+
54
+ await fs.outputFile(outputPath, JSON.stringify(sortedJson, null, 2), {
55
+ encoding: "utf8",
56
+ });
57
+
58
+ console.log("mergeJson success =>", outputPath);
59
+ }
60
+
61
+ await fs.remove(extendDir);
62
+ console.log("remove extendDir =>", extendDir);
63
+ }
64
+
65
+ module.exports = {
66
+ mergeJson,
67
+ };
@@ -0,0 +1,50 @@
1
+ const fs = require("fs-extra");
2
+ const path = require("path");
3
+ const { checkFileExists, findJsonFiles } = require("./utils");
4
+
5
+ /**
6
+ * Separate json file default and extend key values based on the key
7
+ * @param {string} inputDir - The input directory for locale JSON files
8
+ * @param {string} outputDir - The output directory for locale JSON files
9
+ * @param {string} separateKey - The key to separate the json files
10
+ */
11
+ async function separateJson(inputDir, outputDir, separateKey) {
12
+ const jsonFiles = await findJsonFiles(inputDir);
13
+
14
+ const separateKeys = separateKey.split(",");
15
+ for (const file of jsonFiles) {
16
+ const json = await fs.readJSON(path.resolve(inputDir, file), {
17
+ encoding: "utf8",
18
+ });
19
+
20
+ const defaultJson = {};
21
+ const extendJson = {};
22
+ Object.keys(json).forEach((key) => {
23
+ if (separateKeys.some((k) => key.startsWith(k))) {
24
+ extendJson[key] = json[key];
25
+ } else {
26
+ defaultJson[key] = json[key];
27
+ }
28
+ });
29
+
30
+ const jsonPath = path.resolve(outputDir, file);
31
+ await checkFileExists(jsonPath);
32
+
33
+ await fs.outputFile(jsonPath, JSON.stringify(defaultJson, null, 2), {
34
+ encoding: "utf8",
35
+ });
36
+
37
+ const extendPath = path.resolve(outputDir, "extend", file);
38
+ await checkFileExists(extendPath);
39
+
40
+ await fs.outputFile(extendPath, JSON.stringify(extendJson, null, 2), {
41
+ encoding: "utf8",
42
+ });
43
+
44
+ console.log("separateJson success =>", jsonPath, extendPath);
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ separateJson,
50
+ };
@@ -0,0 +1,94 @@
1
+ const fs = require("fs-extra");
2
+
3
+ async function checkFileExists(filePath) {
4
+ if (await fs.exists(filePath)) {
5
+ throw new Error(`${filePath} already exists, please modify file path`);
6
+ }
7
+ }
8
+
9
+ const i18nValidErrors = {
10
+ string: "Value must be a string",
11
+ empty: "Value cannot be empty",
12
+ interpolation: "Invalid interpolation",
13
+ mismatchedClosingTag: (tagName) => `Mismatched closing tag: </${tagName}>`,
14
+ unclosedTag: (tagName) => `Unclosed tag: <${tagName}>`,
15
+ };
16
+
17
+ /**
18
+ * Validate the value of i18n
19
+ * @param {string} value - The value to validate
20
+ * @returns {Object} - The validation result
21
+ */
22
+ function validateI18nValue(value) {
23
+ if (typeof value !== "string") {
24
+ return { valid: false, error: i18nValidErrors.string };
25
+ }
26
+
27
+ // 1. check if value is empty
28
+ if (value.trim() === "") {
29
+ return { valid: false, error: i18nValidErrors.empty };
30
+ }
31
+
32
+ // 2. check if placeholder format is correct (allow `{{variable}}`)
33
+ // allow `{{variable}}` or `{{ variable }}`
34
+ const interpolationRegex = /{{\s*[\w.-]+\s*}}/g;
35
+ const strippedInterpolation = value.replace(interpolationRegex, "");
36
+ if (
37
+ strippedInterpolation.includes("{{") ||
38
+ strippedInterpolation.includes("}}")
39
+ ) {
40
+ return { valid: false, error: i18nValidErrors.interpolation };
41
+ }
42
+ // mistaken single-brace placeholders like `{user.name}` (should be `{{user.name}}`)
43
+ if (/\{\s*[\w.-]+\s*\}/.test(strippedInterpolation)) {
44
+ return { valid: false, error: i18nValidErrors.interpolation };
45
+ }
46
+ // `{` without a closing `}` on the same line segment (typo vs `{{`)
47
+ if (/\{[^}]*$/.test(strippedInterpolation)) {
48
+ return { valid: false, error: i18nValidErrors.interpolation };
49
+ }
50
+
51
+ // 3. check if HTML tags are correctly closed (supports attributes, e.g. <script async src="...">)
52
+ const tagRegex = /<(\/?)\s*([a-zA-Z0-9][\w-]*)[^>]*>/g;
53
+ let stack = [];
54
+ let match;
55
+
56
+ while ((match = tagRegex.exec(value)) !== null) {
57
+ const [, slashPrefix, tagName] = match;
58
+ const fullTag = match[0];
59
+ const isClosing = slashPrefix === "/";
60
+ const isSelfClosing = !isClosing && /\/\s*>$/.test(fullTag.trim());
61
+
62
+ if (isSelfClosing) {
63
+ continue;
64
+ }
65
+ if (isClosing) {
66
+ if (stack.length === 0 || stack.pop() !== tagName) {
67
+ return {
68
+ valid: false,
69
+ error: i18nValidErrors.mismatchedClosingTag(tagName),
70
+ };
71
+ }
72
+ } else {
73
+ stack.push(tagName);
74
+ }
75
+ }
76
+
77
+ if (stack.length > 0) {
78
+ return { valid: false, error: i18nValidErrors.unclosedTag(stack.pop()) };
79
+ }
80
+
81
+ return { valid: true, error: null };
82
+ }
83
+
84
+ async function findJsonFiles(dir) {
85
+ const files = await fs.readdir(dir);
86
+ return files.filter((file) => file.endsWith(".json"));
87
+ }
88
+
89
+ module.exports = {
90
+ checkFileExists,
91
+ validateI18nValue,
92
+ i18nValidErrors,
93
+ findJsonFiles,
94
+ };