unused-i18n
Version:
Simplifies managing and cleaning up unused translation keys in localization files
11 lines (10 loc) • 6.96 kB
JavaScript
;const B=require("commander"),F=require("fs"),M=require("path"),v=require("perf_hooks");function V(e){if(e&&typeof e=="object"&&"default"in e)return e;const s=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const t in e)if(t!=="default"){const n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(s,t,n.get?n:{enumerable:!0,get:()=>e[t]})}}return s.default=e,Object.freeze(s)}const $=V(F),j=V(M),R=[".json",".js",".cjs"],O=async()=>{const e=process.cwd();let s="";for(const n of R){const l=j.resolve(e,`unused-i18n.config${n}`);if($.existsSync(l)){s=l;break}}if(!s)throw new Error("Configuration file unused-i18n.config not found. Supported extensions: .json, .js, .cjs.");if(j.extname(s)===".json"){const n=$.readFileSync(s,"utf-8");return JSON.parse(n)}else return(await import(s)).default},k=({excludeKey:e,line:s})=>e?typeof e=="string"?s.includes(e):e.some(t=>s.includes(t)):!1,A=({localLines:e,excludeKey:s,extractedTranslations:t})=>e.filter(n=>k({line:n,excludeKey:s})||t.includes(n)?!1:!t.some(l=>l==="**"?/^\w+$/.test(n):new RegExp(`^${D(l).replace(/\\\*\\\*/g,".*?")}$`).test(n)));function D(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const q=({unusedLocalesCountByPath:e,totalUnusedLocales:s})=>{e.forEach(({messages:t,warning:n})=>{console.log(t??""),n&&console.log(n??"")}),console.log(`Total unused locales: \x1B[33m${s}\x1B[0m`)},z=({baseDir:e,regex:s,excludePatterns:t})=>{const n=[];function l(o){const c=$.readdirSync(o);for(const r of c){const a=j.join(o,r);t.some(i=>i.test(a))||($.lstatSync(a).isDirectory()?l(a):s.test($.readFileSync(a,"utf-8"))&&n.push(a))}}return l(e),n},N=({fileContent:e})=>{const s=/namespaceTranslation\(\s*['"`]([\s\S]*?)['"`]\s*,?\s*\)/g,t=/namespaceTranslation\(\s*([^()]*\?)\s*['"`]([^'"`]*?)['"`]\s*:\s*['"`]([^'"`]*?)['"`]\s*\)/,n=[];let l;for(;(l=s.exec(e))!==null;)n.push(l[1].trim());const o=e.match(t);if(o){const c=o[2].trim(),r=o[3].trim();n.push(c,r)}return n},U=({scopedName:e,namespaceTranslation:s,fileContent:t})=>{var y,P;const n=new RegExp(`${e}\\(\\s*['"\`']([\\s\\S]*?)['"\`']\\s*(?:,|\\))`,"g"),l=new RegExp(`${e}\\(\\s*([\\s\\S]+?)\\s*\\?\\s*['"\`']([^'"\\\`
]+)['"\`']\\s*:\\s*['"\`']([^'"\\\`
]+)['"\`'],?\\s*\\)`,"gm"),o=new RegExp(`${e}\\(\\s*([^?
]+)\\s*\\?\\s*['"\`']([^'"\\\`]+)['"\`']\\s*:\\s*['"\`']([^'"\\\`]+)['"\`'],\\s*\\{[\\s\\S]*?\\},?\\s*\\)`,"gm"),c=new RegExp(`${e}\\(\\s*([a-zA-Z_$][\\w.$]*)\\s*\\)`,"g"),r=new RegExp(`${e}\\(\\s*\`([\\s\\S]*?)\`\\s*\\)`,"g"),a=new Set,i=s.replace(/'/g,"").replace(/,/g,"");let d;for(;d=l.exec(t);){const p=d[2],u=d[3];a.add(`${i}.${p.replace(/'/g,"").replace(/,/g,"").trim()}`),a.add(`${i}.${u.replace(/'/g,"").replace(/,/g,"").trim()}`)}for(;d=o.exec(t);){const p=((y=d[2])==null?void 0:y.trim())??"",u=((P=d[3])==null?void 0:P.trim())??"";a.add(`${i}.${p.replace(/'/g,"").replace(/,/g,"")}`),a.add(`${i}.${u.replace(/'/g,"").replace(/,/g,"")}`)}for(;d=n.exec(t);){const p=d[1].trim(),u=`${i}.${p}`;if(p.includes("${")){const m=/\${([^}]*\s\?\s[^}]*:[^}]*)}/,f=u.match(m);if(f){const[S,x]=f,[T,E]=x.split(" ? ")[1].split(":").map(g=>g.trim().replace(/'/g,"")),w=`${u.replace(S,T)}`,h=`${u.replace(S,E)}`;a.add(w),a.add(h)}else a.add(`${u.replace(/\${[^}]*}/g,"**")}`)}else a.add(u)}for(;d=c.exec(t);){const p=`${i}.**`;a.add(p)}for(;d=r.exec(t);){const u=d[1].split(/(\$\{[^}]+\})/).map(f=>f.startsWith("${")&&f.endsWith("}")?"**":f),m=`${i}.${u.join("")}`;a.add(m)}return Array.from(a)},C=({fileContent:e})=>{const s=/t\(\s*['"`]([\s\S]*?)['"`]\s*(?:,|$|\))/g,t=/t\(\s*([\s\S]+?)\s*\?\s*['"`]([^'"`\n]+)['"`]\s*:\s*['"`]([^'"`\n]+)['"`]\s*\)/gm,n=/t\(\s*([a-zA-Z_$][\w.$]*)\s*\)/g,l=/t\(\s*`([\s\S]*?)`\s*\)/g,o=new Set;let c;for(;c=t.exec(e);){const r=c[2],a=c[3];o.add(r.replace(/'/g,"").replace(/,/g,"").trim()),o.add(a.replace(/'/g,"").replace(/,/g,"").trim())}for(;c=s.exec(e);){const r=c[1].trim();if(r.includes("${")){const a=/\${([^}]*\s\?\s[^}]*:[^}]*)}/,i=r.match(a);if(i){const[d,y]=i,[P,p]=y.split(":").map(f=>f.trim().replace(/'/g,"")),u=`${r.replace(d,P)}`,m=`${r.replace(d,p)}`;o.add(u),o.add(m)}else o.add(`${r.replace(/\${[^}]*}/g,"**")}`)}else o.add(r)}for(;c=n.exec(e);)o.add("**");for(;c=l.exec(e);){const a=c[1].split(/(\$\{[^}]+\})/).map(i=>i.startsWith("${")&&i.endsWith("}")?"**":i);o.add(a.join(""))}return Array.from(o)},I=({filePath:e,scopedNames:s})=>{const t=$.readFileSync(e,"utf-8"),n=N({fileContent:t}),l=((s==null?void 0:s.map(r=>n.flatMap(a=>U({fileContent:t,namespaceTranslation:a,scopedName:r}))))??[]).flat(2),c=[...C({fileContent:t}),...l];return[...new Set(c)].sort()},Z=({localePath:e,missingTranslations:s})=>{const t=$.readFileSync(e,"utf-8").split(`
`);let n=-1;const l=t.reduce((o,c,r)=>{const a=c.match(/^\s*'([^']+)':/);if(a){const i=a[1];s.includes(i)?(c.trim().endsWith("} as const")&&o.push(c),c.trim().endsWith(":")&&(n=r+1)):o.push(c)}else n===r?n=-1:o.push(c);return o},[]);$.writeFileSync(e,l.join(`
`),"utf-8")},L=async({paths:e,action:s})=>{const t=await O(),n=[/\.test\./,/__mock__/],l=t.localesExtensions??"js",o=t.localesNames??"en";let c=0;const r=[],a=e||t.paths,i=v.performance.now();a.forEach(({srcPath:y,localPath:P})=>{var S;let p=[],u=0;y.forEach(x=>{var h;if((h=t.ignorePaths)==null?void 0:h.some(g=>x.includes(g)))return;const w=z({excludePatterns:n,regex:/use-i18n/,baseDir:x}).flatMap(g=>I({filePath:g,scopedNames:t.scopedNames})).sort().filter((g,W,_)=>_.indexOf(g)===W);p=[...p,...w]}),p=[...new Set(p)].sort();const m=`${P}/${o}.${l}`,f=(S=t.ignorePaths)==null?void 0:S.some(x=>m.includes(x));if($.existsSync(m)&&!f){console.log(`${m}...`);const x=$.readFileSync(m,"utf-8").split(`
`).map(h=>h.trim()).filter(h=>h.match(/'[^']*':/)).map(h=>{var g;return((g=h.match(/'([^']+)':/))==null?void 0:g[1])??""}).sort(),T=A({localLines:x,extractedTranslations:p,excludeKey:t.excludeKey});u=T.length,c+=u;const E=T.map(h=>`\x1B[31m${h}\x1B[0m`).join(`
`),w=T.length?`Unused translations in \x1B[33m${m}\x1B[0m : \x1B[31m${u}
${E}\x1B[0m`:void 0;w&&r.push({path:P,messages:w}),s==="remove"&&Z({localePath:m,missingTranslations:T})}});const d=v.performance.now();q({unusedLocalesCountByPath:r,totalUnusedLocales:c}),console.log(`\x1B[38;2;128;128;128mDuration : ${(d-i).toFixed(0)}ms\x1B[0m`),c>0&&process.exit(1)},b=new B.Command;b.command("display").description("Display missing translations").option("--srcPath <path>","Source path to search for translations").option("--localPath <path>","Local path to the translation files").action(e=>{const s=e.srcPath&&e.localPath?[{srcPath:[e.srcPath],localPath:e.localPath}]:void 0;L({paths:s,action:"display"})});b.command("remove").description("Remove unused translations").option("--srcPath <path>","Source path to search for translations").option("--localPath <path>","Local path to the translation files").action(e=>{const s=e.srcPath&&e.localPath?[{srcPath:[e.srcPath],localPath:e.localPath}]:void 0;L({paths:s,action:"remove"})});b.parse(process.argv);