@fluenti/cli 0.6.2 → 0.6.3

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,18 @@
1
+ import { ExtractedMessage } from '@fluenti/core/compiler';
2
+ interface PathApi {
3
+ isAbsolute(filePath: string): boolean;
4
+ normalize(filePath: string): string;
5
+ relative(from: string, to: string): string;
6
+ resolve(...paths: string[]): string;
7
+ }
8
+ export interface ExtractFilePaths {
9
+ absoluteFile: string;
10
+ displayFile: string;
11
+ }
12
+ export declare function toForwardSlash(filePath: string): string;
13
+ export declare function resolveExtractFilePaths(cwd: string, filePath: string, pathApi?: PathApi): ExtractFilePaths;
14
+ export declare function normalizeMessageOrigins(messages: readonly ExtractedMessage[], displayFile: string): {
15
+ messages: ExtractedMessage[];
16
+ changed: boolean;
17
+ };
18
+ export {};
@@ -0,0 +1,17 @@
1
+ import { UpdateResult } from './catalog';
2
+ import { FluentiBuildConfig } from '@fluenti/core/compiler';
3
+ export interface ExtractWorkflowOptions {
4
+ clean?: boolean;
5
+ stripFuzzy?: boolean;
6
+ useCache?: boolean;
7
+ }
8
+ export interface ExtractWorkflowResult {
9
+ fileCount: number;
10
+ messageCount: number;
11
+ cacheHits: number;
12
+ localeResults: Array<{
13
+ locale: string;
14
+ result: UpdateResult;
15
+ }>;
16
+ }
17
+ export declare function runExtractWorkflow(cwd: string, config: FluentiBuildConfig, options?: ExtractWorkflowOptions): Promise<ExtractWorkflowResult>;
package/dist/cli.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- const e=require(`./doctor-xp8WS8sr.cjs`),t=require(`./tsx-extractor-AOjsbOmp.cjs`),n=require(`./compile-C3VLvhUf.cjs`);let r=require(`@fluenti/core/compiler`),i=require(`node:fs`),a=require(`node:path`),o=require(`node:url`),s=require(`fast-glob`);s=e.m(s);let c=require(`node:crypto`),l=require(`consola`);l=e.m(l);let u=require(`citty`),d=require(`node:child_process`),f=require(`node:util`),p=require(`node:timers/promises`),m=require(`@fluenti/core/config`);var h=`█`,g=`░`;function _(e,t=20){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return h.repeat(n)+g.repeat(t-n)}function v(e){let t=e.toFixed(1)+`%`;return e>=90?`\x1b[32m${t}\x1b[0m`:e>=70?`\x1b[33m${t}\x1b[0m`:`\x1b[31m${t}\x1b[0m`}function y(e,t,n){let r=t>0?n/t*100:0,i=t>0?v(r):`—`,a=t>0?_(r):``;return` ${e.padEnd(8)}│ ${String(t).padStart(5)} │ ${String(n).padStart(10)} │ ${a} ${i}`}var b=/\{(\w+),\s*(plural|select|selectordinal)\s*,/;function x(e){try{let t=(0,r.parse)(e),n=new Set;return S(t,n),[...n].sort()}catch{let t=new Set,n=0,r=0;for(;r<e.length;){if(e[r]===`{`){if(n++,n===1){let n=e.indexOf(`}`,r+1);if(n!==-1){let i=e.slice(r+1,n).trim(),a=/^(\w+)/.exec(i);a&&t.add(a[1])}}}else e[r]===`}`&&n--;r++}return[...t].sort()}}function S(e,t){for(let n of e)if(n.type===`variable`&&n.name!==`#`)t.add(n.name);else if(n.type===`plural`||n.type===`select`){t.add(n.variable);for(let e of Object.values(n.options))S(e,t)}else n.type===`function`&&t.add(n.variable)}function C(e){let t=new Set,n=/<\/?([a-zA-Z][\w-]*)[^>]*>/g,r;for(;(r=n.exec(e))!==null;)t.add(r[1].toLowerCase());return[...t].sort()}function ee(e,t){let n=x(e),i=x(t),a=C(e),o=C(t),s=n.filter(e=>!i.includes(e)),c=i.filter(e=>!n.includes(e)),l=a.filter(e=>!o.includes(e)),u=o.filter(e=>!a.includes(e)),d=[];if(b.test(t))try{(0,r.parse)(t)}catch(e){d.push(e.message)}return{valid:s.length===0&&c.length===0&&l.length===0&&u.length===0&&d.length===0,missingPlaceholders:s,extraPlaceholders:c,missingHtmlTags:l,extraHtmlTags:u,syntaxErrors:d}}function w(e,t){let n=[],{sourceLocale:r}=t,i=t.locales??Object.keys(e),a=e[r];if(!a)return n.push({rule:`missing-source`,severity:`error`,message:`Source locale catalog "${r}" not found`}),n;let o=Object.entries(a).filter(([,e])=>!e.obsolete).map(([e])=>e);for(let t of i){if(t===r)continue;let i=e[t];if(!i){n.push({rule:`missing-locale`,severity:`error`,message:`Catalog for locale "${t}" not found`,locale:t});continue}for(let e of o){let r=a[e],o=i[e];if(!o||!o.translation||o.translation.length===0){n.push({rule:`missing-translation`,severity:`error`,message:`Missing translation for "${e}" in locale "${t}"`,messageId:e,locale:t});continue}let s=x(r.message??e),c=x(o.translation),l=s.filter(e=>!c.includes(e)),u=c.filter(e=>!s.includes(e));l.length>0&&n.push({rule:`inconsistent-placeholders`,severity:`error`,message:`Translation for "${e}" in "${t}" is missing placeholders: ${l.map(e=>`{${e}}`).join(`, `)}`,messageId:e,locale:t}),u.length>0&&n.push({rule:`inconsistent-placeholders`,severity:`warning`,message:`Translation for "${e}" in "${t}" has extra placeholders: ${u.map(e=>`{${e}}`).join(`, `)}`,messageId:e,locale:t}),o.fuzzy&&n.push({rule:`fuzzy-translation`,severity:`warning`,message:`Translation for "${e}" in "${t}" is marked as fuzzy`,messageId:e,locale:t})}for(let[e,r]of Object.entries(i))r.obsolete||(!a[e]||a[e].obsolete)&&n.push({rule:`orphan-translation`,severity:`warning`,message:`Translation "${e}" in "${t}" has no corresponding source message`,messageId:e,locale:t})}let s=new Map;for(let[e,t]of Object.entries(a)){if(t.obsolete)continue;let n=t.message??e,r=s.get(n);r?r.push(e):s.set(n,[e])}for(let[e,t]of s)t.length>1&&n.push({rule:`duplicate-message`,severity:`info`,message:`Duplicate source message "${e.slice(0,60)}${e.length>60?`...`:``}" used by ${t.length} entries: ${t.join(`, `)}`,locale:r});return n}function T(e){if(e.length===0)return` ✓ All checks passed`;let t=[],n=E(e,e=>e.rule);for(let[e,r]of Object.entries(n)){t.push(` ${e} (${r.length}):`);for(let e of r){let n=e.severity===`error`?`✗`:e.severity===`warning`?`⚠`:`ℹ`;t.push(` ${n} ${e.message}`)}}let r=e.filter(e=>e.severity===`error`).length,i=e.filter(e=>e.severity===`warning`).length,a=e.filter(e=>e.severity===`info`).length;return t.push(``),t.push(` Summary: ${r} errors, ${i} warnings, ${a} info`),t.join(`
3
- `)}function E(e,t){let n={};for(let r of e){let e=t(r);(n[e]??(n[e]=[])).push(r)}return n}function D(e,t){let{sourceLocale:n,minCoverage:r,locale:i}=t,a=e[n];if(!a)return{results:[],passed:!1,minCoverage:r,actualCoverage:0,diagnostics:[{rule:`missing-source`,severity:`error`,message:`Source locale catalog "${n}" not found`}]};let o=Object.entries(a).filter(([,e])=>!e.obsolete).map(([e])=>e),s=o.length,c=i?[i]:Object.keys(e).filter(e=>e!==n),l=[];for(let t of c){let n=e[t];if(!n){l.push({locale:t,total:s,translated:0,missing:s,fuzzy:0,coverage:0});continue}let r=0,i=0,a=0;for(let e of o){let t=n[e];!t||!t.translation||t.translation.length===0?i++:(r++,t.fuzzy&&a++)}let c=s>0?r/s*100:100;l.push({locale:t,total:s,translated:r,missing:i,fuzzy:a,coverage:c})}let u=l.reduce((e,t)=>e+t.translated,0),d=l.reduce((e,t)=>e+t.total,0),f=d>0?u/d*100:100,p=l.every(e=>e.coverage>=r),m={sourceLocale:n};return i&&(m.locales=[n,i]),{results:l,passed:p,minCoverage:r,actualCoverage:f,diagnostics:w(e,m)}}function te(e){let t=[];for(let n of e.results){let r=n.coverage>=e.minCoverage?`✓`:`✗`,i=n.coverage.toFixed(1),a=n.missing>0?` — ${n.missing} missing`:``,o=n.fuzzy>0?`, ${n.fuzzy} fuzzy`:``;t.push(`${r} ${n.locale}: ${i}% (${n.translated}/${n.total})${a}${o}`)}t.push(``);let n=e.actualCoverage.toFixed(1),r=e.passed?`PASSED`:`FAILED`;return t.push(`Coverage: ${n}% (min: ${e.minCoverage}%) — ${r}`),t.join(`
4
- `)}function O(e,t,n){let r=[],i=n===`json`?`.json`:`.po`;for(let n of e.results)if(n.coverage<e.minCoverage){let a=`${t}/${n.locale}${i}`;r.push(`::error file=${a}::Translation coverage ${n.coverage.toFixed(1)}% below minimum ${e.minCoverage}%`)}let a=e.diagnostics.filter(e=>e.rule===`missing-translation`);for(let e of a)if(e.locale){let n=`${t}/${e.locale}${i}`;r.push(`::warning file=${n}::${e.message}`)}return r.join(`
5
- `)}function k(e){return JSON.stringify({results:e.results,passed:e.passed,minCoverage:e.minCoverage,actualCoverage:Math.round(e.actualCoverage*10)/10},null,2)}var A=`1`,j=class{data;cachePath;dirty=!1;constructor(e,t){this.cachePath=(0,a.resolve)(t?(0,a.resolve)(e,`.cache`,t):(0,a.resolve)(e,`.cache`),`compile-cache.json`),this.data=this.load()}isUpToDate(e,t){let n=this.data.entries[e];if(!n)return!1;let r=M(t);return n.inputHash===r}set(e,t){this.data.entries[e]={inputHash:M(t)},this.dirty=!0}save(){if(this.dirty)try{(0,i.mkdirSync)((0,a.dirname)(this.cachePath),{recursive:!0}),(0,i.writeFileSync)(this.cachePath,JSON.stringify(this.data),`utf-8`),this.dirty=!1}catch{}}load(){try{if((0,i.existsSync)(this.cachePath)){let e=(0,i.readFileSync)(this.cachePath,`utf-8`),t=JSON.parse(e);if(t.version===A)return t}}catch{}return{version:A,entries:{}}}};function M(e){return(0,c.createHash)(`md5`).update(e).digest(`hex`)}var ne=(0,f.promisify)(d.execFile),re={claude:`npm install -g @anthropic-ai/claude-code`,codex:`npm install -g @openai/codex`};function ie(e,t){return e===`claude`?[`-p`,t]:[`-p`,t,`--full-auto`]}function ae(e){return e.code===`ENOENT`}async function N(e){let{provider:t,prompt:n,maxRetries:r=3,initialDelayMs:i=1e3,maxBuffer:a=10*1024*1024,timeoutMs:o=12e4}=e,s=ie(t,n),c=t===`claude`?`claude`:`codex`,l;for(let e=0;e<=r;e++)try{let t=ne(c,[...s],{maxBuffer:a}),{stdout:n}=await Promise.race([t,(0,p.setTimeout)(o).then(()=>{throw Error(`AI provider timed out after ${o}ms`)})]);return{stdout:n,attempts:e+1}}catch(n){if(ae(n))throw Error(`"${t}" CLI not found. Please install it first:\n ${re[t]}`);if(n instanceof Error&&n.message.includes(`timed out`))throw n;l=n,e<r&&await(0,p.setTimeout)(i*2**e)}throw l}var P=1048576;function F(e){if(!(0,i.existsSync)(e))return{};let t=(0,i.statSync)(e).size;if(t>P)throw Error(`Glossary file exceeds maximum size of ${P} bytes (got ${t} bytes)`);let n;try{let t=(0,i.readFileSync)(e,`utf-8`);n=JSON.parse(t)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Invalid glossary format: failed to parse JSON — ${t}`)}if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`Invalid glossary format: root must be a plain object`);for(let[e,t]of Object.entries(n)){if(typeof t!=`object`||!t||Array.isArray(t))throw Error(`Invalid glossary format: value for "${e}" must be a plain object`);for(let[n,r]of Object.entries(t))if(typeof r!=`string`)throw Error(`Invalid glossary format: "${e}.${n}" must be a string, got ${typeof r}`)}return n}function I(e,t){let n={};for(let[r,i]of Object.entries(e))t in i&&(n[r]=i[t]);return n}function L(e){return e.replace(/\\/g,`\\\\`).replace(/"/g,`\\"`).replace(/[\n\r]/g,` `).replace(/\t/g,` `)}function R(e){let t=Object.entries(e);return t.length===0?``:`=== GLOSSARY (use these exact translations) ===\n${[...t].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`"${L(e)}" → "${L(t)}"`).join(`
2
+ const e=require(`./doctor-BjxAnm7z.cjs`),t=require(`./compile-1ie6pZBL.cjs`);let n=require(`@fluenti/core/compiler`),r=require(`node:fs`),i=require(`node:path`),a=require(`node:url`),o=require(`fast-glob`);o=e.m(o);let s=require(`node:crypto`),c=require(`consola`);c=e.m(c);let l=require(`citty`),u=require(`node:child_process`),d=require(`node:util`),f=require(`node:timers/promises`),p=require(`@fluenti/core/config`);var m=`█`,h=`░`;function g(e,t=20){let n=Math.round(Math.max(0,Math.min(100,e))/100*t);return m.repeat(n)+h.repeat(t-n)}function _(e){let t=e.toFixed(1)+`%`;return e>=90?`\x1b[32m${t}\x1b[0m`:e>=70?`\x1b[33m${t}\x1b[0m`:`\x1b[31m${t}\x1b[0m`}function v(e,t,n){let r=t>0?n/t*100:0,i=t>0?_(r):`—`,a=t>0?g(r):``;return` ${e.padEnd(8)}│ ${String(t).padStart(5)} │ ${String(n).padStart(10)} │ ${a} ${i}`}var y=/\{(\w+),\s*(plural|select|selectordinal)\s*,/;function b(e){try{let t=(0,n.parse)(e),r=new Set;return x(t,r),[...r].sort()}catch{let t=new Set,n=0,r=0;for(;r<e.length;){if(e[r]===`{`){if(n++,n===1){let n=e.indexOf(`}`,r+1);if(n!==-1){let i=e.slice(r+1,n).trim(),a=/^(\w+)/.exec(i);a&&t.add(a[1])}}}else e[r]===`}`&&n--;r++}return[...t].sort()}}function x(e,t){for(let n of e)if(n.type===`variable`&&n.name!==`#`)t.add(n.name);else if(n.type===`plural`||n.type===`select`){t.add(n.variable);for(let e of Object.values(n.options))x(e,t)}else n.type===`function`&&t.add(n.variable)}function S(e){let t=new Set,n=/<\/?([a-zA-Z][\w-]*)[^>]*>/g,r;for(;(r=n.exec(e))!==null;)t.add(r[1].toLowerCase());return[...t].sort()}function ee(e,t){let r=b(e),i=b(t),a=S(e),o=S(t),s=r.filter(e=>!i.includes(e)),c=i.filter(e=>!r.includes(e)),l=a.filter(e=>!o.includes(e)),u=o.filter(e=>!a.includes(e)),d=[];if(y.test(t))try{(0,n.parse)(t)}catch(e){d.push(e.message)}return{valid:s.length===0&&c.length===0&&l.length===0&&u.length===0&&d.length===0,missingPlaceholders:s,extraPlaceholders:c,missingHtmlTags:l,extraHtmlTags:u,syntaxErrors:d}}function C(e,t){let n=[],{sourceLocale:r}=t,i=t.locales??Object.keys(e),a=e[r];if(!a)return n.push({rule:`missing-source`,severity:`error`,message:`Source locale catalog "${r}" not found`}),n;let o=Object.entries(a).filter(([,e])=>!e.obsolete).map(([e])=>e);for(let t of i){if(t===r)continue;let i=e[t];if(!i){n.push({rule:`missing-locale`,severity:`error`,message:`Catalog for locale "${t}" not found`,locale:t});continue}for(let e of o){let r=a[e],o=i[e];if(!o||!o.translation||o.translation.length===0){n.push({rule:`missing-translation`,severity:`error`,message:`Missing translation for "${e}" in locale "${t}"`,messageId:e,locale:t});continue}let s=b(r.message??e),c=b(o.translation),l=s.filter(e=>!c.includes(e)),u=c.filter(e=>!s.includes(e));l.length>0&&n.push({rule:`inconsistent-placeholders`,severity:`error`,message:`Translation for "${e}" in "${t}" is missing placeholders: ${l.map(e=>`{${e}}`).join(`, `)}`,messageId:e,locale:t}),u.length>0&&n.push({rule:`inconsistent-placeholders`,severity:`warning`,message:`Translation for "${e}" in "${t}" has extra placeholders: ${u.map(e=>`{${e}}`).join(`, `)}`,messageId:e,locale:t}),o.fuzzy&&n.push({rule:`fuzzy-translation`,severity:`warning`,message:`Translation for "${e}" in "${t}" is marked as fuzzy`,messageId:e,locale:t})}for(let[e,r]of Object.entries(i))r.obsolete||(!a[e]||a[e].obsolete)&&n.push({rule:`orphan-translation`,severity:`warning`,message:`Translation "${e}" in "${t}" has no corresponding source message`,messageId:e,locale:t})}let s=new Map;for(let[e,t]of Object.entries(a)){if(t.obsolete)continue;let n=t.message??e,r=s.get(n);r?r.push(e):s.set(n,[e])}for(let[e,t]of s)t.length>1&&n.push({rule:`duplicate-message`,severity:`info`,message:`Duplicate source message "${e.slice(0,60)}${e.length>60?`...`:``}" used by ${t.length} entries: ${t.join(`, `)}`,locale:r});return n}function te(e){if(e.length===0)return` ✓ All checks passed`;let t=[],n=w(e,e=>e.rule);for(let[e,r]of Object.entries(n)){t.push(` ${e} (${r.length}):`);for(let e of r){let n=e.severity===`error`?`✗`:e.severity===`warning`?`⚠`:`ℹ`;t.push(` ${n} ${e.message}`)}}let r=e.filter(e=>e.severity===`error`).length,i=e.filter(e=>e.severity===`warning`).length,a=e.filter(e=>e.severity===`info`).length;return t.push(``),t.push(` Summary: ${r} errors, ${i} warnings, ${a} info`),t.join(`
3
+ `)}function w(e,t){let n={};for(let r of e){let e=t(r);(n[e]??(n[e]=[])).push(r)}return n}function T(e,t){let{sourceLocale:n,minCoverage:r,locale:i}=t,a=e[n];if(!a)return{results:[],passed:!1,minCoverage:r,actualCoverage:0,diagnostics:[{rule:`missing-source`,severity:`error`,message:`Source locale catalog "${n}" not found`}]};let o=Object.entries(a).filter(([,e])=>!e.obsolete).map(([e])=>e),s=o.length,c=i?[i]:Object.keys(e).filter(e=>e!==n),l=[];for(let t of c){let n=e[t];if(!n){l.push({locale:t,total:s,translated:0,missing:s,fuzzy:0,coverage:0});continue}let r=0,i=0,a=0;for(let e of o){let t=n[e];!t||!t.translation||t.translation.length===0?i++:(r++,t.fuzzy&&a++)}let c=s>0?r/s*100:100;l.push({locale:t,total:s,translated:r,missing:i,fuzzy:a,coverage:c})}let u=l.reduce((e,t)=>e+t.translated,0),d=l.reduce((e,t)=>e+t.total,0),f=d>0?u/d*100:100,p=l.every(e=>e.coverage>=r),m={sourceLocale:n};return i&&(m.locales=[n,i]),{results:l,passed:p,minCoverage:r,actualCoverage:f,diagnostics:C(e,m)}}function E(e){let t=[];for(let n of e.results){let r=n.coverage>=e.minCoverage?`✓`:`✗`,i=n.coverage.toFixed(1),a=n.missing>0?` — ${n.missing} missing`:``,o=n.fuzzy>0?`, ${n.fuzzy} fuzzy`:``;t.push(`${r} ${n.locale}: ${i}% (${n.translated}/${n.total})${a}${o}`)}t.push(``);let n=e.actualCoverage.toFixed(1),r=e.passed?`PASSED`:`FAILED`;return t.push(`Coverage: ${n}% (min: ${e.minCoverage}%) — ${r}`),t.join(`
4
+ `)}function D(e,t,n){let r=[],i=n===`json`?`.json`:`.po`;for(let n of e.results)if(n.coverage<e.minCoverage){let a=`${t}/${n.locale}${i}`;r.push(`::error file=${a}::Translation coverage ${n.coverage.toFixed(1)}% below minimum ${e.minCoverage}%`)}let a=e.diagnostics.filter(e=>e.rule===`missing-translation`);for(let e of a)if(e.locale){let n=`${t}/${e.locale}${i}`;r.push(`::warning file=${n}::${e.message}`)}return r.join(`
5
+ `)}function O(e){return JSON.stringify({results:e.results,passed:e.passed,minCoverage:e.minCoverage,actualCoverage:Math.round(e.actualCoverage*10)/10},null,2)}var k=`1`,ne=class{data;cachePath;dirty=!1;constructor(e,t){this.cachePath=(0,i.resolve)(t?(0,i.resolve)(e,`.cache`,t):(0,i.resolve)(e,`.cache`),`compile-cache.json`),this.data=this.load()}isUpToDate(e,t){let n=this.data.entries[e];if(!n)return!1;let r=A(t);return n.inputHash===r}set(e,t){this.data.entries[e]={inputHash:A(t)},this.dirty=!0}save(){if(this.dirty)try{(0,r.mkdirSync)((0,i.dirname)(this.cachePath),{recursive:!0}),(0,r.writeFileSync)(this.cachePath,JSON.stringify(this.data),`utf-8`),this.dirty=!1}catch{}}load(){try{if((0,r.existsSync)(this.cachePath)){let e=(0,r.readFileSync)(this.cachePath,`utf-8`),t=JSON.parse(e);if(t.version===k)return t}}catch{}return{version:k,entries:{}}}};function A(e){return(0,s.createHash)(`md5`).update(e).digest(`hex`)}var re=(0,d.promisify)(u.execFile),ie={claude:`npm install -g @anthropic-ai/claude-code`,codex:`npm install -g @openai/codex`};function j(e,t){return e===`claude`?[`-p`,t]:[`-p`,t,`--full-auto`]}function M(e){return e.code===`ENOENT`}async function N(e){let{provider:t,prompt:n,maxRetries:r=3,initialDelayMs:i=1e3,maxBuffer:a=10*1024*1024,timeoutMs:o=12e4}=e,s=j(t,n),c=t===`claude`?`claude`:`codex`,l;for(let e=0;e<=r;e++)try{let t=re(c,[...s],{maxBuffer:a}),{stdout:n}=await Promise.race([t,(0,f.setTimeout)(o).then(()=>{throw Error(`AI provider timed out after ${o}ms`)})]);return{stdout:n,attempts:e+1}}catch(n){if(M(n))throw Error(`"${t}" CLI not found. Please install it first:\n ${ie[t]}`);if(n instanceof Error&&n.message.includes(`timed out`))throw n;l=n,e<r&&await(0,f.setTimeout)(i*2**e)}throw l}var P=1048576;function F(e){if(!(0,r.existsSync)(e))return{};let t=(0,r.statSync)(e).size;if(t>P)throw Error(`Glossary file exceeds maximum size of ${P} bytes (got ${t} bytes)`);let n;try{let t=(0,r.readFileSync)(e,`utf-8`);n=JSON.parse(t)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Invalid glossary format: failed to parse JSON — ${t}`)}if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`Invalid glossary format: root must be a plain object`);for(let[e,t]of Object.entries(n)){if(typeof t!=`object`||!t||Array.isArray(t))throw Error(`Invalid glossary format: value for "${e}" must be a plain object`);for(let[n,r]of Object.entries(t))if(typeof r!=`string`)throw Error(`Invalid glossary format: "${e}.${n}" must be a string, got ${typeof r}`)}return n}function I(e,t){let n={};for(let[r,i]of Object.entries(e))t in i&&(n[r]=i[t]);return n}function L(e){return e.replace(/\\/g,`\\\\`).replace(/"/g,`\\"`).replace(/[\n\r]/g,` `).replace(/\t/g,` `)}function R(e){let t=Object.entries(e);return t.length===0?``:`=== GLOSSARY (use these exact translations) ===\n${[...t].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`"${L(e)}" → "${L(t)}"`).join(`
6
6
  `)}`}function z(e){let{sourceLocale:t,targetLocale:n,messages:r,glossary:i,context:a}=e,o=JSON.stringify(r,null,2),s=[`You are a professional translator. Translate the following messages from "${t}" to "${n}".`,``,`Rules:`,`1. Output ONLY a valid JSON object with the same keys and translated values.`,`2. Keep ICU MessageFormat placeholders unchanged: {name}, {count, plural, ...}, {val, number}, etc.`,`3. Keep HTML tags unchanged: <b>, <a href="...">, </span>, <br/>, etc.`,`4. Keep numbered rich-text tags unchanged: <0>, </0>, <1/>, etc.`,`5. Do not add any explanation, markdown formatting, or code fences — output raw JSON only.`];if(i&&Object.keys(i).length>0){let e=R(i);e&&s.push(``,e)}return a&&s.push(``,`=== PROJECT CONTEXT ===`,a),s.push(``,`Input (JSON):`,o),s.join(`
7
- `)}function B(e){let t=e.indexOf(`{`);if(t===-1)throw Error(`No JSON object found in AI response`);let n=0,r=!1,i=!1;for(let a=t;a<e.length;a++){let o=e[a];if(i){i=!1;continue}if(o===`\\`&&r){i=!0;continue}if(o===`"`&&!i){r=!r;continue}if(!r&&(o===`{`&&n++,o===`}`&&(n--,n===0)))return e.slice(t,a+1)}throw Error(`Unterminated JSON object in AI response`)}function V(e){let t=e.match(/```(?:json)?\s*\n?([\s\S]*?)```/);return t?t[1]:e}function H(e,t){let n=[],r=B(V(e)),i;try{i=JSON.parse(r)}catch{throw Error(`Failed to parse JSON from AI response: ${r.slice(0,200)}`)}if(typeof i!=`object`||!i||Array.isArray(i))throw Error(`AI response is not a valid JSON object`);let a=i,o={},s=new Set(Object.keys(t));for(let[e,r]of Object.entries(a)){if(!s.has(e)){n.push(`Extra key in AI response (ignored): "${e}"`);continue}if(typeof r!=`string`){n.push(`Non-string value for key "${e}" (ignored)`);continue}o[e]=r;let i=t[e],a=ee(i,r);if(!a.valid){let t=[];a.missingPlaceholders.length>0&&t.push(`missing placeholders: ${a.missingPlaceholders.join(`, `)}`),a.extraPlaceholders.length>0&&t.push(`extra placeholders: ${a.extraPlaceholders.join(`, `)}`),a.missingHtmlTags.length>0&&t.push(`missing HTML tags: ${a.missingHtmlTags.join(`, `)}`),a.extraHtmlTags.length>0&&t.push(`extra HTML tags: ${a.extraHtmlTags.join(`, `)}`),a.syntaxErrors.length>0&&t.push(`ICU syntax errors: ${a.syntaxErrors.join(`; `)}`),n.push(`QA issue for "${e}": ${t.join(`; `)}`)}}for(let e of s)e in o||n.push(`Missing translation for key: "${e}"`);return{translations:o,warnings:n}}function U(e){let t={};for(let[n,r]of Object.entries(e))r.obsolete||(!r.translation||r.translation.length===0)&&(t[n]=r.message??n);return t}function W(e,t){let n=Object.keys(e),r=[];for(let i=0;i<n.length;i+=t){let a={};for(let r of n.slice(i,i+t))a[r]=e[r];r.push(a)}return r}async function G(e){let{provider:t,sourceLocale:n,targetLocale:r,catalog:i,batchSize:a,context:o,glossary:s,timeoutMs:c}=e,u=U(i),d=Object.keys(u).length;if(d===0)return{catalog:{...i},translated:0,warnings:[]};l.default.info(` ${d} untranslated messages, translating with ${t}...`);let f={...i},p=W(u,a),m=0,h=[];for(let e=0;e<p.length;e++){let i=p[e],a=Object.keys(i);p.length>1&&l.default.info(` Batch ${e+1}/${p.length} (${a.length} messages)`);try{let{stdout:e}=await N({provider:t,prompt:z({sourceLocale:n,targetLocale:r,messages:i,glossary:s,context:o}),timeoutMs:c}),{translations:u,warnings:d}=H(e,i);for(let e of d)h.push(`[${r}] ${e}`),l.default.warn(` ${e}`);for(let e of a)e in u&&(f[e]={...f[e],translation:u[e]},m++)}catch(t){let n=t instanceof Error?t.message:String(t);h.push(`[${r}] Batch ${e+1} failed: ${n}`),l.default.error(` Batch ${e+1} failed: ${n}`)}}return{catalog:f,translated:m,warnings:h}}var K=(0,f.promisify)(d.execFile),q={"vue-i18n":{name:`vue-i18n`,framework:`Vue`,configPatterns:[`i18n.ts`,`i18n.js`,`i18n/index.ts`,`i18n/index.js`,`src/i18n.ts`,`src/i18n.js`,`src/i18n/index.ts`,`src/plugins/i18n.ts`],localePatterns:[`locales/*.json`,`src/locales/*.json`,`i18n/*.json`,`src/i18n/*.json`,`lang/*.json`,`src/lang/*.json`,`locales/*.yaml`,`locales/*.yml`],sourcePatterns:[`src/**/*.vue`],migrationGuide:`vue/llms-migration.txt`},"nuxt-i18n":{name:`nuxt-i18n`,framework:`Nuxt`,configPatterns:[`nuxt.config.ts`,`nuxt.config.js`,`i18n.config.ts`,`i18n.config.js`],localePatterns:[`locales/*.json`,`lang/*.json`,`i18n/*.json`,`locales/*.yaml`,`locales/*.yml`],sourcePatterns:[`pages/**/*.vue`,`components/**/*.vue`,`layouts/**/*.vue`],migrationGuide:`nuxt/llms-migration.txt`},"react-i18next":{name:`react-i18next`,framework:`React`,configPatterns:[`i18n.ts`,`i18n.js`,`src/i18n.ts`,`src/i18n.js`,`src/i18n/index.ts`,`src/i18n/config.ts`],localePatterns:[`locales/*.json`,`src/locales/*.json`,`public/locales/**/*.json`,`translations/*.json`,`src/translations/*.json`],sourcePatterns:[`src/**/*.tsx`,`src/**/*.jsx`,`src/**/*.ts`],migrationGuide:`react/llms-migration.txt`},"next-intl":{name:`next-intl`,framework:`Next.js`,configPatterns:[`next.config.ts`,`next.config.js`,`next.config.mjs`,`i18n.ts`,`src/i18n.ts`,`i18n/request.ts`,`src/i18n/request.ts`],localePatterns:[`messages/*.json`,`locales/*.json`,`src/messages/*.json`,`src/locales/*.json`],sourcePatterns:[`app/**/*.tsx`,`src/app/**/*.tsx`,`pages/**/*.tsx`,`components/**/*.tsx`],migrationGuide:`next-plugin/llms-migration.txt`},"next-i18next":{name:`next-i18next`,framework:`Next.js`,configPatterns:[`next-i18next.config.js`,`next-i18next.config.mjs`,`next.config.ts`,`next.config.js`],localePatterns:[`public/locales/**/*.json`],sourcePatterns:[`pages/**/*.tsx`,`src/pages/**/*.tsx`,`components/**/*.tsx`,`src/components/**/*.tsx`],migrationGuide:`next-plugin/llms-migration.txt`},lingui:{name:`lingui`,framework:`React`,configPatterns:[`lingui.config.ts`,`lingui.config.js`,`.linguirc`],localePatterns:[`locales/*.po`,`src/locales/*.po`,`locales/*/messages.po`,`src/locales/*/messages.po`],sourcePatterns:[`src/**/*.tsx`,`src/**/*.jsx`,`src/**/*.ts`],migrationGuide:`react/llms-migration.txt`}},J=Object.keys(q);function oe(e){let t=e.toLowerCase().replace(/^@nuxtjs\//,`nuxt-`).replace(/^@/,``);return J.find(e=>e===t)}async function se(e){let t={configFiles:[],localeFiles:[],sampleSources:[],packageJson:void 0},n=(0,a.resolve)(`package.json`);(0,i.existsSync)(n)&&(t.packageJson=(0,i.readFileSync)(n,`utf-8`));for(let n of e.configPatterns){let e=(0,a.resolve)(n);(0,i.existsSync)(e)&&t.configFiles.push({path:n,content:(0,i.readFileSync)(e,`utf-8`)})}let r=await(0,s.default)(e.localePatterns,{absolute:!1});for(let e of r.slice(0,10)){let n=(0,i.readFileSync)((0,a.resolve)(e),`utf-8`);t.localeFiles.push({path:e,content:n.length>5e3?n.slice(0,5e3)+`
8
- ... (truncated)`:n})}let o=await(0,s.default)(e.sourcePatterns,{absolute:!1});for(let e of o.slice(0,5)){let n=(0,i.readFileSync)((0,a.resolve)(e),`utf-8`);t.sampleSources.push({path:e,content:n.length>3e3?n.slice(0,3e3)+`
9
- ... (truncated)`:n})}return t}function ce(e){let t=typeof __dirname<`u`?__dirname:(0,a.dirname)((0,o.fileURLToPath)({}.url)),n=[(0,a.resolve)(`node_modules`,`@fluenti`,`cli`,`..`,`..`,e),(0,a.join)(t,`..`,`..`,`..`,e),(0,a.join)(t,`..`,`..`,e)];for(let e of n)if((0,i.existsSync)(e))return(0,i.readFileSync)(e,`utf-8`);return``}function le(e,t,n){let r=[];if(r.push(`You are a migration tool converting a ${e.framework} project from "${e.name}" to Fluenti (@fluenti).`,``,`Tasks:`,"1. Generate a `fluenti.config.ts` based on the existing i18n configuration",`2. Convert each locale/translation file to standard gettext PO format`,`3. Generate unified diff patches for every source file that needs changes`,`4. Generate install/uninstall commands`,``,`=== TRANSLATION API RULES ===`,"Fluenti provides a compile-time `t` tagged template that does NOT require useI18n():",``,` import { t } from '@fluenti/react' // or @fluenti/vue, @fluenti/solid`,` const name = "World"`," t`Hello, ${name}!`",``,"Use `import { t }` for ALL translation calls. Only use `useI18n()` when you need:","- `d()` / `n()` for date/number formatting","- `setLocale()` for locale switching","- `locale` for reading the current locale reactively",``,`=== SOURCE CODE REWRITING RULES ===`,`Imports:`,`- ${e.name}: remove all imports from "${e.name}" (and related packages)`,`- Add: import { t } from '@fluenti/${e.framework===`Vue`?`vue`:e.framework===`Next.js`?`react`:e.framework.toLowerCase()}'`,`- Only add useI18n import if d()/n()/setLocale is needed in that file`,``,`Translation calls:`,"- t('key') → t`Source text` (tagged template with the actual source text, not the key)","- t('key', { name }) → t`Hello, ${name}` (interpolate directly in template)","- $t('key') → t`Source text` (Vue template)",`- Remove useI18n()/useTranslation()/useTranslations() destructuring if only t was used`,``,`Components:`,`- <i18n-t keypath="key"> → <Trans>Source text</Trans>`,`- <Trans i18nKey="key"> → <Trans>Source text</Trans>`,``,`ICU syntax conversion:`,`- {{variable}} (double braces) → {variable} (single braces)`,`- _one/_other suffixes → ICU {count, plural, one {...} other {...}}`,`- @:key references → inline the referenced text directly`,`- Pipe-separated plurals → ICU plural`,``,`=== PO FORMAT RULES ===`,`Each PO file must have a standard header:`,` msgid ""`,` msgstr ""`,` "Content-Type: text/plain; charset=UTF-8\\n"`,` "Content-Transfer-Encoding: 8bit\\n"`,` "Language: {locale}\\n"`,``,`Message entries: msgid is the source text (English), msgstr is the translation.`,`Flatten nested JSON keys: "home.title" → use the actual source text as msgid.`,``),n&&r.push(`=== MIGRATION GUIDE ===`,n,``),t.packageJson&&r.push(`=== package.json ===`,t.packageJson,``),t.configFiles.length>0){r.push(`=== EXISTING CONFIG FILES ===`);for(let e of t.configFiles)r.push(`--- ${e.path} ---`,e.content,``)}if(t.localeFiles.length>0){r.push(`=== EXISTING LOCALE FILES ===`);for(let e of t.localeFiles)r.push(`--- ${e.path} ---`,e.content,``)}if(t.sampleSources.length>0){r.push(`=== SAMPLE SOURCE FILES ===`);for(let e of t.sampleSources)r.push(`--- ${e.path} ---`,e.content,``)}return r.push(``,`=== OUTPUT FORMAT ===`,`Output ONLY the following sections. No explanations, no commentary.`,``,`### FLUENTI_CONFIG`,"```ts",`// Complete fluenti.config.ts`,"```",``,`### LOCALE_FILES`,`#### LOCALE: {locale_code}`,"```po",`// Complete PO file with standard header`,"```",`(repeat for each locale)`,``,`### SOURCE_PATCHES`,`#### FILE: {relative_file_path}`,"```diff",`--- a/{file_path}`,`+++ b/{file_path}`,`@@ ... @@`,` context line`,`-removed line`,`+added line`,"```",`(repeat for each file that needs changes)`,``,`### INSTALL_COMMANDS`,"```bash",`// install + uninstall commands`,"```"),r.join(`
10
- `)}async function ue(e,t){let n=10*1024*1024;try{if(e===`claude`){let{stdout:e}=await K(`claude`,[`-p`,t],{maxBuffer:n});return e}else{let{stdout:e}=await K(`codex`,[`-p`,t,`--full-auto`],{maxBuffer:n});return e}}catch(t){let n=t;throw n.code===`ENOENT`||n.code===`EPERM`||n.code===`EACCES`?Error(`"${e}" CLI not found or not executable. Please install it first:\n`+(e===`claude`?` npm install -g @anthropic-ai/claude-code`:` npm install -g @openai/codex`)):t}}function de(e){let t=5e5,n=e.length>t?e.slice(0,t):e,r={config:void 0,localeFiles:[],sourcePatches:[],steps:void 0,installCommands:void 0},i=n.match(/### FLUENTI_CONFIG[\s\S]*?```(?:ts|typescript)?\n([\s\S]*?)```/);i&&(r.config=i[1].trim());let a=n.match(/### LOCALE_FILES([\s\S]*?)(?=### SOURCE_PATCHES|### MIGRATION_STEPS|### INSTALL_COMMANDS|$)/);if(a){let e=/#### LOCALE:\s*(\S+)\s*\n```(?:po)?\n([\s\S]*?)```/g,t;for(;(t=e.exec(a[1]))!==null;)r.localeFiles.push({locale:t[1],content:t[2].trim()})}let o=n.match(/### SOURCE_PATCHES([\s\S]*?)(?=### INSTALL_COMMANDS|$)/);if(o){let e=/#### FILE:\s*(\S+)\s*\n```(?:diff)?\n([\s\S]*?)```/g,t;for(;(t=e.exec(o[1]))!==null;)r.sourcePatches.push({file:t[1],patch:t[2].trim()})}let s=n.match(/### MIGRATION_STEPS\s*\n([\s\S]*?)(?=### INSTALL_COMMANDS|$)/);s&&(r.steps=s[1].trim());let c=n.match(/### INSTALL_COMMANDS[\s\S]*?```(?:bash|sh)?\n([\s\S]*?)```/);return c&&(r.installCommands=c[1].trim()),r}async function fe(e){let{from:t,provider:n,write:r}=e,i=oe(t);if(!i){l.default.error(`Unsupported library "${t}". Supported libraries:`);for(let e of J)l.default.log(` - ${e}`);return}let o=q[i];l.default.info(`Migrating from ${o.name} (${o.framework}) to Fluenti`),l.default.info(`Scanning project for existing i18n files...`);let s=await se(o);if(s.configFiles.length===0&&s.localeFiles.length===0){l.default.warn(`No ${o.name} configuration or locale files found.`),l.default.info(`Make sure you are running this command from the project root directory.`);return}l.default.info(`Found: ${s.configFiles.length} config file(s), ${s.localeFiles.length} locale file(s), ${s.sampleSources.length} source file(s)`);let c=ce(o.migrationGuide);l.default.info(`Generating migration plan with ${n}...`);let u=de(await ue(n,le(o,s,c)));if(u.installCommands&&(l.default.log(``),l.default.box({title:`Install Commands`,message:u.installCommands})),u.config)if(r){let{writeFileSync:e}=await import(`node:fs`),t=(0,a.resolve)(`fluenti.config.ts`);e(t,u.config,`utf-8`),l.default.success(`Written: ${t}`)}else l.default.log(``),l.default.box({title:`fluenti.config.ts`,message:u.config});if(u.localeFiles.length>0)if(r){let{writeFileSync:e,mkdirSync:t}=await import(`node:fs`),n=`./locales`;t((0,a.resolve)(n),{recursive:!0});for(let t of u.localeFiles){let r=(0,a.resolve)(n,`${t.locale}.po`);e(r,t.content,`utf-8`),l.default.success(`Written: ${r}`)}}else for(let e of u.localeFiles)l.default.log(``),l.default.box({title:`locales/${e.locale}.po`,message:e.content.length>500?e.content.slice(0,500)+`
11
- ... (use --write to save full file)`:e.content});if(u.sourcePatches.length>0)if(r){l.default.log(``),l.default.info(`Generated ${u.sourcePatches.length} source patch(es). Apply with:`);for(let e of u.sourcePatches){let t=(0,a.resolve)(`.fluenti-migrate-${e.file.replace(/[/\\]/g,`-`)}.patch`),{writeFileSync:n}=await import(`node:fs`);n(t,e.patch,`utf-8`),l.default.success(`Patch written: ${t}`),l.default.log(` patch -p1 < ${t}`)}}else for(let e of u.sourcePatches)l.default.log(``),l.default.box({title:`Patch: ${e.file}`,message:e.patch.length>800?e.patch.slice(0,800)+`
12
- ... (use --write to save full patch)`:e.patch});u.steps&&u.sourcePatches.length===0&&(l.default.log(``),l.default.box({title:`Migration Steps`,message:u.steps})),!r&&(u.config||u.localeFiles.length>0||u.sourcePatches.length>0)&&(l.default.log(``),l.default.info(`Run with --write to save generated files and patches to disk:`),l.default.log(` fluenti migrate --from ${t} --write`))}function Y(e){return(0,c.createHash)(`md5`).update(e).digest(`hex`).slice(0,8)}function X(t,n){if(!(0,i.existsSync)(t))return{};let r=(0,i.readFileSync)(t,`utf-8`);return n===`json`?e.u(r):e.c(r)}function Z(t,n,r){(0,i.mkdirSync)((0,a.dirname)(t),{recursive:!0}),(0,i.writeFileSync)(t,r===`json`?e.d(n):e.l(n),`utf-8`)}async function pe(e,n,r){if((0,a.extname)(e)===`.vue`)try{let{extractFromVue:t}=await Promise.resolve().then(()=>require(`./vue-extractor.cjs`));return t(n,e,r)}catch{return l.default.warn(`Skipping ${e}: install @vue/compiler-sfc to extract from .vue files`),[]}return t.t(n,e,r)}var me=(0,u.defineCommand)({meta:{name:`extract`,description:`Extract messages from source files`},args:{config:{type:`string`,description:`Path to config file`},clean:{type:`boolean`,description:`Remove obsolete entries instead of marking them`,default:!1},"no-fuzzy":{type:`boolean`,description:`Strip fuzzy flags from all entries`,default:!1},"no-cache":{type:`boolean`,description:`Disable incremental extraction cache`,default:!1}},async run({args:t}){let n=await(0,m.loadConfig)(t.config),o=(0,r.resolveLocaleCodes)(n.locales);l.default.info(`Extracting messages from ${n.include.join(`, `)}`);let c=await(0,s.default)(n.include,{ignore:n.exclude??[],absolute:!1}),u=[],d=t[`no-cache`]??!1?null:new e.o(n.catalogDir,Y(process.cwd())),f=0;for(let e of c){if(d){let t=d.get(e);if(t){u.push(...t),f++;continue}}let t=await pe(e,(0,i.readFileSync)(e,`utf-8`),n.idGenerator);u.push(...t),d&&d.set(e,t)}d&&(d.prune(new Set(c)),d.save()),f>0?l.default.info(`Found ${u.length} messages in ${c.length} files (${f} cached)`):l.default.info(`Found ${u.length} messages in ${c.length} files`);let p=n.format===`json`?`.json`:`.po`,h=t.clean??!1,g=t[`no-fuzzy`]??!1;for(let t of o){let r=(0,a.resolve)(n.catalogDir,`${t}${p}`),{catalog:i,result:o}=e.f(X(r,n.format),u,{stripFuzzy:g});Z(r,h?Object.fromEntries(Object.entries(i).filter(([,e])=>!e.obsolete)):i,n.format);let s=h?`${o.obsolete} removed`:`${o.obsolete} obsolete`;l.default.success(`${t}: ${o.added} added, ${o.unchanged} unchanged, ${s}`)}for(let e of n.plugins??[])await e.onAfterExtract?.({messages:new Map(u.map(e=>[e.id,e])),sourceLocale:n.sourceLocale,targetLocales:o.filter(e=>e!==n.sourceLocale),config:n})}});function he(e){let t={};for(let[n,r]of Object.entries(e))r.translation&&r.translation.length>0?t[n]=r.translation:r.message&&(t[n]=r.message);return t}async function Q(e,t,n){let r=he(e);for(let e of n)e.transformMessages&&(r=await e.transformMessages(r,t));let i={};for(let[t,n]of Object.entries(e)){let e=r[t];i[t]=e===void 0?{...n}:{...n,translation:e}}return i}function $(e,t,n,r){let i={};for(let[e,n]of Object.entries(t))n.translation&&n.translation.length>0?i[e]=n.translation:n.message&&(i[e]=n.message);return{locale:e,messages:i,outDir:n,config:r}}var ge=(0,u.defineCommand)({meta:{name:`compile`,description:`Compile message catalogs to JS modules`},args:{config:{type:`string`,description:`Path to config file`},"skip-fuzzy":{type:`boolean`,description:`Exclude fuzzy entries from compilation`,default:!1},"no-cache":{type:`boolean`,description:`Disable compilation cache`,default:!1},parallel:{type:`boolean`,description:`Enable parallel compilation using worker threads`,default:!1},concurrency:{type:`string`,description:`Max number of worker threads (default: auto)`}},async run({args:t}){let o=await(0,m.loadConfig)(t.config),s=(0,r.resolveLocaleCodes)(o.locales),c=o.format===`json`?`.json`:`.po`;(0,i.mkdirSync)(o.compileOutDir,{recursive:!0});let u={},d={};for(let t of s){let n=(0,a.resolve)(o.catalogDir,`${t}${c}`);if((0,i.existsSync)(n)){let r=(0,i.readFileSync)(n,`utf-8`);d[t]=r,u[t]=o.format===`json`?e.u(r):e.c(r)}else d[t]=``,u[t]={}}let f=n.t(u);l.default.info(`Compiling ${f.length} messages across ${s.length} locales`);let p=t[`skip-fuzzy`]??!1,h=t[`no-cache`]??!1?null:new j(o.catalogDir,Y(process.cwd())),g=t.parallel??!1,_=t.concurrency?parseInt(t.concurrency,10):void 0;if(_!==void 0&&(isNaN(_)||_<1)){l.default.error(`Invalid --concurrency. Must be a positive integer.`),process.exitCode=1;return}let v=0,y=!1,b=[];for(let e of s){if(h&&h.isUpToDate(e,d[e])&&(0,i.existsSync)((0,a.resolve)(o.compileOutDir,`${e}.js`))){v++;continue}b.push(e)}if(b.length>0&&(y=!0),g&&b.length>1){let t=o.plugins??[],n={};for(let e of b){for(let n of t)await n.onBeforeCompile?.($(e,u[e],o.compileOutDir,o));n[e]=t.length>0?await Q(u[e],e,t):u[e]}let r=await e.s(b.map(e=>({locale:e,catalog:n[e],allIds:f,sourceLocale:o.sourceLocale,options:{skipFuzzy:p}})),_);for(let e of r){let t=(0,a.resolve)(o.compileOutDir,`${e.locale}.js`);if((0,i.writeFileSync)(t,e.code,`utf-8`),h&&h.set(e.locale,d[e.locale]),e.stats.missing.length>0){l.default.warn(`${e.locale}: ${e.stats.compiled} compiled, ${e.stats.missing.length} missing translations`);for(let t of e.stats.missing)l.default.warn(` ⤷ ${t}`)}else l.default.success(`Compiled ${e.locale}: ${e.stats.compiled} messages → ${t}`)}for(let e of b)for(let r of t)await r.onAfterCompile?.($(e,n[e],o.compileOutDir,o))}else{let e=o.plugins??[];for(let t of b){let r=(0,a.resolve)(o.compileOutDir,`${t}.js`);for(let n of e)await n.onBeforeCompile?.($(t,u[t],o.compileOutDir,o));let s=e.length>0?await Q(u[t],t,e):u[t],{code:c,stats:m}=n.n(s,t,f,o.sourceLocale,{skipFuzzy:p});if((0,i.writeFileSync)(r,c,`utf-8`),h&&h.set(t,d[t]),m.missing.length>0){l.default.warn(`${t}: ${m.compiled} compiled, ${m.missing.length} missing translations`);for(let e of m.missing)l.default.warn(` ⤷ ${e}`)}else l.default.success(`Compiled ${t}: ${m.compiled} messages → ${r}`);for(let n of e)await n.onAfterCompile?.($(t,s,o.compileOutDir,o))}}v>0&&l.default.info(`${v} locale(s) unchanged — skipped`),h&&h.save();let x=(0,a.resolve)(o.compileOutDir,`index.js`),S=(0,a.resolve)(o.compileOutDir,`messages.d.ts`);(y||!(0,i.existsSync)(x))&&((0,i.writeFileSync)(x,n.r(s,o.compileOutDir),`utf-8`),l.default.success(`Generated index → ${x}`)),(y||!(0,i.existsSync)(S))&&((0,i.writeFileSync)(S,n.i(f,u,o.sourceLocale),`utf-8`),l.default.success(`Generated types → ${S}`))}}),_e=(0,u.defineCommand)({meta:{name:`stats`,description:`Show translation progress`},args:{config:{type:`string`,description:`Path to config file`}},async run({args:e}){let t=await(0,m.loadConfig)(e.config),n=(0,r.resolveLocaleCodes)(t.locales),i=t.format===`json`?`.json`:`.po`,o=[];for(let e of n){let n=X((0,a.resolve)(t.catalogDir,`${e}${i}`),t.format),r=Object.values(n).filter(e=>!e.obsolete),s=r.length,c=r.filter(e=>e.translation&&e.translation.length>0).length,l=s>0?(c/s*100).toFixed(1)+`%`:`—`;o.push({locale:e,total:s,translated:c,pct:l})}l.default.log(``),l.default.log(` Locale │ Total │ Translated │ Progress`),l.default.log(` ────────┼───────┼────────────┼─────────────────────────────`);for(let e of o)l.default.log(y(e.locale,e.total,e.translated));l.default.log(``)}}),ve=(0,u.defineCommand)({meta:{name:`lint`,description:`Check translation quality (missing, inconsistent placeholders, fuzzy)`},args:{config:{type:`string`,description:`Path to config file`},strict:{type:`boolean`,description:`Treat warnings as errors`,default:!1},locale:{type:`string`,description:`Lint a specific locale only`}},async run({args:e}){let t=await(0,m.loadConfig)(e.config),n=(0,r.resolveLocaleCodes)(t.locales),i=t.format===`json`?`.json`:`.po`,o={};for(let e of n)o[e]=X((0,a.resolve)(t.catalogDir,`${e}${i}`),t.format);let s=e.locale?[e.locale]:void 0;l.default.info(`Linting ${s?s.join(`, `):`all locales`} (source: ${t.sourceLocale})`);let c={sourceLocale:t.sourceLocale,strict:e.strict??!1};s&&(c.locales=s);let u=w(o,c);l.default.log(``),l.default.log(T(u)),l.default.log(``);let d=u.filter(e=>e.severity===`error`),f=u.filter(e=>e.severity===`warning`);(d.length>0||e.strict&&f.length>0)&&(process.exitCode=1)}}),ye=(0,u.defineCommand)({meta:{name:`check`,description:`Check translation coverage for CI`},args:{config:{type:`string`,description:`Path to config file`},ci:{type:`boolean`,description:`Alias for --format github`,default:!1},"min-coverage":{type:`string`,description:`Minimum coverage percentage (0-100)`,default:`100`},format:{type:`string`,description:`Output format: text | json | github`},locale:{type:`string`,description:`Check a specific locale only`}},async run({args:e}){let t=await(0,m.loadConfig)(e.config),n=(0,r.resolveLocaleCodes)(t.locales),i=t.format===`json`?`.json`:`.po`,o={};for(let e of n)o[e]=X((0,a.resolve)(t.catalogDir,`${e}${i}`),t.format);let s=parseFloat(e[`min-coverage`]??`100`);if(isNaN(s)||s<0||s>100){l.default.error(`Invalid --min-coverage. Must be a number between 0 and 100.`),process.exitCode=1;return}let c=e.format??(e.ci?`github`:`text`),u={sourceLocale:t.sourceLocale,minCoverage:s,format:c};e.locale&&(u.locale=e.locale);let d=D(o,u);switch(c){case`json`:l.default.log(k(d));break;case`github`:l.default.log(O(d,t.catalogDir,t.format));break;default:l.default.log(``),l.default.log(te(d)),l.default.log(``);break}d.passed||(process.exitCode=1)}});async function be(e,t,n){let r=Array(e.length),i=0,a=Array.from({length:Math.min(n,e.length)},async()=>{for(;i<e.length;){let n=i++;r[n]=await t(e[n])}});return await Promise.all(a),r}var xe=(0,u.defineCommand)({meta:{name:`translate`,description:`Translate messages using AI (Claude Code or Codex CLI)`},args:{config:{type:`string`,description:`Path to config file`},provider:{type:`string`,description:`AI provider: claude or codex`,default:`claude`},locale:{type:`string`,description:`Translate a specific locale only`},"batch-size":{type:`string`,description:`Messages per batch`,default:`50`},"dry-run":{type:`boolean`,description:`Preview translation results without writing files`,default:!1},context:{type:`string`,description:`Project context description to improve translation quality`},glossary:{type:`string`,description:`Path to glossary JSON file`},concurrency:{type:`string`,description:`Max parallel locale translations (default: 3)`},timeout:{type:`string`,description:`AI call timeout in seconds (default: 120)`}},async run({args:e}){let t=await(0,m.loadConfig)(e.config),n=(0,r.resolveLocaleCodes)(t.locales),i=e.provider;if(i!==`claude`&&i!==`codex`){l.default.error(`Invalid provider "${i}". Use "claude" or "codex".`);return}let o=parseInt(e[`batch-size`]??`50`,10);if(isNaN(o)||o<1){l.default.error(`Invalid batch-size. Must be a positive integer.`);return}let s=e.glossary?F((0,a.resolve)(e.glossary)):void 0,c=e.concurrency?parseInt(e.concurrency,10):3;if(isNaN(c)||c<1){l.default.error(`Invalid concurrency. Must be a positive integer.`);return}let u=e.timeout?parseInt(e.timeout,10):120;if(isNaN(u)||u<1){l.default.error(`Invalid timeout. Must be a positive integer (seconds).`);return}let d=u*1e3,f=e.locale?[e.locale]:n.filter(e=>e!==t.sourceLocale);if(f.length===0){l.default.warn(`No target locales to translate.`);return}l.default.info(`Translating with ${i} (batch size: ${o})`);let p=t.format===`json`?`.json`:`.po`;await be(f,async n=>{l.default.info(`\n[${n}]`);let r=(0,a.resolve)(t.catalogDir,`${n}${p}`),c=X(r,t.format);if(e[`dry-run`]){let e=Object.entries(c).filter(([,e])=>!e.obsolete&&(!e.translation||e.translation.length===0));if(e.length>0){for(let[t,n]of e)l.default.log(` ${t}: ${n.message??t}`);l.default.success(` ${n}: ${e.length} messages would be translated (dry-run)`)}else l.default.success(` ${n}: already fully translated`);return}let u=s?I(s,n):void 0,{catalog:f,translated:m,warnings:h}=await G({provider:i,sourceLocale:t.sourceLocale,targetLocale:n,catalog:c,batchSize:o,glossary:u,timeoutMs:d,...e.context?{context:e.context}:{}});m>0?(Z(r,f,t.format),l.default.success(` ${n}: ${m} messages translated`)):l.default.success(` ${n}: already fully translated`),h.length>0&&l.default.warn(` ${n}: ${h.length} QA warnings`)},c)}}),Se=(0,u.defineCommand)({meta:{name:`migrate`,description:`Migrate from another i18n library using AI`},args:{from:{type:`string`,description:`Source library: vue-i18n, nuxt-i18n, react-i18next, next-intl, next-i18next, lingui`,required:!0},provider:{type:`string`,description:`AI provider: claude or codex`,default:`claude`},write:{type:`boolean`,description:`Write generated files to disk`,default:!1}},async run({args:e}){let t=e.provider;if(t!==`claude`&&t!==`codex`){l.default.error(`Invalid provider "${t}". Use "claude" or "codex".`);return}await fe({from:e.from,provider:t,write:e.write??!1})}});(0,u.runMain)((0,u.defineCommand)({meta:{name:`fluenti`,version:`0.0.1`,description:`Compile-time i18n for modern frameworks`},subCommands:{init:(0,u.defineCommand)({meta:{name:`init`,description:`Initialize Fluenti in your project`},args:{},async run(){await e.r({cwd:process.cwd()})}}),doctor:(0,u.defineCommand)({meta:{name:`doctor`,description:`Diagnose Fluenti setup and vNext import issues`},args:{config:{type:`string`,description:`Optional path to fluenti config file`},strict:{type:`boolean`,description:`Treat warnings as errors`,default:!1}},async run({args:t}){let n=await e.n({cwd:process.cwd(),...t.config?{config:t.config}:{}}),r=e.t(n);l.default.log(r);let i=n.findings.some(e=>e.severity===`error`),a=n.findings.some(e=>e.severity===`warning`);(i||(t.strict??!1)&&a)&&(process.exitCode=1)}}),codemod:(0,u.defineCommand)({meta:{name:`codemod`,description:`Rewrite imports to the vNext Fluenti entry layout`},args:{write:{type:`boolean`,description:`Write modified files to disk`,default:!1},include:{type:`string`,description:`Comma-separated glob list to scan`}},async run({args:t}){let n=t.include?t.include.split(`,`).map(e=>e.trim()).filter(Boolean):void 0,r=await e.a({cwd:process.cwd(),...n?{include:n}:{},write:t.write??!1});if(r.changedCount===0){l.default.success(`No files need import rewrites.`);return}for(let e of r.changedFiles)l.default.log(`${t.write?`updated`:`would update`} ${e.file}`);l.default.success(`${t.write?`Updated`:`Detected`} ${r.changedCount} file(s) with Fluenti import rewrites.`)}}),extract:me,compile:ge,stats:_e,lint:ve,check:ye,translate:xe,migrate:Se}}));
7
+ `)}function B(e){let t=e.indexOf(`{`);if(t===-1)throw Error(`No JSON object found in AI response`);let n=0,r=!1,i=!1;for(let a=t;a<e.length;a++){let o=e[a];if(i){i=!1;continue}if(o===`\\`&&r){i=!0;continue}if(o===`"`&&!i){r=!r;continue}if(!r&&(o===`{`&&n++,o===`}`&&(n--,n===0)))return e.slice(t,a+1)}throw Error(`Unterminated JSON object in AI response`)}function V(e){let t=e.match(/```(?:json)?\s*\n?([\s\S]*?)```/);return t?t[1]:e}function H(e,t){let n=[],r=B(V(e)),i;try{i=JSON.parse(r)}catch{throw Error(`Failed to parse JSON from AI response: ${r.slice(0,200)}`)}if(typeof i!=`object`||!i||Array.isArray(i))throw Error(`AI response is not a valid JSON object`);let a=i,o={},s=new Set(Object.keys(t));for(let[e,r]of Object.entries(a)){if(!s.has(e)){n.push(`Extra key in AI response (ignored): "${e}"`);continue}if(typeof r!=`string`){n.push(`Non-string value for key "${e}" (ignored)`);continue}o[e]=r;let i=t[e],a=ee(i,r);if(!a.valid){let t=[];a.missingPlaceholders.length>0&&t.push(`missing placeholders: ${a.missingPlaceholders.join(`, `)}`),a.extraPlaceholders.length>0&&t.push(`extra placeholders: ${a.extraPlaceholders.join(`, `)}`),a.missingHtmlTags.length>0&&t.push(`missing HTML tags: ${a.missingHtmlTags.join(`, `)}`),a.extraHtmlTags.length>0&&t.push(`extra HTML tags: ${a.extraHtmlTags.join(`, `)}`),a.syntaxErrors.length>0&&t.push(`ICU syntax errors: ${a.syntaxErrors.join(`; `)}`),n.push(`QA issue for "${e}": ${t.join(`; `)}`)}}for(let e of s)e in o||n.push(`Missing translation for key: "${e}"`);return{translations:o,warnings:n}}function U(e){let t={};for(let[n,r]of Object.entries(e))r.obsolete||(!r.translation||r.translation.length===0)&&(t[n]=r.message??n);return t}function W(e,t){let n=Object.keys(e),r=[];for(let i=0;i<n.length;i+=t){let a={};for(let r of n.slice(i,i+t))a[r]=e[r];r.push(a)}return r}async function G(e){let{provider:t,sourceLocale:n,targetLocale:r,catalog:i,batchSize:a,context:o,glossary:s,timeoutMs:l}=e,u=U(i),d=Object.keys(u).length;if(d===0)return{catalog:{...i},translated:0,warnings:[]};c.default.info(` ${d} untranslated messages, translating with ${t}...`);let f={...i},p=W(u,a),m=0,h=[];for(let e=0;e<p.length;e++){let i=p[e],a=Object.keys(i);p.length>1&&c.default.info(` Batch ${e+1}/${p.length} (${a.length} messages)`);try{let{stdout:e}=await N({provider:t,prompt:z({sourceLocale:n,targetLocale:r,messages:i,glossary:s,context:o}),timeoutMs:l}),{translations:u,warnings:d}=H(e,i);for(let e of d)h.push(`[${r}] ${e}`),c.default.warn(` ${e}`);for(let e of a)e in u&&(f[e]={...f[e],translation:u[e]},m++)}catch(t){let n=t instanceof Error?t.message:String(t);h.push(`[${r}] Batch ${e+1} failed: ${n}`),c.default.error(` Batch ${e+1} failed: ${n}`)}}return{catalog:f,translated:m,warnings:h}}var K=(0,d.promisify)(u.execFile),q={"vue-i18n":{name:`vue-i18n`,framework:`Vue`,configPatterns:[`i18n.ts`,`i18n.js`,`i18n/index.ts`,`i18n/index.js`,`src/i18n.ts`,`src/i18n.js`,`src/i18n/index.ts`,`src/plugins/i18n.ts`],localePatterns:[`locales/*.json`,`src/locales/*.json`,`i18n/*.json`,`src/i18n/*.json`,`lang/*.json`,`src/lang/*.json`,`locales/*.yaml`,`locales/*.yml`],sourcePatterns:[`src/**/*.vue`],migrationGuide:`vue/llms-migration.txt`},"nuxt-i18n":{name:`nuxt-i18n`,framework:`Nuxt`,configPatterns:[`nuxt.config.ts`,`nuxt.config.js`,`i18n.config.ts`,`i18n.config.js`],localePatterns:[`locales/*.json`,`lang/*.json`,`i18n/*.json`,`locales/*.yaml`,`locales/*.yml`],sourcePatterns:[`pages/**/*.vue`,`components/**/*.vue`,`layouts/**/*.vue`],migrationGuide:`nuxt/llms-migration.txt`},"react-i18next":{name:`react-i18next`,framework:`React`,configPatterns:[`i18n.ts`,`i18n.js`,`src/i18n.ts`,`src/i18n.js`,`src/i18n/index.ts`,`src/i18n/config.ts`],localePatterns:[`locales/*.json`,`src/locales/*.json`,`public/locales/**/*.json`,`translations/*.json`,`src/translations/*.json`],sourcePatterns:[`src/**/*.tsx`,`src/**/*.jsx`,`src/**/*.ts`],migrationGuide:`react/llms-migration.txt`},"next-intl":{name:`next-intl`,framework:`Next.js`,configPatterns:[`next.config.ts`,`next.config.js`,`next.config.mjs`,`i18n.ts`,`src/i18n.ts`,`i18n/request.ts`,`src/i18n/request.ts`],localePatterns:[`messages/*.json`,`locales/*.json`,`src/messages/*.json`,`src/locales/*.json`],sourcePatterns:[`app/**/*.tsx`,`src/app/**/*.tsx`,`pages/**/*.tsx`,`components/**/*.tsx`],migrationGuide:`next-plugin/llms-migration.txt`},"next-i18next":{name:`next-i18next`,framework:`Next.js`,configPatterns:[`next-i18next.config.js`,`next-i18next.config.mjs`,`next.config.ts`,`next.config.js`],localePatterns:[`public/locales/**/*.json`],sourcePatterns:[`pages/**/*.tsx`,`src/pages/**/*.tsx`,`components/**/*.tsx`,`src/components/**/*.tsx`],migrationGuide:`next-plugin/llms-migration.txt`},lingui:{name:`lingui`,framework:`React`,configPatterns:[`lingui.config.ts`,`lingui.config.js`,`.linguirc`],localePatterns:[`locales/*.po`,`src/locales/*.po`,`locales/*/messages.po`,`src/locales/*/messages.po`],sourcePatterns:[`src/**/*.tsx`,`src/**/*.jsx`,`src/**/*.ts`],migrationGuide:`react/llms-migration.txt`}},J=Object.keys(q);function Y(e){let t=e.toLowerCase().replace(/^@nuxtjs\//,`nuxt-`).replace(/^@/,``);return J.find(e=>e===t)}async function ae(e){let t={configFiles:[],localeFiles:[],sampleSources:[],packageJson:void 0},n=(0,i.resolve)(`package.json`);(0,r.existsSync)(n)&&(t.packageJson=(0,r.readFileSync)(n,`utf-8`));for(let n of e.configPatterns){let e=(0,i.resolve)(n);(0,r.existsSync)(e)&&t.configFiles.push({path:n,content:(0,r.readFileSync)(e,`utf-8`)})}let a=await(0,o.default)(e.localePatterns,{absolute:!1});for(let e of a.slice(0,10)){let n=(0,r.readFileSync)((0,i.resolve)(e),`utf-8`);t.localeFiles.push({path:e,content:n.length>5e3?n.slice(0,5e3)+`
8
+ ... (truncated)`:n})}let s=await(0,o.default)(e.sourcePatterns,{absolute:!1});for(let e of s.slice(0,5)){let n=(0,r.readFileSync)((0,i.resolve)(e),`utf-8`);t.sampleSources.push({path:e,content:n.length>3e3?n.slice(0,3e3)+`
9
+ ... (truncated)`:n})}return t}function oe(e){let t=typeof __dirname<`u`?__dirname:(0,i.dirname)((0,a.fileURLToPath)({}.url)),n=[(0,i.resolve)(`node_modules`,`@fluenti`,`cli`,`..`,`..`,e),(0,i.join)(t,`..`,`..`,`..`,e),(0,i.join)(t,`..`,`..`,e)];for(let e of n)if((0,r.existsSync)(e))return(0,r.readFileSync)(e,`utf-8`);return``}function se(e,t,n){let r=[];if(r.push(`You are a migration tool converting a ${e.framework} project from "${e.name}" to Fluenti (@fluenti).`,``,`Tasks:`,"1. Generate a `fluenti.config.ts` based on the existing i18n configuration",`2. Convert each locale/translation file to standard gettext PO format`,`3. Generate unified diff patches for every source file that needs changes`,`4. Generate install/uninstall commands`,``,`=== TRANSLATION API RULES ===`,"Fluenti provides a compile-time `t` tagged template that does NOT require useI18n():",``,` import { t } from '@fluenti/react' // or @fluenti/vue, @fluenti/solid`,` const name = "World"`," t`Hello, ${name}!`",``,"Use `import { t }` for ALL translation calls. Only use `useI18n()` when you need:","- `d()` / `n()` for date/number formatting","- `setLocale()` for locale switching","- `locale` for reading the current locale reactively",``,`=== SOURCE CODE REWRITING RULES ===`,`Imports:`,`- ${e.name}: remove all imports from "${e.name}" (and related packages)`,`- Add: import { t } from '@fluenti/${e.framework===`Vue`?`vue`:e.framework===`Next.js`?`react`:e.framework.toLowerCase()}'`,`- Only add useI18n import if d()/n()/setLocale is needed in that file`,``,`Translation calls:`,"- t('key') → t`Source text` (tagged template with the actual source text, not the key)","- t('key', { name }) → t`Hello, ${name}` (interpolate directly in template)","- $t('key') → t`Source text` (Vue template)",`- Remove useI18n()/useTranslation()/useTranslations() destructuring if only t was used`,``,`Components:`,`- <i18n-t keypath="key"> → <Trans>Source text</Trans>`,`- <Trans i18nKey="key"> → <Trans>Source text</Trans>`,``,`ICU syntax conversion:`,`- {{variable}} (double braces) → {variable} (single braces)`,`- _one/_other suffixes → ICU {count, plural, one {...} other {...}}`,`- @:key references → inline the referenced text directly`,`- Pipe-separated plurals → ICU plural`,``,`=== PO FORMAT RULES ===`,`Each PO file must have a standard header:`,` msgid ""`,` msgstr ""`,` "Content-Type: text/plain; charset=UTF-8\\n"`,` "Content-Transfer-Encoding: 8bit\\n"`,` "Language: {locale}\\n"`,``,`Message entries: msgid is the source text (English), msgstr is the translation.`,`Flatten nested JSON keys: "home.title" → use the actual source text as msgid.`,``),n&&r.push(`=== MIGRATION GUIDE ===`,n,``),t.packageJson&&r.push(`=== package.json ===`,t.packageJson,``),t.configFiles.length>0){r.push(`=== EXISTING CONFIG FILES ===`);for(let e of t.configFiles)r.push(`--- ${e.path} ---`,e.content,``)}if(t.localeFiles.length>0){r.push(`=== EXISTING LOCALE FILES ===`);for(let e of t.localeFiles)r.push(`--- ${e.path} ---`,e.content,``)}if(t.sampleSources.length>0){r.push(`=== SAMPLE SOURCE FILES ===`);for(let e of t.sampleSources)r.push(`--- ${e.path} ---`,e.content,``)}return r.push(``,`=== OUTPUT FORMAT ===`,`Output ONLY the following sections. No explanations, no commentary.`,``,`### FLUENTI_CONFIG`,"```ts",`// Complete fluenti.config.ts`,"```",``,`### LOCALE_FILES`,`#### LOCALE: {locale_code}`,"```po",`// Complete PO file with standard header`,"```",`(repeat for each locale)`,``,`### SOURCE_PATCHES`,`#### FILE: {relative_file_path}`,"```diff",`--- a/{file_path}`,`+++ b/{file_path}`,`@@ ... @@`,` context line`,`-removed line`,`+added line`,"```",`(repeat for each file that needs changes)`,``,`### INSTALL_COMMANDS`,"```bash",`// install + uninstall commands`,"```"),r.join(`
10
+ `)}async function ce(e,t){let n=10*1024*1024;try{if(e===`claude`){let{stdout:e}=await K(`claude`,[`-p`,t],{maxBuffer:n});return e}else{let{stdout:e}=await K(`codex`,[`-p`,t,`--full-auto`],{maxBuffer:n});return e}}catch(t){let n=t;throw n.code===`ENOENT`||n.code===`EPERM`||n.code===`EACCES`?Error(`"${e}" CLI not found or not executable. Please install it first:\n`+(e===`claude`?` npm install -g @anthropic-ai/claude-code`:` npm install -g @openai/codex`)):t}}function le(e){let t=5e5,n=e.length>t?e.slice(0,t):e,r={config:void 0,localeFiles:[],sourcePatches:[],steps:void 0,installCommands:void 0},i=n.match(/### FLUENTI_CONFIG[\s\S]*?```(?:ts|typescript)?\n([\s\S]*?)```/);i&&(r.config=i[1].trim());let a=n.match(/### LOCALE_FILES([\s\S]*?)(?=### SOURCE_PATCHES|### MIGRATION_STEPS|### INSTALL_COMMANDS|$)/);if(a){let e=/#### LOCALE:\s*(\S+)\s*\n```(?:po)?\n([\s\S]*?)```/g,t;for(;(t=e.exec(a[1]))!==null;)r.localeFiles.push({locale:t[1],content:t[2].trim()})}let o=n.match(/### SOURCE_PATCHES([\s\S]*?)(?=### INSTALL_COMMANDS|$)/);if(o){let e=/#### FILE:\s*(\S+)\s*\n```(?:diff)?\n([\s\S]*?)```/g,t;for(;(t=e.exec(o[1]))!==null;)r.sourcePatches.push({file:t[1],patch:t[2].trim()})}let s=n.match(/### MIGRATION_STEPS\s*\n([\s\S]*?)(?=### INSTALL_COMMANDS|$)/);s&&(r.steps=s[1].trim());let c=n.match(/### INSTALL_COMMANDS[\s\S]*?```(?:bash|sh)?\n([\s\S]*?)```/);return c&&(r.installCommands=c[1].trim()),r}async function ue(e){let{from:t,provider:n,write:r}=e,a=Y(t);if(!a){c.default.error(`Unsupported library "${t}". Supported libraries:`);for(let e of J)c.default.log(` - ${e}`);return}let o=q[a];c.default.info(`Migrating from ${o.name} (${o.framework}) to Fluenti`),c.default.info(`Scanning project for existing i18n files...`);let s=await ae(o);if(s.configFiles.length===0&&s.localeFiles.length===0){c.default.warn(`No ${o.name} configuration or locale files found.`),c.default.info(`Make sure you are running this command from the project root directory.`);return}c.default.info(`Found: ${s.configFiles.length} config file(s), ${s.localeFiles.length} locale file(s), ${s.sampleSources.length} source file(s)`);let l=oe(o.migrationGuide);c.default.info(`Generating migration plan with ${n}...`);let u=le(await ce(n,se(o,s,l)));if(u.installCommands&&(c.default.log(``),c.default.box({title:`Install Commands`,message:u.installCommands})),u.config)if(r){let{writeFileSync:e}=await import(`node:fs`),t=(0,i.resolve)(`fluenti.config.ts`);e(t,u.config,`utf-8`),c.default.success(`Written: ${t}`)}else c.default.log(``),c.default.box({title:`fluenti.config.ts`,message:u.config});if(u.localeFiles.length>0)if(r){let{writeFileSync:e,mkdirSync:t}=await import(`node:fs`),n=`./locales`;t((0,i.resolve)(n),{recursive:!0});for(let t of u.localeFiles){let r=(0,i.resolve)(n,`${t.locale}.po`);e(r,t.content,`utf-8`),c.default.success(`Written: ${r}`)}}else for(let e of u.localeFiles)c.default.log(``),c.default.box({title:`locales/${e.locale}.po`,message:e.content.length>500?e.content.slice(0,500)+`
11
+ ... (use --write to save full file)`:e.content});if(u.sourcePatches.length>0)if(r){c.default.log(``),c.default.info(`Generated ${u.sourcePatches.length} source patch(es). Apply with:`);for(let e of u.sourcePatches){let t=(0,i.resolve)(`.fluenti-migrate-${e.file.replace(/[/\\]/g,`-`)}.patch`),{writeFileSync:n}=await import(`node:fs`);n(t,e.patch,`utf-8`),c.default.success(`Patch written: ${t}`),c.default.log(` patch -p1 < ${t}`)}}else for(let e of u.sourcePatches)c.default.log(``),c.default.box({title:`Patch: ${e.file}`,message:e.patch.length>800?e.patch.slice(0,800)+`
12
+ ... (use --write to save full patch)`:e.patch});u.steps&&u.sourcePatches.length===0&&(c.default.log(``),c.default.box({title:`Migration Steps`,message:u.steps})),!r&&(u.config||u.localeFiles.length>0||u.sourcePatches.length>0)&&(c.default.log(``),c.default.info(`Run with --write to save generated files and patches to disk:`),c.default.log(` fluenti migrate --from ${t} --write`))}function de(e){return(0,s.createHash)(`md5`).update(e).digest(`hex`).slice(0,8)}function X(t,n){if(!(0,r.existsSync)(t))return{};let i=(0,r.readFileSync)(t,`utf-8`);return n===`json`?e.u(i):e.c(i)}function fe(t,n,a){(0,r.mkdirSync)((0,i.dirname)(t),{recursive:!0}),(0,r.writeFileSync)(t,a===`json`?e.d(n):e.l(n),`utf-8`)}var pe=(0,l.defineCommand)({meta:{name:`extract`,description:`Extract messages from source files`},args:{config:{type:`string`,description:`Path to config file`},clean:{type:`boolean`,description:`Remove obsolete entries instead of marking them`,default:!1},"no-fuzzy":{type:`boolean`,description:`Strip fuzzy flags from all entries`,default:!1},"no-cache":{type:`boolean`,description:`Disable incremental extraction cache`,default:!1}},async run({args:t}){let n=await(0,p.loadConfig)(t.config);c.default.info(`Extracting messages from ${n.include.join(`, `)}`);let r=await e.o(process.cwd(),n,{clean:t.clean??!1,stripFuzzy:t[`no-fuzzy`]??!1,useCache:!(t[`no-cache`]??!1)});r.cacheHits>0?c.default.info(`Found ${r.messageCount} messages in ${r.fileCount} files (${r.cacheHits} cached)`):c.default.info(`Found ${r.messageCount} messages in ${r.fileCount} files`);for(let e of r.localeResults){let n=t.clean?`${e.result.obsolete} removed`:`${e.result.obsolete} obsolete`;c.default.success(`${e.locale}: ${e.result.added} added, ${e.result.unchanged} unchanged, ${n}`)}}});function me(e){let t={};for(let[n,r]of Object.entries(e))r.translation&&r.translation.length>0?t[n]=r.translation:r.message&&(t[n]=r.message);return t}async function Z(e,t,n){let r=me(e);for(let e of n)e.transformMessages&&(r=await e.transformMessages(r,t));let i={};for(let[t,n]of Object.entries(e)){let e=r[t];i[t]=e===void 0?{...n}:{...n,translation:e}}return i}function Q(e,t,n,r){let i={};for(let[e,n]of Object.entries(t))n.translation&&n.translation.length>0?i[e]=n.translation:n.message&&(i[e]=n.message);return{locale:e,messages:i,outDir:n,config:r}}var he=(0,l.defineCommand)({meta:{name:`compile`,description:`Compile message catalogs to JS modules`},args:{config:{type:`string`,description:`Path to config file`},"skip-fuzzy":{type:`boolean`,description:`Exclude fuzzy entries from compilation`,default:!1},"no-cache":{type:`boolean`,description:`Disable compilation cache`,default:!1},parallel:{type:`boolean`,description:`Enable parallel compilation using worker threads`,default:!1},concurrency:{type:`string`,description:`Max number of worker threads (default: auto)`}},async run({args:a}){let o=await(0,p.loadConfig)(a.config),s=(0,n.resolveLocaleCodes)(o.locales),l=o.format===`json`?`.json`:`.po`;(0,r.mkdirSync)(o.compileOutDir,{recursive:!0});let u={},d={};for(let t of s){let n=(0,i.resolve)(o.catalogDir,`${t}${l}`);if((0,r.existsSync)(n)){let i=(0,r.readFileSync)(n,`utf-8`);d[t]=i,u[t]=o.format===`json`?e.u(i):e.c(i)}else d[t]=``,u[t]={}}let f=t.t(u);c.default.info(`Compiling ${f.length} messages across ${s.length} locales`);let m=a[`skip-fuzzy`]??!1,h=a[`no-cache`]??!1?null:new ne(o.catalogDir,de(process.cwd())),g=a.parallel??!1,_=a.concurrency?parseInt(a.concurrency,10):void 0;if(_!==void 0&&(isNaN(_)||_<1)){c.default.error(`Invalid --concurrency. Must be a positive integer.`),process.exitCode=1;return}let v=0,y=!1,b=[];for(let e of s){if(h&&h.isUpToDate(e,d[e])&&(0,r.existsSync)((0,i.resolve)(o.compileOutDir,`${e}.js`))){v++;continue}b.push(e)}if(b.length>0&&(y=!0),g&&b.length>1){let t=o.plugins??[],n={};for(let e of b){for(let n of t)await n.onBeforeCompile?.(Q(e,u[e],o.compileOutDir,o));n[e]=t.length>0?await Z(u[e],e,t):u[e]}let a=await e.s(b.map(e=>({locale:e,catalog:n[e],allIds:f,sourceLocale:o.sourceLocale,options:{skipFuzzy:m}})),_);for(let e of a){let t=(0,i.resolve)(o.compileOutDir,`${e.locale}.js`);if((0,r.writeFileSync)(t,e.code,`utf-8`),h&&h.set(e.locale,d[e.locale]),e.stats.missing.length>0){c.default.warn(`${e.locale}: ${e.stats.compiled} compiled, ${e.stats.missing.length} missing translations`);for(let t of e.stats.missing)c.default.warn(` ⤷ ${t}`)}else c.default.success(`Compiled ${e.locale}: ${e.stats.compiled} messages → ${t}`)}for(let e of b)for(let r of t)await r.onAfterCompile?.(Q(e,n[e],o.compileOutDir,o))}else{let e=o.plugins??[];for(let n of b){let a=(0,i.resolve)(o.compileOutDir,`${n}.js`);for(let t of e)await t.onBeforeCompile?.(Q(n,u[n],o.compileOutDir,o));let s=e.length>0?await Z(u[n],n,e):u[n],{code:l,stats:p}=t.n(s,n,f,o.sourceLocale,{skipFuzzy:m});if((0,r.writeFileSync)(a,l,`utf-8`),h&&h.set(n,d[n]),p.missing.length>0){c.default.warn(`${n}: ${p.compiled} compiled, ${p.missing.length} missing translations`);for(let e of p.missing)c.default.warn(` ⤷ ${e}`)}else c.default.success(`Compiled ${n}: ${p.compiled} messages → ${a}`);for(let t of e)await t.onAfterCompile?.(Q(n,s,o.compileOutDir,o))}}v>0&&c.default.info(`${v} locale(s) unchanged — skipped`),h&&h.save();let x=(0,i.resolve)(o.compileOutDir,`index.js`),S=(0,i.resolve)(o.compileOutDir,`messages.d.ts`);(y||!(0,r.existsSync)(x))&&((0,r.writeFileSync)(x,t.r(s,o.compileOutDir),`utf-8`),c.default.success(`Generated index → ${x}`)),(y||!(0,r.existsSync)(S))&&((0,r.writeFileSync)(S,t.i(f,u,o.sourceLocale),`utf-8`),c.default.success(`Generated types → ${S}`))}}),ge=(0,l.defineCommand)({meta:{name:`stats`,description:`Show translation progress`},args:{config:{type:`string`,description:`Path to config file`}},async run({args:e}){let t=await(0,p.loadConfig)(e.config),r=(0,n.resolveLocaleCodes)(t.locales),a=t.format===`json`?`.json`:`.po`,o=[];for(let e of r){let n=X((0,i.resolve)(t.catalogDir,`${e}${a}`),t.format),r=Object.values(n).filter(e=>!e.obsolete),s=r.length,c=r.filter(e=>e.translation&&e.translation.length>0).length,l=s>0?(c/s*100).toFixed(1)+`%`:`—`;o.push({locale:e,total:s,translated:c,pct:l})}c.default.log(``),c.default.log(` Locale │ Total │ Translated │ Progress`),c.default.log(` ────────┼───────┼────────────┼─────────────────────────────`);for(let e of o)c.default.log(v(e.locale,e.total,e.translated));c.default.log(``)}}),$=(0,l.defineCommand)({meta:{name:`lint`,description:`Check translation quality (missing, inconsistent placeholders, fuzzy)`},args:{config:{type:`string`,description:`Path to config file`},strict:{type:`boolean`,description:`Treat warnings as errors`,default:!1},locale:{type:`string`,description:`Lint a specific locale only`}},async run({args:e}){let t=await(0,p.loadConfig)(e.config),r=(0,n.resolveLocaleCodes)(t.locales),a=t.format===`json`?`.json`:`.po`,o={};for(let e of r)o[e]=X((0,i.resolve)(t.catalogDir,`${e}${a}`),t.format);let s=e.locale?[e.locale]:void 0;c.default.info(`Linting ${s?s.join(`, `):`all locales`} (source: ${t.sourceLocale})`);let l={sourceLocale:t.sourceLocale,strict:e.strict??!1};s&&(l.locales=s);let u=C(o,l);c.default.log(``),c.default.log(te(u)),c.default.log(``);let d=u.filter(e=>e.severity===`error`),f=u.filter(e=>e.severity===`warning`);(d.length>0||e.strict&&f.length>0)&&(process.exitCode=1)}}),_e=(0,l.defineCommand)({meta:{name:`check`,description:`Check translation coverage for CI`},args:{config:{type:`string`,description:`Path to config file`},ci:{type:`boolean`,description:`Alias for --format github`,default:!1},"min-coverage":{type:`string`,description:`Minimum coverage percentage (0-100)`,default:`100`},format:{type:`string`,description:`Output format: text | json | github`},locale:{type:`string`,description:`Check a specific locale only`}},async run({args:e}){let t=await(0,p.loadConfig)(e.config),r=(0,n.resolveLocaleCodes)(t.locales),a=t.format===`json`?`.json`:`.po`,o={};for(let e of r)o[e]=X((0,i.resolve)(t.catalogDir,`${e}${a}`),t.format);let s=parseFloat(e[`min-coverage`]??`100`);if(isNaN(s)||s<0||s>100){c.default.error(`Invalid --min-coverage. Must be a number between 0 and 100.`),process.exitCode=1;return}let l=e.format??(e.ci?`github`:`text`),u={sourceLocale:t.sourceLocale,minCoverage:s,format:l};e.locale&&(u.locale=e.locale);let d=T(o,u);switch(l){case`json`:c.default.log(O(d));break;case`github`:c.default.log(D(d,t.catalogDir,t.format));break;default:c.default.log(``),c.default.log(E(d)),c.default.log(``);break}d.passed||(process.exitCode=1)}});async function ve(e,t,n){let r=Array(e.length),i=0,a=Array.from({length:Math.min(n,e.length)},async()=>{for(;i<e.length;){let n=i++;r[n]=await t(e[n])}});return await Promise.all(a),r}var ye=(0,l.defineCommand)({meta:{name:`translate`,description:`Translate messages using AI (Claude Code or Codex CLI)`},args:{config:{type:`string`,description:`Path to config file`},provider:{type:`string`,description:`AI provider: claude or codex`,default:`claude`},locale:{type:`string`,description:`Translate a specific locale only`},"batch-size":{type:`string`,description:`Messages per batch`,default:`50`},"dry-run":{type:`boolean`,description:`Preview translation results without writing files`,default:!1},context:{type:`string`,description:`Project context description to improve translation quality`},glossary:{type:`string`,description:`Path to glossary JSON file`},concurrency:{type:`string`,description:`Max parallel locale translations (default: 3)`},timeout:{type:`string`,description:`AI call timeout in seconds (default: 120)`}},async run({args:e}){let t=await(0,p.loadConfig)(e.config),r=(0,n.resolveLocaleCodes)(t.locales),a=e.provider;if(a!==`claude`&&a!==`codex`){c.default.error(`Invalid provider "${a}". Use "claude" or "codex".`);return}let o=parseInt(e[`batch-size`]??`50`,10);if(isNaN(o)||o<1){c.default.error(`Invalid batch-size. Must be a positive integer.`);return}let s=e.glossary?F((0,i.resolve)(e.glossary)):void 0,l=e.concurrency?parseInt(e.concurrency,10):3;if(isNaN(l)||l<1){c.default.error(`Invalid concurrency. Must be a positive integer.`);return}let u=e.timeout?parseInt(e.timeout,10):120;if(isNaN(u)||u<1){c.default.error(`Invalid timeout. Must be a positive integer (seconds).`);return}let d=u*1e3,f=e.locale?[e.locale]:r.filter(e=>e!==t.sourceLocale);if(f.length===0){c.default.warn(`No target locales to translate.`);return}c.default.info(`Translating with ${a} (batch size: ${o})`);let m=t.format===`json`?`.json`:`.po`;await ve(f,async n=>{c.default.info(`\n[${n}]`);let r=(0,i.resolve)(t.catalogDir,`${n}${m}`),l=X(r,t.format);if(e[`dry-run`]){let e=Object.entries(l).filter(([,e])=>!e.obsolete&&(!e.translation||e.translation.length===0));if(e.length>0){for(let[t,n]of e)c.default.log(` ${t}: ${n.message??t}`);c.default.success(` ${n}: ${e.length} messages would be translated (dry-run)`)}else c.default.success(` ${n}: already fully translated`);return}let u=s?I(s,n):void 0,{catalog:f,translated:p,warnings:h}=await G({provider:a,sourceLocale:t.sourceLocale,targetLocale:n,catalog:l,batchSize:o,glossary:u,timeoutMs:d,...e.context?{context:e.context}:{}});p>0?(fe(r,f,t.format),c.default.success(` ${n}: ${p} messages translated`)):c.default.success(` ${n}: already fully translated`),h.length>0&&c.default.warn(` ${n}: ${h.length} QA warnings`)},l)}}),be=(0,l.defineCommand)({meta:{name:`migrate`,description:`Migrate from another i18n library using AI`},args:{from:{type:`string`,description:`Source library: vue-i18n, nuxt-i18n, react-i18next, next-intl, next-i18next, lingui`,required:!0},provider:{type:`string`,description:`AI provider: claude or codex`,default:`claude`},write:{type:`boolean`,description:`Write generated files to disk`,default:!1}},async run({args:e}){let t=e.provider;if(t!==`claude`&&t!==`codex`){c.default.error(`Invalid provider "${t}". Use "claude" or "codex".`);return}await ue({from:e.from,provider:t,write:e.write??!1})}});(0,l.runMain)((0,l.defineCommand)({meta:{name:`fluenti`,version:`0.0.1`,description:`Compile-time i18n for modern frameworks`},subCommands:{init:(0,l.defineCommand)({meta:{name:`init`,description:`Initialize Fluenti in your project`},args:{},async run(){await e.r({cwd:process.cwd()})}}),doctor:(0,l.defineCommand)({meta:{name:`doctor`,description:`Diagnose Fluenti setup and vNext import issues`},args:{config:{type:`string`,description:`Optional path to fluenti config file`},strict:{type:`boolean`,description:`Treat warnings as errors`,default:!1}},async run({args:t}){let n=await e.n({cwd:process.cwd(),...t.config?{config:t.config}:{}}),r=e.t(n);c.default.log(r);let i=n.findings.some(e=>e.severity===`error`),a=n.findings.some(e=>e.severity===`warning`);(i||(t.strict??!1)&&a)&&(process.exitCode=1)}}),codemod:(0,l.defineCommand)({meta:{name:`codemod`,description:`Rewrite imports to the vNext Fluenti entry layout`},args:{write:{type:`boolean`,description:`Write modified files to disk`,default:!1},include:{type:`string`,description:`Comma-separated glob list to scan`}},async run({args:t}){let n=t.include?t.include.split(`,`).map(e=>e.trim()).filter(Boolean):void 0,r=await e.a({cwd:process.cwd(),...n?{include:n}:{},write:t.write??!1});if(r.changedCount===0){c.default.success(`No files need import rewrites.`);return}for(let e of r.changedFiles)c.default.log(`${t.write?`updated`:`would update`} ${e.file}`);c.default.success(`${t.write?`Updated`:`Detected`} ${r.changedCount} file(s) with Fluenti import rewrites.`)}}),extract:pe,compile:he,stats:ge,lint:$,check:_e,translate:ye,migrate:be}}));