anylang-dev 0.1.2 → 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
@@ -86,10 +96,14 @@ anylang translate
86
96
  "exclude": ["node_modules", ".git", "dist", "build", ".next"],
87
97
  "outDir": "locales",
88
98
  "runtime": {
89
- "output": "anylang.ts",
99
+ "output": "src/anylang.ts",
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"
@@ -151,19 +165,23 @@ locales/
151
165
  en.json
152
166
  hi.json
153
167
  anylang.lock.json
154
- anylang.ts
168
+ src/
169
+ anylang.ts
155
170
  ```
156
171
 
157
172
  The lock file stores SHA-256 fingerprints so unchanged strings are skipped on later runs.
158
173
 
159
174
  ## Workflow
160
175
 
161
- 1. Wrap source text in your app:
176
+ 1. Write normal static JSX text:
162
177
 
163
178
  ```tsx
164
- <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>
165
181
  ```
166
182
 
183
+ Use `$tr("key", "source text")` only for dynamic or special cases.
184
+
167
185
  2. Scan the project:
168
186
 
169
187
  ```bash
@@ -171,7 +189,7 @@ anylang scan
171
189
  ```
172
190
 
173
191
  This writes keyed source entries to `locales/en.json` and creates placeholder entries in each target locale.
174
- It also generates `anylang.ts`, which imports all locale JSON files and exports runtime helpers.
192
+ It also generates `src/anylang.ts`, which imports all locale JSON files and exports runtime helpers.
175
193
 
176
194
  3. Translate with Gemini:
177
195
 
@@ -189,7 +207,7 @@ Source locale output:
189
207
 
190
208
  ```json
191
209
  {
192
- "hero.title": {
210
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
193
211
  "text": "Translate your website with anylang",
194
212
  "variables": []
195
213
  }
@@ -200,7 +218,7 @@ Target locale output:
200
218
 
201
219
  ```json
202
220
  {
203
- "hero.title": {
221
+ "auto.src_app.translate_your_website_with_anylang_a1b2c3d4": {
204
222
  "source": "Translate your website with anylang",
205
223
  "text": "anylang से अपनी वेबसाइट का अनुवाद करें",
206
224
  "variables": []
@@ -220,7 +238,7 @@ import {
220
238
  useLanguage,
221
239
  useTr,
222
240
  type LanguageCode
223
- } from "./anylang";
241
+ } from "@/anylang";
224
242
  ```
225
243
 
226
244
  You do not manually import `en.json`, `hi.json`, `ja.json`, etc. The generated file does that for you based on `sourceLocale` and `targetLocales`.
@@ -239,8 +257,16 @@ Then use translations in any component:
239
257
 
240
258
  ```tsx
241
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() {
242
268
  const $tr = useTr();
243
- return <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>;
269
+ return <button>{$tr("actions.save", "Save")}</button>;
244
270
  }
245
271
  ```
246
272
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anylang-dev",
3
- "version": "0.1.2",
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
@@ -8,10 +8,14 @@ export const DEFAULT_CONFIG = {
8
8
  exclude: ["node_modules", ".git", "dist", "build", ".next"],
9
9
  outDir: "locales",
10
10
  runtime: {
11
- output: "anylang.ts",
11
+ output: "src/anylang.ts",
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
@@ -131,7 +131,7 @@ function normalizeTargetEntry(entry) {
131
131
  async function writeGeneratedRuntime(config) {
132
132
  if (config.runtime === false) return;
133
133
 
134
- const output = path.resolve(config.runtime?.output || "anylang.ts");
134
+ const output = path.resolve(config.runtime?.output || "src/anylang.ts");
135
135
  const outDir = path.resolve(config.outDir);
136
136
  const locales = [config.sourceLocale, ...config.targetLocales.filter((locale) => locale !== config.sourceLocale)];
137
137
  const importFrom = config.runtime?.importFrom || "anylang-dev/runtime";
@@ -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
+ }