anylang-dev 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # anylang-dev
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.
4
+
5
+ ```js
6
+ const title = $tr("home.title", "This would get translated");
7
+ const untouched = "This stays as it is";
8
+ ```
9
+
10
+ It works in JSX and TSX when the text is wrapped in a JavaScript expression:
11
+
12
+ ```tsx
13
+ export function Hero() {
14
+ const $tr = useAnyLang(language);
15
+
16
+ return (
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>
23
+ </section>
24
+ );
25
+ }
26
+ ```
27
+
28
+ By default, `anylang` scans `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`, and `.html` files under `src`.
29
+
30
+ ## Language selector
31
+
32
+ `anylang` does not require a built-in selector. Build any selector UI you want and pass the selected locale to `setLanguage`.
33
+
34
+ ```tsx
35
+ const [language, setSelectedLanguage] = useState<LanguageCode>("en");
36
+ const $tr = useAnyLang(language);
37
+
38
+ function handleLanguageChange(nextLanguage: LanguageCode) {
39
+ setLanguage(nextLanguage);
40
+ setSelectedLanguage(nextLanguage);
41
+ }
42
+
43
+ return (
44
+ <select
45
+ value={language}
46
+ onChange={(event) => handleLanguageChange(event.target.value as LanguageCode)}
47
+ >
48
+ <option value="en">English</option>
49
+ <option value="hi">हिन्दी</option>
50
+ <option value="ja">日本語</option>
51
+ </select>
52
+ );
53
+ ```
54
+
55
+ Use `$tr("key", "source text")` anywhere in the same render tree:
56
+
57
+ ```tsx
58
+ <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
59
+ ```
60
+
61
+ ## Quick start
62
+
63
+ ```bash
64
+ npm link
65
+ anylang init
66
+ anylang scan
67
+ ```
68
+
69
+ `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`:
70
+
71
+ ```env
72
+ GEMINI_API_KEY=your-provider-key
73
+ ```
74
+
75
+ Then run:
76
+
77
+ ```bash
78
+ anylang translate
79
+ ```
80
+
81
+ ## Config
82
+
83
+ `anylang init` creates:
84
+
85
+ ```json
86
+ {
87
+ "sourceLocale": "en",
88
+ "targetLocales": ["hi"],
89
+ "include": ["src/**/*.{js,jsx,ts,tsx,vue,html}"],
90
+ "exclude": ["node_modules", ".git", "dist", "build", ".next"],
91
+ "outDir": "locales",
92
+ "runtime": {
93
+ "output": "src/anylang.generated.ts",
94
+ "importFrom": "anylang-dev/runtime"
95
+ },
96
+ "functionName": "$tr",
97
+ "provider": {
98
+ "name": "gemini",
99
+ "model": "gemini-2.5-flash"
100
+ }
101
+ }
102
+ ```
103
+
104
+ The provider is intentionally BYOK. `anylang` does not include a platform key, proxy requests, track usage, or store billing data. It automatically loads `.env` from the current project before calling the provider.
105
+
106
+ ## Providers
107
+
108
+ Choose a provider by setting `provider.name`. Each provider reads its standard API key from `.env`.
109
+
110
+ | Provider | `provider.name` | `.env` key |
111
+ | --- | --- | --- |
112
+ | Gemini | `gemini` | `GEMINI_API_KEY` |
113
+ | OpenAI | `openai` | `OPENAI_API_KEY` |
114
+ | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
115
+ | Cohere | `cohere` | `COHERE_API_KEY` |
116
+ | Mistral | `mistral` | `MISTRAL_API_KEY` |
117
+ | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` |
118
+ | Groq | `groq` | `GROQ_API_KEY` |
119
+ | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
120
+ | Perplexity | `perplexity` | `PERPLEXITY_API_KEY` |
121
+ | xAI | `xai` | `XAI_API_KEY` |
122
+ | Together AI | `together` | `TOGETHER_API_KEY` |
123
+ | Fireworks AI | `fireworks` | `FIREWORKS_API_KEY` |
124
+ | Custom OpenAI-compatible | `openai-compatible` | `ANYLANG_API_KEY` |
125
+
126
+ Example:
127
+
128
+ ```json
129
+ {
130
+ "provider": {
131
+ "name": "anthropic",
132
+ "model": "claude-3-5-haiku-latest"
133
+ }
134
+ }
135
+ ```
136
+
137
+ For custom OpenAI-compatible gateways, provide `baseUrl` and `model`:
138
+
139
+ ```json
140
+ {
141
+ "provider": {
142
+ "name": "openai-compatible",
143
+ "baseUrl": "https://your-gateway.example.com/v1",
144
+ "model": "your-model"
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Output
150
+
151
+ Scanning or translating creates:
152
+
153
+ ```text
154
+ locales/
155
+ en.json
156
+ hi.json
157
+ anylang.lock.json
158
+ src/
159
+ anylang.generated.ts
160
+ ```
161
+
162
+ The lock file stores SHA-256 fingerprints so unchanged strings are skipped on later runs.
163
+
164
+ ## Workflow
165
+
166
+ 1. Wrap source text in your app:
167
+
168
+ ```tsx
169
+ <h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
170
+ ```
171
+
172
+ 2. Scan the project:
173
+
174
+ ```bash
175
+ anylang scan
176
+ ```
177
+
178
+ This writes keyed source entries to `locales/en.json` and creates placeholder entries in each target locale.
179
+ It also generates `src/anylang.generated.ts`, which imports all locale JSON files and exports runtime helpers.
180
+
181
+ 3. Translate with Gemini:
182
+
183
+ ```env
184
+ GEMINI_API_KEY=your-gemini-api-key
185
+ ```
186
+
187
+ ```bash
188
+ anylang translate
189
+ ```
190
+
191
+ This scans again, sends missing or changed target entries to Gemini, and writes the translated text into files like `locales/hi.json`.
192
+
193
+ Source locale output:
194
+
195
+ ```json
196
+ {
197
+ "hero.title": {
198
+ "text": "Translate your website with anylang",
199
+ "variables": []
200
+ }
201
+ }
202
+ ```
203
+
204
+ Target locale output:
205
+
206
+ ```json
207
+ {
208
+ "hero.title": {
209
+ "source": "Translate your website with anylang",
210
+ "text": "anylang से अपनी वेबसाइट का अनुवाद करें",
211
+ "variables": []
212
+ }
213
+ }
214
+ ```
215
+
216
+ On later runs, `anylang translate` compares `targetEntry.source` against the current source text. If they differ, it retranslates that key. If they match, it keeps the existing translation and skips the AI call.
217
+
218
+ ## Runtime
219
+
220
+ Import the generated runtime file in your app:
221
+
222
+ ```tsx
223
+ import {
224
+ languages,
225
+ setLanguage,
226
+ useAnyLang,
227
+ type LanguageCode
228
+ } from "./anylang.generated";
229
+ ```
230
+
231
+ You do not manually import `en.json`, `hi.json`, `ja.json`, etc. The generated file does that for you based on `sourceLocale` and `targetLocales`.
package/bin/anylang.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from "../src/cli.js";
4
+
5
+ runCli(process.argv.slice(2)).catch((error) => {
6
+ console.error(error instanceof Error ? error.message : String(error));
7
+ process.exitCode = 1;
8
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "anylang-dev",
3
+ "version": "0.1.0",
4
+ "description": "Bring-your-own-key website translation JSON generator.",
5
+ "type": "module",
6
+ "files": [
7
+ "bin",
8
+ "src",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "bin": {
13
+ "anylang": "bin/anylang.js"
14
+ },
15
+ "exports": {
16
+ ".": "./src/runtime.js",
17
+ "./runtime": {
18
+ "types": "./src/runtime.d.ts",
19
+ "default": "./src/runtime.js"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "test": "node --test"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "keywords": [
29
+ "i18n",
30
+ "translation",
31
+ "localization",
32
+ "gemini",
33
+ "react",
34
+ "tsx"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+ssh://git@github.com/akshaywritescode/anylang-dev.git"
39
+ },
40
+ "license": "MIT"
41
+ }
package/src/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_CONFIG, loadConfig } from "./config.js";
4
+ import { extractProjectStrings } from "./extract.js";
5
+ import { runPipeline } from "./pipeline.js";
6
+
7
+ const help = `anylang
8
+
9
+ Commands:
10
+ init Create anylang.config.json
11
+ extract Print discovered $tr(...) strings
12
+ scan Scan source files and prepare locale JSON files
13
+ translate Scan source files, translate missing entries, and update JSON files
14
+ run Alias for translate
15
+
16
+ Options:
17
+ --config <path> Config path (default: anylang.config.json)
18
+ --dry-run Do not call a translation provider
19
+ `;
20
+
21
+ export async function runCli(argv) {
22
+ const { command, options } = parseArgs(argv);
23
+
24
+ if (!command || command === "help" || command === "--help" || command === "-h") {
25
+ console.log(help);
26
+ return;
27
+ }
28
+
29
+ if (command === "init") {
30
+ await initConfig(options.config);
31
+ return;
32
+ }
33
+
34
+ if (command === "extract") {
35
+ const config = await loadConfig(options.config);
36
+ const result = await extractProjectStrings(config);
37
+ for (const item of result.items) {
38
+ console.log(`${item.value}\t${path.relative(process.cwd(), item.file)}:${item.line}:${item.column}`);
39
+ }
40
+ console.log(`Found ${result.items.length} translatable string${result.items.length === 1 ? "" : "s"}.`);
41
+ return;
42
+ }
43
+
44
+ if (command === "scan") {
45
+ const config = await loadConfig(options.config);
46
+ const summary = await runPipeline(config, { dryRun: true });
47
+ console.log(`Scanned ${summary.sourceCount} source string${summary.sourceCount === 1 ? "" : "s"}.`);
48
+ console.log(`Updated ${summary.localeCount} locale file${summary.localeCount === 1 ? "" : "s"} in ${summary.outDir}.`);
49
+ console.log("No provider calls were made.");
50
+ return;
51
+ }
52
+
53
+ if (command === "run" || command === "translate") {
54
+ const config = await loadConfig(options.config);
55
+ const summary = await runPipeline(config, { dryRun: options.dryRun });
56
+ console.log(`Extracted ${summary.sourceCount} source string${summary.sourceCount === 1 ? "" : "s"}.`);
57
+ console.log(`Updated ${summary.localeCount} locale file${summary.localeCount === 1 ? "" : "s"} in ${summary.outDir}.`);
58
+ if (summary.translatedCount > 0) {
59
+ console.log(`Translated ${summary.translatedCount} new/changed entr${summary.translatedCount === 1 ? "y" : "ies"}.`);
60
+ }
61
+ if (summary.skippedTranslationCount > 0) {
62
+ console.log(`Skipped ${summary.skippedTranslationCount} entr${summary.skippedTranslationCount === 1 ? "y" : "ies"} without provider calls.`);
63
+ }
64
+ return;
65
+ }
66
+
67
+ throw new Error(`Unknown command: ${command}\n\n${help}`);
68
+ }
69
+
70
+ function parseArgs(argv) {
71
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
72
+ return { command: argv[0], options: { config: "anylang.config.json", dryRun: false } };
73
+ }
74
+
75
+ const options = {
76
+ config: "anylang.config.json",
77
+ dryRun: false
78
+ };
79
+ let command;
80
+
81
+ for (let index = 0; index < argv.length; index += 1) {
82
+ const arg = argv[index];
83
+ if (!command && !arg.startsWith("--")) {
84
+ command = arg;
85
+ continue;
86
+ }
87
+ if (arg === "--config") {
88
+ options.config = argv[index + 1];
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (arg === "--dry-run") {
93
+ options.dryRun = true;
94
+ continue;
95
+ }
96
+ throw new Error(`Unknown option: ${arg}`);
97
+ }
98
+
99
+ return { command, options };
100
+ }
101
+
102
+ async function initConfig(configPath = "anylang.config.json") {
103
+ const resolved = path.resolve(configPath);
104
+ await mkdir(path.dirname(resolved), { recursive: true });
105
+ await writeFile(resolved, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, { flag: "wx" });
106
+ console.log(`Created ${path.relative(process.cwd(), resolved)}`);
107
+ }
package/src/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export const DEFAULT_CONFIG = {
5
+ sourceLocale: "en",
6
+ targetLocales: ["hi"],
7
+ include: ["src/**/*.{js,jsx,ts,tsx,vue,html}"],
8
+ exclude: ["node_modules", ".git", "dist", "build", ".next"],
9
+ outDir: "locales",
10
+ runtime: {
11
+ output: "src/anylang.generated.ts",
12
+ importFrom: "anylang-dev/runtime"
13
+ },
14
+ functionName: "$tr",
15
+ provider: {
16
+ name: "gemini",
17
+ baseUrl: "https://generativelanguage.googleapis.com/v1beta",
18
+ model: "gemini-2.5-flash"
19
+ }
20
+ };
21
+
22
+ export async function loadConfig(configPath = "anylang.config.json") {
23
+ const resolved = path.resolve(configPath);
24
+ let raw;
25
+ try {
26
+ raw = await readFile(resolved, "utf8");
27
+ } catch (error) {
28
+ if (error && error.code === "ENOENT") {
29
+ throw new Error(`Missing config file: ${configPath}. Run "anylang init" first.`);
30
+ }
31
+ throw error;
32
+ }
33
+
34
+ const parsed = JSON.parse(raw);
35
+ const config = {
36
+ ...DEFAULT_CONFIG,
37
+ ...parsed,
38
+ provider: {
39
+ ...DEFAULT_CONFIG.provider,
40
+ ...(parsed.provider || {})
41
+ }
42
+ };
43
+
44
+ if (!config.sourceLocale) throw new Error("Config must include sourceLocale.");
45
+ if (!Array.isArray(config.targetLocales)) throw new Error("Config targetLocales must be an array.");
46
+ if (!Array.isArray(config.include)) throw new Error("Config include must be an array.");
47
+ if (!config.outDir) throw new Error("Config must include outDir.");
48
+ if (!config.functionName) throw new Error("Config must include functionName.");
49
+
50
+ return config;
51
+ }
package/src/env.js ADDED
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function loadDotEnv(cwd = process.cwd()) {
5
+ const envPath = path.join(cwd, ".env");
6
+ if (!existsSync(envPath)) return;
7
+
8
+ const contents = readFileSync(envPath, "utf8");
9
+ for (const line of contents.split(/\r?\n/)) {
10
+ const trimmed = line.trim();
11
+ if (!trimmed || trimmed.startsWith("#")) continue;
12
+
13
+ const separatorIndex = trimmed.indexOf("=");
14
+ if (separatorIndex === -1) continue;
15
+
16
+ const key = trimmed.slice(0, separatorIndex).trim();
17
+ const rawValue = trimmed.slice(separatorIndex + 1).trim();
18
+ if (!key || process.env[key] !== undefined) continue;
19
+
20
+ process.env[key] = unquote(rawValue);
21
+ }
22
+ }
23
+
24
+ function unquote(value) {
25
+ if (
26
+ (value.startsWith("\"") && value.endsWith("\"")) ||
27
+ (value.startsWith("'") && value.endsWith("'"))
28
+ ) {
29
+ return value.slice(1, -1);
30
+ }
31
+ return value;
32
+ }