@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 CHANGED
@@ -1,38 +1,27 @@
1
1
  # @gen-epix/merge-locales
2
2
 
3
- Merge locale JSON files from multiple source directories into one locale directory, either from a CLI or Vite.
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
- plugins: [
26
- mergeLocales({
27
- outputDirectory: './public/locale',
28
- sourceDirectories: [
29
- './src/locale',
30
- '../ui-core-components/src/locale',
31
- '../ui-core-form/src/locale',
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
- Paths are resolved relative to Vite's configured root. Locale files are merged when the dev server starts and whenever a source locale changes, with a full page reload. Production builds merge the files during the build lifecycle. Later source directories take precedence.
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
- outputDirectory: string;
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 { join, extname, basename, resolve, sep } from 'path';
2
- import { mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';
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 = ({ outputDirectory, sourceDirectories }) => {
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
- mkdirSync(outputDirectory, { recursive: true });
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
- const outputPath = join(outputDirectory, `${locale}.json`);
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 outputPaths;
61
+ return mergedLocales;
65
62
  };
66
63
 
64
+ const trimSlashes = (value) => value.replace(/^[./]+/, "").replace(/\/+$/, "");
67
65
  const mergeLocales = (options) => {
68
- let resolvedOptions = {
69
- outputDirectory: resolve(options.outputDirectory),
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
- resolvedOptions = {
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
- const mergeAndReload = () => {
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 = resolvedOptions.sourceDirectories.some((sourceDirectory) => filePath.startsWith(`${sourceDirectory}${sep}`));
77
+ const isSourceFile = sourceDirectories.some((sourceDirectory) => filePath.startsWith(`${sourceDirectory}${sep}`));
93
78
  if (isLocaleFile && isSourceFile) {
94
- mergeAndReload();
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.2",
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
- "merge-locales": "vite-node --loader ts-node/esm ./src/merge-locales.ts",
47
- "build": "rimraf ./bin && rollup -c && chmod +x ./bin/merge-locales.js"
41
+ "build": "rimraf ./bin && rollup -c"
48
42
  }
49
43
  }
@@ -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
- }