@astroscope/i18n 0.3.0 → 0.3.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 +14 -0
- package/dist/{chunk-EXMM4JID.js → chunk-Z3LDPCYD.js} +110 -6
- package/dist/extraction/index.d.ts +2 -0
- package/dist/extraction/index.js +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +6 -5
- package/dist/types-B_8vAmvv.d.ts +18 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -52,6 +52,20 @@ export default defineConfig({
|
|
|
52
52
|
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
#### Consistency checking
|
|
56
|
+
|
|
57
|
+
The integration checks for translation key consistency — when the same key is used in multiple files with different fallbacks, variables, or descriptions.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
i18n({
|
|
61
|
+
consistency: 'warn', // 'warn' (default) | 'error' | 'off'
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- **`'warn'`** — Log a warning but continue (default)
|
|
66
|
+
- **`'error'`** — Fail the build
|
|
67
|
+
- **`'off'`** — Disable consistency checking
|
|
68
|
+
|
|
55
69
|
### 2. Configure i18n in your boot file
|
|
56
70
|
|
|
57
71
|
**VERY IMPORTANT:** `i18n.configure` must be awaited during boot before handling any requests!
|
|
@@ -85,11 +85,16 @@ async function extractKeysFromFile(options) {
|
|
|
85
85
|
|
|
86
86
|
// src/extraction/key-store.ts
|
|
87
87
|
var KeyStore = class {
|
|
88
|
-
constructor(logger) {
|
|
88
|
+
constructor(logger, consistency = "warn") {
|
|
89
89
|
this.logger = logger;
|
|
90
|
+
this.consistency = consistency;
|
|
90
91
|
}
|
|
91
92
|
/** All key occurrences (may have duplicates) */
|
|
92
93
|
occurrences = [];
|
|
94
|
+
/** Tracks reported inconsistencies to avoid duplicate warnings */
|
|
95
|
+
reportedInconsistencies = /* @__PURE__ */ new Set();
|
|
96
|
+
/** Whether an error-level inconsistency was found (for build failure) */
|
|
97
|
+
hasInconsistencyError = false;
|
|
93
98
|
fileToKeys = /* @__PURE__ */ new Map();
|
|
94
99
|
filesWithI18n = /* @__PURE__ */ new Set();
|
|
95
100
|
/**
|
|
@@ -119,11 +124,103 @@ var KeyStore = class {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
this.filesWithI18n.add(filename);
|
|
127
|
+
if (this.consistency !== "off") {
|
|
128
|
+
this.checkConsistency(keys);
|
|
129
|
+
}
|
|
122
130
|
if (keys.length > 0) {
|
|
123
131
|
this.occurrences.push(...keys);
|
|
124
132
|
}
|
|
125
133
|
this.fileToKeys.set(filename, newKeyStrings);
|
|
126
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Check new keys for inconsistencies with existing occurrences.
|
|
137
|
+
*/
|
|
138
|
+
checkConsistency(newKeys) {
|
|
139
|
+
const existingByKey = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const occ of this.occurrences) {
|
|
141
|
+
if (!existingByKey.has(occ.key)) {
|
|
142
|
+
existingByKey.set(occ.key, occ);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const newKey of newKeys) {
|
|
146
|
+
const existing = existingByKey.get(newKey.key);
|
|
147
|
+
if (!existing) continue;
|
|
148
|
+
const inconsistencies = this.findInconsistencies(newKey, existing);
|
|
149
|
+
for (const inc of inconsistencies) {
|
|
150
|
+
this.reportInconsistency(inc);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Compare two occurrences and find any metadata inconsistencies.
|
|
156
|
+
*/
|
|
157
|
+
findInconsistencies(a, b) {
|
|
158
|
+
const result = [];
|
|
159
|
+
const locA = `${a.file}:${a.line}`;
|
|
160
|
+
const locB = `${b.file}:${b.line}`;
|
|
161
|
+
if (a.meta.fallback !== b.meta.fallback) {
|
|
162
|
+
result.push({
|
|
163
|
+
key: a.key,
|
|
164
|
+
field: "fallback",
|
|
165
|
+
locations: [locA, locB],
|
|
166
|
+
values: [a.meta.fallback, b.meta.fallback]
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (a.meta.description !== b.meta.description) {
|
|
170
|
+
result.push({
|
|
171
|
+
key: a.key,
|
|
172
|
+
field: "description",
|
|
173
|
+
locations: [locA, locB],
|
|
174
|
+
values: [a.meta.description ?? "(none)", b.meta.description ?? "(none)"]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const varsA = this.serializeVariables(a.meta);
|
|
178
|
+
const varsB = this.serializeVariables(b.meta);
|
|
179
|
+
if (varsA !== varsB) {
|
|
180
|
+
result.push({
|
|
181
|
+
key: a.key,
|
|
182
|
+
field: "variables",
|
|
183
|
+
locations: [locA, locB],
|
|
184
|
+
values: [varsA || "(none)", varsB || "(none)"]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Serialize variables for comparison.
|
|
191
|
+
*/
|
|
192
|
+
serializeVariables(meta) {
|
|
193
|
+
if (!meta.variables) return "";
|
|
194
|
+
const sorted = {};
|
|
195
|
+
for (const key of Object.keys(meta.variables).sort()) {
|
|
196
|
+
sorted[key] = meta.variables[key];
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify(sorted);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Report an inconsistency via logger.
|
|
202
|
+
*/
|
|
203
|
+
reportInconsistency(inc) {
|
|
204
|
+
const incKey = `${inc.key}:${inc.field}`;
|
|
205
|
+
if (this.reportedInconsistencies.has(incKey)) return;
|
|
206
|
+
this.reportedInconsistencies.add(incKey);
|
|
207
|
+
const message = `inconsistent ${inc.field} for key "${inc.key}"
|
|
208
|
+
${inc.locations[0]}: ${JSON.stringify(inc.values[0])}
|
|
209
|
+
${inc.locations[1]}: ${JSON.stringify(inc.values[1])}`;
|
|
210
|
+
if (this.consistency === "error") {
|
|
211
|
+
this.logger.error(message);
|
|
212
|
+
this.hasInconsistencyError = true;
|
|
213
|
+
} else {
|
|
214
|
+
this.logger.warn(message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Check if any error-level inconsistencies were found.
|
|
219
|
+
* Call this at the end of build to determine if it should fail.
|
|
220
|
+
*/
|
|
221
|
+
get hasErrors() {
|
|
222
|
+
return this.hasInconsistencyError;
|
|
223
|
+
}
|
|
127
224
|
/**
|
|
128
225
|
* Get deduplicated keys with all file locations merged.
|
|
129
226
|
* Last occurrence's meta wins for each key.
|
|
@@ -164,6 +261,9 @@ var KeyStore = class {
|
|
|
164
261
|
for (const file of other.filesWithI18n) {
|
|
165
262
|
this.filesWithI18n.add(file);
|
|
166
263
|
}
|
|
264
|
+
if (this.consistency !== "off") {
|
|
265
|
+
this.checkConsistency(other.occurrences);
|
|
266
|
+
}
|
|
167
267
|
this.occurrences.push(...other.occurrences);
|
|
168
268
|
}
|
|
169
269
|
/**
|
|
@@ -187,8 +287,9 @@ async function compileAstro(code, filename) {
|
|
|
187
287
|
return null;
|
|
188
288
|
}
|
|
189
289
|
}
|
|
190
|
-
async function scan(
|
|
191
|
-
const
|
|
290
|
+
async function scan(options) {
|
|
291
|
+
const { projectRoot, logger, consistency } = options;
|
|
292
|
+
const store = new KeyStore(logger, consistency);
|
|
192
293
|
const files = await glob(GLOB_PATTERN, {
|
|
193
294
|
cwd: projectRoot,
|
|
194
295
|
absolute: true,
|
|
@@ -231,8 +332,8 @@ var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
|
231
332
|
var isResolvedVirtualModuleId = (id) => id === RESOLVED_VIRTUAL_MODULE_ID || id.startsWith(`\0${VIRTUAL_MODULE_ID}?`);
|
|
232
333
|
var MANIFEST_FILE_NAME = "i18n-manifest.json";
|
|
233
334
|
function i18nVitePlugin(options) {
|
|
234
|
-
const { logger } = options;
|
|
235
|
-
const store = new KeyStore(logger);
|
|
335
|
+
const { logger, consistency } = options;
|
|
336
|
+
const store = new KeyStore(logger, consistency);
|
|
236
337
|
const syncGlobalState = () => {
|
|
237
338
|
const state = getGlobalState();
|
|
238
339
|
state.extractedKeys = store.extractedKeys;
|
|
@@ -254,7 +355,7 @@ function i18nVitePlugin(options) {
|
|
|
254
355
|
if (server.config.isProduction || isBuild) {
|
|
255
356
|
return;
|
|
256
357
|
}
|
|
257
|
-
const result = await scan(projectRoot, logger);
|
|
358
|
+
const result = await scan({ projectRoot, logger, consistency });
|
|
258
359
|
store.merge(result);
|
|
259
360
|
syncGlobalState();
|
|
260
361
|
logger.info(`dev mode: scanned ${result.fileToKeys.size} files, found ${result.uniqueKeyCount} keys`);
|
|
@@ -379,6 +480,9 @@ export function getManifest() { return _getManifest(); }
|
|
|
379
480
|
}
|
|
380
481
|
}
|
|
381
482
|
logger.info(`manifest: ${Object.keys(state.chunkManifest).length} chunks, ${store.uniqueKeyCount} keys`);
|
|
483
|
+
if (consistency === "error" && store.hasErrors) {
|
|
484
|
+
throw new Error("i18n: build failed due to translation key inconsistencies");
|
|
485
|
+
}
|
|
382
486
|
},
|
|
383
487
|
writeBundle(outputOptions) {
|
|
384
488
|
if (!isSSR && outputOptions.dir) {
|
|
@@ -2,6 +2,7 @@ import { a as ExtractionManifest } from '../types-moTgocy7.js';
|
|
|
2
2
|
export { C as ChunkManifest, E as ExtractedKey } from '../types-moTgocy7.js';
|
|
3
3
|
import { AstroIntegrationLogger } from 'astro';
|
|
4
4
|
import { Plugin } from 'vite';
|
|
5
|
+
import { C as ConsistencyCheckLevel } from '../types-B_8vAmvv.js';
|
|
5
6
|
import '../types-DNRFj1XA.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -18,6 +19,7 @@ declare function getManifest(): ExtractionManifest;
|
|
|
18
19
|
|
|
19
20
|
type I18nVitePluginOptions = {
|
|
20
21
|
logger: AstroIntegrationLogger;
|
|
22
|
+
consistency: ConsistencyCheckLevel;
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
23
25
|
* Vite plugin for i18n extraction, chunk mapping, and loader injection
|
package/dist/extraction/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { AstroIntegration, MiddlewareHandler, APIContext } from 'astro';
|
|
2
|
+
import { I as I18nOptions } from './types-B_8vAmvv.js';
|
|
2
3
|
export { F as FallbackBehavior, I as I18nConfig, i as i18n } from './i18n-DaLnNvss.js';
|
|
3
4
|
import { ExcludePattern } from '@astroscope/excludes';
|
|
4
5
|
export { R as RawTranslations, T as TranslationMeta } from './types-DNRFj1XA.js';
|
|
5
6
|
export { E as ExtractedKey, a as ExtractionManifest } from './types-moTgocy7.js';
|
|
6
7
|
|
|
7
|
-
interface I18nOptions {
|
|
8
|
-
}
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Astro integration for i18n with automatic per-chunk translation loading.
|
|
12
10
|
*
|
|
@@ -14,7 +12,7 @@ interface I18nOptions {
|
|
|
14
12
|
* - Client directives (`client:load-x`, `client:idle-x`, `client:visible-x`, `client:media-x`, `client:only-x`)
|
|
15
13
|
* - Vite plugin for translation key extraction
|
|
16
14
|
*/
|
|
17
|
-
declare function i18nIntegration(
|
|
15
|
+
declare function i18nIntegration(options?: I18nOptions): AstroIntegration;
|
|
18
16
|
|
|
19
17
|
type I18nMiddlewareOptions = {
|
|
20
18
|
locale: (ctx: APIContext) => string;
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
i18nVitePlugin
|
|
3
|
-
} from "./chunk-EXMM4JID.js";
|
|
4
1
|
import {
|
|
5
2
|
i18n,
|
|
6
3
|
runWithContext
|
|
7
4
|
} from "./chunk-MSJHXRJB.js";
|
|
8
5
|
import "./chunk-33JVAFGQ.js";
|
|
6
|
+
import {
|
|
7
|
+
i18nVitePlugin
|
|
8
|
+
} from "./chunk-Z3LDPCYD.js";
|
|
9
9
|
import "./chunk-4A5BKHFN.js";
|
|
10
10
|
|
|
11
11
|
// src/integration/integration.ts
|
|
12
12
|
var CLIENT_DIRECTIVES = ["load-x", "idle-x", "visible-x", "media-x", "only-x"];
|
|
13
|
-
function i18nIntegration(
|
|
13
|
+
function i18nIntegration(options = {}) {
|
|
14
|
+
const { consistency = "warn" } = options;
|
|
14
15
|
return {
|
|
15
16
|
name: "@astroscope/i18n",
|
|
16
17
|
hooks: {
|
|
@@ -24,7 +25,7 @@ function i18nIntegration(_options = {}) {
|
|
|
24
25
|
}
|
|
25
26
|
updateConfig({
|
|
26
27
|
vite: {
|
|
27
|
-
plugins: [i18nVitePlugin({ logger })]
|
|
28
|
+
plugins: [i18nVitePlugin({ logger, consistency })]
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
},
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consistency check level for translation keys.
|
|
3
|
+
* Controls behavior when the same key has different fallbacks/variables across files.
|
|
4
|
+
*
|
|
5
|
+
* - 'warn' (default): Log a warning but continue
|
|
6
|
+
* - 'error': Fail the build
|
|
7
|
+
* - 'off': Disable consistency checking
|
|
8
|
+
*/
|
|
9
|
+
type ConsistencyCheckLevel = 'off' | 'warn' | 'error';
|
|
10
|
+
interface I18nOptions {
|
|
11
|
+
/**
|
|
12
|
+
* How to handle inconsistent translation keys (same key with different fallbacks/variables).
|
|
13
|
+
* @default 'warn'
|
|
14
|
+
*/
|
|
15
|
+
consistency?: ConsistencyCheckLevel | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type { ConsistencyCheckLevel as C, I18nOptions as I };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/i18n",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "i18n for Astro + React islands — MessageFormat 2, automatic tree-shaking, parallel loading, any translation source",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -88,17 +88,17 @@
|
|
|
88
88
|
"lint:fix": "eslint 'src/**/*.ts' --fix"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@astrojs/compiler": "^2.
|
|
92
|
-
"@babel/core": "^7.
|
|
91
|
+
"@astrojs/compiler": "^2.13.0",
|
|
92
|
+
"@babel/core": "^7.29.0",
|
|
93
93
|
"@babel/preset-typescript": "^7.28.5",
|
|
94
94
|
"messageformat": "^4.0.0",
|
|
95
|
-
"glob": "^
|
|
95
|
+
"glob": "^13.0.1",
|
|
96
96
|
"magic-string": "^0.30.21"
|
|
97
97
|
},
|
|
98
98
|
"devDependencies": {
|
|
99
|
-
"@babel/types": "^7.
|
|
99
|
+
"@babel/types": "^7.29.0",
|
|
100
100
|
"@types/babel__core": "^7.20.5",
|
|
101
|
-
"astro": "^5.
|
|
101
|
+
"astro": "^5.17.1",
|
|
102
102
|
"tsup": "^8.5.1",
|
|
103
103
|
"typescript": "^5.9.3",
|
|
104
104
|
"vite": "^6.4.1"
|