@fisharmy100/auto-i18n-cli 1.0.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 ADDED
@@ -0,0 +1,210 @@
1
+ # auto-i18n-cli
2
+
3
+ A command-line tool for automatically generating translation databases for use with the [`react-auto-i18n`](https://github.com/your-org/react-auto-i18n) library. It scans your TypeScript source files for all `__t()` and `__tv()` calls and produces a JSON translation database.
4
+
5
+ **NOTE:** You must have a compatible version of python installed `^3.12`
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install -D auto-i18n-cli
13
+ ```
14
+
15
+ Then run it directly with `npx`:
16
+
17
+ ```bash
18
+ npx auto-i18n-cli [options]
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ auto-i18n-cli -i <input> -b <backend> [options]
27
+ ```
28
+
29
+ ### Example — Azure (remote) backend
30
+
31
+ ```bash
32
+ npx auto-i18n-cli \
33
+ -i "./src" \
34
+ -o "./assets/translations.json" \
35
+ -s eng_Latn \
36
+ -l spa_Latn fra_Latn deu_Latn \
37
+ -b azure \
38
+ --azureKey "YOUR_AZURE_KEY"
39
+ ```
40
+
41
+ ### Example — NLLB (local) backend
42
+
43
+ ```bash
44
+ npx auto-i18n-cli \
45
+ -i "./src" \
46
+ -o "./assets/translations.json" \
47
+ -l spa_Latn jpn_Jpan \
48
+ -b nllb
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Options
54
+
55
+ | Flag | Alias | Required | Description |
56
+ |---|---|---|---|
57
+ | `--input` | `-i` | ✅ | Path to a TypeScript file or project directory to scan |
58
+ | `--backend` | `-b` | ✅ | Translation backend: `azure` or `nllb` |
59
+ | `--languages` | `-l` | | One or more target language codes to translate into (e.g. `spa_Latn fra_Latn`) |
60
+ | `--out` | `-o` | | Output file path for the translation JSON (default: `out.json`) |
61
+ | `--sourceLang` | `-s` | | The language your source strings are written in (default: `eng_Latn`) |
62
+ | `--maxTokens` | `-t` | | Maximum tokens per translation chunk — must be ≥ 100 (default: `250`) |
63
+ | `--azureKey` | | ✅ (Azure) | Azure Translator API key |
64
+ | `--azureRegion` | | | Azure Translator region (default: `eastus`) |
65
+ | `--azureEndpoint` | | | Custom Azure Translator endpoint URL |
66
+ | `--nllbModel` | | | NLLB model name (default: `facebook/nllb-200-distilled-1.3B`) |
67
+ | `--multiFile` | `-m` | | Separate each language into its own file with a manifest (default: `false`) |
68
+ | `--help` | `-h` | | Show help message |
69
+
70
+ ---
71
+
72
+ ## Backends
73
+
74
+ ### Azure Translator
75
+
76
+ Uses Microsoft's Azure Cognitive Services Translator API. Requires an API key.
77
+
78
+ ```bash
79
+ npx auto-i18n-cli -i "./src" -b azure --azureKey "YOUR_KEY" -l spa_Latn
80
+ ```
81
+
82
+ Note: not all NLLB language codes have an Azure equivalent. The CLI will report an error if you specify an unsupported language for the Azure backend.
83
+
84
+ ### NLLB (Local)
85
+
86
+ Uses Meta's [NLLB-200](https://ai.meta.com/research/no-language-left-behind/) model, running locally via Python. No API key required, but requires a compatible Python environment with the model available.
87
+
88
+ ```bash
89
+ npx auto-i18n-cli -i "./src" -b nllb -l fra_Latn
90
+ ```
91
+
92
+ You can specify a custom model with `--nllbModel`:
93
+
94
+ ```bash
95
+ npx auto-i18n-cli -i "./src" -b nllb --nllbModel "facebook/nllb-200-1.3B" -l fra_Latn
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Output
101
+
102
+ ### Single File Format (default)
103
+
104
+ The CLI produces a JSON file structured as an `I18nDatabase`, with one top-level key per language:
105
+
106
+ ```json
107
+ {
108
+ "eng_Latn": {
109
+ "greeting": "Hello!",
110
+ "farewell": "Goodbye!"
111
+ },
112
+ "spa_Latn": {
113
+ "greeting": "¡Hola!",
114
+ "farewell": "¡Adiós!"
115
+ },
116
+ "fra_Latn": {
117
+ "greeting": "Bonjour!",
118
+ "farewell": "Au revoir!"
119
+ }
120
+ }
121
+ ```
122
+
123
+ This file can be imported directly and passed to `I18nProvider` in your React app:
124
+
125
+ ```tsx
126
+ import db from './assets/translations.json'
127
+ import { I18nProvider, SimpleI18nDb } from 'react-auto-i18n'
128
+
129
+ <I18nProvider defaultLang="eng_Latn" db={new SimpleI18nDb(db)}>
130
+ <App />
131
+ </I18nProvider>
132
+ ```
133
+
134
+ Or, you can also load the file if it is in your public folder.
135
+
136
+ ```tsx
137
+ import { I18nFileProvider } from 'react-auto-i18n'
138
+
139
+ <I18nFileProvider defaultLang="eng_Latn" path="./translations.json">
140
+ <App />
141
+ </I18nFileProvider>
142
+ ```
143
+
144
+ ### Multi-File Format (`--multiFile`)
145
+
146
+ When using the `--multiFile` (or `-m`) flag, translations are split into separate JSON files organized in a directory with a manifest:
147
+
148
+ ```
149
+ translations/
150
+ ├── manifest.json
151
+ ├── eng_Latn.json
152
+ ├── spa_Latn.json
153
+ └── fra_Latn.json
154
+ ```
155
+
156
+ The `manifest.json` file contains:
157
+
158
+ ```json
159
+ {
160
+ "langs": ["eng_Latn", "spa_Latn", "fra_Latn"]
161
+ }
162
+ ```
163
+
164
+ Each language file contains only that language's translations:
165
+
166
+ ```json
167
+ {
168
+ "greeting": "¡Hola!",
169
+ "farewell": "¡Adiós!"
170
+ }
171
+ ```
172
+
173
+ You can use `I18nMultiFileProvider` with the multi-file format:
174
+
175
+ ```tsx
176
+ import { I18nMultiFileProvider } from 'react-auto-i18n'
177
+
178
+ <I18nMultiFileProvider
179
+ defaultLang="eng_Latn"
180
+ path="./translations"
181
+ >
182
+ <App />
183
+ </I18nMultiFileProvider>
184
+ ```
185
+
186
+ ---
187
+
188
+ ## How It Works
189
+
190
+ 1. The CLI scans your TypeScript source files (or project directory) for all `__t()` and `__tv()` calls.
191
+ 2. It extracts the key and message string from each call.
192
+ 3. It sends those strings to the configured translation backend.
193
+ 4. It writes the resulting translations to the output JSON file.
194
+
195
+ Because extraction is based on static analysis, both arguments to `__t` and `__tv` **must be raw string literals** — not variables or expressions.
196
+
197
+ ```ts
198
+ // ✅ Valid — static string literals
199
+ __t("greeting", "Hello!")
200
+
201
+ // ❌ Invalid — cannot be statically extracted
202
+ const msg = "Hello!"
203
+ __t("greeting", msg)
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Language Codes
209
+
210
+ Language codes follow the `{lang}_{Script}` format used by the NLLB-200 model (e.g. `eng_Latn`, `spa_Latn`, `cmn_Hans`). See the `react-auto-i18n` documentation for the full list of supported codes.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ import { runPython } from "./python_ffi.js";
3
+ import { parse } from 'ts-command-line-args';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import pkg from "../package.json" with { type: 'json' };
7
+ import { nllbToAzure, stringToLanguageCode } from "./langs.js";
8
+ import { parseTSFiles } from "./parser.js";
9
+ import chalk from "chalk";
10
+ import { logError, logMessage } from "./utils.js";
11
+ import { SingleBar } from "cli-progress";
12
+ const DEFAULT_MAX_TOKENS = 250;
13
+ async function main() {
14
+ let options;
15
+ try {
16
+ options = parse({
17
+ input: {
18
+ type: String,
19
+ alias: 'i',
20
+ description: 'The input typescript file or typescript project directory'
21
+ },
22
+ languages: {
23
+ type: String,
24
+ alias: 'l',
25
+ multiple: true,
26
+ optional: true,
27
+ description: 'The languages to generate translations for'
28
+ },
29
+ out: {
30
+ type: String,
31
+ alias: 'o',
32
+ optional: true,
33
+ description: 'The output file path'
34
+ },
35
+ sourceLang: {
36
+ type: String,
37
+ alias: 's',
38
+ optional: true,
39
+ description: 'The source language that all __t use. Defaults to English'
40
+ },
41
+ maxTokens: {
42
+ type: Number,
43
+ alias: 't',
44
+ optional: true,
45
+ description: `The maximum number of tokens. Defaults to ${DEFAULT_MAX_TOKENS}`
46
+ },
47
+ backend: {
48
+ type: String,
49
+ alias: 'b',
50
+ description: 'Translation backend to use: "nllb" or "azure"',
51
+ },
52
+ azureKey: {
53
+ type: String,
54
+ optional: true,
55
+ description: 'Azure Translator API key (required when backend is "azure")',
56
+ },
57
+ azureRegion: {
58
+ type: String,
59
+ optional: true,
60
+ description: 'Azure Translator region (default: eastus)',
61
+ },
62
+ azureEndpoint: {
63
+ type: String,
64
+ optional: true,
65
+ description: 'Azure Translator endpoint URL',
66
+ },
67
+ nllbModel: {
68
+ type: String,
69
+ optional: true,
70
+ description: 'NLLB model name (default: facebook/nllb-200-distilled-1.3B)',
71
+ },
72
+ multiFile: {
73
+ type: Boolean,
74
+ optional: true,
75
+ alias: 'm',
76
+ description: 'If used will separate each language into its own file with a manifest'
77
+ },
78
+ help: {
79
+ type: Boolean,
80
+ optional: true,
81
+ alias: 'h',
82
+ description: 'Show this help message'
83
+ },
84
+ }, {
85
+ helpArg: 'help',
86
+ headerContentSections: [
87
+ {
88
+ header: 'auto-i18n-cli',
89
+ content: `A translation database generator library for the react-auto-i18n library\n\nVersion: ${pkg.version}`
90
+ }
91
+ ],
92
+ });
93
+ }
94
+ catch (error) {
95
+ logError("Console Error:\n");
96
+ console.log(error);
97
+ return;
98
+ }
99
+ runWithOptions(options);
100
+ }
101
+ async function runWithOptions(options) {
102
+ logMessage("Running command...");
103
+ const validated = validateProgramOptions(options);
104
+ if (!validated)
105
+ return;
106
+ const { input, output, translateArgs } = validated;
107
+ logMessage("Parsing TS files...");
108
+ const result = await parseTSFiles(input);
109
+ if (result.type === "error") {
110
+ logError("Error:\n" + result.value.join("\n\n"));
111
+ return;
112
+ }
113
+ const segments = Object.values(result.value)
114
+ .filter(v => v !== undefined)
115
+ .reduce((acc, v) => {
116
+ acc[v.key] = v.message;
117
+ return acc;
118
+ }, {});
119
+ translateArgs.segments = segments;
120
+ logMessage("Translating...");
121
+ let success = await generateTranslationFile(translateArgs, output, options.multiFile ?? false);
122
+ if (!success) {
123
+ logError("Translation Failed");
124
+ return;
125
+ }
126
+ const fullOutPath = path.resolve(output);
127
+ logMessage(`Extraction & Translation complete! Translations located at: ${fullOutPath}`);
128
+ }
129
+ function validateProgramOptions(options) {
130
+ const inputPath = path.resolve(options.input);
131
+ if (!fs.existsSync(inputPath)) {
132
+ logError(`Error: file path '${chalk.bold(inputPath)}' does not exist`);
133
+ return null;
134
+ }
135
+ if (options.backend !== "nllb" && options.backend !== "azure") {
136
+ logError(`Error: backend must be ${chalk.bold("nllb")} or ${chalk.bold("azure")}`);
137
+ return null;
138
+ }
139
+ const langs = [];
140
+ const option_langs = options.languages ?? [];
141
+ for (let lang of option_langs) {
142
+ const code = stringToLanguageCode(lang);
143
+ if (!code) {
144
+ logError(`Error: language code ${chalk.bold(lang)} is not valid`);
145
+ return null;
146
+ }
147
+ if (options.backend === "azure" && nllbToAzure(code) === null) {
148
+ logError(`Error: language code ${chalk.bold(lang)} has no Azure equivalent`);
149
+ return null;
150
+ }
151
+ langs.push(code);
152
+ }
153
+ let src_lang = "eng_Latn";
154
+ if (options.sourceLang) {
155
+ const code = stringToLanguageCode(options.sourceLang);
156
+ if (!code) {
157
+ logError(`Error: language code ${chalk.bold(options.sourceLang)} is not valid`);
158
+ return null;
159
+ }
160
+ src_lang = code;
161
+ }
162
+ let max_tokens = options.maxTokens;
163
+ if (max_tokens === undefined) {
164
+ max_tokens = DEFAULT_MAX_TOKENS;
165
+ }
166
+ if (max_tokens < 100) {
167
+ logError(`Error: maxTokens cannot be less than 100`);
168
+ return null;
169
+ }
170
+ let translateArgs;
171
+ if (options.backend === "azure") {
172
+ if (!options.azureKey) {
173
+ logError(`Error: ${chalk.bold("--azureKey")} is required when using the Azure backend`);
174
+ return null;
175
+ }
176
+ translateArgs = {
177
+ langs,
178
+ src_lang,
179
+ segments: {},
180
+ max_tokens,
181
+ backend: "azure",
182
+ azure_key: options.azureKey,
183
+ ...(options.azureRegion && { azure_region: options.azureRegion }),
184
+ ...(options.azureEndpoint && { azure_endpoint: options.azureEndpoint }),
185
+ };
186
+ }
187
+ else {
188
+ translateArgs = {
189
+ langs,
190
+ src_lang,
191
+ segments: {},
192
+ max_tokens,
193
+ backend: "nllb",
194
+ ...(options.nllbModel && { nllb_model: options.nllbModel }),
195
+ };
196
+ }
197
+ const output = options.out ?? (options.multiFile ? "translations" : "translations.json");
198
+ return { input: inputPath, output, translateArgs };
199
+ }
200
+ async function generateTranslationFile(args, out, multiFile) {
201
+ const bar = new SingleBar({
202
+ format: chalk.green(`Generation Progress |${chalk.bold("{bar}")}| {percentage}% || {value}/{total} Chunks || ETA: {eta_formatted}`),
203
+ barCompleteChar: '\u2588',
204
+ barCompleteString: '\u2591',
205
+ hideCursor: true,
206
+ });
207
+ let bar_started = false;
208
+ let is_error = false;
209
+ await runPython(args, (p) => {
210
+ if (!bar_started) {
211
+ bar.start(args.langs.length * Object.values(args.segments).length, 0);
212
+ bar_started = true;
213
+ }
214
+ bar.update(p.current);
215
+ })
216
+ .then(o => {
217
+ if (multiFile) {
218
+ fs.mkdirSync(out, { recursive: true });
219
+ Object.entries(o.values).forEach(([k, v]) => {
220
+ const filePath = path.join(out, `${k}.json`);
221
+ fs.writeFileSync(filePath, JSON.stringify(v, null, 2));
222
+ });
223
+ const langs = Object.keys(o.values);
224
+ const manifestData = { langs };
225
+ const manifestPath = path.join(out, "manifest.json");
226
+ fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
227
+ }
228
+ else {
229
+ fs.mkdirSync(path.dirname(out), { recursive: true });
230
+ fs.writeFileSync(out, JSON.stringify(o.values, null, 2));
231
+ }
232
+ })
233
+ .catch(e => {
234
+ is_error = true;
235
+ bar.stop();
236
+ logError(e);
237
+ })
238
+ .finally(() => {
239
+ bar.stop();
240
+ });
241
+ return !is_error;
242
+ }
243
+ main();
@@ -0,0 +1,8 @@
1
+ export declare const LANGUAGE_CODES: readonly ["ace_Arab", "ace_Latn", "acm_Arab", "acq_Arab", "aeb_Arab", "afr_Latn", "als_Latn", "amh_Ethi", "apc_Arab", "arb_Arab", "arb_Latn", "arg_Latn", "ars_Arab", "ary_Arab", "arz_Arab", "asm_Beng", "ast_Latn", "awa_Deva", "ayr_Latn", "azb_Arab", "azj_Latn", "bak_Cyrl", "bam_Latn", "ban_Latn", "bel_Cyrl", "bem_Latn", "ben_Beng", "bho_Deva", "bjn_Arab", "bjn_Latn", "bod_Tibt", "bos_Latn", "brx_Deva", "bug_Latn", "bul_Cyrl", "cat_Latn", "ceb_Latn", "ces_Latn", "chv_Cyrl", "cjk_Latn", "ckb_Arab", "cmn_Hans", "cmn_Hant", "crh_Latn", "cym_Latn", "dan_Latn", "dar_Cyrl", "deu_Latn", "dgo_Deva", "dik_Latn", "dyu_Latn", "dzo_Tibt", "ekk_Latn", "ell_Grek", "eng_Latn", "epo_Latn", "eus_Latn", "ewe_Latn", "fao_Latn", "fij_Latn", "fil_Latn", "fin_Latn", "fon_Latn", "fra_Latn", "fur_Latn", "fuv_Latn", "gaz_Latn", "gla_Latn", "gle_Latn", "glg_Latn", "gom_Deva", "gug_Latn", "guj_Gujr", "hat_Latn", "hau_Latn", "heb_Hebr", "hin_Deva", "hne_Deva", "hrv_Latn", "hun_Latn", "hye_Armn", "ibo_Latn", "ilo_Latn", "ind_Latn", "isl_Latn", "ita_Latn", "jav_Latn", "jpn_Jpan", "kaa_Latn", "kab_Latn", "kac_Latn", "kam_Latn", "kan_Knda", "kas_Arab", "kas_Deva", "kat_Geor", "kaz_Cyrl", "kbp_Latn", "kea_Latn", "khk_Cyrl", "khm_Khmr", "kik_Latn", "kin_Latn", "kir_Cyrl", "kmb_Latn", "kmr_Latn", "knc_Arab", "knc_Latn", "kor_Hang", "ktu_Latn", "lao_Laoo", "lij_Latn", "lim_Latn", "lin_Latn", "lit_Latn", "lld_Latn", "lmo_Latn", "ltg_Latn", "ltz_Latn", "lua_Latn", "lug_Latn", "luo_Latn", "lus_Latn", "lvs_Latn", "mag_Deva", "mai_Deva", "mal_Mlym", "mar_Deva", "mfe_Latn", "mhr_Cyrl", "min_Arab", "min_Latn", "mkd_Cyrl", "mlt_Latn", "mni_Beng", "mni_Mtei", "mos_Latn", "mri_Latn", "mya_Mymr", "myv_Cyrl", "nld_Latn", "nno_Latn", "nob_Latn", "npi_Deva", "nqo_Nkoo", "nso_Latn", "nus_Latn", "nya_Latn", "oci_Latn", "ory_Orya", "pag_Latn", "pan_Guru", "pap_Latn", "pbt_Arab", "pes_Arab", "plt_Latn", "pol_Latn", "por_Latn", "prs_Arab", "quy_Latn", "ron_Latn", "run_Latn", "rus_Cyrl", "sag_Latn", "san_Deva", "sat_Olck", "scn_Latn", "shn_Mymr", "sin_Sinh", "slk_Latn", "slv_Latn", "smo_Latn", "sna_Latn", "snd_Arab", "snd_Deva", "som_Latn", "sot_Latn", "spa_Latn", "srd_Latn", "srp_Cyrl", "ssw_Latn", "sun_Latn", "swe_Latn", "swh_Latn", "szl_Latn", "tam_Taml", "taq_Latn", "taq_Tfng", "tat_Cyrl", "tel_Telu", "tgk_Cyrl", "tha_Thai", "tir_Ethi", "tpi_Latn", "tsn_Latn", "tso_Latn", "tuk_Latn", "tum_Latn", "tur_Latn", "twi_Latn", "tyv_Cyrl", "uig_Arab", "ukr_Cyrl", "umb_Latn", "urd_Arab", "uzn_Latn", "uzs_Arab", "vec_Latn", "vie_Latn", "vmw_Latn", "war_Latn", "wol_Latn", "wuu_Hans", "xho_Latn", "ydd_Hebr", "yor_Latn", "yue_Hant", "zgh_Tfng", "zsm_Latn", "zul_Latn"];
2
+ export type LanguageCode = typeof LANGUAGE_CODES[number];
3
+ export declare function stringToLanguageCode(str: string): LanguageCode | null;
4
+ /**
5
+ * Converts an NLLB LanguageCode to an Azure Translator BCP-47 language tag.
6
+ * Returns null if there is no Azure equivalent for the given code.
7
+ */
8
+ export declare function nllbToAzure(code: LanguageCode): string | null;