anylang-dev 0.1.3 → 0.2.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
@@ -1,13 +1,13 @@
1
1
  # anylang-dev
2
2
 
3
- `anylang-dev` is a small bring-your-own-key website translation CLI. It scans your source code for explicit translation calls and writes JSON locale files.
3
+ `anylang-dev` is a bring-your-own-key website translation CLI and Vite plugin. It scans your source code, writes JSON locale files, and can automatically translate static JSX text.
4
4
 
5
- ```js
6
- const title = $tr("home.title", "This would get translated");
7
- const untouched = "This stays as it is";
5
+ ```tsx
6
+ <h1>Translate your website with anylang</h1>
7
+ <p tr="false">This text stays as it is</p>
8
8
  ```
9
9
 
10
- It works in JSX and TSX when the text is wrapped in a JavaScript expression:
10
+ For dynamic text, use the generated `useTr` hook:
11
11
 
12
12
  ```tsx
13
13
  export function Hero() {
@@ -15,11 +15,9 @@ export function Hero() {
15
15
 
16
16
  return (
17
17
  <section>
18
- <h1>{$tr("home.title", "Welcome back")}</h1>
19
- <button aria-label={$tr("actions.saveChanges", "Save changes")}>
20
- {$tr("actions.save", "Save")}
21
- </button>
22
- <p>This plain JSX text stays as it is.</p>
18
+ <h1>Welcome back</h1>
19
+ <button>{$tr("actions.save", "Save")}</button>
20
+ <p tr="false">BrandName</p>
23
21
  </section>
24
22
  );
25
23
  }
@@ -62,6 +60,30 @@ anylang init
62
60
  anylang scan
63
61
  ```
64
62
 
63
+ Add the Vite plugin:
64
+
65
+ ```ts
66
+ import { defineConfig } from "vite";
67
+ import react from "@vitejs/plugin-react";
68
+ import anylang from "anylang-dev/vite";
69
+
70
+ export default defineConfig({
71
+ plugins: [anylang(), react()],
72
+ });
73
+ ```
74
+
75
+ If you use `tr="false"`, add the JSX type augmentation once in `src/vite-env.d.ts`:
76
+
77
+ ```ts
78
+ import "anylang-dev/jsx-runtime";
79
+ ```
80
+
81
+ That makes this TypeScript-safe:
82
+
83
+ ```tsx
84
+ <p tr="false">BrandName</p>
85
+ ```
86
+
65
87
  `anylang scan` creates locale files without calling a translation provider. To translate for real with Gemini, add your own API key to `.env` in the project where you run `anylang`:
66
88
 
67
89
  ```env
@@ -90,6 +112,10 @@ anylang translate
90
112
  "importFrom": "anylang-dev/runtime"
91
113
  },
92
114
  "functionName": "$tr",
115
+ "autoTranslate": {
116
+ "jsx": true,
117
+ "keyPrefix": "auto"
118
+ },
93
119
  "provider": {
94
120
  "name": "gemini",
95
121
  "model": "gemini-2.5-flash"
@@ -159,12 +185,15 @@ The lock file stores SHA-256 fingerprints so unchanged strings are skipped on la
159
185
 
160
186
  ## Workflow
161
187
 
162
- 1. Wrap source text in your app:
188
+ 1. Write normal static JSX text:
163
189
 
164
190
  ```tsx
165
- <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
191
+ <h1>Translate your website with anylang</h1>
192
+ <p tr="false">Do not translate this text</p>
166
193
  ```
167
194
 
195
+ Use `$tr("key", "source text")` only for dynamic or special cases.
196
+
168
197
  2. Scan the project:
169
198
 
170
199
  ```bash
@@ -190,7 +219,7 @@ Source locale output:
190
219
 
191
220
  ```json
192
221
  {
193
- "hero.title": {
222
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
194
223
  "text": "Translate your website with anylang",
195
224
  "variables": []
196
225
  }
@@ -201,7 +230,7 @@ Target locale output:
201
230
 
202
231
  ```json
203
232
  {
204
- "hero.title": {
233
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
205
234
  "source": "Translate your website with anylang",
206
235
  "text": "anylang से अपनी वेबसाइट का अनुवाद करें",
207
236
  "variables": []
@@ -240,8 +269,16 @@ Then use translations in any component:
240
269
 
241
270
  ```tsx
242
271
  function Hero() {
272
+ return <h1>Translate your website with anylang</h1>;
273
+ }
274
+ ```
275
+
276
+ For dynamic text:
277
+
278
+ ```tsx
279
+ function SaveButton() {
243
280
  const $tr = useTr();
244
- return <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>;
281
+ return <button>{$tr("actions.save", "Save")}</button>;
245
282
  }
246
283
  ```
247
284
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anylang-dev",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Bring-your-own-key website translation JSON generator.",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,6 +17,14 @@
17
17
  "./runtime": {
18
18
  "types": "./src/runtime.d.ts",
19
19
  "default": "./src/runtime.js"
20
+ },
21
+ "./vite": {
22
+ "types": "./src/vite.d.ts",
23
+ "default": "./src/vite.js"
24
+ },
25
+ "./jsx-runtime": {
26
+ "types": "./src/jsx-runtime.d.ts",
27
+ "default": "./src/runtime.js"
20
28
  }
21
29
  },
22
30
  "scripts": {
@@ -37,5 +45,11 @@
37
45
  "type": "git",
38
46
  "url": "git+ssh://git@github.com/akshaywritescode/anylang-dev.git"
39
47
  },
40
- "license": "MIT"
48
+ "license": "MIT",
49
+ "dependencies": {
50
+ "@babel/generator": "^7.29.7",
51
+ "@babel/parser": "^7.29.7",
52
+ "@babel/traverse": "^7.29.7",
53
+ "@babel/types": "^7.29.7"
54
+ }
41
55
  }
package/src/config.js CHANGED
@@ -12,6 +12,10 @@ export const DEFAULT_CONFIG = {
12
12
  importFrom: "anylang-dev/runtime"
13
13
  },
14
14
  functionName: "$tr",
15
+ autoTranslate: {
16
+ jsx: true,
17
+ keyPrefix: "auto"
18
+ },
15
19
  provider: {
16
20
  name: "gemini",
17
21
  baseUrl: "https://generativelanguage.googleapis.com/v1beta",
package/src/extract.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { extractAutoJsxStrings } from "./jsx.js";
3
4
 
4
5
  const DEFAULT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".vue", ".html", ".svelte", ".astro"]);
5
6
 
@@ -20,11 +21,27 @@ export async function extractProjectStrings(config) {
20
21
  ...lineColumnForIndex(source, match.index)
21
22
  });
22
23
  }
24
+ if (config.autoTranslate?.jsx !== false && isJsxFile(file)) {
25
+ for (const match of extractAutoJsxStrings(source, file, config.autoTranslate || {})) {
26
+ const key = `${match.key}\0${file}\0${match.index}`;
27
+ if (seen.has(key)) continue;
28
+ seen.add(key);
29
+ items.push({
30
+ ...match,
31
+ file,
32
+ ...lineColumnForIndex(source, match.index)
33
+ });
34
+ }
35
+ }
23
36
  }
24
37
 
25
38
  return { files, items };
26
39
  }
27
40
 
41
+ function isJsxFile(file) {
42
+ return /\.(jsx|tsx)$/.test(file);
43
+ }
44
+
28
45
  export function extractFromSource(source, functionName = "$tr") {
29
46
  const matches = [];
30
47
  let index = 0;
@@ -0,0 +1,7 @@
1
+ import "react";
2
+
3
+ declare module "react" {
4
+ interface HTMLAttributes<T> {
5
+ tr?: boolean | "false";
6
+ }
7
+ }
package/src/jsx.js ADDED
@@ -0,0 +1,179 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { parse } from "@babel/parser";
4
+ import generate from "@babel/generator";
5
+ import traverseModule from "@babel/traverse";
6
+ import * as t from "@babel/types";
7
+
8
+ const traverse = traverseModule.default || traverseModule;
9
+ export function extractAutoJsxStrings(source, filePath, options = {}) {
10
+ const ast = parseJsx(source, filePath);
11
+ const items = [];
12
+
13
+ traverse(ast, {
14
+ JSXElement(pathRef) {
15
+ if (isTranslationDisabled(pathRef.node.openingElement)) {
16
+ pathRef.skip();
17
+ }
18
+ },
19
+ JSXText(pathRef) {
20
+ if (hasDisabledAncestor(pathRef)) return;
21
+ const text = normalizeJsxText(pathRef.node.value);
22
+ if (!isTranslatableText(text)) return;
23
+
24
+ items.push(autoItem({
25
+ text,
26
+ filePath,
27
+ index: pathRef.node.start || 0,
28
+ prefix: options.keyPrefix
29
+ }));
30
+ }
31
+ });
32
+
33
+ return items;
34
+ }
35
+
36
+ export function transformAutoJsx(source, filePath, options = {}) {
37
+ const ast = parseJsx(source, filePath);
38
+ let changed = false;
39
+
40
+ traverse(ast, {
41
+ JSXElement(pathRef) {
42
+ if (isTranslationDisabled(pathRef.node.openingElement)) {
43
+ removeTrFalseAttribute(pathRef.node.openingElement);
44
+ pathRef.skip();
45
+ }
46
+ },
47
+ JSXText(pathRef) {
48
+ if (hasDisabledAncestor(pathRef)) return;
49
+ const text = normalizeJsxText(pathRef.node.value);
50
+ if (!isTranslatableText(text)) return;
51
+
52
+ pathRef.replaceWith(t.jsxExpressionContainer(anyLangTextElement({
53
+ key: autoKey(text, filePath, options.keyPrefix),
54
+ text
55
+ })));
56
+ changed = true;
57
+ }
58
+ });
59
+
60
+ if (!changed) return { code: source, changed: false };
61
+ ensureAnyLangTextImport(ast, options.runtimeImport || "/src/anylang.ts");
62
+
63
+ return {
64
+ code: generate.default(ast, { retainLines: true }, source).code,
65
+ changed: true
66
+ };
67
+ }
68
+
69
+ function parseJsx(source, filePath) {
70
+ return parse(source, {
71
+ sourceType: "module",
72
+ sourceFilename: filePath,
73
+ plugins: ["jsx", "typescript"]
74
+ });
75
+ }
76
+
77
+ function autoItem({ text, filePath, index, prefix }) {
78
+ return {
79
+ key: autoKey(text, filePath, prefix),
80
+ value: text,
81
+ variables: [],
82
+ index,
83
+ raw: text,
84
+ auto: true
85
+ };
86
+ }
87
+
88
+ function autoKey(text, filePath, prefix = "auto") {
89
+ const relative = path.relative(process.cwd(), filePath).split(path.sep).join("/");
90
+ const fileSlug = slug(relative.replace(/\.[^.]+$/, ""));
91
+ const textSlug = slug(text).slice(0, 36) || "text";
92
+ const hash = createHash("sha1").update(`${relative}\0${text}`).digest("hex").slice(0, 8);
93
+ return `${prefix}.${fileSlug}.${textSlug}_${hash}`;
94
+ }
95
+
96
+ function slug(value) {
97
+ return value
98
+ .toLowerCase()
99
+ .replace(/[^a-z0-9]+/g, "_")
100
+ .replace(/^_+|_+$/g, "");
101
+ }
102
+
103
+ function normalizeJsxText(value) {
104
+ return value
105
+ .split(/\r?\n/)
106
+ .map((line) => line.replace(/\s+/g, " ").trim())
107
+ .filter(Boolean)
108
+ .join(" ");
109
+ }
110
+
111
+ function isTranslatableText(text) {
112
+ if (!text) return false;
113
+ if (!/[A-Za-z0-9]/.test(text)) return false;
114
+ return true;
115
+ }
116
+
117
+ function isTranslationDisabled(openingElement) {
118
+ return openingElement.attributes.some((attribute) => {
119
+ if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name) || attribute.name.name !== "tr") {
120
+ return false;
121
+ }
122
+ if (!attribute.value) return true;
123
+ if (t.isStringLiteral(attribute.value)) return attribute.value.value === "false";
124
+ return (
125
+ t.isJSXExpressionContainer(attribute.value) &&
126
+ t.isBooleanLiteral(attribute.value.expression) &&
127
+ attribute.value.expression.value === false
128
+ );
129
+ });
130
+ }
131
+
132
+ function removeTrFalseAttribute(openingElement) {
133
+ openingElement.attributes = openingElement.attributes.filter((attribute) => {
134
+ return !(t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && attribute.name.name === "tr");
135
+ });
136
+ }
137
+
138
+ function hasDisabledAncestor(pathRef) {
139
+ return Boolean(pathRef.findParent((parent) => {
140
+ return parent.isJSXElement() && isTranslationDisabled(parent.node.openingElement);
141
+ }));
142
+ }
143
+
144
+ function anyLangTextElement({ key, text }) {
145
+ return t.jsxElement(
146
+ t.jsxOpeningElement(t.jsxIdentifier("AnyLangText"), [
147
+ t.jsxAttribute(t.jsxIdentifier("k"), t.stringLiteral(key)),
148
+ t.jsxAttribute(t.jsxIdentifier("source"), t.stringLiteral(text))
149
+ ], true),
150
+ null,
151
+ [],
152
+ true
153
+ );
154
+ }
155
+
156
+ function ensureAnyLangTextImport(ast, runtimeImport) {
157
+ const body = ast.program.body;
158
+ const existing = body.find((node) => {
159
+ return t.isImportDeclaration(node) && node.source.value === runtimeImport;
160
+ });
161
+
162
+ if (existing) {
163
+ ensureSpecifier(existing, "AnyLangText");
164
+ return;
165
+ }
166
+
167
+ body.unshift(t.importDeclaration([
168
+ t.importSpecifier(t.identifier("AnyLangText"), t.identifier("AnyLangText"))
169
+ ], t.stringLiteral(runtimeImport)));
170
+ }
171
+
172
+ function ensureSpecifier(importDeclaration, name) {
173
+ const hasSpecifier = importDeclaration.specifiers.some((specifier) => {
174
+ return t.isImportSpecifier(specifier) && specifier.imported.name === name;
175
+ });
176
+ if (!hasSpecifier) {
177
+ importDeclaration.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name)));
178
+ }
179
+ }
package/src/pipeline.js CHANGED
@@ -233,6 +233,11 @@ export function useTr() {
233
233
  if (!context) throw new Error('useTr must be used inside AnyLangProvider')
234
234
  return context.$tr
235
235
  }
236
+
237
+ export function AnyLangText({ k, source }: { k: string; source: string }) {
238
+ const $tr = useTr()
239
+ return $tr(k, source)
240
+ }
236
241
  `;
237
242
  }
238
243
 
package/src/vite.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ export type AnyLangViteOptions = {
4
+ keyPrefix?: string;
5
+ runtimeImport?: string;
6
+ };
7
+
8
+ export default function anylang(options?: AnyLangViteOptions): Plugin;
package/src/vite.js ADDED
@@ -0,0 +1,25 @@
1
+ import { transformAutoJsx } from "./jsx.js";
2
+
3
+ const JSX_EXTENSIONS = /\.(jsx|tsx)$/;
4
+
5
+ export default function anylang(options = {}) {
6
+ return {
7
+ name: "anylang-dev",
8
+ enforce: "pre",
9
+ transform(code, id) {
10
+ if (!JSX_EXTENSIONS.test(id)) return null;
11
+ if (id.includes("node_modules")) return null;
12
+
13
+ const result = transformAutoJsx(code, id, {
14
+ keyPrefix: options.keyPrefix,
15
+ runtimeImport: options.runtimeImport || "@/anylang"
16
+ });
17
+
18
+ if (!result.changed) return null;
19
+ return {
20
+ code: result.code,
21
+ map: null
22
+ };
23
+ }
24
+ };
25
+ }