@gen-epix/merge-locales 0.0.2 → 0.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 +16 -27
- package/bin/index.d.ts +20 -2
- package/bin/index.js +35 -28
- package/package.json +4 -10
- package/bin/merge-locales.js +0 -90
package/README.md
CHANGED
|
@@ -1,38 +1,27 @@
|
|
|
1
1
|
# @gen-epix/merge-locales
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vite plugin that merges locale JSON files from multiple source directories into a single set of locale files.
|
|
4
4
|
|
|
5
|
-
Later source directories take precedence when the same translation key occurs in more than one source. Nested objects are merged recursively; arrays and scalar values are replaced.
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
merge-locales <output-directory> <source-directory> [...source-directories]
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
For example:
|
|
12
|
-
|
|
13
|
-
```sh
|
|
14
|
-
merge-locales ./public/locale ./src/locale ../ui-core-components/src/locale ../ui-core-form/src/locale
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The command discovers locale names from the JSON filenames, so it does not require a fixed list of locales.
|
|
18
|
-
|
|
19
|
-
## Vite plugin
|
|
5
|
+
Later source directories take precedence when the same translation key occurs in more than one source. Nested objects are merged recursively; arrays and scalar values are replaced. Locale names are discovered from the JSON filenames, so no fixed list of locales is required.
|
|
20
6
|
|
|
21
7
|
```ts
|
|
22
8
|
import { mergeLocales } from '@gen-epix/merge-locales';
|
|
23
9
|
|
|
24
10
|
export default {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
plugins: [
|
|
12
|
+
mergeLocales({
|
|
13
|
+
outputPath: 'locale',
|
|
14
|
+
sourceDirectories: [
|
|
15
|
+
'./src/locale',
|
|
16
|
+
'../ui-core-components/src/locale',
|
|
17
|
+
'../ui-core-form/src/locale',
|
|
18
|
+
],
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
35
21
|
};
|
|
36
22
|
```
|
|
37
23
|
|
|
38
|
-
|
|
24
|
+
Source directories are resolved relative to Vite's configured root. Nothing is written to the source tree:
|
|
25
|
+
|
|
26
|
+
- During development the merged locales are served from `outputPath` (for example `/locale/en.json`), re-merged on every request, with a full page reload whenever a source locale file changes.
|
|
27
|
+
- During a build the merged locales are emitted as assets under `outputPath`, relative to the build output directory.
|
package/bin/index.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
interface MergeLocalesOptions {
|
|
2
|
-
|
|
2
|
+
outputPath: string;
|
|
3
3
|
sourceDirectories: string[];
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
interface MergeLocalesPlugin {
|
|
7
|
-
buildStart: () => void;
|
|
8
7
|
configResolved: (config: ResolvedConfig) => void;
|
|
9
8
|
configureServer: (server: DevServer) => void;
|
|
9
|
+
generateBundle: (this: EmitFileContext) => void;
|
|
10
10
|
name: string;
|
|
11
11
|
}
|
|
12
12
|
interface DevServer {
|
|
13
|
+
middlewares: {
|
|
14
|
+
use: (handler: (request: ServerRequest, response: ServerResponse, next: () => void) => void) => void;
|
|
15
|
+
};
|
|
13
16
|
watcher: {
|
|
14
17
|
add: (paths: string[]) => void;
|
|
15
18
|
on: (event: 'add' | 'change' | 'unlink', handler: (filePath: string) => void) => void;
|
|
@@ -21,9 +24,24 @@ interface DevServer {
|
|
|
21
24
|
}) => void;
|
|
22
25
|
};
|
|
23
26
|
}
|
|
27
|
+
interface EmitFileContext {
|
|
28
|
+
emitFile: (file: {
|
|
29
|
+
fileName: string;
|
|
30
|
+
source: string;
|
|
31
|
+
type: 'asset';
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
24
34
|
interface ResolvedConfig {
|
|
25
35
|
root: string;
|
|
26
36
|
}
|
|
37
|
+
interface ServerRequest {
|
|
38
|
+
url?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ServerResponse {
|
|
41
|
+
end: (chunk: string) => void;
|
|
42
|
+
setHeader: (name: string, value: string) => void;
|
|
43
|
+
statusCode: number;
|
|
44
|
+
}
|
|
27
45
|
declare const mergeLocales: (options: MergeLocalesOptions) => MergeLocalesPlugin;
|
|
28
46
|
|
|
29
47
|
export { mergeLocales };
|
package/bin/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { extname, basename, join, resolve, sep } from 'path';
|
|
2
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
3
3
|
|
|
4
4
|
const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
5
|
const mergeJsonValues = (base, override) => {
|
|
@@ -33,7 +33,7 @@ const readJsonFile = (filePath) => {
|
|
|
33
33
|
throw new Error(`Unable to parse locale file ${filePath}: ${getErrorMessage(error)}`, { cause: error });
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
-
const mergeLocales$1 = (
|
|
36
|
+
const mergeLocales$1 = (sourceDirectories) => {
|
|
37
37
|
const locales = /* @__PURE__ */ new Map();
|
|
38
38
|
for (const sourceDirectory of sourceDirectories) {
|
|
39
39
|
let sourceLocales;
|
|
@@ -49,52 +49,59 @@ const mergeLocales$1 = ({ outputDirectory, sourceDirectories }) => {
|
|
|
49
49
|
if (locales.size === 0) {
|
|
50
50
|
throw new Error("No locale JSON files found in the source directories.");
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
const outputPaths = [];
|
|
52
|
+
const mergedLocales = /* @__PURE__ */ new Map();
|
|
54
53
|
for (const [locale, files] of [...locales.entries()].sort(([first], [second]) => first.localeCompare(second))) {
|
|
55
54
|
let merged = {};
|
|
56
55
|
for (const file of files) {
|
|
57
56
|
merged = mergeJsonValues(merged, readJsonFile(file));
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
writeFileSync(outputPath, `${JSON.stringify(merged, null, 2)}
|
|
58
|
+
mergedLocales.set(locale, `${JSON.stringify(merged, null, 2)}
|
|
61
59
|
`);
|
|
62
|
-
outputPaths.push(outputPath);
|
|
63
60
|
}
|
|
64
|
-
return
|
|
61
|
+
return mergedLocales;
|
|
65
62
|
};
|
|
66
63
|
|
|
64
|
+
const trimSlashes = (value) => value.replace(/^[./]+/, "").replace(/\/+$/, "");
|
|
67
65
|
const mergeLocales = (options) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
sourceDirectories: options.sourceDirectories.map((sourceDirectory) => resolve(sourceDirectory))
|
|
71
|
-
};
|
|
66
|
+
const outputPath = trimSlashes(options.outputPath);
|
|
67
|
+
let sourceDirectories = options.sourceDirectories.map((sourceDirectory) => resolve(sourceDirectory));
|
|
72
68
|
return {
|
|
73
|
-
buildStart: () => {
|
|
74
|
-
mergeLocales$1(resolvedOptions);
|
|
75
|
-
},
|
|
76
69
|
configResolved: (config) => {
|
|
77
|
-
|
|
78
|
-
outputDirectory: resolve(config.root, options.outputDirectory),
|
|
79
|
-
sourceDirectories: options.sourceDirectories.map((sourceDirectory) => resolve(config.root, sourceDirectory))
|
|
80
|
-
};
|
|
70
|
+
sourceDirectories = options.sourceDirectories.map((sourceDirectory) => resolve(config.root, sourceDirectory));
|
|
81
71
|
},
|
|
82
72
|
configureServer: (server) => {
|
|
83
|
-
|
|
84
|
-
mergeLocales$1(resolvedOptions);
|
|
85
|
-
server.ws.send({ path: "*", type: "full-reload" });
|
|
86
|
-
};
|
|
87
|
-
mergeLocales$1(resolvedOptions);
|
|
88
|
-
server.watcher.add(resolvedOptions.sourceDirectories);
|
|
73
|
+
server.watcher.add(sourceDirectories);
|
|
89
74
|
for (const event of ["add", "change", "unlink"]) {
|
|
90
75
|
server.watcher.on(event, (filePath) => {
|
|
91
76
|
const isLocaleFile = filePath.endsWith(".json");
|
|
92
|
-
const isSourceFile =
|
|
77
|
+
const isSourceFile = sourceDirectories.some((sourceDirectory) => filePath.startsWith(`${sourceDirectory}${sep}`));
|
|
93
78
|
if (isLocaleFile && isSourceFile) {
|
|
94
|
-
|
|
79
|
+
server.ws.send({ path: "*", type: "full-reload" });
|
|
95
80
|
}
|
|
96
81
|
});
|
|
97
82
|
}
|
|
83
|
+
server.middlewares.use((request, response, next) => {
|
|
84
|
+
const pathname = (request.url ?? "").split("?")[0];
|
|
85
|
+
const match = new RegExp(`(?:^|/)${outputPath}/([^/]+)\\.json$`).exec(pathname);
|
|
86
|
+
const source = match ? mergeLocales$1(sourceDirectories).get(match[1]) : void 0;
|
|
87
|
+
if (source === void 0) {
|
|
88
|
+
next();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
response.statusCode = 200;
|
|
92
|
+
response.setHeader("Content-Type", "application/json");
|
|
93
|
+
response.setHeader("Cache-Control", "no-cache");
|
|
94
|
+
response.end(source);
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
generateBundle: function emitLocales() {
|
|
98
|
+
for (const [locale, source] of mergeLocales$1(sourceDirectories)) {
|
|
99
|
+
this.emitFile({
|
|
100
|
+
fileName: `${outputPath}/${locale}.json`,
|
|
101
|
+
source,
|
|
102
|
+
type: "asset"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
98
105
|
},
|
|
99
106
|
name: "merge-locales"
|
|
100
107
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gen-epix/merge-locales",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "EUPL-1.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,11 +22,7 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./bin/index.d.ts",
|
|
24
24
|
"import": "./bin/index.js"
|
|
25
|
-
}
|
|
26
|
-
"./cli": "./bin/merge-locales.js"
|
|
27
|
-
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"merge-locales": "bin/merge-locales.js"
|
|
25
|
+
}
|
|
30
26
|
},
|
|
31
27
|
"dependencies": {},
|
|
32
28
|
"devDependencies": {
|
|
@@ -37,13 +33,11 @@
|
|
|
37
33
|
"rollup": "4.63.1",
|
|
38
34
|
"rollup-plugin-dts": "6.5.1",
|
|
39
35
|
"rollup-plugin-esbuild": "6.2.1",
|
|
40
|
-
"typescript": "6.0.3"
|
|
41
|
-
"vite-node": "6.0.0"
|
|
36
|
+
"typescript": "6.0.3"
|
|
42
37
|
},
|
|
43
38
|
"scripts": {
|
|
44
39
|
"lint": "eslint ./src --report-unused-disable-directives --max-warnings 0",
|
|
45
40
|
"check-types": "tsc --noemit",
|
|
46
|
-
"
|
|
47
|
-
"build": "rimraf ./bin && rollup -c && chmod +x ./bin/merge-locales.js"
|
|
41
|
+
"build": "rimraf ./bin && rollup -c"
|
|
48
42
|
}
|
|
49
43
|
}
|
package/bin/merge-locales.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { join, extname, basename, resolve } from 'path';
|
|
3
|
-
import { mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';
|
|
4
|
-
|
|
5
|
-
const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
-
const mergeJsonValues = (base, override) => {
|
|
7
|
-
if (!isJsonObject(base) || !isJsonObject(override)) {
|
|
8
|
-
return override;
|
|
9
|
-
}
|
|
10
|
-
const merged = { ...base };
|
|
11
|
-
for (const [key, value] of Object.entries(override)) {
|
|
12
|
-
merged[key] = key in merged ? mergeJsonValues(merged[key], value) : value;
|
|
13
|
-
}
|
|
14
|
-
return merged;
|
|
15
|
-
};
|
|
16
|
-
const getErrorMessage = (error) => error instanceof Error ? error.message : JSON.stringify(error);
|
|
17
|
-
const readLocaleFiles = (sourceDirectory) => {
|
|
18
|
-
const localeFiles = /* @__PURE__ */ new Map();
|
|
19
|
-
for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
20
|
-
if (!entry.isFile() || extname(entry.name) !== ".json") {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const locale = basename(entry.name, ".json");
|
|
24
|
-
const files = localeFiles.get(locale) ?? [];
|
|
25
|
-
files.push(join(sourceDirectory, entry.name));
|
|
26
|
-
localeFiles.set(locale, files);
|
|
27
|
-
}
|
|
28
|
-
return localeFiles;
|
|
29
|
-
};
|
|
30
|
-
const readJsonFile = (filePath) => {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
33
|
-
} catch (error) {
|
|
34
|
-
throw new Error(`Unable to parse locale file ${filePath}: ${getErrorMessage(error)}`, { cause: error });
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const mergeLocales = ({ outputDirectory, sourceDirectories }) => {
|
|
38
|
-
const locales = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const sourceDirectory of sourceDirectories) {
|
|
40
|
-
let sourceLocales;
|
|
41
|
-
try {
|
|
42
|
-
sourceLocales = readLocaleFiles(sourceDirectory);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw new Error(`Unable to read source directory ${sourceDirectory}: ${getErrorMessage(error)}`, { cause: error });
|
|
45
|
-
}
|
|
46
|
-
for (const [locale, files] of sourceLocales) {
|
|
47
|
-
locales.set(locale, [...locales.get(locale) ?? [], ...files]);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (locales.size === 0) {
|
|
51
|
-
throw new Error("No locale JSON files found in the source directories.");
|
|
52
|
-
}
|
|
53
|
-
mkdirSync(outputDirectory, { recursive: true });
|
|
54
|
-
const outputPaths = [];
|
|
55
|
-
for (const [locale, files] of [...locales.entries()].sort(([first], [second]) => first.localeCompare(second))) {
|
|
56
|
-
let merged = {};
|
|
57
|
-
for (const file of files) {
|
|
58
|
-
merged = mergeJsonValues(merged, readJsonFile(file));
|
|
59
|
-
}
|
|
60
|
-
const outputPath = join(outputDirectory, `${locale}.json`);
|
|
61
|
-
writeFileSync(outputPath, `${JSON.stringify(merged, null, 2)}
|
|
62
|
-
`);
|
|
63
|
-
outputPaths.push(outputPath);
|
|
64
|
-
}
|
|
65
|
-
return outputPaths;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const parseArguments = () => {
|
|
69
|
-
const [outputDirectory2, ...sourceDirectories2] = process.argv.slice(2);
|
|
70
|
-
if (!outputDirectory2 || sourceDirectories2.length === 0) {
|
|
71
|
-
console.error("Usage: merge-locales <output-directory> <source-directory> [...source-directories]");
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
outputDirectory: resolve(outputDirectory2),
|
|
76
|
-
sourceDirectories: sourceDirectories2.map((sourceDirectory) => resolve(sourceDirectory))
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
const { outputDirectory, sourceDirectories } = parseArguments();
|
|
80
|
-
try {
|
|
81
|
-
for (const outputPath of mergeLocales({
|
|
82
|
-
outputDirectory: resolve(outputDirectory),
|
|
83
|
-
sourceDirectories: sourceDirectories.map((sourceDirectory) => resolve(sourceDirectory))
|
|
84
|
-
})) {
|
|
85
|
-
console.log(`Merged locale into ${outputPath}`);
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error(error instanceof Error ? error.message : JSON.stringify(error));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|