@mxml3gend/gloss 0.1.0 → 0.1.2
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 +52 -0
- package/dist/check.js +382 -0
- package/dist/config.js +139 -10
- package/dist/gitDiff.js +113 -0
- package/dist/index.js +178 -9
- package/dist/renameKeyUsage.js +4 -12
- package/dist/server.js +25 -1
- package/dist/translationKeys.js +20 -0
- package/dist/translationTree.js +22 -0
- package/dist/typegen.js +30 -0
- package/dist/ui/Gloss_logo.png +0 -0
- package/dist/ui/assets/index-CgyZVU2h.css +1 -0
- package/dist/ui/assets/index-DfgO64nU.js +12 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/logo_full.png +0 -0
- package/dist/usage.js +5 -23
- package/dist/usageExtractor.js +151 -0
- package/dist/usageScanner.js +12 -23
- package/package.json +13 -3
- package/dist/ui/assets/index-CREq9Gop.css +0 -1
- package/dist/ui/assets/index-Dhb2pVPI.js +0 -10
package/dist/gitDiff.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readAllTranslations } from "./fs.js";
|
|
5
|
+
import { flattenObject } from "./translationTree.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
8
|
+
const normalizePath = (value) => value.split(path.sep).join("/");
|
|
9
|
+
const runGit = async (cwd, args) => {
|
|
10
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
11
|
+
return stdout.trim();
|
|
12
|
+
};
|
|
13
|
+
const resolveLocaleFileRelativePath = (cfg, locale, cwd) => {
|
|
14
|
+
const directory = path.isAbsolute(cfg.path) ? cfg.path : path.resolve(cwd, cfg.path);
|
|
15
|
+
const filePath = path.resolve(directory, `${locale}.json`);
|
|
16
|
+
return normalizePath(path.relative(cwd, filePath));
|
|
17
|
+
};
|
|
18
|
+
const readJsonFromGit = async (cwd, ref, fileRelativePath) => {
|
|
19
|
+
if (!fileRelativePath ||
|
|
20
|
+
fileRelativePath.startsWith("../") ||
|
|
21
|
+
path.isAbsolute(fileRelativePath)) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const raw = await runGit(cwd, ["show", `${ref}:${fileRelativePath}`]);
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export async function buildGitKeyDiff(cfg, baseRef = "origin/main") {
|
|
33
|
+
const cwd = projectRoot();
|
|
34
|
+
try {
|
|
35
|
+
const insideRepo = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
36
|
+
if (insideRepo !== "true") {
|
|
37
|
+
return {
|
|
38
|
+
available: false,
|
|
39
|
+
baseRef,
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
keys: [],
|
|
42
|
+
error: "Current directory is not a git repository.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
baseRef,
|
|
50
|
+
generatedAt: new Date().toISOString(),
|
|
51
|
+
keys: [],
|
|
52
|
+
error: "Git is not available in this project.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
let resolvedBaseRef = "";
|
|
56
|
+
try {
|
|
57
|
+
resolvedBaseRef = await runGit(cwd, ["rev-parse", "--verify", `${baseRef}^{commit}`]);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return {
|
|
61
|
+
available: false,
|
|
62
|
+
baseRef,
|
|
63
|
+
generatedAt: new Date().toISOString(),
|
|
64
|
+
keys: [],
|
|
65
|
+
error: `Base ref "${baseRef}" was not found.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const currentTranslations = await readAllTranslations(cfg);
|
|
69
|
+
const currentFlatByLocale = {};
|
|
70
|
+
const baseFlatByLocale = {};
|
|
71
|
+
for (const locale of cfg.locales) {
|
|
72
|
+
currentFlatByLocale[locale] = flattenObject((currentTranslations[locale] ?? {}));
|
|
73
|
+
const localeRelativePath = resolveLocaleFileRelativePath(cfg, locale, cwd);
|
|
74
|
+
const baseJson = await readJsonFromGit(cwd, resolvedBaseRef, localeRelativePath);
|
|
75
|
+
baseFlatByLocale[locale] = flattenObject(baseJson);
|
|
76
|
+
}
|
|
77
|
+
const allKeys = Array.from(new Set(cfg.locales.flatMap((locale) => [
|
|
78
|
+
...Object.keys(baseFlatByLocale[locale] ?? {}),
|
|
79
|
+
...Object.keys(currentFlatByLocale[locale] ?? {}),
|
|
80
|
+
]))).sort((left, right) => left.localeCompare(right));
|
|
81
|
+
const keys = [];
|
|
82
|
+
for (const key of allKeys) {
|
|
83
|
+
const changes = [];
|
|
84
|
+
for (const locale of cfg.locales) {
|
|
85
|
+
const before = baseFlatByLocale[locale]?.[key] ?? "";
|
|
86
|
+
const after = currentFlatByLocale[locale]?.[key] ?? "";
|
|
87
|
+
if (before === after) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const beforeMissing = before.trim() === "";
|
|
91
|
+
const afterMissing = after.trim() === "";
|
|
92
|
+
const kind = beforeMissing && !afterMissing
|
|
93
|
+
? "added"
|
|
94
|
+
: !beforeMissing && afterMissing
|
|
95
|
+
? "removed"
|
|
96
|
+
: "changed";
|
|
97
|
+
changes.push({ locale, kind, before, after });
|
|
98
|
+
}
|
|
99
|
+
if (changes.length > 0) {
|
|
100
|
+
keys.push({
|
|
101
|
+
key,
|
|
102
|
+
changes: changes.sort((left, right) => left.locale.localeCompare(right.locale)),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
available: true,
|
|
108
|
+
baseRef,
|
|
109
|
+
resolvedBaseRef,
|
|
110
|
+
generatedAt: new Date().toISOString(),
|
|
111
|
+
keys,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import open from "open";
|
|
5
|
+
import { printGlossCheck, runGlossCheck } from "./check.js";
|
|
5
6
|
import { GlossConfigError, loadGlossConfig } from "./config.js";
|
|
6
7
|
import { startServer } from "./server.js";
|
|
8
|
+
import { generateKeyTypes } from "./typegen.js";
|
|
7
9
|
const DEFAULT_PORT = 5179;
|
|
8
10
|
const getVersion = async () => {
|
|
9
11
|
const packagePath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
@@ -16,23 +18,175 @@ const printHelp = () => {
|
|
|
16
18
|
|
|
17
19
|
Usage:
|
|
18
20
|
gloss [options]
|
|
21
|
+
gloss check [options]
|
|
22
|
+
gloss gen-types [options]
|
|
23
|
+
gloss open key <translation-key> [options]
|
|
19
24
|
|
|
20
25
|
Options:
|
|
21
|
-
-h, --help
|
|
22
|
-
-v, --version
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
-h, --help Show help
|
|
27
|
+
-v, --version Show version
|
|
28
|
+
|
|
29
|
+
Serve options:
|
|
30
|
+
--no-open Do not open browser automatically
|
|
31
|
+
-p, --port Set server port (default: ${DEFAULT_PORT})
|
|
32
|
+
|
|
33
|
+
Check options:
|
|
34
|
+
--format <human|json|both> Output format (default: human)
|
|
35
|
+
--json Shortcut for --format json
|
|
36
|
+
|
|
37
|
+
Type generation options:
|
|
38
|
+
--out <path> Output file for generated key types (default: i18n-keys.d.ts)
|
|
39
|
+
|
|
40
|
+
Open key options:
|
|
41
|
+
gloss open key <key> Open Gloss focused on a translation key
|
|
25
42
|
`);
|
|
26
43
|
};
|
|
27
44
|
const parseArgs = (args) => {
|
|
28
|
-
const
|
|
45
|
+
const firstArg = args[0];
|
|
46
|
+
if (firstArg === "open") {
|
|
47
|
+
const commandArgs = args.slice(1);
|
|
48
|
+
const base = { help: false, version: false };
|
|
49
|
+
const options = {
|
|
50
|
+
command: "open-key",
|
|
51
|
+
...base,
|
|
52
|
+
noOpen: false,
|
|
53
|
+
port: DEFAULT_PORT,
|
|
54
|
+
key: "",
|
|
55
|
+
};
|
|
56
|
+
if (commandArgs.length === 0) {
|
|
57
|
+
throw new Error("Usage: gloss open key <translation-key> [--port <number>]");
|
|
58
|
+
}
|
|
59
|
+
if (commandArgs[0] === "-h" || commandArgs[0] === "--help") {
|
|
60
|
+
options.help = true;
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
if (commandArgs[0] === "-v" || commandArgs[0] === "--version") {
|
|
64
|
+
options.version = true;
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
let index = 0;
|
|
68
|
+
if (commandArgs[0] === "key") {
|
|
69
|
+
index = 1;
|
|
70
|
+
}
|
|
71
|
+
const keyValue = commandArgs[index];
|
|
72
|
+
if (!keyValue || keyValue.startsWith("-")) {
|
|
73
|
+
throw new Error("Missing translation key. Usage: gloss open key <translation-key>");
|
|
74
|
+
}
|
|
75
|
+
options.key = keyValue.trim();
|
|
76
|
+
index += 1;
|
|
77
|
+
for (; index < commandArgs.length; index += 1) {
|
|
78
|
+
const arg = commandArgs[index];
|
|
79
|
+
if (arg === "-h" || arg === "--help") {
|
|
80
|
+
options.help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg === "-v" || arg === "--version") {
|
|
84
|
+
options.version = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === "--no-open") {
|
|
88
|
+
options.noOpen = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "-p" || arg === "--port") {
|
|
92
|
+
const nextValue = commandArgs[index + 1];
|
|
93
|
+
if (!nextValue) {
|
|
94
|
+
throw new Error("Missing value for --port.");
|
|
95
|
+
}
|
|
96
|
+
const parsed = Number.parseInt(nextValue, 10);
|
|
97
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
98
|
+
throw new Error("Port must be a positive integer.");
|
|
99
|
+
}
|
|
100
|
+
options.port = parsed;
|
|
101
|
+
index += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Unknown argument for gloss open: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
return options;
|
|
107
|
+
}
|
|
108
|
+
const isCommand = firstArg && !firstArg.startsWith("-");
|
|
109
|
+
const command = firstArg === "check" || firstArg === "gen-types" ? firstArg : "serve";
|
|
110
|
+
const commandArgs = command === "serve" ? args : args.slice(1);
|
|
111
|
+
const base = {
|
|
29
112
|
help: false,
|
|
30
113
|
version: false,
|
|
114
|
+
};
|
|
115
|
+
if (command === "check") {
|
|
116
|
+
const options = {
|
|
117
|
+
command,
|
|
118
|
+
...base,
|
|
119
|
+
format: "human",
|
|
120
|
+
};
|
|
121
|
+
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
122
|
+
const arg = commandArgs[index];
|
|
123
|
+
if (arg === "-h" || arg === "--help") {
|
|
124
|
+
options.help = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "-v" || arg === "--version") {
|
|
128
|
+
options.version = true;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (arg === "--json") {
|
|
132
|
+
options.format = "json";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--format") {
|
|
136
|
+
const nextValue = commandArgs[index + 1];
|
|
137
|
+
if (!nextValue) {
|
|
138
|
+
throw new Error("Missing value for --format.");
|
|
139
|
+
}
|
|
140
|
+
if (nextValue !== "human" && nextValue !== "json" && nextValue !== "both") {
|
|
141
|
+
throw new Error("Invalid value for --format. Use human, json, or both.");
|
|
142
|
+
}
|
|
143
|
+
options.format = nextValue;
|
|
144
|
+
index += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Unknown argument for gloss check: ${arg}`);
|
|
148
|
+
}
|
|
149
|
+
return options;
|
|
150
|
+
}
|
|
151
|
+
if (command === "gen-types") {
|
|
152
|
+
const options = {
|
|
153
|
+
command,
|
|
154
|
+
...base,
|
|
155
|
+
};
|
|
156
|
+
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
157
|
+
const arg = commandArgs[index];
|
|
158
|
+
if (arg === "-h" || arg === "--help") {
|
|
159
|
+
options.help = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg === "-v" || arg === "--version") {
|
|
163
|
+
options.version = true;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg === "--out") {
|
|
167
|
+
const nextValue = commandArgs[index + 1];
|
|
168
|
+
if (!nextValue) {
|
|
169
|
+
throw new Error("Missing value for --out.");
|
|
170
|
+
}
|
|
171
|
+
options.outFile = nextValue;
|
|
172
|
+
index += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Unknown argument for gloss gen-types: ${arg}`);
|
|
176
|
+
}
|
|
177
|
+
return options;
|
|
178
|
+
}
|
|
179
|
+
if (isCommand && command === "serve" && firstArg !== undefined) {
|
|
180
|
+
throw new Error(`Unknown command: ${firstArg}`);
|
|
181
|
+
}
|
|
182
|
+
const options = {
|
|
183
|
+
command: "serve",
|
|
184
|
+
...base,
|
|
31
185
|
noOpen: false,
|
|
32
186
|
port: DEFAULT_PORT,
|
|
33
187
|
};
|
|
34
|
-
for (let index = 0; index <
|
|
35
|
-
const arg =
|
|
188
|
+
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
189
|
+
const arg = commandArgs[index];
|
|
36
190
|
if (arg === "-h" || arg === "--help") {
|
|
37
191
|
options.help = true;
|
|
38
192
|
continue;
|
|
@@ -46,7 +200,7 @@ const parseArgs = (args) => {
|
|
|
46
200
|
continue;
|
|
47
201
|
}
|
|
48
202
|
if (arg === "-p" || arg === "--port") {
|
|
49
|
-
const nextValue =
|
|
203
|
+
const nextValue = commandArgs[index + 1];
|
|
50
204
|
if (!nextValue) {
|
|
51
205
|
throw new Error("Missing value for --port.");
|
|
52
206
|
}
|
|
@@ -88,8 +242,23 @@ async function main() {
|
|
|
88
242
|
return;
|
|
89
243
|
}
|
|
90
244
|
const cfg = await loadGlossConfig();
|
|
245
|
+
if (options.command === "check") {
|
|
246
|
+
const result = await runGlossCheck(cfg);
|
|
247
|
+
printGlossCheck(result, options.format);
|
|
248
|
+
if (!result.ok) {
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (options.command === "gen-types") {
|
|
254
|
+
const result = await generateKeyTypes(cfg, { outFile: options.outFile });
|
|
255
|
+
console.log(`Generated ${result.keyCount} keys in ${result.outFile}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
91
258
|
const { port } = await startServer(cfg, options.port);
|
|
92
|
-
const url =
|
|
259
|
+
const url = options.command === "open-key"
|
|
260
|
+
? `http://localhost:${port}/?key=${encodeURIComponent(options.key)}`
|
|
261
|
+
: `http://localhost:${port}`;
|
|
93
262
|
console.log(`Gloss running at ${url}`);
|
|
94
263
|
if (options.noOpen || process.env.CI) {
|
|
95
264
|
return;
|
package/dist/renameKeyUsage.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { replaceTranslationKeyLiterals } from "./usageExtractor.js";
|
|
3
4
|
const IGNORED_DIRECTORIES = new Set([
|
|
4
5
|
"node_modules",
|
|
5
6
|
"dist",
|
|
@@ -8,11 +9,10 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
8
9
|
"coverage",
|
|
9
10
|
]);
|
|
10
11
|
const SCANNED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
11
|
-
const TRANSLATION_CALL_REGEX = /(\b(?:t|translate)\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
12
12
|
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
13
13
|
const normalizePath = (filePath) => filePath.split(path.sep).join("/");
|
|
14
14
|
const isScannableFile = (fileName) => SCANNED_EXTENSIONS.has(path.extname(fileName));
|
|
15
|
-
export async function renameKeyUsage(oldKey, newKey, rootDir = projectRoot()) {
|
|
15
|
+
export async function renameKeyUsage(oldKey, newKey, rootDir = projectRoot(), mode) {
|
|
16
16
|
if (!oldKey || !newKey || oldKey === newKey) {
|
|
17
17
|
return {
|
|
18
18
|
changedFiles: [],
|
|
@@ -39,19 +39,11 @@ export async function renameKeyUsage(oldKey, newKey, rootDir = projectRoot()) {
|
|
|
39
39
|
}
|
|
40
40
|
filesScanned += 1;
|
|
41
41
|
const source = await fs.readFile(fullPath, "utf8");
|
|
42
|
-
|
|
43
|
-
const updated = source.replace(TRANSLATION_CALL_REGEX, (match, prefix, quote, key) => {
|
|
44
|
-
if (key !== oldKey) {
|
|
45
|
-
return match;
|
|
46
|
-
}
|
|
47
|
-
fileReplacements += 1;
|
|
48
|
-
replacements += 1;
|
|
49
|
-
return `${prefix}${quote}${newKey}${quote}`;
|
|
50
|
-
});
|
|
51
|
-
TRANSLATION_CALL_REGEX.lastIndex = 0;
|
|
42
|
+
const { updated, replacements: fileReplacements } = replaceTranslationKeyLiterals(source, fullPath, oldKey, newKey, mode);
|
|
52
43
|
if (fileReplacements === 0 || updated === source) {
|
|
53
44
|
continue;
|
|
54
45
|
}
|
|
46
|
+
replacements += fileReplacements;
|
|
55
47
|
await fs.writeFile(fullPath, updated, "utf8");
|
|
56
48
|
changedFiles.push(normalizePath(path.relative(rootDir, fullPath)));
|
|
57
49
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,9 @@ import path from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import express from "express";
|
|
5
5
|
import cors from "cors";
|
|
6
|
+
import { runGlossCheck } from "./check.js";
|
|
6
7
|
import { readAllTranslations, writeAllTranslations } from "./fs.js";
|
|
8
|
+
import { buildGitKeyDiff } from "./gitDiff.js";
|
|
7
9
|
import { buildKeyUsageMap } from "./usage.js";
|
|
8
10
|
import { inferUsageRoot, scanUsage } from "./usageScanner.js";
|
|
9
11
|
import { renameKeyUsage } from "./renameKeyUsage.js";
|
|
@@ -39,6 +41,28 @@ export function createServerApp(cfg) {
|
|
|
39
41
|
const usage = await buildKeyUsageMap(cfg);
|
|
40
42
|
res.json(usage);
|
|
41
43
|
});
|
|
44
|
+
app.get("/api/check", async (req, res) => {
|
|
45
|
+
const result = await runGlossCheck(cfg);
|
|
46
|
+
const summaryValue = typeof req.query.summary === "string" ? req.query.summary : "";
|
|
47
|
+
const summaryOnly = summaryValue === "1" || summaryValue === "true";
|
|
48
|
+
if (summaryOnly) {
|
|
49
|
+
res.json({
|
|
50
|
+
ok: result.ok,
|
|
51
|
+
generatedAt: result.generatedAt,
|
|
52
|
+
summary: result.summary,
|
|
53
|
+
hardcodedTexts: result.hardcodedTexts.slice(0, 20),
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
res.json(result);
|
|
58
|
+
});
|
|
59
|
+
app.get("/api/git-diff", async (req, res) => {
|
|
60
|
+
const base = typeof req.query.base === "string" && req.query.base.trim()
|
|
61
|
+
? req.query.base.trim()
|
|
62
|
+
: "origin/main";
|
|
63
|
+
const diff = await buildGitKeyDiff(cfg, base);
|
|
64
|
+
res.json(diff);
|
|
65
|
+
});
|
|
42
66
|
app.post("/api/translations", async (req, res) => {
|
|
43
67
|
const data = req.body;
|
|
44
68
|
await writeAllTranslations(cfg, data);
|
|
@@ -56,7 +80,7 @@ export function createServerApp(cfg) {
|
|
|
56
80
|
return;
|
|
57
81
|
}
|
|
58
82
|
try {
|
|
59
|
-
const result = await renameKeyUsage(oldKey, newKey);
|
|
83
|
+
const result = await renameKeyUsage(oldKey, newKey, undefined, cfg.scan?.mode);
|
|
60
84
|
res.json({ ok: true, ...result });
|
|
61
85
|
}
|
|
62
86
|
catch (error) {
|
package/dist/translationKeys.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const TRANSLATION_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
|
|
2
|
+
const KEY_CHAR_PATTERN = /^[A-Za-z0-9._:/-]+$/;
|
|
2
3
|
export const isLikelyTranslationKey = (value) => {
|
|
3
4
|
const key = value.trim();
|
|
4
5
|
if (key.length === 0 || key.length > 160) {
|
|
@@ -15,3 +16,22 @@ export const isLikelyTranslationKey = (value) => {
|
|
|
15
16
|
}
|
|
16
17
|
return TRANSLATION_KEY_PATTERN.test(key);
|
|
17
18
|
};
|
|
19
|
+
export const getInvalidTranslationKeyReason = (value) => {
|
|
20
|
+
const key = value.trim();
|
|
21
|
+
if (!key) {
|
|
22
|
+
return "Key is empty.";
|
|
23
|
+
}
|
|
24
|
+
if (key.startsWith(".") || key.endsWith(".")) {
|
|
25
|
+
return "Key cannot start or end with a dot.";
|
|
26
|
+
}
|
|
27
|
+
if (key.includes("..")) {
|
|
28
|
+
return "Key cannot contain consecutive dots.";
|
|
29
|
+
}
|
|
30
|
+
if (key.split(".").some((segment) => segment.trim() === "")) {
|
|
31
|
+
return "Key contains an empty segment.";
|
|
32
|
+
}
|
|
33
|
+
if (!KEY_CHAR_PATTERN.test(key)) {
|
|
34
|
+
return "Key contains unsupported characters.";
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
|
+
export const flattenObject = (obj) => {
|
|
3
|
+
const result = {};
|
|
4
|
+
const visit = (node, path) => {
|
|
5
|
+
for (const [key, value] of Object.entries(node)) {
|
|
6
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
7
|
+
if (isPlainObject(value)) {
|
|
8
|
+
visit(value, nextPath);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
result[nextPath] = value;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (value !== undefined) {
|
|
16
|
+
result[nextPath] = String(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
visit(obj, "");
|
|
21
|
+
return result;
|
|
22
|
+
};
|
package/dist/typegen.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readAllTranslations } from "./fs.js";
|
|
4
|
+
import { flattenObject } from "./translationTree.js";
|
|
5
|
+
const projectRoot = () => process.env.INIT_CWD || process.cwd();
|
|
6
|
+
const renderTypeFile = (keys) => {
|
|
7
|
+
const keyUnion = keys.length === 0
|
|
8
|
+
? "never"
|
|
9
|
+
: keys.map((key) => ` | ${JSON.stringify(key)}`).join("\n");
|
|
10
|
+
return `/* This file is auto-generated by Gloss. Do not edit manually. */
|
|
11
|
+
|
|
12
|
+
export type I18nKey =
|
|
13
|
+
${keyUnion};
|
|
14
|
+
|
|
15
|
+
export type TranslateFn = (
|
|
16
|
+
key: I18nKey,
|
|
17
|
+
variables?: Record<string, string | number>,
|
|
18
|
+
) => string;
|
|
19
|
+
|
|
20
|
+
export type GlossI18nKey = I18nKey;
|
|
21
|
+
`;
|
|
22
|
+
};
|
|
23
|
+
export async function generateKeyTypes(cfg, options = {}) {
|
|
24
|
+
const data = await readAllTranslations(cfg);
|
|
25
|
+
const keys = Array.from(new Set(Object.values(data).flatMap((tree) => Object.keys(flattenObject((tree ?? {})))))).sort((left, right) => left.localeCompare(right));
|
|
26
|
+
const outFile = path.resolve(projectRoot(), options.outFile ?? "i18n-keys.d.ts");
|
|
27
|
+
const content = renderTypeFile(keys);
|
|
28
|
+
await fs.writeFile(outFile, content, "utf8");
|
|
29
|
+
return { outFile, keyCount: keys.length };
|
|
30
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--text-primary: #0f2343;--text-secondary: #3f5478;--text-muted: #5b7196;--surface-card: #ffffff;--surface-border: #d6e0f0;font-family:Manrope,Avenir Next,Segoe UI,sans-serif;line-height:1.45;font-weight:500;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,*:before,*:after{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:radial-gradient(circle at 8% 0%,rgba(255,186,132,.26),transparent 42%),radial-gradient(circle at 90% 18%,rgba(120,184,255,.28),transparent 40%),linear-gradient(180deg,#f3f7ff,#f8f1e8)}#root{width:100%}.gloss-app{--space-1: .5rem;--space-2: .75rem;--space-3: 1rem;--space-4: 1.5rem;--space-5: 2rem;max-width:1360px;margin:0 auto;padding:var(--space-4) var(--space-3) calc(var(--space-5) + var(--space-2));color:var(--text-primary)}.hero{padding:var(--space-3) var(--space-4);border-radius:16px;border:1px solid #d9e3f6;background:linear-gradient(140deg,#fdfeff,#f8faff 55%,#f4f8ff);box-shadow:0 10px 24px #16264612}.hero__top{display:flex;justify-content:flex-end;align-items:center;gap:1rem;flex-wrap:wrap}.hero__eyebrow{margin:0;text-transform:uppercase;letter-spacing:.08em;font-size:.75rem;color:var(--text-muted)}.hero__title{margin:.4rem 0 0;font-size:clamp(2.1rem,5vw,3.5rem);line-height:1;letter-spacing:-.02em}.hero__logo{display:block;width:min(420px,100%);height:auto;margin:.4rem 0 0}.hero__summary{margin:var(--space-2) 0 var(--space-2);max-width:72ch;color:var(--text-secondary);line-height:1.45}.hero__stats{display:flex;flex-wrap:wrap;gap:var(--space-1);margin-bottom:var(--space-3)}.stat-chip{display:flex;align-items:baseline;gap:.45rem;padding:.4rem .68rem;border-radius:999px;background:#fffffff5;border:1px solid rgba(21,37,66,.09)}.stat-chip span{font-size:.75rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.stat-chip strong{font-size:1rem}.hero__actions{display:flex;flex-wrap:wrap;gap:.625rem}.language-switch{display:inline-flex;align-items:center;gap:.45rem;padding:.32rem .4rem;border-radius:999px;border:1px solid var(--surface-border);background:#fffffff0}.support-cards{margin-top:var(--space-2);display:grid;gap:var(--space-1);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));opacity:.88}.support-card{border-radius:12px;border:1px solid #e2e9f7;background:#fbfcff;padding:var(--space-2) var(--space-3)}.support-card h2{margin:0;font-size:.95rem}.support-card p{margin:var(--space-1) 0 0;color:var(--text-secondary);font-size:.88rem;line-height:1.45}.editor-shell{margin-top:var(--space-3);border-radius:18px;border:1px solid #dbe4f5;background:#fdfefe;padding:var(--space-3);box-shadow:0 16px 30px #0f234c12;display:grid;gap:var(--space-2)}.status-bar{margin:0;border-radius:12px;border:1px solid #dbe5f7;background:#f8fbff;padding:.625rem .75rem;display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);flex-wrap:wrap}.status-bar__main{margin:0;font-weight:600;font-size:.9rem}.status-bar__main--error{color:#8f2935}.status-bar__main--warning{color:#7b5b23;display:flex;align-items:center;gap:var(--space-1);flex-wrap:wrap}.status-bar__main--success{color:#1d6a42}.status-bar__main--info{color:var(--text-secondary)}.status-bar__meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.status-chip{display:inline-flex;align-items:center;gap:.35rem;border-radius:999px;border:1px solid #cad8f2;background:#f0f5ff;color:#29476f;font-size:.76rem;font-weight:600;padding:.18rem .5rem}.status-chip[type=button]{cursor:pointer}.status-chip--muted{background:#f6f8fc;border-color:#dde4f2;color:#4d6285}.status-chip--warning{background:#fff5de;border-color:#f4d090;color:#7a4f08}.status-chip__action{opacity:.85;font-size:.72rem;border-left:1px solid currentColor;padding-left:.4rem}.status-bar__details{width:100%;border:1px solid #e0e8f7;border-radius:10px;background:#fff;padding:.6rem .75rem}.status-bar__details strong{display:block;margin-bottom:.45rem;color:#2f4b75;font-size:.82rem}.status-bar__details p{margin:0;color:var(--text-secondary);font-size:.84rem}.status-bar__details ul{margin:0;padding-left:1rem;display:grid;gap:.3rem}.status-bar__details li{color:#35527c;font-size:.82rem;line-height:1.35}.loading-state{margin:20vh auto 0;max-width:280px;border-radius:12px;border:1px solid #d5e2fb;background:#f7faff;padding:var(--space-3);text-align:center;color:var(--text-secondary)}.notice{margin:0 0 .7rem;padding:.6rem .75rem;border-radius:10px;border:1px solid transparent}.notice--error{color:#901f1a;background:#fff3f2;border-color:#f7cdc8}.notice--success{color:#0d6837;background:#edf8f0;border-color:#b6e4c3}.notice--warning{color:#7a4f08;background:#fff5de;border-color:#f4d090}.notice--stale{color:#7a2b31;background:#fff2f4;border-color:#f4c0c9;display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}.editor-tabs{display:flex;gap:var(--space-1);margin:0;padding:.25rem;border:1px solid #e2e9f6;border-radius:11px;background:#f7faff}.translations-workspace{display:grid;gap:var(--space-2)}.editor-controls{display:grid;gap:var(--space-1);padding:var(--space-1);border-radius:14px;border:1px solid #e1e8f6;background:linear-gradient(180deg,#f9fbff,#f6f9ff)}.toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.toolbar__primary{flex:1 1 280px;min-width:240px}.toolbar__secondary{display:flex;align-items:flex-end;gap:.5rem;flex-wrap:wrap}.toolbar__field{display:grid;gap:.32rem;min-width:160px}.toolbar__field--search input{width:100%}.toolbar__field span{font-size:.75rem;font-weight:600;color:#5f7394;text-transform:uppercase;letter-spacing:.06em}.toolbar__toggle{display:flex;align-items:center;gap:.4rem;font-size:.88rem;padding:.42rem .6rem;border:1px solid #dbe5f7;border-radius:10px;background:#fdfefe}.toolbar__advanced{width:100%;border:1px solid #dbe5f7;border-radius:12px;background:#f8fbff;padding:.7rem;display:grid;gap:.6rem}.toolbar__advanced-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap}.toolbar__advanced-head strong{font-size:.84rem;color:#2f4b75}.toolbar__advanced-actions{display:flex;gap:.4rem;flex-wrap:wrap}.toolbar__advanced-empty{margin:0;color:var(--text-secondary);font-size:.84rem}.toolbar__git-warning{margin:0;font-size:.82rem;color:#8f3c45}.toolbar__rules{display:grid;gap:.45rem}.toolbar__rule{display:grid;grid-template-columns:minmax(140px,1fr) minmax(140px,1fr) minmax(140px,1fr) auto;gap:.45rem;align-items:end;padding:.55rem;border:1px solid #d9e4f8;border-radius:10px;background:#fff}.toolbar__sort{display:flex;align-items:end;gap:.45rem;flex-wrap:wrap;padding-top:.25rem;border-top:1px dashed #d4dff4}.add-key-form{display:flex;align-items:flex-end;gap:var(--space-1);flex-wrap:wrap;margin:0;padding:var(--space-2);border:1px solid #e4ebf8;border-radius:12px;background:#fff}.add-key-form input{min-width:260px;flex:1 1 280px}.add-key-form__error{margin:0;color:#8f3c45;font-size:.82rem;padding:0 .125rem}.editor-main{display:grid;grid-template-columns:minmax(230px,280px) minmax(0,1fr);gap:var(--space-3);align-items:start}.editor-content{min-width:0;border:1px solid #dfe8f6;border-radius:15px;background:#fcfdff;padding:var(--space-2);box-shadow:inset 0 1px #fff}.file-tree{border:1px solid #d8e3f4;border-radius:14px;background:linear-gradient(180deg,#f8faff,#f2f7ff);padding:var(--space-2);max-height:74vh;overflow:auto;box-shadow:inset 0 1px #fff}.file-tree__title{margin:0 0 var(--space-1);font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);position:sticky;top:-.5rem;z-index:2;background:linear-gradient(180deg,#f8faff,#f8fafff5);padding:.45rem 0 .35rem}.file-tree__all-btn,.file-tree__folder-btn,.file-tree__file-btn{width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;padding:.42rem .54rem;color:var(--text-primary);font:inherit;cursor:pointer}.file-tree__all-btn,.file-tree__file-btn{display:flex;align-items:center;justify-content:space-between;gap:.5rem}.file-tree__folder-btn{display:inline-flex;align-items:center;gap:.38rem;color:#26456f;font-weight:600}.file-tree__caret{width:.8rem;color:#50688f}.file-tree__folder-name{overflow:hidden;text-overflow:ellipsis}.file-tree__all-btn:hover,.file-tree__folder-btn:hover,.file-tree__file-btn:hover{background:#e9f1ff}.file-tree__all-btn.is-selected,.file-tree__file-btn.is-selected{border-color:#8caadd;background:#e4edff;box-shadow:inset 3px 0 #466fcf}.file-tree__file-name{overflow:hidden;text-overflow:ellipsis}.file-tree__file-count{color:var(--text-muted);font-size:.72rem;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;background:#edf2fd;border:1px solid #d4e0f5;border-radius:999px;padding:.1rem .38rem}.file-tree__list{list-style:none;margin:var(--space-1) 0 0;padding:0;display:grid;gap:.1rem}.file-tree__item{margin:0}.file-tree__empty{margin:.55rem 0 0;color:var(--text-secondary);font-size:.88rem}.duplicates-panel{border:1px solid var(--surface-border);border-radius:12px;background:#f8fafe;padding:.7rem}.duplicates-panel__title{margin:0 0 .65rem;font-size:.95rem;font-weight:700}.duplicates-panel__list{display:grid;gap:.75rem}.duplicate-locale{border:1px solid #d7e2f7;border-radius:10px;background:#fff;padding:.55rem}.duplicate-locale__title{margin:0 0 .45rem;font-size:.78rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}.duplicate-group{border:1px solid #e3ebfb;border-radius:9px;padding:.5rem;margin-top:.45rem}.duplicate-group__top{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;align-items:center}.duplicate-group__value{overflow-wrap:anywhere}.duplicate-group__count{font-size:.78rem;color:var(--text-muted)}.duplicate-group__keys{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.2rem;color:var(--text-secondary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.table-wrap{border-radius:12px;border:1px solid #d8e3f4;background:#fff;overflow:auto;max-height:74vh}.grid-table{width:100%;min-width:1180px;border-collapse:collapse}.grid-table th{text-align:left;padding:.78rem .72rem;border-bottom:1px solid #d9e3f5;background:#f6f9ff;position:sticky;top:0;z-index:2;color:#3a557f;font-size:.76rem;text-transform:uppercase;letter-spacing:.06em}.grid-table td{padding:.72rem .66rem;border-bottom:1px solid #edf1f8;vertical-align:top}.grid-table tr.row-state--none>td{background:#fff7f6}.grid-table tr.row-state--partial>td{background:#fffaf2}.grid-table tr.row-state--highlighted>td{background:#edf4ff}.grid-table tr.row-state--none td.value-cell--dirty,.grid-table tr.row-state--partial td.value-cell--dirty{background:#e9f2ff}.grid-table tr.row-state--none td.value-cell--dirty-missing,.grid-table tr.row-state--partial td.value-cell--dirty-missing{background:#fff2df}.grid-table tbody tr:not(.usage-files-row):not(.virtual-spacer):hover>td{background-color:#f7fafe}.key-col{width:320px;min-width:320px;font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.83rem;color:#1f3659;border-right:1px solid #d7e2f4;background-clip:padding-box}.key-col--unused{color:#7a5b2f}.key-col--file-selected{box-shadow:inset 3px 0 #4a74d9}.key-col--placeholder-warning{box-shadow:inset 0 -2px #e59f35}.usage-col{width:130px}.usage-cell{color:var(--text-secondary);white-space:nowrap}.usage-cell--unused{background:#fffaf3}.usage-toggle{border:0;background:none;padding:0;font:inherit;color:#1848a3;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.usage-tag{font-size:.74rem;font-weight:600;color:#8b6a39;text-transform:lowercase}.usage-files-row td{background:#f8fbff;padding-top:.7rem;padding-bottom:.8rem}.virtual-spacer td{border-bottom:0;padding:0}.usage-files{font-size:.82rem;color:var(--text-secondary);border:1px solid #dde6f6;border-radius:10px;background:#fff;padding:.6rem .7rem}.usage-files strong{margin-right:.45rem;color:var(--text-primary)}.usage-files-list{margin:.45rem 0 0;padding-left:1.2rem;display:grid;gap:.26rem;color:var(--text-primary);font-family:JetBrains Mono,SFMono-Regular,ui-monospace,monospace;font-size:.78rem}.key-diff-block{margin-top:.7rem;padding-top:.55rem;border-top:1px dashed #d7e3f8}.key-diff-line{overflow-wrap:anywhere}.status-col{width:176px;min-width:176px;color:#4a5f80;font-size:.84rem;line-height:1.4}.status-col__tags{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.25rem}.status-inline-tag{display:inline-flex;align-items:center;border-radius:999px;padding:.1rem .42rem;font-size:.7rem;font-weight:700;letter-spacing:.03em}.status-inline-tag--warning{color:#8a5a15;background:#fff4df;border:1px solid #efd2a1}.status-inline-tag--info{color:#214f98;background:#edf4ff;border:1px solid #c8daf8}.locale-col,.locale-cell{min-width:220px}.value-cell{background:transparent}.value-cell--missing{background:#fffaf4}.value-cell--dirty{background:#e9f2ff}.value-cell--dirty-missing{background:#fff3e3}.value-cell--active{box-shadow:inset 0 0 0 2px #2c5ecf57}.value-input{box-sizing:border-box;width:100%;resize:vertical;min-height:2.25rem;line-height:1.35}.value-input--dirty{border-color:#0e5fd8;box-shadow:0 0 0 2px #0e5fd814}.actions-col{width:220px;min-width:220px}.row-actions{display:flex;gap:.4rem;flex-wrap:wrap}.rename-form{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}.inline-error{color:#9a241f;width:100%;font-size:.82rem}.empty-state{margin:1rem auto;padding:2.25rem 1.25rem;border:1px dashed #dbe4f5;border-radius:12px;background:linear-gradient(180deg,#fbfdff,#f6f9ff);text-align:center;max-width:680px;color:var(--text-secondary);line-height:1.5}.footer-actions{margin-top:var(--space-2);padding:var(--space-2) 0 0;border-top:1px solid #e2e9f6;display:flex;justify-content:flex-end;position:sticky;bottom:0;background:linear-gradient(180deg,#fdfeff00,#fdfeff 32%);z-index:3}.footer-actions .btn--primary{min-width:152px;min-height:2.5rem;font-size:.95rem}.btn{border-radius:10px;border:1px solid transparent;padding:.5rem .8rem;font-size:.9rem;font-weight:600;text-decoration:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:transform .12s ease,box-shadow .12s ease,background .12s ease}.btn:hover{transform:translateY(-.5px)}.btn:disabled{opacity:.55;cursor:not-allowed;transform:none}.btn--primary{color:#fff;background:linear-gradient(135deg,#245fdf,#1b49be);box-shadow:0 10px 18px #1f59d942}.btn--ghost{color:#1d355c;border-color:#c2d0ea;background:#f6f9ff}.btn--support{color:#fff;background:linear-gradient(135deg,#ff7849,#f14c4c);box-shadow:0 7px 16px #f14c4c3b}.btn--danger{color:#9c212a;border-color:#eec1c7;background:#fff6f7}.btn--small{padding:.4rem .6rem;font-size:.8rem}.is-active{border-color:#4d73d7;background:#eaf1ff}input,textarea,select{border-radius:9px;border:1px solid #c4d2ea;background:#fff;color:var(--text-primary);padding:.48rem .6rem;font:inherit}input:focus,textarea:focus,select:focus{outline:none;border-color:#1f5eff;box-shadow:0 0 0 3px #1f5eff24}.modal-overlay{position:fixed;inset:0;background:#0e162480;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.modal-dialog{width:min(480px,100%);border-radius:14px;border:1px solid var(--surface-border);background:var(--surface-card);box-shadow:0 24px 44px #0e1f4847;padding:1rem;display:grid;gap:.75rem}.modal-dialog__message{margin:0;color:var(--text-primary);white-space:pre-line}.modal-dialog__actions{display:flex;justify-content:flex-end;gap:.5rem}@media(max-width:860px){.gloss-app{padding:var(--space-3) var(--space-2) calc(var(--space-5) + var(--space-2))}.hero{padding:var(--space-3)}.hero__top{align-items:flex-start}.editor-shell{padding:var(--space-2)}.status-bar{align-items:flex-start}.editor-controls{padding:.5rem}.toolbar__primary,.toolbar__secondary,.toolbar__field,.toolbar__field--search,.toolbar__rule{width:100%}.toolbar__rule{grid-template-columns:1fr;align-items:stretch}.toolbar__field input,.toolbar__field select,.toolbar__secondary .btn,.add-key-form input{width:100%}.editor-main{grid-template-columns:1fr}.file-tree{max-height:none}}
|