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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.mjs').catch(err => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-NFSRAD6K.mjs";export{a as AnthropicProvider};
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-Q5SEXPLC.mjs";export{a as BackendProvider};
@@ -0,0 +1 @@
1
+ var o={"en-us":"EN-US","en-gb":"EN-GB","pt-br":"PT-BR","pt-pt":"PT-PT","zh-cn":"ZH-HANS","zh-tw":"ZH-HANT","zh-hans":"ZH-HANS","zh-hant":"ZH-HANT"};function u(n){return n.split("-")[0].toUpperCase()}function T(n){let t=n.toLowerCase();return o[t]?o[t]:n.split("-")[0].toUpperCase()}var i=50;function d(n){return n.replace(/(\{\{-?\s*\w+\s*\}\}|\{[\w.]+\})/g,"<i18n-boost>$1</i18n-boost>")}function y(n){return n.replace(/<i18n-boost>(.*?)<\/i18n-boost>/g,"$1")}var l=class{name="deepl";apiKey;baseUrl;constructor(t){this.apiKey=t,this.baseUrl=t.endsWith(":fx")?"https://api-free.deepl.com/v2/translate":"https://api.deepl.com/v2/translate"}async translate(t){let p=u(t.sourceLocale),g=T(t.targetLocale),a=[];for(let r=0;r<t.texts.length;r+=i){let c=t.texts.slice(r,r+i).map(d),s=await fetch(this.baseUrl,{method:"POST",headers:{Authorization:`DeepL-Auth-Key ${this.apiKey}`,"Content-Type":"application/json"},body:JSON.stringify({text:c,source_lang:p,target_lang:g,tag_handling:"xml",ignore_tags:["i18n-boost"]})});if(!s.ok){let e=await s.text();throw new Error(`DeepL API error ${s.status}: ${e}`)}let h=await s.json();a.push(...h.translations.map(e=>y(e.text)))}return{translations:a}}};export{l as a};
@@ -0,0 +1 @@
1
+ function l(o){return o.split("-")[0].toLowerCase()}var c=class{name="libre-translate";apiUrl;apiKey;constructor(t="https://libretranslate.com/translate",n){let s=t.endsWith("/translate")?t:t.replace(/\/$/,"")+"/translate";this.apiUrl=s,this.apiKey=n}async translate(t){let n=l(t.sourceLocale),s=l(t.targetLocale),r=new Array(t.texts.length);for(let e=0;e<t.texts.length;e+=5){let a=t.texts.slice(e,e+5);(await Promise.all(a.map(i=>this.translateOne(i,n,s)))).forEach((i,d)=>{r[e+d]=i})}return{translations:r}}async translateOne(t,n,s){let r={q:t,source:n,target:s,format:"text"};this.apiKey!==void 0&&(r.api_key=this.apiKey);let e=await fetch(this.apiUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!e.ok)throw new Error(`LibreTranslate error ${e.status}: ${await e.text()}`);let a=await e.json();if(a.translatedText===void 0)throw new Error("LibreTranslate response missing translatedText field");return a.translatedText}};export{c as a};
@@ -0,0 +1 @@
1
+ var t=class{name="none";async translate(n){return{translations:[...n.texts]}}};export{t as a};
@@ -0,0 +1,11 @@
1
+ function u(e,t,n){return`You are a professional translator. Translate the JSON object values from ${e} to ${t}.
2
+
3
+ Rules:
4
+ 1. Keep JSON keys exactly unchanged
5
+ 2. Preserve interpolation markers exactly: {{variable}}, {variable}, {{- variable}}
6
+ 3. Preserve ICU plural syntax exactly: {count, plural, one {# item} other {# items}}
7
+ 4. Preserve HTML tags if present
8
+ 5. Return ONLY the JSON object, no explanations
9
+
10
+ Input:
11
+ ${JSON.stringify(n,null,2)}`}function g(e){let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t)return t[1].trim();let n=e.lastIndexOf("{");if(n!==-1)return e.slice(n);throw new Error("Could not extract JSON from Anthropic response")}var l=class{name="anthropic";apiKey;model;client;constructor(t,n="claude-haiku-4-5-20251001"){this.apiKey=t,this.model=n}async getClient(){if(!this.client){let{Anthropic:t}=await import("@anthropic-ai/sdk");this.client=new t({apiKey:this.apiKey})}return this.client}async translate(t){let n=await this.getClient(),s={};t.texts.forEach((i,r)=>{s[String(r)]=i});let c=u(t.sourceLocale,t.targetLocale,s),a=(await n.messages.create({model:this.model,max_tokens:4096,messages:[{role:"user",content:c}]})).content[0];if(a.type!=="text")throw new Error("Unexpected response type from Anthropic");let p=g(a.text),m=JSON.parse(p);return{translations:t.texts.map((i,r)=>{let o=m[String(r)];if(o===void 0)throw new Error(`Missing translation for index ${r}`);return o})}}};export{l as a};
@@ -0,0 +1 @@
1
+ var l=[1e3,2e3,4e3];function u(s){return new Promise(t=>setTimeout(t,s))}function d(s){let t=s.lastIndexOf("/");return t===-1?s+"/sync":s.slice(0,t)+"/sync"}var c=class{name="backend";url;syncUrl;secret;constructor(t){if(!t.url)throw new Error("provider.url is required when provider.name is 'backend'");this.url=t.url,this.syncUrl=d(t.url),this.secret=t.secret,this.secret&&this.url.startsWith("http://")&&!this.url.startsWith("http://localhost")&&console.warn("[i18n-boost] Secret sent over plain HTTP \u2014 use HTTPS in production")}buildHeaders(){let t={"Content-Type":"application/json"};return this.secret!==void 0&&(t["x-i18n-secret"]=this.secret),t}async postWithRetry(t,n){let e=this.buildHeaders(),i=new Error("Backend failed after 3 attempts");for(let o=0;o<3;o++){let r;try{r=await fetch(t,{method:"POST",headers:e,body:n})}catch{throw new Error(`Cannot connect to backend at ${t}. Make sure the backend is running.`)}if(r.status===401||r.status===403)throw new Error("Invalid i18n secret. Check x-i18n-secret matches the backend config.");if(r.status>=500&&r.status<600){let a=await r.text();i=new Error(`Backend returned ${r.status}: ${a}`),o<2&&await u(l[o]);continue}if(!r.ok){let a=await r.text();throw new Error(`Backend returned ${r.status}: ${a}`)}return await r.json()}throw i}async translate(t){let n=JSON.stringify({texts:t.texts,sourceLocale:t.sourceLocale,targetLocale:t.targetLocale}),e=await this.postWithRetry(this.url,n);if(!Array.isArray(e.translations))throw new Error(`Backend response missing translations array: expected ${t.texts.length} item(s)`);if(e.translations.length!==t.texts.length)throw new Error(`Backend response translations count mismatch: expected ${t.texts.length}, got ${e.translations.length}`);return{translations:e.translations}}async sync(t){let n=JSON.stringify({targetLocales:t.targetLocales,frontendTexts:t.frontendTexts}),e=await this.postWithRetry(this.syncUrl,n);if(typeof e.sourceKeys!="object"||e.sourceKeys===null||Array.isArray(e.sourceKeys))throw new Error("Backend sync response missing sourceKeys object");if(typeof e.locales!="object"||e.locales===null||Array.isArray(e.locales))throw new Error("Backend sync response missing locales object");return{sourceKeys:e.sourceKeys,locales:e.locales}}};export{c as a};
@@ -0,0 +1,11 @@
1
+ function u(n,t,e){return`You are a professional translator. Translate the JSON object values from ${n} to ${t}.
2
+
3
+ Rules:
4
+ 1. Keep JSON keys exactly unchanged
5
+ 2. Preserve interpolation markers exactly: {{variable}}, {variable}, {{- variable}}
6
+ 3. Preserve ICU plural syntax exactly: {count, plural, one {# item} other {# items}}
7
+ 4. Preserve HTML tags if present
8
+ 5. Return ONLY the JSON object, no explanations
9
+
10
+ Input:
11
+ ${JSON.stringify(e,null,2)}`}function g(n){let t=n.match(/```(?:json)?\s*([\s\S]*?)```/);if(t)return t[1].trim();let e=n.lastIndexOf("{");if(e!==-1)return n.slice(e);throw new Error("Could not extract JSON from OpenAI response")}var l=class{name="openai";apiKey;model;baseUrl;client;constructor(t,e="gpt-4o-mini",r){this.apiKey=t,this.model=e,this.baseUrl=r}async getClient(){if(!this.client){let{OpenAI:t}=await import("openai");this.client=new t({apiKey:this.apiKey,...this.baseUrl?{baseURL:this.baseUrl}:{}})}return this.client}async translate(t){let e=await this.getClient(),r={};t.texts.forEach((a,s)=>{r[String(s)]=a});let c=u(t.sourceLocale,t.targetLocale,r),i=(await e.chat.completions.create({model:this.model,messages:[{role:"user",content:c}],max_tokens:4096})).choices[0]?.message?.content;if(!i)throw new Error("Empty response from OpenAI-compatible API");let p=g(i),m=JSON.parse(p);return{translations:t.texts.map((a,s)=>{let o=m[String(s)];if(o===void 0)throw new Error(`Missing translation for index ${s}`);return o})}}};export{l as a};
package/dist/cli.mjs ADDED
@@ -0,0 +1,12 @@
1
+ import"./chunk-BCR6DFAS.mjs";import"./chunk-Q5SEXPLC.mjs";import"./chunk-NFSRAD6K.mjs";import"./chunk-XH7NJHX6.mjs";import"./chunk-3V2WXULC.mjs";import"./chunk-4GJXND3H.mjs";import{cac as Ce}from"cac";import X from"picocolors";import*as O from"fs";import*as F from"path";import{createRequire as ce}from"module";import{pathToFileURL as le}from"url";import de from"picocolors";var h={type:"frontend",sourceLocale:"en-US",targetLocales:[],include:["src/**/*.{ts,tsx,js,jsx}"],exclude:["**/*.spec.*","**/*.test.*","**/*.d.ts","**/node_modules/**","**/.git/**"],outputDir:"src/locales",preset:"i18next",provider:{name:"none"},transform:{enabled:!1,importSource:"i18next",functionName:"t"}};function ue(){try{let e=new URL("../locales/languages.json",import.meta.url),t=O.readFileSync(e,"utf-8");return JSON.parse(t).map(r=>r.code)}catch(e){throw new Error(`i18n-boost: failed to load supported locales from languages.json. The package may be installed incorrectly. (${String(e)})`)}}var z=ue();function I(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Z(e,t){let n={...e};for(let r of Object.keys(t)){let o=t[r],s=e[r];I(o)&&I(s)?n[r]=Z(s,o):o!==void 0&&(n[r]=o)}return n}function fe(e){let t=[".i18nrc.json",".i18nrc.js",".i18nrc.cjs"];for(let r of t){let o=F.join(e,r);if(O.existsSync(o))return o}let n=F.join(e,"package.json");if(O.existsSync(n))try{let r=JSON.parse(O.readFileSync(n,"utf-8"));if(Object.prototype.hasOwnProperty.call(r,"i18n-boost"))return n}catch{}return null}async function M(e){let t=fe(e);if(t===null)return h;let n;t.endsWith(".json")?F.basename(t)==="package.json"?n=JSON.parse(O.readFileSync(t,"utf-8"))["i18n-boost"]:n=JSON.parse(O.readFileSync(t,"utf-8")):n=ce(le(t).href)(t);let r=Z(h,n??{});return pe(r)}function pe(e){if(!I(e))throw new Error("Config must be a plain object.");let t=e.type??h.type;if(t!=="backend"&&t!=="frontend")throw new Error(`Invalid type '${String(t)}'. Must be 'backend' or 'frontend'.`);let n=e.sourceLocale??h.sourceLocale;if(typeof n!="string"||n.trim()==="")throw new Error("sourceLocale must be a non-empty string.");if(!z.includes(n))throw new Error(`Invalid sourceLocale '${n}'. Must be a BCP-47 code from the supported list (e.g. "en-US", "es-ES").`);let r=e.targetLocales??h.targetLocales;if(!Array.isArray(r))throw new Error("targetLocales must be an array of BCP-47 locale codes.");for(let T of r)if(typeof T!="string"||!z.includes(T))throw new Error(`Invalid targetLocale '${String(T)}'. Must be a BCP-47 code from the supported list.`);if(r.includes(n))throw new Error(`sourceLocale '${n}' cannot be in targetLocales.`);let o=e.include??h.include;if(!Array.isArray(o)||o.length===0)throw new Error("include must be a non-empty array of glob strings.");for(let T of o)if(typeof T!="string")throw new Error("Each entry in include must be a string.");let s=e.exclude??h.exclude;if(!Array.isArray(s))throw new Error("exclude must be an array of glob strings.");for(let T of s)if(typeof T!="string")throw new Error("Each entry in exclude must be a string.");let d=e.outputDir??h.outputDir;if(typeof d!="string")throw new Error("outputDir must be a non-empty string.");let c=d.trim();if(!c)throw new Error("outputDir must be a non-empty string.");let w=e.preset??h.preset;if(w!=="i18next"&&w!=="next-intl")throw new Error(`preset must be "i18next" or "next-intl". Got: '${String(w)}'.`);let a=e.provider??h.provider;if(!I(a))throw new Error('provider must be an object with at least a "name" field.');let y=["anthropic","openai","deepl","libre-translate","backend","none"],u=a.name;if(!y.includes(u))throw new Error(`provider.name must be one of: ${y.map(T=>`"${T}"`).join(", ")}. Got: '${String(u)}'.`);if(u==="backend"&&!a.url)throw new Error("provider.url is required when provider.name is 'backend'.");let v={name:u,...a.model!==void 0?{model:String(a.model)}:{},...a.baseUrl!==void 0?{baseUrl:String(a.baseUrl)}:{},...a.apiUrl!==void 0?{apiUrl:String(a.apiUrl)}:{},...a.url!==void 0?{url:String(a.url)}:{},...a.secret!==void 0?{secret:String(a.secret)}:{}},x=e.transform??h.transform;if(!I(x))throw new Error("transform must be an object.");let j=x.enabled??h.transform.enabled;if(typeof j!="boolean")throw new Error(`transform.enabled must be a boolean. Got: '${String(j)}'.`);let C={enabled:j,importSource:typeof x.importSource=="string"?x.importSource:h.transform.importSource,functionName:typeof x.functionName=="string"?x.functionName:h.transform.functionName};return t==="backend"&&r.length>0&&console.warn(de.yellow("Backend projects only use sourceLocale. targetLocales will be ignored.")),{type:t,sourceLocale:n,targetLocales:r,include:o,exclude:s,outputDir:c,preset:w,provider:v,transform:C}}import ge from"fs/promises";import me from"fast-glob";import ye from"picocolors";function Q(e){let t=e.replace(/([a-z0-9])([A-Z])/g,"$1 $2");return t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()}function J(e){let t=5381;for(let n=0;n<e.length;n++)t=(t<<5)+t^e.charCodeAt(n),t=t>>>0;return t.toString(16).padStart(8,"0")}function W(e,t="",n="."){let r={};for(let[o,s]of Object.entries(e)){let d=t?`${t}${n}${o}`:o;if(s!==null&&typeof s=="object"&&!Array.isArray(s)){let c=W(s,d,n);Object.assign(r,c)}else typeof s=="string"&&(r[d]=s)}return r}function we(){return/\btt\(\s*(['"`])((?:(?!\1)[^\\]|\\.)*)?\1(?:\s*,\s*(['"`])((?:(?!\3)[^\\]|\\.)*)\3)?\s*\)/g}function he(e){let t="",n=0;for(;n<e.length;){if(e[n]==='"'||e[n]==="'"||e[n]==="`"){let r=e[n];for(t+=e[n],n++;n<e.length;)if(e[n]==="\\")t+=e[n],n++,n<e.length&&(t+=e[n],n++);else if(e[n]===r){t+=e[n],n++;break}else t+=e[n],n++;continue}if(e[n]==="/"&&e[n+1]==="/"){for(t+=" ",n+=2;n<e.length&&e[n]!==`
2
+ `;)t+=" ",n++;continue}if(e[n]==="/"&&e[n+1]==="*"){for(t+=" ",n+=2;n<e.length;){if(e[n]==="*"&&e[n+1]==="/"){t+=" ",n+=2;break}e[n]===`
3
+ `?t+=`
4
+ `:t+=" ",n++}continue}t+=e[n],n++}return t}function xe(e,t){let n=0;for(let r=0;r<e.length;r++){if(n+e[r].length>=t)return{line:r+1,column:t-n+1};n+=e[r].length+1}return{line:e.length,column:1}}function be(e,t){if(t!==void 0)return t;let n=e.includes(".")?e.split(".").pop():e;return Q(n)}async function ke(e){let t=await ge.readFile(e,"utf-8"),n=he(t),r=n.split(`
5
+ `),o=[],s=we(),d;for(;(d=s.exec(n))!==null;){let c=d[2];if(!c||c.includes("${"))continue;let w=d[3]!==void 0?d[4]:void 0,a=be(c,w),y=d.index,{line:u,column:v}=xe(r,y);o.push({key:c,defaultText:w,generatedText:a,filePath:e,line:u,column:v})}return o}async function B(e){let t=await me(e.include,{ignore:e.exclude,cwd:e.cwd,absolute:!0}),n=new Map;for(let r of t){let o=await ke(r);for(let s of o){if(n.has(s.key)){n.get(s.key).generatedText!==s.generatedText&&console.warn(ye.yellow(`Warning: duplicate key "${s.key}" with different text. Using first occurrence.`));continue}n.set(s.key,s)}}return Array.from(n.values())}async function ee(e){let t=e.cwd??process.cwd();try{let n=await M(t),r=await B({include:n.include,exclude:n.exclude,cwd:t});for(let o of r)console.log(` ${X.cyan(o.key)}`),console.log(` Text: "${o.generatedText}"`),console.log(` File: ${X.dim(o.filePath+":"+o.line)}`);console.log(`Found ${r.length} translation keys`)}catch(n){let r=n instanceof Error?n.message:String(n);console.error(`Error: ${r}`),process.exit(1)}}import je from"picocolors";import $ from"path";import Y from"fs/promises";function Te(){return{version:1,entries:{}}}async function te(e){try{let t=await Y.readFile(e,"utf-8");return JSON.parse(t)}catch(t){if(t!=null&&typeof t=="object"&&"code"in t&&t.code==="ENOENT")return null;throw t}}async function U(e,t){await Y.mkdir($.dirname(e),{recursive:!0}),await Y.writeFile(e,JSON.stringify(t,null,2),"utf-8")}async function q(e,t){let n=$.join(e,`${t}.json`),r=await te(n);return r===null?{}:W(r)}async function ne(e){let{config:t,scannedEntries:n,provider:r,dryRun:o=!1}=e,{sourceLocale:s,targetLocales:d,outputDir:c}=t,w=$.join(c,".i18n-meta.json"),a=await te(w)??Te();a.entries||(a.entries={});let y=new Map;for(let i of n)y.set(i.key,i);let u=new Set(y.keys()),v=await q(c,s),x=[],j=[],C=[],T=[];for(let i of Object.keys(a.entries))u.has(i)||T.push(i);let G=new Set;for(let[i,b]of y){let p=J(b.generatedText),g=a.entries[i];g?g.sourceHash!==p?(j.push(i),G.add(i)):C.push(i):(x.push(i),G.add(i))}let L={};for(let[i,b]of y){let p=a.entries[i],g=v[i],E=p?.lastAutoValues?.[s];g!==void 0&&E!==void 0&&g!==E?L[i]=g:L[i]=b.generatedText}let S={};for(let[i,b]of Object.entries(a.entries))u.has(i)&&(S[i]={...b});let ie=new Date().toISOString();for(let[i,b]of y){let p=J(b.generatedText),g=S[i];S[i]={key:i,sourceHash:p,autoGenerated:!0,lastModified:ie,lastAutoValues:{...g?.lastAutoValues??{},[s]:b.generatedText}}}let N=[];if(o||(await U($.join(c,`${s}.json`),L),N.push(s)),t.type!=="backend")if(typeof r.sync=="function"){let i=await r.sync({targetLocales:d,frontendTexts:L}),b=i.sourceKeys;for(let p of d){let g=i.locales[p]??{},E=await q(c,p),l={};for(let[f,m]of Object.entries(g))b[f]!==void 0&&(l[f]=m);for(let f of u){let k=a.entries[f]?.lastAutoValues?.[p],R=E[f];R!==void 0&&k!==void 0&&R!==k?l[f]=R:g[f]!==void 0?l[f]=g[f]:R!==void 0&&(l[f]=R)}for(let f of u)if(g[f]!==void 0){let m=E[f],R=a.entries[f]?.lastAutoValues?.[p];m!==void 0&&R!==void 0&&m!==R||(S[f]={...S[f],lastAutoValues:{...S[f].lastAutoValues,[p]:g[f]}})}o||(await U($.join(c,`${p}.json`),l),N.push(p))}}else for(let i of d){let b=await q(c,i),p=[];for(let l of u){let m=a.entries[l]?.lastAutoValues?.[i],k=b[l];k!==void 0&&m!==void 0&&k!==m||(G.has(l)||k===void 0)&&p.push(l)}let g={};if(p.length>0){let l=p.map(m=>L[m]),f=await r.translate({texts:l,sourceLocale:s,targetLocale:i});for(let m=0;m<p.length;m++)g[p[m]]=f.translations[m]}let E={};for(let l of u){let m=a.entries[l]?.lastAutoValues?.[i],k=b[l];k!==void 0&&m!==void 0&&k!==m?E[l]=k:g[l]!==void 0?E[l]=g[l]:k!==void 0&&(E[l]=k)}for(let l of u)g[l]!==void 0&&(S[l]={...S[l],lastAutoValues:{...S[l].lastAutoValues,[i]:g[l]}});o||(await U($.join(c,`${i}.json`),E),N.push(i))}return o||await U(w,{version:1,entries:S}),{added:x,updated:j,removed:T,skipped:C,localesWritten:N,dryRun:o}}function H(){let e=process.env.I18NBOOST_KEY;if(!e){let t=["Missing environment variable: I18NBOOST_KEY",""," Set it before running generate:"," export I18NBOOST_KEY=<your-api-key>"," npx i18n-boost generate --translate",""," Run `i18n-boost --help` to see all available providers."];throw new Error(t.join(`
6
+ `))}return e}async function re(e){switch(e.name){case"none":{let{NoneProvider:t}=await import("./none-JRFHRYLI.mjs");return new t}case"backend":{let{BackendProvider:t}=await import("./backend-F4QW23OL.mjs");return new t(e)}case"anthropic":{let t=H(),{AnthropicProvider:n}=await import("./anthropic-6WICO575.mjs");return new n(t,e.model)}case"openai":{let t=H(),{OpenAIProvider:n}=await import("./openai-O4RCRFGT.mjs");return new n(t,e.model,e.baseUrl)}case"deepl":{let t=H(),{DeepLProvider:n}=await import("./deepl-N327FF46.mjs");return new n(t)}case"libre-translate":{let t=process.env.I18NBOOST_KEY,{LibreTranslateProvider:n}=await import("./libre-translate-NQGYY2HV.mjs");return new n(e.apiUrl,t)}default:throw new Error(`Unknown provider: ${e.name}`)}}import{readFile as Ee,writeFile as Re}from"fs/promises";function ve(){return/(\/\/[^\r\n]*|\/\*[\s\S]*?\*\/|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|\btt\s*\(\s*'((?:[^'\\]|\\.)*?)'\s*(?:,\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`))?\s*\)|\btt\s*\(\s*"((?:[^"\\]|\\.)*?)"\s*(?:,\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`))?\s*\)|\btt\s*\(\s*`((?:[^`\\]|\\.)*?)`\s*(?:,\s*(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`))?\s*\)/g}function Se(){return/^import\s*\{([^}]*)\}\s*from\s*(['"])(i18n-boost(?:\/tt)?)\2\s*;?[ \t]*(\r?\n)?/gm}var P=class{async transformFile(t,n){let r=n.config.transform.functionName,o=t.includes(`\r
7
+ `)?`\r
8
+ `:`
9
+ `,s=0,d=t.replace(ve(),(a,y,u,v,x)=>y!==void 0?a:(s++,u!==void 0?`${r}('${u}')`:v!==void 0?`${r}("${v}")`:r+"(`"+x+"`)"));if(s===0)return{source:t,changed:!1,replacements:0};let c=d.replace(Se(),(a,y,u,v,x="")=>{let j=y.split(",").map(C=>C.trim()).filter(C=>C!==""&&C!=="tt");return j.length===0?"":`import { ${j.join(", ")} } from ${u}${v}${u};${x}`});return{source:this.importPresent(c)?c:this.importLine(n,o)+c,changed:!0,replacements:s}}};var D=class extends P{importPresent(t){return/from\s*['"]i18next['"]/.test(t)}importLine(t,n){return`import { ${t.config.transform.functionName} } from 'i18next';${n}`}};var V=class extends P{importPresent(t){return/from\s*['"]next-intl\/server['"]/.test(t)}importLine(t,n){return`import { getTranslations } from 'next-intl/server';${n}`}};var K=class extends P{importPresent(t){return/from\s*['"]react-i18next['"]/.test(t)}importLine(t,n){return`import { useTranslation } from 'react-i18next';${n}`}};var _=class extends P{importPresent(t){return/from\s*['"]next-intl['"]/.test(t)}importLine(t,n){return`import { useTranslations } from 'next-intl';${n}`}};function Pe(e){switch(e.transform.importSource){case"react-i18next":return new K;case"next-intl/server":return new V;case"next-intl":return new _;default:return new D}}async function oe(e){let{config:t,filePaths:n}=e,r=Pe(t),o=0,s=0;for(let d of n){let c=await Ee(d,"utf-8"),w=await r.transformFile(c,{config:t,filePath:d});w.changed&&(await Re(d,w.source,"utf-8"),o++,s+=w.replacements)}return{filesChanged:o,totalReplacements:s}}async function se(e){let t=e.cwd??process.cwd(),n=e.dryRun??!1;try{let r=await M(t),o=await B({include:r.include,exclude:r.exclude,cwd:t});console.log(je.cyan(`Found ${o.length} keys`));let s=e.translate===!1?{name:"none"}:r.provider,d=await re(s),c=await ne({config:r,scannedEntries:o,provider:d,dryRun:n,cwd:t});if(console.log(`Added: ${c.added.length} keys`),console.log(`Updated: ${c.updated.length} keys`),console.log(`Removed: ${c.removed.length} keys`),console.log(`Skipped: ${c.skipped.length} keys (unchanged)`),c.dryRun?console.log("Dry run \u2014 no files written"):console.log(`Wrote locales: ${c.localesWritten.join(", ")}`),e.transform||r.transform.enabled){let a=o.map(u=>u.filePath),y=await oe({config:r,filePaths:a,cwd:t});console.log(`Transform: ${y.filesChanged} files changed, ${y.totalReplacements} replacements`)}}catch(r){let o=r instanceof Error?r.message:String(r);console.error(`Error: ${o}`),process.exit(1)}}var A=Ce("i18n-boost");A.version("0.1.0");A.help(e=>{e.push({title:"Translation providers",body:[" deepl DeepL API"," Requires: I18NBOOST_KEY=<api-key>",""," openai OpenAI-compatible API \u2014 works with OpenAI, DeepSeek,"," Groq, Ollama and any OpenAI-compatible endpoint"," Requires: I18NBOOST_KEY=<api-key>"," Optional: set provider.baseUrl for custom endpoints",""," anthropic Anthropic Claude models"," Requires: I18NBOOST_KEY=<api-key>",""," libre-translate Self-hosted open-source translation engine"," Requires: provider.apiUrl in .i18nrc.json"," Optional: I18NBOOST_KEY=<api-key> if auth is enabled",""," backend Frontend delegates translation to your own backend"," (no API keys on the frontend)"," Requires: provider.url pointing to your /sync endpoint",""," none No translation \u2014 only extracts and writes source locale"].join(`
10
+ `)}),e.push({title:"Backend project setup",body:[" 1. Run `i18n-boost init` and choose Back-End"," 2. Add createI18nRouter() or createI18nPlugin() to your server"," 3. Run `i18n-boost generate` to extract all tt() keys to src/locales/en-US.json"," 4. Run `i18n-boost generate --translate` to also translate to target locales"].join(`
11
+ `)}),e.push({title:"Frontend project setup",body:[" 1. Run `i18n-boost init` and choose Front-End"," 2. Choose a translation strategy:"," Via backend \u2014 your backend does translation (API keys stay server-side)"," Direct \u2014 API key in env var, runs at build time or in CI/CD",' 3. Mark strings with tt(): const label = tt("ui.submit", "Submit")'," 4. Run `i18n-boost generate --translate` to extract and translate"].join(`
12
+ `)})});A.command("init","Interactive wizard \u2014 creates .i18nrc.json for your project").option("--cwd <dir>","Working directory",{default:process.cwd()}).action(async e=>{let{runInit:t}=await import("./init-EPCKGU3C.mjs");await t({cwd:e.cwd})});A.command("setup","Alias for `init`").option("--cwd <dir>","Working directory",{default:process.cwd()}).action(async e=>{let{runInit:t}=await import("./init-EPCKGU3C.mjs");await t({cwd:e.cwd})});A.command("generate","Extract tt() keys, write source locale JSON, optionally translate to target locales").option("--translate","Call the configured translation provider to translate new/changed strings").option("--no-translate","Skip translation \u2014 only write the source locale JSON file (default)").option("--dry-run","Preview what would change without writing any files").option("--transform","After generating, rewrite tt() calls to your i18n library syntax (t(), useTranslation, etc.)").option("--cwd <dir>","Working directory",{default:process.cwd()}).action(async e=>{await se({dryRun:e.dryRun,translate:e.translate,transform:e.transform,cwd:e.cwd})});A.command("scan","List all tt() keys found in source files \u2014 no files written").option("--cwd <dir>","Working directory",{default:process.cwd()}).action(async e=>{await ee({cwd:e.cwd})});A.parse();
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-3V2WXULC.mjs";export{a as DeepLProvider};
package/dist/index.cjs ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ tt: () => tt
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/runtime/tt.ts
28
+ function tt(key, defaultText) {
29
+ if (defaultText !== void 0) return defaultText;
30
+ const segment = key.includes(".") ? key.split(".").pop() : key;
31
+ return camelCaseToSentence(segment);
32
+ }
33
+ function camelCaseToSentence(str) {
34
+ const spaced = str.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
35
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
36
+ }
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ tt
40
+ });
@@ -0,0 +1,28 @@
1
+ declare function tt(key: string, defaultText?: string): string;
2
+
3
+ interface I18nBoostConfig {
4
+ type: 'backend' | 'frontend';
5
+ sourceLocale: string;
6
+ targetLocales: string[];
7
+ include: string[];
8
+ exclude: string[];
9
+ outputDir: string;
10
+ preset: 'i18next' | 'next-intl';
11
+ provider: ProviderConfig;
12
+ transform: TransformConfig;
13
+ }
14
+ interface ProviderConfig {
15
+ name: 'anthropic' | 'openai' | 'deepl' | 'libre-translate' | 'backend' | 'none';
16
+ model?: string;
17
+ baseUrl?: string;
18
+ apiUrl?: string;
19
+ url?: string;
20
+ secret?: string;
21
+ }
22
+ interface TransformConfig {
23
+ enabled: boolean;
24
+ importSource: string;
25
+ functionName: string;
26
+ }
27
+
28
+ export { type I18nBoostConfig, type ProviderConfig, type TransformConfig, tt };
@@ -0,0 +1,28 @@
1
+ declare function tt(key: string, defaultText?: string): string;
2
+
3
+ interface I18nBoostConfig {
4
+ type: 'backend' | 'frontend';
5
+ sourceLocale: string;
6
+ targetLocales: string[];
7
+ include: string[];
8
+ exclude: string[];
9
+ outputDir: string;
10
+ preset: 'i18next' | 'next-intl';
11
+ provider: ProviderConfig;
12
+ transform: TransformConfig;
13
+ }
14
+ interface ProviderConfig {
15
+ name: 'anthropic' | 'openai' | 'deepl' | 'libre-translate' | 'backend' | 'none';
16
+ model?: string;
17
+ baseUrl?: string;
18
+ apiUrl?: string;
19
+ url?: string;
20
+ secret?: string;
21
+ }
22
+ interface TransformConfig {
23
+ enabled: boolean;
24
+ importSource: string;
25
+ functionName: string;
26
+ }
27
+
28
+ export { type I18nBoostConfig, type ProviderConfig, type TransformConfig, tt };
package/dist/index.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // src/runtime/tt.ts
2
+ function tt(key, defaultText) {
3
+ if (defaultText !== void 0) return defaultText;
4
+ const segment = key.includes(".") ? key.split(".").pop() : key;
5
+ return camelCaseToSentence(segment);
6
+ }
7
+ function camelCaseToSentence(str) {
8
+ const spaced = str.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
9
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
10
+ }
11
+ export {
12
+ tt
13
+ };
@@ -0,0 +1,33 @@
1
+ import t from"picocolors";import{readFile as E,writeFile as f}from"fs/promises";import m from"path";import o from"prompts";var y=[{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 _={anthropic:"claude-haiku-4-5-20251001",openai:"gpt-4o-mini"};async function w(a){try{let e=await E(m.join(a,"package.json"),"utf-8");return JSON.parse(e)}catch{return{}}}function k(a){let e=a.scripts;return!e||typeof e!="object"?!1:"i18gen"in e}async function B(a){let{cwd:e}=a,{projectType:i}=await o({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(!i){console.log(t.yellow("Setup cancelled."));return}i==="backend"?await R(e):await O(e)}async function R(a){let{outputDir:e}=await o({type:"text",name:"outputDir",message:"Output directory:",initial:"src/locales"});if(e===void 0){console.log(t.yellow("Setup cancelled."));return}let{provider:i}=await o({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(t.yellow("Setup cancelled."));return}b(i);let c={name:i};if(i==="anthropic"||i==="openai"){let{model:r}=await o({type:"text",name:"model",message:`Preferred model (${i}):`,initial:_[i]??""});c.model=r}if(i==="libre-translate"){let{apiUrl:r}=await o({type:"text",name:"apiUrl",message:"libre-translate API URL:",initial:"http://localhost:5000"});c.apiUrl=r}let d=await w(a),u=!1;k(d)||(u=(await o({type:"confirm",name:"addScript",message:'Add "i18gen": "i18n-boost generate" to package.json?',initial:!0})).addScript??!1);let{confirm:g}=await o({type:"confirm",name:"confirm",message:"Write .i18nrc.json?",initial:!0});if(!g){console.log(t.yellow("Aborted. No files written."));return}let s={type:"backend",outputDir:e,provider:c},p=m.join(a,".i18nrc.json");if(await f(p,JSON.stringify(s,null,2),"utf-8"),console.log(t.green(`\u2713 Written ${p}`)),u){let r=d.scripts??{};r.i18gen="i18n-boost generate",d.scripts=r;let h=m.join(a,"package.json");await f(h,JSON.stringify(d,null,2),"utf-8"),console.log(t.green('\u2713 Added "i18gen" script to package.json'))}I(i),C(i,c.model)}function C(a,e){let i=`{
2
+ provider: '${a}',
3
+ secret: process.env.I18N_SECRET,
4
+ localesDir: './src/locales',
5
+ sourceLocale: 'en-US',
6
+ }`;console.log(`
7
+ `+t.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(${i}));`),console.log(`
9
+ `+t.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(${i}), { prefix: '/api/i18n' });`),console.log(`
11
+ `+t.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 O(a){let e=y.map(n=>({title:`${n.code} (${n.english_name})`,value:n.code})),{preset:i}=await o({type:"select",name:"preset",message:"Preset:",choices:[{title:"i18next",value:"i18next"},{title:"next-intl",value:"next-intl"}]});if(i===void 0){console.log(t.yellow("Setup cancelled."));return}let{sourceLocale:c}=await o({type:"select",name:"sourceLocale",message:"Source locale:",choices:e,initial:e.findIndex(n=>n.value==="en-US")});if(c===void 0){console.log(t.yellow("Setup cancelled."));return}let{targetLocales:d}=await o({type:"multiselect",name:"targetLocales",message:"Target locales:",choices:e.filter(n=>n.value!==c),min:1});if(!d||d.length===0){console.log(t.yellow("Setup cancelled."));return}let{outputDir:u}=await o({type:"text",name:"outputDir",message:"Output directory:",initial:"src/locales"}),{strategy:g}=await o({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(g===void 0){console.log(t.yellow("Setup cancelled."));return}let s;if(g==="backend"){let{backendUrl:n}=await o({type:"text",name:"backendUrl",message:"Backend URL:",initial:"http://localhost:3001/api/i18n/sync"}),{secret:l}=await o({type:"text",name:"secret",message:"Secret (blank for none):",initial:""});s={name:"backend",url:n},l&&l.trim()!==""&&(s.secret=l)}else{let{provider:n}=await o({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(n===void 0){console.log(t.yellow("Setup cancelled."));return}if(b(n),s={name:n},n==="anthropic"||n==="openai"){let{model:l}=await o({type:"text",name:"model",message:`Preferred model (${n}):`,initial:_[n]??""});s.model=l}if(n==="libre-translate"){let{apiUrl:l}=await o({type:"text",name:"apiUrl",message:"libre-translate API URL:",initial:"http://localhost:5000"});s.apiUrl=l}}let p=await w(a),r=!1;k(p)||(r=(await o({type:"confirm",name:"addScript",message:'Add "i18gen": "i18n-boost generate" to package.json?',initial:!0})).addScript??!1);let{confirm:h}=await o({type:"confirm",name:"confirm",message:"Write .i18nrc.json?",initial:!0});if(!h){console.log(t.yellow("Aborted. No files written."));return}let P={type:"frontend",preset:i,sourceLocale:c,targetLocales:d,outputDir:u,provider:s},v=m.join(a,".i18nrc.json");if(await f(v,JSON.stringify(P,null,2),"utf-8"),console.log(t.green(`\u2713 Written ${v}`)),r){let n=p.scripts??{};n.i18gen="i18n-boost generate",p.scripts=n;let l=m.join(a,"package.json");await f(l,JSON.stringify(p,null,2),"utf-8"),console.log(t.green('\u2713 Added "i18gen" script to package.json'))}g==="direct"&&I(s.name),L()}var S={anthropic:"Anthropic",openai:"OpenAI",deepl:"DeepL","libre-translate":"LibreTranslate"};function b(a){let e=S[a];e&&console.log(t.dim(` \u2192 Obtain an API key from ${e} and set ${t.bold("I18NBOOST_KEY=<your-api-key>")}`))}function I(a){let e=S[a];e&&(console.log(`
28
+ `+t.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 L(){console.log(`
29
+ `+t.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!');`)}export{B as runInit};
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-4GJXND3H.mjs";export{a as LibreTranslateProvider};
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-BCR6DFAS.mjs";export{a as NoneProvider};
@@ -0,0 +1 @@
1
+ import{a}from"./chunk-XH7NJHX6.mjs";export{a as OpenAIProvider};
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server/index.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ I18nHandlerError: () => I18nHandlerError,
34
+ createI18nPlugin: () => createI18nPlugin,
35
+ createI18nRouter: () => createI18nRouter
36
+ });
37
+ module.exports = __toCommonJS(server_exports);
38
+
39
+ // src/server/express.ts
40
+ var import_express = __toESM(require("express"), 1);
41
+
42
+ // src/server/handler.ts
43
+ var import_node_crypto = require("crypto");
44
+ var import_promises = require("fs/promises");
45
+ var import_node_path = __toESM(require("path"), 1);
46
+
47
+ // src/core/key-utils.ts
48
+ function flattenObject(obj, prefix = "", separator = ".") {
49
+ const result = {};
50
+ for (const [key, value] of Object.entries(obj)) {
51
+ const fullKey = prefix ? `${prefix}${separator}${key}` : key;
52
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
53
+ const nested = flattenObject(
54
+ value,
55
+ fullKey,
56
+ separator
57
+ );
58
+ Object.assign(result, nested);
59
+ } else if (typeof value === "string") {
60
+ result[fullKey] = value;
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+
66
+ // src/server/handler.ts
67
+ var MAX_TARGET_LOCALES = 30;
68
+ var MAX_FRONTEND_KEYS = 5e3;
69
+ var LOCALE_PATTERN = /^[a-z]{2,3}(-[A-Z][a-zA-Z]{1,7})?$/;
70
+ var warnedNoSecret = false;
71
+ function safeCompare(a, b) {
72
+ const ha = (0, import_node_crypto.createHash)("sha256").update(a).digest();
73
+ const hb = (0, import_node_crypto.createHash)("sha256").update(b).digest();
74
+ return (0, import_node_crypto.timingSafeEqual)(ha, hb);
75
+ }
76
+ function isValidBody(body) {
77
+ if (typeof body !== "object" || body === null) return false;
78
+ const b = body;
79
+ if (!Array.isArray(b.targetLocales)) return false;
80
+ if (!b.targetLocales.every((l) => typeof l === "string" && LOCALE_PATTERN.test(l))) return false;
81
+ if (b.frontendTexts !== void 0) {
82
+ if (typeof b.frontendTexts !== "object" || b.frontendTexts === null) return false;
83
+ if (Array.isArray(b.frontendTexts)) return false;
84
+ const ft = b.frontendTexts;
85
+ if (!Object.values(ft).every((v) => typeof v === "string")) return false;
86
+ }
87
+ return true;
88
+ }
89
+ var I18nHandlerError = class extends Error {
90
+ constructor(statusCode, message) {
91
+ super(message);
92
+ this.statusCode = statusCode;
93
+ this.name = "I18nHandlerError";
94
+ }
95
+ statusCode;
96
+ };
97
+ async function readFlatJson(filePath) {
98
+ try {
99
+ const raw = await (0, import_promises.readFile)(filePath, "utf-8");
100
+ const parsed = JSON.parse(raw);
101
+ return flattenObject(parsed);
102
+ } catch {
103
+ return {};
104
+ }
105
+ }
106
+ async function handleSync(body, options) {
107
+ if (options.secret !== void 0) {
108
+ if (options.requestSecret === void 0 || !safeCompare(options.requestSecret, options.secret)) {
109
+ throw new I18nHandlerError(401, "Unauthorized");
110
+ }
111
+ } else if (!warnedNoSecret) {
112
+ warnedNoSecret = true;
113
+ console.warn("[i18n-boost] No secret configured \u2014 /sync endpoint is unauthenticated");
114
+ }
115
+ if (!isValidBody(body)) {
116
+ throw new I18nHandlerError(400, "Invalid request body");
117
+ }
118
+ const { targetLocales, frontendTexts = {} } = body;
119
+ if (targetLocales.length > MAX_TARGET_LOCALES) {
120
+ throw new I18nHandlerError(400, `Too many targetLocales (max ${MAX_TARGET_LOCALES})`);
121
+ }
122
+ if (Object.keys(frontendTexts).length > MAX_FRONTEND_KEYS) {
123
+ throw new I18nHandlerError(400, `Too many frontendTexts keys (max ${MAX_FRONTEND_KEYS})`);
124
+ }
125
+ const sourceLocalePath = import_node_path.default.join(options.localesDir, `${options.sourceLocale}.json`);
126
+ const backendKeys = await readFlatJson(sourceLocalePath);
127
+ const collisions = Object.keys(frontendTexts).filter((k) => backendKeys[k] !== void 0);
128
+ if (collisions.length > 0) {
129
+ console.warn(
130
+ `[i18n-boost] Key collision(s) \u2014 backend wins: ${collisions.join(", ")}`
131
+ );
132
+ }
133
+ const allTexts = { ...frontendTexts, ...backendKeys };
134
+ const allKeys = Object.keys(allTexts);
135
+ const allValues = allKeys.map((k) => allTexts[k]);
136
+ const locales = {};
137
+ for (const targetLocale of targetLocales) {
138
+ try {
139
+ const result = await options.provider.translate({
140
+ texts: allValues,
141
+ sourceLocale: options.sourceLocale,
142
+ targetLocale
143
+ });
144
+ const translated = {};
145
+ for (let i = 0; i < allKeys.length; i++) {
146
+ translated[allKeys[i]] = result.translations[i];
147
+ }
148
+ locales[targetLocale] = translated;
149
+ } catch (err) {
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ throw new I18nHandlerError(500, `Translation failed for ${targetLocale}: ${message}`);
152
+ }
153
+ }
154
+ return { sourceKeys: backendKeys, locales };
155
+ }
156
+
157
+ // src/server/express.ts
158
+ var BLOCKED_ENVS = ["production", "staging"];
159
+ function createI18nRouter(options) {
160
+ const env = process.env["NODE_ENV"];
161
+ if (BLOCKED_ENVS.includes(env ?? "")) {
162
+ throw new Error(
163
+ `[i18n-boost] createI18nRouter() is a development-only tool and must not run in '${env}'. Remove it from your production server setup.`
164
+ );
165
+ }
166
+ const router = (0, import_express.Router)();
167
+ router.use(import_express.default.json({ limit: "1mb" }));
168
+ router.post("/sync", async (req, res) => {
169
+ try {
170
+ const rawSecret = req.headers["x-i18n-secret"];
171
+ const requestSecret = Array.isArray(rawSecret) ? rawSecret[0] : rawSecret;
172
+ const result = await handleSync(req.body, {
173
+ ...options,
174
+ requestSecret
175
+ });
176
+ res.json(result);
177
+ } catch (err) {
178
+ if (err instanceof I18nHandlerError) {
179
+ res.status(err.statusCode).json({ error: err.message });
180
+ } else {
181
+ res.status(500).json({ error: "Internal server error" });
182
+ }
183
+ }
184
+ });
185
+ return router;
186
+ }
187
+
188
+ // src/server/fastify.ts
189
+ var BLOCKED_ENVS2 = ["production", "staging"];
190
+ function createI18nPlugin(options) {
191
+ const env = process.env["NODE_ENV"];
192
+ if (BLOCKED_ENVS2.includes(env ?? "")) {
193
+ throw new Error(
194
+ `[i18n-boost] createI18nPlugin() is a development-only tool and must not run in '${env}'. Remove it from your production server setup.`
195
+ );
196
+ }
197
+ return async function plugin(fastify) {
198
+ fastify.post("/sync", { bodyLimit: 1048576 }, async (request, reply) => {
199
+ try {
200
+ const result = await handleSync(request.body, {
201
+ ...options,
202
+ requestSecret: request.headers["x-i18n-secret"]
203
+ });
204
+ return result;
205
+ } catch (err) {
206
+ if (err instanceof I18nHandlerError) {
207
+ return reply.status(err.statusCode).send({ error: err.message });
208
+ }
209
+ return reply.status(500).send({ error: "Internal server error" });
210
+ }
211
+ });
212
+ };
213
+ }
214
+ // Annotate the CommonJS export names for ESM import in node:
215
+ 0 && (module.exports = {
216
+ I18nHandlerError,
217
+ createI18nPlugin,
218
+ createI18nRouter
219
+ });
@@ -0,0 +1,50 @@
1
+ import { Router } from 'express';
2
+ import { FastifyInstance } from 'fastify';
3
+
4
+ interface TranslationRequest {
5
+ texts: string[];
6
+ sourceLocale: string;
7
+ targetLocale: string;
8
+ }
9
+ interface TranslationResult {
10
+ translations: string[];
11
+ }
12
+ interface SyncRequest {
13
+ targetLocales: string[];
14
+ frontendTexts: Record<string, string>;
15
+ }
16
+ interface SyncResult {
17
+ sourceKeys: Record<string, string>;
18
+ locales: Record<string, Record<string, string>>;
19
+ }
20
+ interface TranslationProvider {
21
+ name: string;
22
+ translate(request: TranslationRequest): Promise<TranslationResult>;
23
+ sync?(request: SyncRequest): Promise<SyncResult>;
24
+ }
25
+
26
+ interface I18nHandlerOptions {
27
+ provider: TranslationProvider;
28
+ secret?: string;
29
+ localesDir: string;
30
+ sourceLocale: string;
31
+ }
32
+ interface BackendSyncRequest {
33
+ targetLocales: string[];
34
+ frontendTexts?: Record<string, string>;
35
+ }
36
+ interface BackendSyncResponse {
37
+ sourceKeys: Record<string, string>;
38
+ locales: Record<string, Record<string, string>>;
39
+ }
40
+
41
+ declare function createI18nRouter(options: I18nHandlerOptions): Router;
42
+
43
+ declare function createI18nPlugin(options: I18nHandlerOptions): (fastify: FastifyInstance) => Promise<void>;
44
+
45
+ declare class I18nHandlerError extends Error {
46
+ readonly statusCode: number;
47
+ constructor(statusCode: number, message: string);
48
+ }
49
+
50
+ export { type BackendSyncRequest, type BackendSyncResponse, I18nHandlerError, type I18nHandlerOptions, createI18nPlugin, createI18nRouter };
@@ -0,0 +1,50 @@
1
+ import { Router } from 'express';
2
+ import { FastifyInstance } from 'fastify';
3
+
4
+ interface TranslationRequest {
5
+ texts: string[];
6
+ sourceLocale: string;
7
+ targetLocale: string;
8
+ }
9
+ interface TranslationResult {
10
+ translations: string[];
11
+ }
12
+ interface SyncRequest {
13
+ targetLocales: string[];
14
+ frontendTexts: Record<string, string>;
15
+ }
16
+ interface SyncResult {
17
+ sourceKeys: Record<string, string>;
18
+ locales: Record<string, Record<string, string>>;
19
+ }
20
+ interface TranslationProvider {
21
+ name: string;
22
+ translate(request: TranslationRequest): Promise<TranslationResult>;
23
+ sync?(request: SyncRequest): Promise<SyncResult>;
24
+ }
25
+
26
+ interface I18nHandlerOptions {
27
+ provider: TranslationProvider;
28
+ secret?: string;
29
+ localesDir: string;
30
+ sourceLocale: string;
31
+ }
32
+ interface BackendSyncRequest {
33
+ targetLocales: string[];
34
+ frontendTexts?: Record<string, string>;
35
+ }
36
+ interface BackendSyncResponse {
37
+ sourceKeys: Record<string, string>;
38
+ locales: Record<string, Record<string, string>>;
39
+ }
40
+
41
+ declare function createI18nRouter(options: I18nHandlerOptions): Router;
42
+
43
+ declare function createI18nPlugin(options: I18nHandlerOptions): (fastify: FastifyInstance) => Promise<void>;
44
+
45
+ declare class I18nHandlerError extends Error {
46
+ readonly statusCode: number;
47
+ constructor(statusCode: number, message: string);
48
+ }
49
+
50
+ export { type BackendSyncRequest, type BackendSyncResponse, I18nHandlerError, type I18nHandlerOptions, createI18nPlugin, createI18nRouter };