i18n-boost 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 +21 -0
- package/README.md +838 -0
- package/bin/i18n-boost.js +5 -0
- package/dist/anthropic-6WICO575.mjs +1 -0
- package/dist/backend-F4QW23OL.mjs +1 -0
- package/dist/chunk-3V2WXULC.mjs +1 -0
- package/dist/chunk-4GJXND3H.mjs +1 -0
- package/dist/chunk-BCR6DFAS.mjs +1 -0
- package/dist/chunk-NFSRAD6K.mjs +11 -0
- package/dist/chunk-Q5SEXPLC.mjs +1 -0
- package/dist/chunk-XH7NJHX6.mjs +11 -0
- package/dist/cli.mjs +12 -0
- package/dist/deepl-N327FF46.mjs +1 -0
- package/dist/index.cjs +40 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.mjs +13 -0
- package/dist/init-EPCKGU3C.mjs +33 -0
- package/dist/libre-translate-NQGYY2HV.mjs +1 -0
- package/dist/none-JRFHRYLI.mjs +1 -0
- package/dist/openai-O4RCRFGT.mjs +1 -0
- package/dist/server/index.cjs +219 -0
- package/dist/server/index.d.cts +50 -0
- package/dist/server/index.d.ts +50 -0
- package/dist/server/index.mjs +180 -0
- package/dist/setup/postinstall.cjs +33 -0
- package/dist/tt.cjs +1 -0
- package/dist/tt.d.cts +3 -0
- package/dist/tt.d.ts +3 -0
- package/dist/tt.mjs +1 -0
- package/locales/languages.json +72 -0
- package/package.json +90 -0
- package/schema.json +133 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// src/server/express.ts
|
|
2
|
+
import express, { Router } from "express";
|
|
3
|
+
|
|
4
|
+
// src/server/handler.ts
|
|
5
|
+
import { timingSafeEqual, createHash } from "crypto";
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
// src/core/key-utils.ts
|
|
10
|
+
function flattenObject(obj, prefix = "", separator = ".") {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
13
|
+
const fullKey = prefix ? `${prefix}${separator}${key}` : key;
|
|
14
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
15
|
+
const nested = flattenObject(
|
|
16
|
+
value,
|
|
17
|
+
fullKey,
|
|
18
|
+
separator
|
|
19
|
+
);
|
|
20
|
+
Object.assign(result, nested);
|
|
21
|
+
} else if (typeof value === "string") {
|
|
22
|
+
result[fullKey] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/server/handler.ts
|
|
29
|
+
var MAX_TARGET_LOCALES = 30;
|
|
30
|
+
var MAX_FRONTEND_KEYS = 5e3;
|
|
31
|
+
var LOCALE_PATTERN = /^[a-z]{2,3}(-[A-Z][a-zA-Z]{1,7})?$/;
|
|
32
|
+
var warnedNoSecret = false;
|
|
33
|
+
function safeCompare(a, b) {
|
|
34
|
+
const ha = createHash("sha256").update(a).digest();
|
|
35
|
+
const hb = createHash("sha256").update(b).digest();
|
|
36
|
+
return timingSafeEqual(ha, hb);
|
|
37
|
+
}
|
|
38
|
+
function isValidBody(body) {
|
|
39
|
+
if (typeof body !== "object" || body === null) return false;
|
|
40
|
+
const b = body;
|
|
41
|
+
if (!Array.isArray(b.targetLocales)) return false;
|
|
42
|
+
if (!b.targetLocales.every((l) => typeof l === "string" && LOCALE_PATTERN.test(l))) return false;
|
|
43
|
+
if (b.frontendTexts !== void 0) {
|
|
44
|
+
if (typeof b.frontendTexts !== "object" || b.frontendTexts === null) return false;
|
|
45
|
+
if (Array.isArray(b.frontendTexts)) return false;
|
|
46
|
+
const ft = b.frontendTexts;
|
|
47
|
+
if (!Object.values(ft).every((v) => typeof v === "string")) return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
var I18nHandlerError = class extends Error {
|
|
52
|
+
constructor(statusCode, message) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.statusCode = statusCode;
|
|
55
|
+
this.name = "I18nHandlerError";
|
|
56
|
+
}
|
|
57
|
+
statusCode;
|
|
58
|
+
};
|
|
59
|
+
async function readFlatJson(filePath) {
|
|
60
|
+
try {
|
|
61
|
+
const raw = await readFile(filePath, "utf-8");
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
return flattenObject(parsed);
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function handleSync(body, options) {
|
|
69
|
+
if (options.secret !== void 0) {
|
|
70
|
+
if (options.requestSecret === void 0 || !safeCompare(options.requestSecret, options.secret)) {
|
|
71
|
+
throw new I18nHandlerError(401, "Unauthorized");
|
|
72
|
+
}
|
|
73
|
+
} else if (!warnedNoSecret) {
|
|
74
|
+
warnedNoSecret = true;
|
|
75
|
+
console.warn("[i18n-boost] No secret configured \u2014 /sync endpoint is unauthenticated");
|
|
76
|
+
}
|
|
77
|
+
if (!isValidBody(body)) {
|
|
78
|
+
throw new I18nHandlerError(400, "Invalid request body");
|
|
79
|
+
}
|
|
80
|
+
const { targetLocales, frontendTexts = {} } = body;
|
|
81
|
+
if (targetLocales.length > MAX_TARGET_LOCALES) {
|
|
82
|
+
throw new I18nHandlerError(400, `Too many targetLocales (max ${MAX_TARGET_LOCALES})`);
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(frontendTexts).length > MAX_FRONTEND_KEYS) {
|
|
85
|
+
throw new I18nHandlerError(400, `Too many frontendTexts keys (max ${MAX_FRONTEND_KEYS})`);
|
|
86
|
+
}
|
|
87
|
+
const sourceLocalePath = path.join(options.localesDir, `${options.sourceLocale}.json`);
|
|
88
|
+
const backendKeys = await readFlatJson(sourceLocalePath);
|
|
89
|
+
const collisions = Object.keys(frontendTexts).filter((k) => backendKeys[k] !== void 0);
|
|
90
|
+
if (collisions.length > 0) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[i18n-boost] Key collision(s) \u2014 backend wins: ${collisions.join(", ")}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const allTexts = { ...frontendTexts, ...backendKeys };
|
|
96
|
+
const allKeys = Object.keys(allTexts);
|
|
97
|
+
const allValues = allKeys.map((k) => allTexts[k]);
|
|
98
|
+
const locales = {};
|
|
99
|
+
for (const targetLocale of targetLocales) {
|
|
100
|
+
try {
|
|
101
|
+
const result = await options.provider.translate({
|
|
102
|
+
texts: allValues,
|
|
103
|
+
sourceLocale: options.sourceLocale,
|
|
104
|
+
targetLocale
|
|
105
|
+
});
|
|
106
|
+
const translated = {};
|
|
107
|
+
for (let i = 0; i < allKeys.length; i++) {
|
|
108
|
+
translated[allKeys[i]] = result.translations[i];
|
|
109
|
+
}
|
|
110
|
+
locales[targetLocale] = translated;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
throw new I18nHandlerError(500, `Translation failed for ${targetLocale}: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { sourceKeys: backendKeys, locales };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/server/express.ts
|
|
120
|
+
var BLOCKED_ENVS = ["production", "staging"];
|
|
121
|
+
function createI18nRouter(options) {
|
|
122
|
+
const env = process.env["NODE_ENV"];
|
|
123
|
+
if (BLOCKED_ENVS.includes(env ?? "")) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`[i18n-boost] createI18nRouter() is a development-only tool and must not run in '${env}'. Remove it from your production server setup.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const router = Router();
|
|
129
|
+
router.use(express.json({ limit: "1mb" }));
|
|
130
|
+
router.post("/sync", async (req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const rawSecret = req.headers["x-i18n-secret"];
|
|
133
|
+
const requestSecret = Array.isArray(rawSecret) ? rawSecret[0] : rawSecret;
|
|
134
|
+
const result = await handleSync(req.body, {
|
|
135
|
+
...options,
|
|
136
|
+
requestSecret
|
|
137
|
+
});
|
|
138
|
+
res.json(result);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof I18nHandlerError) {
|
|
141
|
+
res.status(err.statusCode).json({ error: err.message });
|
|
142
|
+
} else {
|
|
143
|
+
res.status(500).json({ error: "Internal server error" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return router;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/server/fastify.ts
|
|
151
|
+
var BLOCKED_ENVS2 = ["production", "staging"];
|
|
152
|
+
function createI18nPlugin(options) {
|
|
153
|
+
const env = process.env["NODE_ENV"];
|
|
154
|
+
if (BLOCKED_ENVS2.includes(env ?? "")) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`[i18n-boost] createI18nPlugin() is a development-only tool and must not run in '${env}'. Remove it from your production server setup.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return async function plugin(fastify) {
|
|
160
|
+
fastify.post("/sync", { bodyLimit: 1048576 }, async (request, reply) => {
|
|
161
|
+
try {
|
|
162
|
+
const result = await handleSync(request.body, {
|
|
163
|
+
...options,
|
|
164
|
+
requestSecret: request.headers["x-i18n-secret"]
|
|
165
|
+
});
|
|
166
|
+
return result;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err instanceof I18nHandlerError) {
|
|
169
|
+
return reply.status(err.statusCode).send({ error: err.message });
|
|
170
|
+
}
|
|
171
|
+
return reply.status(500).send({ error: "Internal server error" });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
I18nHandlerError,
|
|
178
|
+
createI18nPlugin,
|
|
179
|
+
createI18nRouter
|
|
180
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";var N=Object.create;var h=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var F=Object.getOwnPropertyNames;var $=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var b=(n,e)=>()=>(n&&(e=n(n=0)),e);var k=(n,e)=>{for(var t in e)h(n,t,{get:e[t],enumerable:!0})},I=(n,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of F(e))!H.call(n,r)&&r!==t&&h(n,r,{get:()=>e[r],enumerable:!(s=B(e,r))||s.enumerable});return n};var _=(n,e,t)=>(t=n!=null?N($(n)):{},I(e||!n||!n.__esModule?h(t,"default",{value:n,enumerable:!0}):t,n)),J=n=>I(h({},"__esModule",{value:!0}),n);var A,P=b(()=>{A=[{code:"en-US",native_name:"English (US)",english_name:"English (US)"},{code:"en-GB",native_name:"English (UK)",english_name:"English (UK)"},{code:"es-419",native_name:"Espa\xF1ol (Latinoam\xE9rica)",english_name:"Spanish (Latin America)"},{code:"es-ES",native_name:"Espa\xF1ol (Espa\xF1a)",english_name:"Spanish (Spain)"},{code:"pt-BR",native_name:"Portugu\xEAs (Brasil)",english_name:"Portuguese (Brazil)"},{code:"fr-FR",native_name:"Fran\xE7ais",english_name:"French"},{code:"fr-CA",native_name:"Fran\xE7ais (Canada)",english_name:"French (Canada)"},{code:"de-DE",native_name:"Deutsch",english_name:"German"},{code:"it-IT",native_name:"Italiano",english_name:"Italian"},{code:"nl-NL",native_name:"Nederlands",english_name:"Dutch"},{code:"zh-CN",native_name:"\u7B80\u4F53\u4E2D\u6587",english_name:"Chinese (Simplified)"},{code:"zh-TW",native_name:"\u7E41\u9AD4\u4E2D\u6587",english_name:"Chinese (Traditional)"},{code:"ja-JP",native_name:"\u65E5\u672C\u8A9E",english_name:"Japanese"},{code:"ko-KR",native_name:"\uD55C\uAD6D\uC5B4",english_name:"Korean"},{code:"ar-SA",native_name:"\u0627\u0644\u0639\u0631\u0628\u064A\u0629",english_name:"Arabic"},{code:"hi-IN",native_name:"\u0939\u093F\u0928\u094D\u0926\u0940",english_name:"Hindi"},{code:"ru-RU",native_name:"\u0420\u0443\u0441\u0441\u043A\u0438\u0439",english_name:"Russian"},{code:"tr-TR",native_name:"T\xFCrk\xE7e",english_name:"Turkish"},{code:"id-ID",native_name:"Bahasa Indonesia",english_name:"Indonesian"},{code:"vi-VN",native_name:"Ti\u1EBFng Vi\u1EC7t",english_name:"Vietnamese"},{code:"pl-PL",native_name:"Polski",english_name:"Polish"},{code:"sv-SE",native_name:"Svenska",english_name:"Swedish"},{code:"no-NO",native_name:"Norsk",english_name:"Norwegian"},{code:"da-DK",native_name:"Dansk",english_name:"Danish"},{code:"fi-FI",native_name:"Suomi",english_name:"Finnish"},{code:"th-TH",native_name:"\u0E44\u0E17\u0E22",english_name:"Thai"},{code:"he-IL",native_name:"\u05E2\u05D1\u05E8\u05D9\u05EA",english_name:"Hebrew"},{code:"uk-UA",native_name:"\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430",english_name:"Ukrainian"},{code:"cs-CZ",native_name:"\u010Ce\u0161tina",english_name:"Czech"},{code:"el-GR",native_name:"\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC",english_name:"Greek"}]});var T={};k(T,{runInit:()=>G});async function R(n){try{let e=await(0,m.readFile)(g.default.join(n,"package.json"),"utf-8");return JSON.parse(e)}catch{return{}}}function j(n){let e=n.scripts;return!e||typeof e!="object"?!1:"i18gen"in e}async function G(n){let{cwd:e}=n,{projectType:t}=await(0,o.default)({type:"select",name:"projectType",message:"Project type:",choices:[{title:"Back-End (API keys here, exposes translation endpoint)",value:"backend"},{title:"Front-End (calls your backend to translate)",value:"frontend"}]});if(!t){console.log(a.default.yellow("Setup cancelled."));return}t==="backend"?await W(e):await z(e)}async function W(n){let{outputDir:e}=await(0,o.default)({type:"text",name:"outputDir",message:"Output directory:",initial:"src/locales"});if(e===void 0){console.log(a.default.yellow("Setup cancelled."));return}let{provider:t}=await(0,o.default)({type:"select",name:"provider",message:"Choose a translation provider:",choices:[{title:"DeepL",value:"deepl"},{title:"OpenAI-compatible API (OpenAI, DeepSeek, Groq, Ollama\u2026)",value:"openai"},{title:"Anthropic (Claude)",value:"anthropic"},{title:"LibreTranslate (self-hosted)",value:"libre-translate"},{title:"I'll decide later (run `i18n-boost --help` for guidance)",value:"none"}]});if(t===void 0){console.log(a.default.yellow("Setup cancelled."));return}C(t);let s={name:t};if(t==="anthropic"||t==="openai"){let{model:l}=await(0,o.default)({type:"text",name:"model",message:`Preferred model (${t}):`,initial:E[t]??""});s.model=l}if(t==="libre-translate"){let{apiUrl:l}=await(0,o.default)({type:"text",name:"apiUrl",message:"libre-translate API URL:",initial:"http://localhost:5000"});s.apiUrl=l}let r=await R(n),f=!1;j(r)||(f=(await(0,o.default)({type:"confirm",name:"addScript",message:'Add "i18gen": "i18n-boost generate" to package.json?',initial:!0})).addScript??!1);let{confirm:u}=await(0,o.default)({type:"confirm",name:"confirm",message:"Write .i18nrc.json?",initial:!0});if(!u){console.log(a.default.yellow("Aborted. No files written."));return}let c={type:"backend",outputDir:e,provider:s},d=g.default.join(n,".i18nrc.json");if(await(0,m.writeFile)(d,JSON.stringify(c,null,2),"utf-8"),console.log(a.default.green(`\u2713 Written ${d}`)),f){let l=r.scripts??{};l.i18gen="i18n-boost generate",r.scripts=l;let w=g.default.join(n,"package.json");await(0,m.writeFile)(w,JSON.stringify(r,null,2),"utf-8"),console.log(a.default.green('\u2713 Added "i18gen" script to package.json'))}O(t),V(t,s.model)}function V(n,e){let t=`{
|
|
2
|
+
provider: '${n}',
|
|
3
|
+
secret: process.env.I18N_SECRET,
|
|
4
|
+
localesDir: './src/locales',
|
|
5
|
+
sourceLocale: 'en-US',
|
|
6
|
+
}`;console.log(`
|
|
7
|
+
`+a.default.cyan("\u2500\u2500 Express \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(` import { createI18nRouter } from 'i18n-boost/server';
|
|
8
|
+
app.use('/api/i18n', createI18nRouter(${t}));`),console.log(`
|
|
9
|
+
`+a.default.cyan("\u2500\u2500 Fastify \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(` import { createI18nPlugin } from 'i18n-boost/server';
|
|
10
|
+
await fastify.register(createI18nPlugin(${t}), { prefix: '/api/i18n' });`),console.log(`
|
|
11
|
+
`+a.default.cyan("\u2500\u2500 NestJS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(` import { handleSync, I18nHandlerError } from 'i18n-boost/server';
|
|
12
|
+
|
|
13
|
+
@Post('sync')
|
|
14
|
+
async sync(@Body() body: unknown, @Headers('x-i18n-secret') secret?: string) {
|
|
15
|
+
try {
|
|
16
|
+
return await handleSync(body, {
|
|
17
|
+
provider: this.provider,
|
|
18
|
+
secret: process.env.I18N_SECRET,
|
|
19
|
+
localesDir: './src/locales',
|
|
20
|
+
sourceLocale: 'en-US',
|
|
21
|
+
requestSecret: secret,
|
|
22
|
+
});
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err instanceof I18nHandlerError) throw new HttpException(err.message, err.statusCode);
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}`)}async function z(n){let e=A.map(i=>({title:`${i.code} (${i.english_name})`,value:i.code})),{preset:t}=await(0,o.default)({type:"select",name:"preset",message:"Preset:",choices:[{title:"i18next",value:"i18next"},{title:"next-intl",value:"next-intl"}]});if(t===void 0){console.log(a.default.yellow("Setup cancelled."));return}let{sourceLocale:s}=await(0,o.default)({type:"select",name:"sourceLocale",message:"Source locale:",choices:e,initial:e.findIndex(i=>i.value==="en-US")});if(s===void 0){console.log(a.default.yellow("Setup cancelled."));return}let{targetLocales:r}=await(0,o.default)({type:"multiselect",name:"targetLocales",message:"Target locales:",choices:e.filter(i=>i.value!==s),min:1});if(!r||r.length===0){console.log(a.default.yellow("Setup cancelled."));return}let{outputDir:f}=await(0,o.default)({type:"text",name:"outputDir",message:"Output directory:",initial:"src/locales"}),{strategy:u}=await(0,o.default)({type:"select",name:"strategy",message:"Translation strategy:",choices:[{title:"Via backend (your backend translates \u2014 no API keys here)",value:"backend"},{title:"Direct (API key in env var \u2014 for SSR / CI builds)",value:"direct"}]});if(u===void 0){console.log(a.default.yellow("Setup cancelled."));return}let c;if(u==="backend"){let{backendUrl:i}=await(0,o.default)({type:"text",name:"backendUrl",message:"Backend URL:",initial:"http://localhost:3001/api/i18n/sync"}),{secret:p}=await(0,o.default)({type:"text",name:"secret",message:"Secret (blank for none):",initial:""});c={name:"backend",url:i},p&&p.trim()!==""&&(c.secret=p)}else{let{provider:i}=await(0,o.default)({type:"select",name:"provider",message:"Choose a translation provider:",choices:[{title:"DeepL",value:"deepl"},{title:"OpenAI-compatible API (OpenAI, DeepSeek, Groq, Ollama\u2026)",value:"openai"},{title:"Anthropic (Claude)",value:"anthropic"},{title:"LibreTranslate (self-hosted)",value:"libre-translate"},{title:"I'll decide later (run `i18n-boost --help` for guidance)",value:"none"}]});if(i===void 0){console.log(a.default.yellow("Setup cancelled."));return}if(C(i),c={name:i},i==="anthropic"||i==="openai"){let{model:p}=await(0,o.default)({type:"text",name:"model",message:`Preferred model (${i}):`,initial:E[i]??""});c.model=p}if(i==="libre-translate"){let{apiUrl:p}=await(0,o.default)({type:"text",name:"apiUrl",message:"libre-translate API URL:",initial:"http://localhost:5000"});c.apiUrl=p}}let d=await R(n),l=!1;j(d)||(l=(await(0,o.default)({type:"confirm",name:"addScript",message:'Add "i18gen": "i18n-boost generate" to package.json?',initial:!0})).addScript??!1);let{confirm:w}=await(0,o.default)({type:"confirm",name:"confirm",message:"Write .i18nrc.json?",initial:!0});if(!w){console.log(a.default.yellow("Aborted. No files written."));return}let D={type:"frontend",preset:t,sourceLocale:s,targetLocales:r,outputDir:f,provider:c},S=g.default.join(n,".i18nrc.json");if(await(0,m.writeFile)(S,JSON.stringify(D,null,2),"utf-8"),console.log(a.default.green(`\u2713 Written ${S}`)),l){let i=d.scripts??{};i.i18gen="i18n-boost generate",d.scripts=i;let p=g.default.join(n,"package.json");await(0,m.writeFile)(p,JSON.stringify(d,null,2),"utf-8"),console.log(a.default.green('\u2713 Added "i18gen" script to package.json'))}u==="direct"&&O(c.name),q()}function C(n){let e=x[n];e&&console.log(a.default.dim(` \u2192 Obtain an API key from ${e} and set ${a.default.bold("I18NBOOST_KEY=<your-api-key>")}`))}function O(n){let e=x[n];e&&(console.log(`
|
|
28
|
+
`+a.default.cyan("\u2500\u2500 Environment variable \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(` Obtain an API key from ${e} and set:`),console.log(" export I18NBOOST_KEY=<your-api-key>"),console.log(" npx i18n-boost generate --translate"))}function q(){console.log(`
|
|
29
|
+
`+a.default.cyan("\u2500\u2500 Usage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(` import { tt } from 'i18n-boost/tt';
|
|
30
|
+
|
|
31
|
+
// Mark strings for extraction and translation:
|
|
32
|
+
const label = tt('ui.submit', 'Submit');
|
|
33
|
+
const greeting = tt('home.hello', 'Hello, world!');`)}var a,m,g,o,E,x,L=b(()=>{"use strict";a=_(require("picocolors"),1),m=require("fs/promises"),g=_(require("path"),1),o=_(require("prompts"),1);P();E={anthropic:"claude-haiku-4-5-20251001",openai:"gpt-4o-mini"};x={anthropic:"Anthropic",openai:"OpenAI",deepl:"DeepL","libre-translate":"LibreTranslate"}});var Y={};k(Y,{runPostinstall:()=>U});module.exports=J(Y);var v=require("fs"),y=require("path");async function U(n){let e=n?.cwd??process.env.INIT_CWD??process.cwd();if((0,v.existsSync)((0,y.join)(e,".i18nrc.json"))||(0,v.existsSync)((0,y.join)(e,".i18nrc.js"))||(0,v.existsSync)((0,y.join)(e,".i18nrc.cjs"))||!process.stdout.isTTY)return;let{runInit:t}=await Promise.resolve().then(()=>(L(),T));try{await t({cwd:e})}catch{console.log("\ni18n-boost: Setup cancelled. Run `npx i18n-boost init` when ready.\n")}}U().catch(()=>process.exit(0));0&&(module.exports={runPostinstall});
|
package/dist/tt.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var o=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var a=(t,n)=>{for(var e in n)s(t,e,{get:n[e],enumerable:!0})},g=(t,n,e,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of o(n))!p.call(t,r)&&r!==e&&s(t,r,{get:()=>n[r],enumerable:!(i=c(n,r))||i.enumerable});return t};var u=t=>g(s({},"__esModule",{value:!0}),t);var f={};a(f,{tt:()=>l});module.exports=u(f);function l(t,n){if(n!==void 0)return n;let e=t.includes(".")?t.split(".").pop():t;return d(e)}function d(t){let n=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2");return n.charAt(0).toUpperCase()+n.slice(1).toLowerCase()}0&&(module.exports={tt});
|
package/dist/tt.d.cts
ADDED
package/dist/tt.d.ts
ADDED
package/dist/tt.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function s(n,t){if(t!==void 0)return t;let e=n.includes(".")?n.split(".").pop():n;return r(e)}function r(n){let t=n.replace(/([a-z0-9])([A-Z])/g,"$1 $2");return t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()}export{s as tt};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"code": "en-US",
|
|
4
|
+
"native_name": "English (US)",
|
|
5
|
+
"english_name": "English (US)"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"code": "en-GB",
|
|
9
|
+
"native_name": "English (UK)",
|
|
10
|
+
"english_name": "English (UK)"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"code": "es-419",
|
|
14
|
+
"native_name": "Español (Latinoamérica)",
|
|
15
|
+
"english_name": "Spanish (Latin America)"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"code": "es-ES",
|
|
19
|
+
"native_name": "Español (España)",
|
|
20
|
+
"english_name": "Spanish (Spain)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"code": "pt-BR",
|
|
24
|
+
"native_name": "Português (Brasil)",
|
|
25
|
+
"english_name": "Portuguese (Brazil)"
|
|
26
|
+
},
|
|
27
|
+
{ "code": "fr-FR", "native_name": "Français", "english_name": "French" },
|
|
28
|
+
{
|
|
29
|
+
"code": "fr-CA",
|
|
30
|
+
"native_name": "Français (Canada)",
|
|
31
|
+
"english_name": "French (Canada)"
|
|
32
|
+
},
|
|
33
|
+
{ "code": "de-DE", "native_name": "Deutsch", "english_name": "German" },
|
|
34
|
+
{ "code": "it-IT", "native_name": "Italiano", "english_name": "Italian" },
|
|
35
|
+
{ "code": "nl-NL", "native_name": "Nederlands", "english_name": "Dutch" },
|
|
36
|
+
{
|
|
37
|
+
"code": "zh-CN",
|
|
38
|
+
"native_name": "简体中文",
|
|
39
|
+
"english_name": "Chinese (Simplified)"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"code": "zh-TW",
|
|
43
|
+
"native_name": "繁體中文",
|
|
44
|
+
"english_name": "Chinese (Traditional)"
|
|
45
|
+
},
|
|
46
|
+
{ "code": "ja-JP", "native_name": "日本語", "english_name": "Japanese" },
|
|
47
|
+
{ "code": "ko-KR", "native_name": "한국어", "english_name": "Korean" },
|
|
48
|
+
{ "code": "ar-SA", "native_name": "العربية", "english_name": "Arabic" },
|
|
49
|
+
{ "code": "hi-IN", "native_name": "हिन्दी", "english_name": "Hindi" },
|
|
50
|
+
{ "code": "ru-RU", "native_name": "Русский", "english_name": "Russian" },
|
|
51
|
+
{ "code": "tr-TR", "native_name": "Türkçe", "english_name": "Turkish" },
|
|
52
|
+
{
|
|
53
|
+
"code": "id-ID",
|
|
54
|
+
"native_name": "Bahasa Indonesia",
|
|
55
|
+
"english_name": "Indonesian"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"code": "vi-VN",
|
|
59
|
+
"native_name": "Tiếng Việt",
|
|
60
|
+
"english_name": "Vietnamese"
|
|
61
|
+
},
|
|
62
|
+
{ "code": "pl-PL", "native_name": "Polski", "english_name": "Polish" },
|
|
63
|
+
{ "code": "sv-SE", "native_name": "Svenska", "english_name": "Swedish" },
|
|
64
|
+
{ "code": "no-NO", "native_name": "Norsk", "english_name": "Norwegian" },
|
|
65
|
+
{ "code": "da-DK", "native_name": "Dansk", "english_name": "Danish" },
|
|
66
|
+
{ "code": "fi-FI", "native_name": "Suomi", "english_name": "Finnish" },
|
|
67
|
+
{ "code": "th-TH", "native_name": "ไทย", "english_name": "Thai" },
|
|
68
|
+
{ "code": "he-IL", "native_name": "עברית", "english_name": "Hebrew" },
|
|
69
|
+
{ "code": "uk-UA", "native_name": "Українська", "english_name": "Ukrainian" },
|
|
70
|
+
{ "code": "cs-CZ", "native_name": "Čeština", "english_name": "Czech" },
|
|
71
|
+
{ "code": "el-GR", "native_name": "Ελληνικά", "english_name": "Greek" }
|
|
72
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "i18n-boost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Automate i18n in TypeScript/JavaScript projects with tt() markers and automatic translation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"i18n-boost": "bin/i18n-boost.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./tt": {
|
|
16
|
+
"types": "./dist/tt.d.ts",
|
|
17
|
+
"import": "./dist/tt.mjs",
|
|
18
|
+
"require": "./dist/tt.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/server/index.d.ts",
|
|
22
|
+
"import": "./dist/server/index.mjs",
|
|
23
|
+
"require": "./dist/server/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"bin",
|
|
29
|
+
"locales",
|
|
30
|
+
"schema.json"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup && node -e \"require('fs').cpSync('src/locales', 'locales', {recursive:true})\"",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"lint": "tsc --noEmit",
|
|
38
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
39
|
+
"postinstall": "node ./dist/setup/postinstall.cjs"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"cac": "^6.7.14",
|
|
43
|
+
"fast-glob": "^3.3.2",
|
|
44
|
+
"picocolors": "^1.1.1",
|
|
45
|
+
"prompts": "^2.4.2"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@anthropic-ai/sdk": ">=0.30.0",
|
|
49
|
+
"express": ">=4.18.0",
|
|
50
|
+
"fastify": ">=4.26.0",
|
|
51
|
+
"openai": ">=4.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@anthropic-ai/sdk": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"openai": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"express": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"fastify": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@anthropic-ai/sdk": "^0.30.0",
|
|
69
|
+
"@types/express": "^4.17.0",
|
|
70
|
+
"@types/prompts": "^2.4.9",
|
|
71
|
+
"@types/supertest": "^6.0.0",
|
|
72
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
73
|
+
"express": "^4.18.0",
|
|
74
|
+
"fastify": "^4.26.0",
|
|
75
|
+
"openai": "^4.0.0",
|
|
76
|
+
"supertest": "^7.0.0",
|
|
77
|
+
"tsup": "^8.0.0",
|
|
78
|
+
"typescript": "^5.5.0",
|
|
79
|
+
"vitest": "^2.0.0"
|
|
80
|
+
},
|
|
81
|
+
"keywords": [
|
|
82
|
+
"i18n",
|
|
83
|
+
"i18next",
|
|
84
|
+
"next-intl",
|
|
85
|
+
"typescript",
|
|
86
|
+
"translation",
|
|
87
|
+
"automation"
|
|
88
|
+
],
|
|
89
|
+
"license": "MIT"
|
|
90
|
+
}
|
package/schema.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "i18n-boost configuration",
|
|
4
|
+
"description": "Configuration schema for i18n-boost (.i18nrc.json)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "JSON Schema reference"
|
|
10
|
+
},
|
|
11
|
+
"type": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": ["backend", "frontend"],
|
|
14
|
+
"description": "Project type. 'backend' projects only use sourceLocale; 'frontend' projects translate to targetLocales.",
|
|
15
|
+
"default": "frontend"
|
|
16
|
+
},
|
|
17
|
+
"sourceLocale": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Source locale code in BCP-47 format with region (default: en-US)",
|
|
20
|
+
"default": "en-US",
|
|
21
|
+
"enum": [
|
|
22
|
+
"en-US", "en-GB", "es-ES", "es-419", "pt-BR",
|
|
23
|
+
"fr-FR", "fr-CA", "de-DE", "it-IT", "nl-NL",
|
|
24
|
+
"zh-CN", "zh-TW", "ja-JP", "ko-KR", "ar-SA",
|
|
25
|
+
"hi-IN", "ru-RU", "tr-TR", "id-ID", "vi-VN",
|
|
26
|
+
"pl-PL", "sv-SE", "no-NO", "da-DK", "fi-FI",
|
|
27
|
+
"th-TH", "he-IL", "uk-UA", "cs-CZ", "el-GR"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"targetLocales": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"description": "Target locale codes to translate to (BCP-47 with region)",
|
|
33
|
+
"default": [],
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": [
|
|
37
|
+
"en-US", "en-GB", "es-ES", "es-419", "pt-BR",
|
|
38
|
+
"fr-FR", "fr-CA", "de-DE", "it-IT", "nl-NL",
|
|
39
|
+
"zh-CN", "zh-TW", "ja-JP", "ko-KR", "ar-SA",
|
|
40
|
+
"hi-IN", "ru-RU", "tr-TR", "id-ID", "vi-VN",
|
|
41
|
+
"pl-PL", "sv-SE", "no-NO", "da-DK", "fi-FI",
|
|
42
|
+
"th-TH", "he-IL", "uk-UA", "cs-CZ", "el-GR"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"include": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"description": "Glob patterns for source files to scan for tt() markers",
|
|
49
|
+
"default": ["src/**/*.{ts,tsx,js,jsx}"],
|
|
50
|
+
"items": { "type": "string" },
|
|
51
|
+
"minItems": 1
|
|
52
|
+
},
|
|
53
|
+
"exclude": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"description": "Glob patterns to exclude from scanning (takes precedence over include)",
|
|
56
|
+
"default": ["**/*.spec.*", "**/*.test.*", "**/*.d.ts", "**/node_modules/**", "**/.git/**"],
|
|
57
|
+
"items": { "type": "string" }
|
|
58
|
+
},
|
|
59
|
+
"outputDir": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Output directory for locale files (created automatically if absent)",
|
|
62
|
+
"default": "src/locales"
|
|
63
|
+
},
|
|
64
|
+
"outputFormat": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["flat", "nested"],
|
|
67
|
+
"description": "JSON structure format. 'flat': { \"a.b\": \"v\" } | 'nested': { \"a\": { \"b\": \"v\" } }. Only applies to i18next preset.",
|
|
68
|
+
"default": "nested"
|
|
69
|
+
},
|
|
70
|
+
"preset": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": ["i18next", "next-intl"],
|
|
73
|
+
"description": "i18n runtime preset that determines the output format and conventions",
|
|
74
|
+
"default": "i18next"
|
|
75
|
+
},
|
|
76
|
+
"provider": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"description": "Translation provider configuration. API keys are read from environment variables, not stored here.",
|
|
79
|
+
"properties": {
|
|
80
|
+
"name": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["anthropic", "openai", "deepl", "libre-translate", "backend", "none"],
|
|
83
|
+
"description": "Translation provider to use"
|
|
84
|
+
},
|
|
85
|
+
"model": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Model identifier (anthropic and openai providers). Defaults: anthropic → claude-haiku-4-5-20251001, openai → gpt-4o-mini"
|
|
88
|
+
},
|
|
89
|
+
"baseUrl": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Base URL for OpenAI-compatible APIs (DeepSeek, Groq, Qwen, Ollama, etc.)"
|
|
92
|
+
},
|
|
93
|
+
"apiUrl": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"description": "Endpoint URL for libre-translate. Default: https://libretranslate.com/translate"
|
|
96
|
+
},
|
|
97
|
+
"url": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"description": "URL of your backend translation endpoint (required when name is 'backend')"
|
|
100
|
+
},
|
|
101
|
+
"secret": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Shared secret sent as x-i18n-secret header to your backend endpoint"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"required": ["name"],
|
|
107
|
+
"if": { "properties": { "name": { "const": "backend" } }, "required": ["name"] },
|
|
108
|
+
"then": { "required": ["url"] }
|
|
109
|
+
},
|
|
110
|
+
"transform": {
|
|
111
|
+
"type": "object",
|
|
112
|
+
"description": "Transform configuration for replacing tt() calls with t() after generation",
|
|
113
|
+
"properties": {
|
|
114
|
+
"enabled": {
|
|
115
|
+
"type": "boolean",
|
|
116
|
+
"description": "If true, every 'generate' run also transforms tt() → t()",
|
|
117
|
+
"default": false
|
|
118
|
+
},
|
|
119
|
+
"importSource": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "Module from which the t() function is imported after transform",
|
|
122
|
+
"default": "i18next"
|
|
123
|
+
},
|
|
124
|
+
"functionName": {
|
|
125
|
+
"type": "string",
|
|
126
|
+
"description": "Name of the target translation function (almost always 't')",
|
|
127
|
+
"default": "t"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"required": ["sourceLocale"]
|
|
133
|
+
}
|