anylang-dev 0.1.3 → 0.2.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 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,18 @@ 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
+
65
75
  `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
76
 
67
77
  ```env
@@ -90,6 +100,10 @@ anylang translate
90
100
  "importFrom": "anylang-dev/runtime"
91
101
  },
92
102
  "functionName": "$tr",
103
+ "autoTranslate": {
104
+ "jsx": true,
105
+ "keyPrefix": "auto"
106
+ },
93
107
  "provider": {
94
108
  "name": "gemini",
95
109
  "model": "gemini-2.5-flash"
@@ -159,12 +173,15 @@ The lock file stores SHA-256 fingerprints so unchanged strings are skipped on la
159
173
 
160
174
  ## Workflow
161
175
 
162
- 1. Wrap source text in your app:
176
+ 1. Write normal static JSX text:
163
177
 
164
178
  ```tsx
165
- <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
179
+ <h1>Translate your website with anylang</h1>
180
+ <p tr="false">Do not translate this text</p>
166
181
  ```
167
182
 
183
+ Use `$tr("key", "source text")` only for dynamic or special cases.
184
+
168
185
  2. Scan the project:
169
186
 
170
187
  ```bash
@@ -190,7 +207,7 @@ Source locale output:
190
207
 
191
208
  ```json
192
209
  {
193
- "hero.title": {
210
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
194
211
  "text": "Translate your website with anylang",
195
212
  "variables": []
196
213
  }
@@ -201,7 +218,7 @@ Target locale output:
201
218
 
202
219
  ```json
203
220
  {
204
- "hero.title": {
221
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
205
222
  "source": "Translate your website with anylang",
206
223
  "text": "anylang से अपनी वेबसाइट का अनुवाद करें",
207
224
  "variables": []
@@ -240,8 +257,16 @@ Then use translations in any component:
240
257
 
241
258
  ```tsx
242
259
  function Hero() {
260
+ return <h1>Translate your website with anylang</h1>;
261
+ }
262
+ ```
263
+
264
+ For dynamic text:
265
+
266
+ ```tsx
267
+ function SaveButton() {
243
268
  const $tr = useTr();
244
- return <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>;
269
+ return <button>{$tr("actions.save", "Save")}</button>;
245
270
  }
246
271
  ```
247
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anylang-dev",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Bring-your-own-key website translation JSON generator.",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,7 +17,8 @@
17
17
  "./runtime": {
18
18
  "types": "./src/runtime.d.ts",
19
19
  "default": "./src/runtime.js"
20
- }
20
+ },
21
+ "./vite": "./src/vite.js"
21
22
  },
22
23
  "scripts": {
23
24
  "test": "node --test"
@@ -37,5 +38,11 @@
37
38
  "type": "git",
38
39
  "url": "git+ssh://git@github.com/akshaywritescode/anylang-dev.git"
39
40
  },
40
- "license": "MIT"
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "@babel/generator": "^7.29.7",
44
+ "@babel/parser": "^7.29.7",
45
+ "@babel/traverse": "^7.29.7",
46
+ "@babel/types": "^7.29.7"
47
+ }
41
48
  }
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;
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.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 || "/src/anylang.ts"
16
+ });
17
+
18
+ if (!result.changed) return null;
19
+ return {
20
+ code: result.code,
21
+ map: null
22
+ };
23
+ }
24
+ };
25
+ }