vite-plugin-ai-auto-i18n 0.2.2 โ†’ 0.3.1

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 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 both in one message
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
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type MessageKind = "text" | "attribute" | "mixed";
3
+ type MessageKind = "text" | "attribute" | "mixed" | "string";
4
4
  interface ExtractedMessage {
5
5
  source: string;
6
6
  prefix?: string;
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.2.2",
3
+ "version": "0.3.1",
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",