@mxml3gend/gloss 0.1.1 → 0.1.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 +74 -0
- package/dist/baseline.js +118 -0
- package/dist/cache.js +78 -0
- package/dist/cacheMetrics.js +120 -0
- package/dist/check.js +510 -0
- package/dist/config.js +214 -10
- package/dist/fs.js +105 -6
- package/dist/gitDiff.js +113 -0
- package/dist/hooks.js +101 -0
- package/dist/index.js +437 -12
- package/dist/renameKeyUsage.js +4 -12
- package/dist/server.js +163 -9
- package/dist/translationKeys.js +20 -0
- package/dist/translationTree.js +42 -0
- package/dist/typegen.js +30 -0
- package/dist/ui/Gloss_logo.png +0 -0
- package/dist/ui/assets/index-BCr07xD_.js +21 -0
- package/dist/ui/assets/index-CjmLcA1x.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/logo_full.png +0 -0
- package/dist/usage.js +105 -22
- package/dist/usageExtractor.js +151 -0
- package/dist/usageScanner.js +110 -28
- package/dist/xliff.js +92 -0
- package/package.json +15 -5
- package/dist/ui/assets/index-CREq9Gop.css +0 -1
- package/dist/ui/assets/index-Dhb2pVPI.js +0 -10
package/README.md
CHANGED
|
@@ -35,6 +35,12 @@ export default {
|
|
|
35
35
|
defaultLocale: "en",
|
|
36
36
|
path: "src/i18n",
|
|
37
37
|
format: "json",
|
|
38
|
+
scan: {
|
|
39
|
+
include: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
40
|
+
exclude: ["**/*.test.tsx"],
|
|
41
|
+
mode: "regex", // or "ast" for strict parsing
|
|
42
|
+
},
|
|
43
|
+
strictPlaceholders: true, // default true; set false to treat placeholder mismatches as warnings
|
|
38
44
|
};
|
|
39
45
|
```
|
|
40
46
|
|
|
@@ -46,6 +52,10 @@ module.exports = {
|
|
|
46
52
|
defaultLocale: "en",
|
|
47
53
|
path: "src/i18n",
|
|
48
54
|
format: "json",
|
|
55
|
+
scan: {
|
|
56
|
+
mode: "ast",
|
|
57
|
+
},
|
|
58
|
+
strictPlaceholders: false,
|
|
49
59
|
};
|
|
50
60
|
```
|
|
51
61
|
|
|
@@ -55,5 +65,69 @@ module.exports = {
|
|
|
55
65
|
gloss --help
|
|
56
66
|
gloss --version
|
|
57
67
|
gloss --no-open
|
|
68
|
+
gloss --no-cache
|
|
58
69
|
gloss --port 5179
|
|
70
|
+
gloss open key auth.login.title
|
|
71
|
+
gloss check --no-cache
|
|
72
|
+
gloss cache status
|
|
73
|
+
gloss cache clear
|
|
74
|
+
npm run test:perf
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## CI Guardrails
|
|
78
|
+
|
|
79
|
+
Run project checks for missing/orphan/invalid keys, placeholder mismatches, and potential hardcoded UI text:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
gloss check
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Machine-readable output:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gloss check --format json
|
|
89
|
+
gloss check --format both
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`gloss check` exits with code `1` when issues are found, so it is CI-friendly.
|
|
93
|
+
|
|
94
|
+
The local UI also consumes this data through `/api/check` and shows a hardcoded-text status chip.
|
|
95
|
+
|
|
96
|
+
## Performance Regression Gate
|
|
97
|
+
|
|
98
|
+
Gloss ships with a deterministic 1000-key fixture regression test for scanner performance.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm run test:perf
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Optional environment overrides:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
GLOSS_PERF_COLD_MAX_MS=5000
|
|
108
|
+
GLOSS_PERF_WARM_MAX_MS=3500
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Typed Key Generation
|
|
112
|
+
|
|
113
|
+
Generate `i18n-keys.d.ts` from current translation keys:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
gloss gen-types
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use the generated `I18nKey` type in your app's `t(...)` signature to get key autocomplete while typing.
|
|
120
|
+
|
|
121
|
+
Custom output path:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
gloss gen-types --out src/types/i18n-keys.d.ts
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Deep-Link Open
|
|
128
|
+
|
|
129
|
+
Open Gloss directly focused on a key:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
gloss open key auth.login.title
|
|
59
133
|
```
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const BASELINE_DIRECTORY = ".gloss";
|
|
4
|
+
const BASELINE_FILENAME = "baseline.json";
|
|
5
|
+
const SUMMARY_KEYS = [
|
|
6
|
+
"missingTranslations",
|
|
7
|
+
"orphanKeys",
|
|
8
|
+
"invalidKeys",
|
|
9
|
+
"placeholderMismatches",
|
|
10
|
+
"hardcodedTexts",
|
|
11
|
+
"errorIssues",
|
|
12
|
+
"warningIssues",
|
|
13
|
+
"totalIssues",
|
|
14
|
+
];
|
|
15
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
16
|
+
const emptyDelta = () => ({
|
|
17
|
+
missingTranslations: 0,
|
|
18
|
+
orphanKeys: 0,
|
|
19
|
+
invalidKeys: 0,
|
|
20
|
+
placeholderMismatches: 0,
|
|
21
|
+
hardcodedTexts: 0,
|
|
22
|
+
errorIssues: 0,
|
|
23
|
+
warningIssues: 0,
|
|
24
|
+
totalIssues: 0,
|
|
25
|
+
});
|
|
26
|
+
const normalizeSummary = (value) => {
|
|
27
|
+
if (!value || typeof value !== "object") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const source = value;
|
|
31
|
+
const next = {};
|
|
32
|
+
for (const key of SUMMARY_KEYS) {
|
|
33
|
+
const entry = source[key];
|
|
34
|
+
if (!isFiniteNumber(entry)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
next[key] = entry;
|
|
38
|
+
}
|
|
39
|
+
return next;
|
|
40
|
+
};
|
|
41
|
+
const baselineFilePath = (rootDir) => path.join(rootDir, BASELINE_DIRECTORY, BASELINE_FILENAME);
|
|
42
|
+
const readBaselineFile = async (rootDir) => {
|
|
43
|
+
const filePath = baselineFilePath(rootDir);
|
|
44
|
+
try {
|
|
45
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (parsed.schemaVersion !== 1 || typeof parsed.updatedAt !== "string") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const summary = normalizeSummary(parsed.summary);
|
|
51
|
+
if (!summary) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
updatedAt: parsed.updatedAt,
|
|
57
|
+
summary,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = error.code;
|
|
62
|
+
if (code === "ENOENT") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const computeDelta = (current, previous) => {
|
|
69
|
+
if (!previous) {
|
|
70
|
+
return emptyDelta();
|
|
71
|
+
}
|
|
72
|
+
const delta = {};
|
|
73
|
+
for (const key of SUMMARY_KEYS) {
|
|
74
|
+
delta[key] = current[key] - previous[key];
|
|
75
|
+
}
|
|
76
|
+
return delta;
|
|
77
|
+
};
|
|
78
|
+
export async function updateIssueBaseline(rootDir, summary) {
|
|
79
|
+
const previous = await readBaselineFile(rootDir);
|
|
80
|
+
const delta = computeDelta(summary, previous?.summary ?? null);
|
|
81
|
+
const currentUpdatedAt = new Date().toISOString();
|
|
82
|
+
const baseline = {
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
updatedAt: currentUpdatedAt,
|
|
85
|
+
summary,
|
|
86
|
+
};
|
|
87
|
+
const filePath = baselineFilePath(rootDir);
|
|
88
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
89
|
+
await fs.writeFile(`${filePath}.tmp`, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
90
|
+
await fs.rename(`${filePath}.tmp`, filePath);
|
|
91
|
+
return {
|
|
92
|
+
hasPrevious: Boolean(previous),
|
|
93
|
+
baselinePath: path.relative(rootDir, filePath) || filePath,
|
|
94
|
+
previousUpdatedAt: previous?.updatedAt ?? null,
|
|
95
|
+
currentUpdatedAt,
|
|
96
|
+
delta,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function resetIssueBaseline(rootDir) {
|
|
100
|
+
const filePath = baselineFilePath(rootDir);
|
|
101
|
+
let existed = true;
|
|
102
|
+
try {
|
|
103
|
+
await fs.rm(filePath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const code = error.code;
|
|
107
|
+
if (code === "ENOENT") {
|
|
108
|
+
existed = false;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
existed,
|
|
116
|
+
baselinePath: path.relative(rootDir, filePath) || filePath,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { clearCacheMetrics, readCacheMetrics, } from "./cacheMetrics.js";
|
|
2
|
+
import { clearKeyUsageCache, getKeyUsageCacheStatus, keyUsageCacheKey, } from "./usage.js";
|
|
3
|
+
import { clearUsageScannerCache, getUsageScannerCacheStatus, inferUsageRoot, usageScannerCacheKey, } from "./usageScanner.js";
|
|
4
|
+
const fromMetricsEntry = (cacheKey, entry, staleRelativeToConfig) => {
|
|
5
|
+
if (!entry) {
|
|
6
|
+
return {
|
|
7
|
+
cacheKey,
|
|
8
|
+
fileCount: 0,
|
|
9
|
+
totalSizeBytes: 0,
|
|
10
|
+
oldestMtimeMs: null,
|
|
11
|
+
staleRelativeToConfig,
|
|
12
|
+
source: "missing",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
cacheKey,
|
|
17
|
+
fileCount: entry.fileCount,
|
|
18
|
+
totalSizeBytes: entry.totalSizeBytes,
|
|
19
|
+
oldestMtimeMs: entry.oldestMtimeMs,
|
|
20
|
+
staleRelativeToConfig,
|
|
21
|
+
source: "metrics",
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export const getCacheStatus = async (rootDir, cfg) => {
|
|
25
|
+
const usageKey = usageScannerCacheKey(inferUsageRoot(cfg), cfg.scan);
|
|
26
|
+
const keyUsageKey = keyUsageCacheKey(cfg);
|
|
27
|
+
const metrics = await readCacheMetrics(rootDir);
|
|
28
|
+
const metricsUsage = metrics?.usageScanner?.[usageKey] ?? null;
|
|
29
|
+
const metricsKeyUsage = metrics?.keyUsage?.[keyUsageKey] ?? null;
|
|
30
|
+
const usageStale = !metricsUsage;
|
|
31
|
+
const keyUsageStale = !metricsKeyUsage;
|
|
32
|
+
const usageBucket = metricsUsage
|
|
33
|
+
? fromMetricsEntry(usageKey, metricsUsage, usageStale)
|
|
34
|
+
: (() => {
|
|
35
|
+
const memory = getUsageScannerCacheStatus(inferUsageRoot(cfg), cfg.scan);
|
|
36
|
+
return {
|
|
37
|
+
...memory,
|
|
38
|
+
staleRelativeToConfig: memory.fileCount === 0,
|
|
39
|
+
source: "memory",
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
const keyUsageBucket = metricsKeyUsage
|
|
43
|
+
? fromMetricsEntry(keyUsageKey, metricsKeyUsage, keyUsageStale)
|
|
44
|
+
: (() => {
|
|
45
|
+
const memory = getKeyUsageCacheStatus(cfg);
|
|
46
|
+
return {
|
|
47
|
+
...memory,
|
|
48
|
+
staleRelativeToConfig: memory.fileCount === 0,
|
|
49
|
+
source: "memory",
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
const totalCachedFiles = usageBucket.fileCount + keyUsageBucket.fileCount;
|
|
53
|
+
const totalCachedSizeBytes = usageBucket.totalSizeBytes + keyUsageBucket.totalSizeBytes;
|
|
54
|
+
const oldestMtimeCandidates = [usageBucket.oldestMtimeMs, keyUsageBucket.oldestMtimeMs]
|
|
55
|
+
.filter((value) => typeof value === "number" && Number.isFinite(value));
|
|
56
|
+
const oldestMtimeMs = oldestMtimeCandidates.length > 0 ? Math.min(...oldestMtimeCandidates) : null;
|
|
57
|
+
const oldestEntryAgeMs = oldestMtimeMs === null ? null : Math.max(0, Date.now() - oldestMtimeMs);
|
|
58
|
+
return {
|
|
59
|
+
metricsFileFound: metrics !== null,
|
|
60
|
+
metricsUpdatedAt: metrics?.updatedAt ?? null,
|
|
61
|
+
usageScanner: usageBucket,
|
|
62
|
+
keyUsage: keyUsageBucket,
|
|
63
|
+
totalCachedFiles,
|
|
64
|
+
totalCachedSizeBytes,
|
|
65
|
+
oldestEntryAgeMs,
|
|
66
|
+
staleRelativeToConfig: usageBucket.staleRelativeToConfig || keyUsageBucket.staleRelativeToConfig,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
export const clearGlossCaches = async (rootDir) => {
|
|
70
|
+
const usage = clearUsageScannerCache();
|
|
71
|
+
const keyUsage = clearKeyUsageCache();
|
|
72
|
+
const metrics = await clearCacheMetrics(rootDir);
|
|
73
|
+
return {
|
|
74
|
+
usage,
|
|
75
|
+
keyUsage,
|
|
76
|
+
metrics,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CACHE_DIRECTORY = ".gloss";
|
|
4
|
+
const CACHE_METRICS_FILENAME = "cache-metrics.json";
|
|
5
|
+
const metricsFilePath = (rootDir) => path.join(rootDir, CACHE_DIRECTORY, CACHE_METRICS_FILENAME);
|
|
6
|
+
const emptyMetrics = () => ({
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
updatedAt: new Date().toISOString(),
|
|
9
|
+
usageScanner: {},
|
|
10
|
+
keyUsage: {},
|
|
11
|
+
});
|
|
12
|
+
const normalizeEntry = (value) => {
|
|
13
|
+
if (!value || typeof value !== "object") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const source = value;
|
|
17
|
+
if (typeof source.cacheKey !== "string" || source.cacheKey.trim().length === 0) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (typeof source.fileCount !== "number" ||
|
|
21
|
+
!Number.isFinite(source.fileCount) ||
|
|
22
|
+
source.fileCount < 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (typeof source.totalSizeBytes !== "number" ||
|
|
26
|
+
!Number.isFinite(source.totalSizeBytes) ||
|
|
27
|
+
source.totalSizeBytes < 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (source.oldestMtimeMs !== null &&
|
|
31
|
+
(typeof source.oldestMtimeMs !== "number" || !Number.isFinite(source.oldestMtimeMs))) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof source.updatedAt !== "string") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
cacheKey: source.cacheKey,
|
|
39
|
+
fileCount: source.fileCount,
|
|
40
|
+
totalSizeBytes: source.totalSizeBytes,
|
|
41
|
+
oldestMtimeMs: source.oldestMtimeMs,
|
|
42
|
+
updatedAt: source.updatedAt,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const normalizeEntries = (value) => {
|
|
46
|
+
if (!value || typeof value !== "object") {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const source = value;
|
|
50
|
+
const entries = {};
|
|
51
|
+
for (const [key, rawEntry] of Object.entries(source)) {
|
|
52
|
+
const entry = normalizeEntry(rawEntry);
|
|
53
|
+
if (entry) {
|
|
54
|
+
entries[key] = entry;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return entries;
|
|
58
|
+
};
|
|
59
|
+
export const readCacheMetrics = async (rootDir) => {
|
|
60
|
+
const filePath = metricsFilePath(rootDir);
|
|
61
|
+
try {
|
|
62
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (parsed.schemaVersion !== 1 || typeof parsed.updatedAt !== "string") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
schemaVersion: 1,
|
|
69
|
+
updatedAt: parsed.updatedAt,
|
|
70
|
+
usageScanner: normalizeEntries(parsed.usageScanner),
|
|
71
|
+
keyUsage: normalizeEntries(parsed.keyUsage),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const code = error.code;
|
|
76
|
+
if (code === "ENOENT") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
export const updateCacheMetrics = async (rootDir, kind, entry) => {
|
|
83
|
+
const existing = (await readCacheMetrics(rootDir)) ?? emptyMetrics();
|
|
84
|
+
const updatedAt = new Date().toISOString();
|
|
85
|
+
const next = {
|
|
86
|
+
...existing,
|
|
87
|
+
updatedAt,
|
|
88
|
+
[kind]: {
|
|
89
|
+
...existing[kind],
|
|
90
|
+
[entry.cacheKey]: {
|
|
91
|
+
...entry,
|
|
92
|
+
updatedAt,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const filePath = metricsFilePath(rootDir);
|
|
97
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
98
|
+
await fs.writeFile(`${filePath}.tmp`, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
99
|
+
await fs.rename(`${filePath}.tmp`, filePath);
|
|
100
|
+
};
|
|
101
|
+
export const clearCacheMetrics = async (rootDir) => {
|
|
102
|
+
const filePath = metricsFilePath(rootDir);
|
|
103
|
+
let existed = true;
|
|
104
|
+
try {
|
|
105
|
+
await fs.rm(filePath);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const code = error.code;
|
|
109
|
+
if (code === "ENOENT") {
|
|
110
|
+
existed = false;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
existed,
|
|
118
|
+
path: path.relative(rootDir, filePath) || filePath,
|
|
119
|
+
};
|
|
120
|
+
};
|