@liberfi.io/i18n 0.1.0

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.
@@ -0,0 +1,201 @@
1
+ const { validateI18nValue } = require("./utils");
2
+
3
+ const separator = ".";
4
+
5
+ function multiJson2Csv(jsonList, header) {
6
+ if (!Array.isArray(jsonList) || jsonList.length === 0) {
7
+ throw "Object array please.";
8
+ }
9
+
10
+ const baseJson = jsonList[0];
11
+ const baseKeys = Object.keys(baseJson);
12
+ const errors = {};
13
+ let result = header.length ? [header] : [];
14
+ for (const key of baseKeys) {
15
+ const values = [];
16
+ for (const [index, json] of jsonList.entries()) {
17
+ const val = json[key] || "";
18
+ values.push(val);
19
+ const bool = validateI18nValue(val);
20
+ if (!bool.valid) {
21
+ const locale = header[index + 1];
22
+ if (!errors[locale]) {
23
+ errors[locale] = {};
24
+ }
25
+ errors[locale][key] = baseJson[key];
26
+ }
27
+ }
28
+ result.push(stringsToCsvLine([key, ...values]));
29
+ }
30
+ if (Object.keys(errors).length > 0) {
31
+ throw new Error(
32
+ "valid i18n value failed, please check the value of the following values: " +
33
+ JSON.stringify(errors, null, 4),
34
+ );
35
+ }
36
+ return result.join("\n");
37
+ }
38
+
39
+ function csv2multiJson(csv) {
40
+ if (typeof csv !== "string") throw "String please.";
41
+ const json = {};
42
+
43
+ const lines = csvToLines(csv);
44
+ const csvLines = lines.filter(Boolean).map(parseCsvLine);
45
+ const headers = csvLines.shift()[0].split(",");
46
+ const errors = {};
47
+ for (const [index, header] of headers.entries()) {
48
+ json[header] = {};
49
+ for (const line of csvLines) {
50
+ const [key, ...values] = line;
51
+ const val = values[index];
52
+ json[header][key] = val;
53
+ const bool = validateI18nValue(val);
54
+ if (!bool.valid) {
55
+ if (!errors[header]) {
56
+ errors[header] = {};
57
+ }
58
+ errors[header][key] = values[0];
59
+ }
60
+ }
61
+
62
+ if (Object.values(json[header]).every((value) => !value)) {
63
+ delete json[header];
64
+ }
65
+ }
66
+
67
+ if (Object.keys(errors).length > 0) {
68
+ throw new Error(
69
+ "valid i18n value failed, please check the value of the following values: " +
70
+ JSON.stringify(errors, null, 4),
71
+ );
72
+ }
73
+
74
+ return json;
75
+ }
76
+
77
+ function json2Csv(json) {
78
+ if (typeof json !== "object" || !json) throw "Object please.";
79
+ return findKeyValues(json).join("\n");
80
+ }
81
+
82
+ function csv2Json(csv) {
83
+ if (typeof csv !== "string") throw "String please.";
84
+ const json = {};
85
+ parseCsvLines(csv).forEach((line) => {
86
+ const [path, value] = line;
87
+ const pathSplit = path.split(separator);
88
+ deepSet(json, pathSplit, value);
89
+ });
90
+ return json;
91
+ }
92
+
93
+ function findKeyValues(o, prefix, l = 0) {
94
+ let result = [];
95
+ for (const key of Object.keys(o)) {
96
+ const fullKey = prefix ? prefix + separator + key : key;
97
+ const value = o[key];
98
+ if (typeof value === "string") {
99
+ result.push(stringsToCsvLine([fullKey, value]));
100
+ } else if (typeof value === "object" && value) {
101
+ result = result.concat(findKeyValues(value, fullKey, l + 1));
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function stringsToCsvLine(line) {
108
+ return line.map((s) => `"${s.replace(/"/g, '""')}"`).join(",");
109
+ }
110
+
111
+ function deepSet(o, path, value) {
112
+ if (path.length > 1) {
113
+ const key = path[0];
114
+ if (!o[key]) {
115
+ o[key] = {};
116
+ }
117
+ deepSet(o[key], path.slice(1), value);
118
+ } else {
119
+ o[path[0]] = value;
120
+ }
121
+ }
122
+
123
+ function parseCsvLines(csv) {
124
+ const lines = csvToLines(csv);
125
+ return lines.filter(Boolean).map(parseCsvLine);
126
+ }
127
+
128
+ function csvToLines(csv) {
129
+ return csv.split(/[\r\n]+/);
130
+ }
131
+
132
+ function parseCsvLine(line) {
133
+ const result = [];
134
+ for (let i = 0; i < line.length; i++) {
135
+ const char = line[i];
136
+ switch (char) {
137
+ case ",":
138
+ continue;
139
+ case '"':
140
+ i++;
141
+ default:
142
+ const [str, i2] = parseCsvString(line, i);
143
+ i = i2;
144
+ result.push(str);
145
+ }
146
+ }
147
+ return result;
148
+ }
149
+
150
+ function parseCsvString(line, i) {
151
+ let result = "";
152
+ loop: for (; i < line.length; i++) {
153
+ const char = line[i];
154
+ switch (char) {
155
+ case '"':
156
+ i++;
157
+ if (line[i] === '"') {
158
+ result += '"';
159
+ } else {
160
+ break loop;
161
+ }
162
+ break;
163
+ default:
164
+ result += char;
165
+ }
166
+ }
167
+ return [result, i];
168
+ }
169
+
170
+ function getMissingKeys(jsonList, header) {
171
+ if (!Array.isArray(jsonList) || jsonList.length === 0) {
172
+ throw "Object array please.";
173
+ }
174
+
175
+ const baseJson = jsonList[0];
176
+ const baseKeys = Object.keys(baseJson);
177
+ const errors = {};
178
+ for (const key of baseKeys) {
179
+ for (const [index, json] of jsonList.entries()) {
180
+ const val = json[key] || "";
181
+ const bool = validateI18nValue(val);
182
+ if (!bool.valid) {
183
+ const locale = header[index + 1];
184
+ if (!errors[locale]) {
185
+ errors[locale] = {};
186
+ }
187
+ errors[locale][key] = baseJson[key];
188
+ }
189
+ }
190
+ }
191
+ return errors;
192
+ }
193
+
194
+ module.exports = {
195
+ separator,
196
+ json2Csv,
197
+ csv2Json,
198
+ multiJson2Csv,
199
+ csv2multiJson,
200
+ getMissingKeys,
201
+ };
@@ -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,62 @@
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
+ for (const [index, file] of jsonFiles.entries()) {
19
+ const defaultJsonPath = path.resolve(inputDir, file);
20
+ const extendJsonPath = path.resolve(inputDir, "extend", file);
21
+
22
+ // Read default JSON file
23
+ const defaultJson = await fs.readJSON(defaultJsonPath, {
24
+ encoding: "utf8",
25
+ });
26
+
27
+ // Read extend JSON file if it exists
28
+ let extendJson = {};
29
+ if (await fs.pathExists(extendJsonPath)) {
30
+ extendJson = await fs.readJSON(extendJsonPath, {
31
+ encoding: "utf8",
32
+ });
33
+ }
34
+
35
+ // Merge the JSON objects
36
+ const mergedJson = { ...defaultJson, ...extendJson };
37
+
38
+ let sortedJson = {};
39
+
40
+ // base json
41
+ if (index === 0) {
42
+ baseJson = mergedJson;
43
+ sortedJson = mergedJson;
44
+ } else {
45
+ for (const key of Object.keys(baseJson)) {
46
+ sortedJson[key] = mergedJson[key];
47
+ }
48
+ }
49
+
50
+ const outputPath = path.resolve(outputDir, file);
51
+
52
+ await fs.outputFile(outputPath, JSON.stringify(sortedJson, null, 2), {
53
+ encoding: "utf8",
54
+ });
55
+
56
+ console.log("mergeJson success =>", outputPath);
57
+ }
58
+ }
59
+
60
+ module.exports = {
61
+ mergeJson,
62
+ };
@@ -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,88 @@
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
+ // check if `{{` is not closed or `}}` is not opened
36
+ const invalidInterpolationRegex =
37
+ /{{[^{}]*$|^[^{}]*}}|[^{]{{|}}[^}]}|^{|}$|{[^{}]*}|}/;
38
+
39
+ if (
40
+ value.match(invalidInterpolationRegex) &&
41
+ !value.match(interpolationRegex)
42
+ ) {
43
+ return { valid: false, error: i18nValidErrors.interpolation };
44
+ }
45
+
46
+ // 3. check if HTML tags are correctly closed
47
+ const tagRegex = /<\/?([a-zA-Z0-9]+)(\s*\/?)>/g;
48
+ let stack = [];
49
+ let match;
50
+
51
+ while ((match = tagRegex.exec(value)) !== null) {
52
+ let [, tagName, selfClosing] = match;
53
+
54
+ if (selfClosing === "/") {
55
+ // self-closing tag, no need to push to stack
56
+ continue;
57
+ } else if (match[0].startsWith("</")) {
58
+ // closing tag, check if stack top matches
59
+ if (stack.length === 0 || stack.pop() !== tagName) {
60
+ return {
61
+ valid: false,
62
+ error: i18nValidErrors.mismatchedClosingTag(tagName),
63
+ };
64
+ }
65
+ } else {
66
+ // opening tag, push to stack
67
+ stack.push(tagName);
68
+ }
69
+ }
70
+
71
+ if (stack.length > 0) {
72
+ return { valid: false, error: i18nValidErrors.unclosedTag(stack.pop()) };
73
+ }
74
+
75
+ return { valid: true, error: null };
76
+ }
77
+
78
+ async function findJsonFiles(dir) {
79
+ const files = await fs.readdir(dir);
80
+ return files.filter((file) => file.endsWith(".json"));
81
+ }
82
+
83
+ module.exports = {
84
+ checkFileExists,
85
+ validateI18nValue,
86
+ i18nValidErrors,
87
+ findJsonFiles,
88
+ };