@zerolith/i18n 0.1.0-alpha.1 → 0.1.0-alpha.10

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/src/cli.ts CHANGED
@@ -6,11 +6,54 @@
6
6
  */
7
7
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
8
8
  import { basename, join } from "node:path";
9
+ import { realpathSync } from "node:fs";
9
10
  import process from "node:process";
11
+ import { pathToFileURL } from "node:url";
10
12
  import { type Catalog, compileIndex, compileMessages, messageNames } from "./compile.js";
11
13
 
12
14
  const USAGE = "usage: zerolith-i18n <folder> [--out <folder>] [--js] [--strict]\n";
13
15
 
16
+ /** The locale a catalog's file name gives, refusing a name that is not a BCP 47 language tag, such as `en_US.json`. */
17
+ function localeOf(file: string): string
18
+ {
19
+ const locale = basename(file, ".json");
20
+ try
21
+ {
22
+ Intl.getCanonicalLocales(locale);
23
+ }
24
+ catch
25
+ {
26
+ throw new Error(`${locale} is not a language tag; name the file as en.json or pt-BR.json`);
27
+ }
28
+ return locale;
29
+ }
30
+
31
+ /** A catalog's messages, refusing a file that is not a JSON object of strings. */
32
+ function catalogOf(text: string): Catalog
33
+ {
34
+ const parsed: unknown = JSON.parse(text);
35
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
36
+ {
37
+ throw new Error("a catalog is a JSON object of messages");
38
+ }
39
+ for (const [key, value] of Object.entries(parsed))
40
+ {
41
+ if (typeof value !== "string")
42
+ {
43
+ throw new Error(`the message "${key}" is not a string`);
44
+ }
45
+ }
46
+ return parsed as Catalog;
47
+ }
48
+
49
+ /** Reports why a catalog cannot be compiled, naming its file, and gives the exit code; nothing has been written. */
50
+ function failure(file: string, error: unknown): number
51
+ {
52
+ const message = error instanceof Error ? error.message.replace(/^zerolith\/i18n: /, "") : String(error);
53
+ process.stderr.write(`zerolith-i18n: ${file}: ${message}\n`);
54
+ return 1;
55
+ }
56
+
14
57
  interface Arguments
15
58
  {
16
59
  folder: string | undefined;
@@ -84,10 +127,25 @@ export async function main(argv: readonly string[]): Promise<number>
84
127
  return 1;
85
128
  }
86
129
  const out = parsed.out ?? parsed.folder;
87
- await mkdir(out, { recursive: true });
88
130
  const language = parsed.javascript ? "js" : "ts";
89
131
  const folder = parsed.folder;
90
- const catalogs = new Map<string, Catalog>(await Promise.all(files.map(async (file): Promise<[string, Catalog]> => [basename(file, ".json"), JSON.parse(await readFile(join(folder, file), "utf8")) as Catalog])));
132
+ const read = await Promise.all(files.map(async (file) =>
133
+ {
134
+ try
135
+ {
136
+ return { file, locale: localeOf(file), catalog: catalogOf(await readFile(join(folder, file), "utf8")) };
137
+ }
138
+ catch (error)
139
+ {
140
+ return { file, error };
141
+ }
142
+ }));
143
+ const unreadable = read.find((entry) => "error" in entry);
144
+ if (unreadable !== undefined)
145
+ {
146
+ return failure(unreadable.file, unreadable.error);
147
+ }
148
+ const catalogs = new Map<string, Catalog>(read.map((entry) => [entry.locale ?? "", entry.catalog ?? {}]));
91
149
  const all = new Set<string>();
92
150
  for (const catalog of catalogs.values())
93
151
  {
@@ -97,7 +155,7 @@ export async function main(argv: readonly string[]): Promise<number>
97
155
  }
98
156
  }
99
157
  let missing = 0;
100
- const writes: Promise<void>[] = [];
158
+ const modules = new Map<string, string>();
101
159
  for (const [locale, catalog] of catalogs)
102
160
  {
103
161
  const present = new Set(messageNames(catalog));
@@ -109,16 +167,37 @@ export async function main(argv: readonly string[]): Promise<number>
109
167
  process.stderr.write(`zerolith-i18n: ${locale} lacks ${name}\n`);
110
168
  }
111
169
  }
112
- const compiled = compileMessages(locale, catalog, { language, source: `${locale}.json` });
113
- writes.push(writeFile(join(out, `${locale}.${language}`), compiled.code));
170
+ try
171
+ {
172
+ modules.set(`${locale}.${language}`, compileMessages(locale, catalog, { language, source: `${locale}.json` }).code);
173
+ }
174
+ catch (error)
175
+ {
176
+ return failure(`${locale}.json`, error);
177
+ }
114
178
  }
115
- writes.push(writeFile(join(out, `index.${language}`), compileIndex([...catalogs.keys()], { language })));
116
- await Promise.all(writes);
179
+ modules.set(`index.${language}`, compileIndex([...catalogs.keys()], { language }));
180
+ await mkdir(out, { recursive: true });
181
+ await Promise.all([...modules].map(([name, code]) => writeFile(join(out, name), code)));
117
182
  process.stdout.write(`zerolith-i18n: ${catalogs.size} locale(s), ${all.size} message(s), written to ${out}\n`);
118
183
  return missing > 0 && parsed.strict ? 1 : 0;
119
184
  }
120
185
 
121
- if (process.argv[1] !== undefined && /zerolith-i18n|[\\/]cli\.js$/.test(process.argv[1]))
186
+ /** True when node started this module, directly or through the bin link npm installs, which it resolves the way node resolves its entry. */
187
+ function started(): boolean
188
+ {
189
+ const entry = process.argv[1];
190
+ try
191
+ {
192
+ return entry !== undefined && pathToFileURL(realpathSync(entry)).href === import.meta.url;
193
+ }
194
+ catch
195
+ {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ if (started())
122
201
  {
123
202
  process.exitCode = await main(process.argv.slice(2));
124
203
  }