vite-plugin-ai-auto-i18n 0.2.2 โ 0.3.0
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 +22 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +15 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Implicit build-time localization for JSX: author English directly, let the compi
|
|
|
5
5
|
## ๐งฉ Current MVP scope
|
|
6
6
|
|
|
7
7
|
- Extracts JSX text nodes and static string JSX attributes.
|
|
8
|
+
- Extracts string literals outside JSX only when they are marked with a leading `/* i18n */` comment.
|
|
8
9
|
- Uses tag/attribute/semantic-attribute/handler context to form fingerprints; component names and file paths are excluded so ordinary moves and renames do not automatically invalidate IDs.
|
|
9
10
|
- Persists opaque message IDs in `.autoi18n/registry.json`.
|
|
10
11
|
- Marks disappeared messages as orphaned and deletes them after a retention period (default 30 days, see `orphanRetentionMinutes`).
|
|
@@ -104,6 +105,8 @@ Messages removed from the source are kept as orphans in `registry.json` so their
|
|
|
104
105
|
|
|
105
106
|
๐ To keep copy out of automatic translation, add `translate="no"` to the element (for example `<h2 translate="no">Sample code</h2>`). This is the standard HTML marker, and it also covers the element's descendants and attributes such as `title`.
|
|
106
107
|
|
|
108
|
+
๐ท๏ธ To translate copy outside JSX, put a `/* i18n */` block comment directly before the string literal, for example `const items = [{ label: /* i18n */ "Shared with your team" }];`. Marked strings are translated in both build and runtime delivery. Unmarked string literals are never extracted.
|
|
109
|
+
|
|
107
110
|
Corrupt `registry.json` or catalogue files are renamed to `<file>.corrupt-<timestamp>` and a warning is logged; the plugin continues with an empty state. Writes are atomic, so an interrupted build does not leave a truncated file.
|
|
108
111
|
|
|
109
112
|
### ๐ Delivery modes
|
|
@@ -175,7 +178,7 @@ interface TranslationRequest {
|
|
|
175
178
|
id: string; // opaque ID to echo back in the result
|
|
176
179
|
source: string; // English text
|
|
177
180
|
context: string; // for example "Toolbar > Save button"
|
|
178
|
-
kind: "text" | "attribute" | "mixed"; // text node, static attribute, or
|
|
181
|
+
kind: "text" | "attribute" | "mixed" | "string"; // text node, static attribute, JSX with expressions, or a /* i18n */-marked string literal
|
|
179
182
|
placeholders?: string[]; // {name} placeholders that must survive translation
|
|
180
183
|
}
|
|
181
184
|
```
|
|
@@ -192,6 +195,23 @@ Return only the IDs you translated. Omitted IDs stay in the source language and
|
|
|
192
195
|
|
|
193
196
|
The `prune` CLI options (`--directory`, `--retention-minutes`, `--debug`) are described in the orphan paragraph of [Try the bundled example](#-try-the-bundled-example).
|
|
194
197
|
|
|
198
|
+
## ๐งช Test coverage
|
|
199
|
+
|
|
200
|
+
Run the suite with coverage (V8 provider, `src/**`):
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx vitest run --coverage --coverage.include='src/**' --coverage.reporter=text
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Metric | Coverage |
|
|
207
|
+
|---|---|
|
|
208
|
+
| Statements | 99.4% |
|
|
209
|
+
| Branches | 89.4% |
|
|
210
|
+
| Functions | 97.6% |
|
|
211
|
+
| Lines | 99.4% |
|
|
212
|
+
|
|
213
|
+
Critical modules such as the plugin hooks (`src/index.ts`), translation pipeline (`src/translate.ts`), build engine (`src/engine.ts`), runtime helpers (`src/runtime/`), and prune CLI (`src/cli/prune.ts`) are all at or above 98% statements.
|
|
214
|
+
|
|
195
215
|
## โ ๏ธ Important prototype limitations
|
|
196
216
|
|
|
197
217
|
This is a compiler MVP, not production-ready localization yet:
|
|
@@ -200,7 +220,7 @@ This is a compiler MVP, not production-ready localization yet:
|
|
|
200
220
|
2. Mixed JSX sentences with simple expressions (`You have {count} documents`, where `count` is an identifier or member path) are translated as one message with named `{name}` placeholders; translations that change the placeholders are rejected. Sentences with complex expressions (calls, JSX, computed access) are left entirely in the source language. ICU plural generation is not implemented yet.
|
|
201
221
|
3. Duplicate identical occurrences are paired by stable source ordering. This handles ordinary line shifts, but cannot guarantee identity if indistinguishable duplicates are reordered or moved independently. Such cases are reported through `onAmbiguous`; they are not resolved automatically.
|
|
202
222
|
4. Dev-server HMR re-indexes the whole project after saved or added/removed source files (debounced by 150 ms) rather than only the changed module. This is simple and correct, but may be slow in very large projects.
|
|
203
|
-
5. Only JSX text, and the attributes `alt`, `title`, `placeholder`, `label`, `summary`, `aria-label`, `aria-description`, `aria-placeholder`, `aria-roledescription` and `aria-valuetext` on intrinsic (lowercase) elements, are extracted. Text inside capitalised components (for example `<Button>Save</Button>`) and other attributes such as `href` are not extracted. JS string literals, template literals, Markdown, server-rendered content and rich-text components need explicit policies.
|
|
223
|
+
5. Only JSX text, and the attributes `alt`, `title`, `placeholder`, `label`, `summary`, `aria-label`, `aria-description`, `aria-placeholder`, `aria-roledescription` and `aria-valuetext` on intrinsic (lowercase) elements, are extracted automatically. String literals outside JSX are extracted only when marked with `/* i18n */`. Text inside capitalised components (for example `<Button>Save</Button>`) and other attributes such as `href` are not extracted. Unmarked JS string literals, template literals, Markdown, server-rendered content and rich-text components need explicit policies.
|
|
204
224
|
6. The example uses a React plugin in its Vite config; install `@vitejs/plugin-react` in the consuming example.
|
|
205
225
|
7. The initial fingerprint is a heuristic. It excludes file and line position, but component names and nearby semantic signals can still change during refactors.
|
|
206
226
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -168,6 +168,7 @@ var traverse = traverseModule.default ?? traverseModule;
|
|
|
168
168
|
var COMPONENT_TAG = /^[A-Z]/;
|
|
169
169
|
var IGNORED_TAGS = /* @__PURE__ */ new Set(["script", "style", "code", "pre"]);
|
|
170
170
|
var TRANSLATABLE_ATTRS = /* @__PURE__ */ new Set(["alt", "title", "placeholder", "label", "summary", "aria-label", "aria-description", "aria-placeholder", "aria-roledescription", "aria-valuetext"]);
|
|
171
|
+
var STRING_MARKER = "i18n";
|
|
171
172
|
var IGNORE_ATTR = "translate";
|
|
172
173
|
var IGNORE_VALUE = "no";
|
|
173
174
|
function hasIgnoreMarker(opening) {
|
|
@@ -289,6 +290,15 @@ function extractMessages(code, filename) {
|
|
|
289
290
|
const item = { source, kind: "attribute", file: filename, start: attr.value.start ?? 0, end: attr.value.end ?? 0, tag, component, attribute: attr.name.name, context, fingerprint: "" };
|
|
290
291
|
item.fingerprint = createFingerprint(item);
|
|
291
292
|
found.push(item);
|
|
293
|
+
},
|
|
294
|
+
StringLiteral(p) {
|
|
295
|
+
if (p.parentPath.isJSXAttribute()) return;
|
|
296
|
+
if (!p.node.leadingComments?.some((c) => c.type === "CommentBlock" && c.value.trim() === STRING_MARKER)) return;
|
|
297
|
+
const source = normalizeSource(p.node.value);
|
|
298
|
+
if (!source || !/[A-Za-z]/.test(source)) return;
|
|
299
|
+
const item = { source, kind: "string", file: filename, start: p.node.start ?? 0, end: p.node.end ?? 0, tag: "", component: componentName(p), context: "", fingerprint: "" };
|
|
300
|
+
item.fingerprint = createFingerprint(item);
|
|
301
|
+
found.push(item);
|
|
292
302
|
}
|
|
293
303
|
});
|
|
294
304
|
return found.sort((a, b) => a.start - b.start);
|
|
@@ -369,6 +379,11 @@ function transformSource(code, filename, messages, locale, catalog, runtime = fa
|
|
|
369
379
|
const message = byStart.get(valueNode.start ?? -1);
|
|
370
380
|
if (!message) return;
|
|
371
381
|
p.node.value = runtime ? t2.jsxExpressionContainer(lookup(message)) : lookup(message);
|
|
382
|
+
},
|
|
383
|
+
StringLiteral(p) {
|
|
384
|
+
const message = byStart.get(p.node.start ?? -1);
|
|
385
|
+
if (message?.kind !== "string") return;
|
|
386
|
+
p.replaceWith(lookup(message));
|
|
372
387
|
}
|
|
373
388
|
});
|
|
374
389
|
const runtimeSpecifiers = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-ai-auto-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Implicit, build-time AI localization for JSX applications",
|
|
6
6
|
"engines": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@types/babel__traverse": "^7.20.6",
|
|
47
47
|
"@types/node": "^22.10.0",
|
|
48
48
|
"@vitejs/plugin-react": "^4.3.4",
|
|
49
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
49
50
|
"react": "^19.3.1",
|
|
50
51
|
"react-dom": "^19.3.1",
|
|
51
52
|
"tsup": "^8.3.5",
|