UNPKG

@gulibs/react-vintl

Version:

Type-safe i18n library for React with Vite plugin and automatic type inference

83 lines (71 loc) 12.5 kB
const e=require(`./logger-qt2bO9d_.cjs`);let t=require(`path`);t=e.__toESM(t);let n=require(`fs/promises`);n=e.__toESM(n);let r=require(`fast-glob`);r=e.__toESM(r);let i=require(`fs`);i=e.__toESM(i);var a=`virtual:@gulibs/react-vintl/generated-locales`,o=[`@gulibs/react-vintl-locales`,`@gulibs/react-vintl/react-vintl-locales`],s={basePath:`src/locales`,extensions:[`.json`,`.ts`,`.js`],localePattern:`directory`,hmr:!0,deep:!0,exclude:[],include:[],debug:!1},c=class{constructor(e={}){this.cache=new Map,this.isGenerating=!1,this.generationPromise=null,this.rootPath=process.cwd(),this.isBuildMode=!1,this.options={...s,...e}}setBuildMode(e){this.isBuildMode=e}setRootPath(e){this.rootPath=e}setupServer(e){this.server=e,this.options.hmr&&this.setupWatcher(e.watcher)}setupWatcher(e){let n=t.default.resolve(this.rootPath,this.options.basePath);e.on(`add`,async e=>{e.startsWith(n)&&(this.logDebug(`Added locale file: ${e}`),await this.invalidateCache())}),e.on(`change`,async e=>{e.startsWith(n)&&(this.logDebug(`Changed locale file: ${e}`),await this.invalidateCache())}),e.on(`unlink`,async e=>{e.startsWith(n)&&(this.logDebug(`Removed locale file: ${e}`),await this.invalidateCache())})}async invalidateCache(){this.cache.clear(),this.isGenerating=!1,this.generationPromise=null;try{if(!this.options.hmr){this.logDebug(`HMR disabled, skipping type update on file change.`);return}let e=await this.parseLocales();await this.updateDeclarationFile(e),this.logDebug(`Cache invalidated, will regenerate on next request`)}catch(e){this.logError(`Failed to update types on file change: ${e instanceof Error?e.message:String(e)}`)}if(this.server)for(let e of o){let t=this.server.moduleGraph.getModuleById(`${a}?id=${e}`);t&&(this.server.reloadModule(t),this.logDebug(`Hot reloaded locale resources for ${e}`))}}logDebug(t){this.options.debug&&e.logger.debug(t)}logError(t){e.logger.error(t)}async initialize(){try{if(this.options.hmr){let e=await this.parseLocales();await this.updateDeclarationFile(e)}}catch(e){this.logError(`Failed to initialize: ${e instanceof Error?e.message:String(e)}`)}}parseRequest(e){let[t,n]=e.split(`?`,2),r=new URLSearchParams(n),i=r.get(`id`);return{moduleId:t,query:r,pageId:i}}async parseFileContent(e){try{let r=t.default.extname(e);switch(r){case`.json`:{let t=await n.default.readFile(e,`utf-8`);return JSON.parse(t)}case`.js`:{let t=await import(`file://${e}?t=${Date.now()}`);return t.default||t}case`.ts`:{let t=(await n.default.readFile(e,`utf-8`)).replace(/:\s*[^=,;\{\}\[\]]+/g,``).replace(/export\s+type\s+[^;]+;/g,``).replace(/import\s+type\s+[^;]+;/g,``),r=e.replace(`.ts`,`.temp.mjs`);await n.default.writeFile(r,t);try{let e=await import(`file://${r}?t=${Date.now()}`),t=e.default||e;return await n.default.unlink(r).catch(()=>{}),t}catch(e){throw await n.default.unlink(r).catch(()=>{}),e}}default:throw Error(`Unsupported file extension: ${r}`)}}catch(n){let r=n instanceof Error?n.message:String(n);throw Error(`Failed to parse file: ${t.default.basename(e)} - ${r}`)}}async scanLocaleFiles(e){let n=this.options.extensions.map(e=>t.default.posix.join(`**`,`*${e}`).replace(/\\/g,`/`));try{let i=await(0,r.default)(n,{cwd:e,absolute:!0,onlyFiles:!0,ignore:[`**/node_modules/**`,`**/dist/**`,`**/.git/**`,...this.options.exclude]});return this.options.include&&this.options.include.length>0?i.filter(n=>{let r=t.default.relative(e,n);return this.options.include.some(e=>r.includes(e))}):i}catch{return this.logDebug(`Failed to scan directory: ${e}`),[]}}extractLocaleFromPath(e,n){let r=e.replace(/\\/g,`/`),i=n.replace(/\\/g,`/`),a=r.replace(i+`/`,``);if(this.options.localePattern===`directory`){let e=a.split(`/`);return e.length>0?e[0]:null}else{let e=t.default.basename(a,t.default.extname(a)).match(/\.([a-z]{2}(-[A-Z]{2})?)$/);return e?e[1]:null}}extractNamespaceFromPath(e,n,r){let i=e.replace(/\\/g,`/`),a=n.replace(/\\/g,`/`),o=i.replace(a+`/`,``);if(this.options.localePattern===`directory`){let e=o.replace(`${r}/`,``);return e.replace(t.default.extname(e),``).replace(/\//g,`.`)}else return t.default.basename(o,t.default.extname(o)).replace(/\.[a-z]{2}(-[A-Z]{2})?$/,``)}async parseLocales(e){let r=e||this.rootPath,i=t.default.resolve(r,this.options.basePath);try{await n.default.access(i)}catch{return this.logDebug(`Locales directory not found: ${i}`),{}}let a=await this.scanLocaleFiles(i);this.logDebug(`Found ${a.length} locale files`);let o={};for(let e of a)try{let n=this.extractLocaleFromPath(e,i);if(!n){this.logDebug(`Could not extract locale code from: ${e}`);continue}let r=this.extractNamespaceFromPath(e,i,n),a=await this.parseFileContent(e);o[n]||(o[n]={});let s=r||`default`;o[n][s]?o[n][s]=this.deepMerge(o[n][s],a):o[n][s]=a,this.logDebug(`✓ Parsed: ${n}/${s} <- ${t.default.basename(e)}`)}catch{let n=t.default.relative(i,e);this.logDebug(`✗ Failed to parse locale file: ${n}`)}return o}deepMerge(e,t){if(typeof e!=`object`||!e||Array.isArray(e))return t;if(typeof t!=`object`||!t||Array.isArray(t))return e;let n={...e};for(let r in t)if(Object.prototype.hasOwnProperty.call(t,r)){let i=t[r],a=e[r];typeof i==`object`&&i&&!Array.isArray(i)&&typeof a==`object`&&a&&!Array.isArray(a)?n[r]=this.deepMerge(a,i):i!==void 0&&(n[r]=i)}return n}collectAllResourceKeys(e){let t=new Set;return Object.values(e).forEach(e=>{Object.entries(e).forEach(([e,n])=>{e===`default`?this.collectKeysFromObject(n,``,t):this.collectKeysFromObject(n,e,t)})}),Array.from(t).sort()}collectKeysFromObject(e,t,n){typeof e!=`object`||!e||Array.isArray(e)||Object.keys(e).forEach(r=>{let i=t?`${t}.${r}`:r,a=e[r];typeof a==`object`&&a&&!Array.isArray(a)?this.collectKeysFromObject(a,i,n):n.add(i)})}async updateDeclarationFile(e,r=!1){if(!this.options.hmr&&!this.isBuildMode){this.logDebug(`HMR disabled and not in build mode, skipping declaration file update.`);return}let a=this.collectAllResourceKeys(e),o=t.default.resolve(this.rootPath,`node_modules/@gulibs/react-vintl/react-vintl-locales.d.ts`),s=t.default.resolve(this.rootPath,`react-vintl-locales.d.ts`),c=(0,i.existsSync)(o)?o:s,l=a.length>0?a.map(e=>` | '${e}'`).join(` `):` | string`,u=new Set;a.forEach(e=>{let t=e.indexOf(`.`);t>0&&u.add(e.substring(0,t))});let d=u.size>0?Array.from(u).sort().map(e=>` | '${e}'`).join(` `):` | string`,f;try{f=await n.default.readFile(c,`utf-8`)}catch{this.logDebug(`Declaration file not found, creating default template`),f=this.createDefaultDeclarationContent()}let p=f;if(p=p.replace(/export type I18nKeys =[\s\S]*?;/,`export type I18nKeys =\n${l};`),p=p.replace(/export type I18nNamespaces =[\s\S]*?;/,`export type I18nNamespaces =\n${d};`),!r&&p===f){this.logDebug(`Declaration file content unchanged, skipping write`);return}let m=null;for(let e=1;e<=3;e++)try{await n.default.writeFile(c,p,`utf-8`);try{let e=new Date;await n.default.utimes(c,e,e),this.logDebug(`Touched declaration file to trigger TypeScript server reload`)}catch(e){this.logDebug(`Failed to touch declaration file: ${e instanceof Error?e.message:String(e)}`)}this.logDebug(`Updated declaration file with ${a.length} keys and ${u.size} namespaces (attempt ${e})`);return}catch(t){m=t instanceof Error?t:Error(String(t)),e<3&&(this.logDebug(`Failed to update declaration file (attempt ${e}/3), retrying in 100ms...`),await new Promise(e=>setTimeout(e,100)))}this.logError(`Failed to update declaration file after 3 attempts: ${m?.message||`Unknown error`}`)}createDefaultDeclarationContent(){return`declare module '@gulibs/react-vintl-locales' { /** * 翻译资源对象(运行时从虚拟模块导入) */ export const resources: Record<string, Record<string, any>>; /** * 支持的语言列表 */ export const supportedLocales: readonly string[]; /** * 插件配置信息 */ export const config: { readonly supportedLocales: readonly string[]; readonly basePath: string; readonly localePattern: 'directory' | 'filename'; }; /** * 所有翻译键的数组(用于运行时验证) */ export const keys: readonly string[]; /** * 翻译资源的类型(自动从虚拟模块推导) */ export type I18nResources = typeof resources; /** * 支持的语言类型 */ export type I18nLocales = typeof supportedLocales[number]; /** * 翻译键的联合类型(由插件自动生成) * 提供完美的 IDE 自动补全支持 * * ⚠️ 此类型在开发时由插件自动更新,请勿手动修改 */ export type I18nKeys = | string; /** * 命名空间的联合类型(由插件自动生成) * 提供完美的 IDE 自动补全支持 * * ⚠️ 此类型在开发时由插件自动更新,请勿手动修改 */ export type I18nNamespaces = | string; export default resources; } declare module '@gulibs/react-vintl/react-vintl-locales' { export * from '@gulibs/react-vintl-locales'; } `}generateTypeScriptExports(e){let t=``;t+=`// Auto-generated by @gulibs/react-vintl - Do not edit manually! `,t+=`// Generated at: ${new Date().toISOString()}\n\n`,t+=`const resources = `,t+=this.stringifyResources(e,0),t+=`; `;let n=Object.keys(e),r=this.collectAllResourceKeys(e);return t+=`/** * 翻译资源对象 */ `,t+=`export { resources }; `,t+=`export default resources; `,t+=`/** * 支持的语言列表 */ `,t+=`export const supportedLocales = ${JSON.stringify(n)};\n\n`,t+=`/** * 插件配置信息 */ `,t+=`export const config = { `,t+=` supportedLocales: ${JSON.stringify(n)},\n`,t+=` basePath: ${JSON.stringify(this.options.basePath)},\n`,t+=` localePattern: ${JSON.stringify(this.options.localePattern)}\n`,t+=`}; `,t+=`/** * 所有翻译键的数组(用于运行时验证) */ `,t+=`export const keys = ${JSON.stringify(r)};\n\n`,t}stringifyResources(e,t=0){if(e===null)return`null`;if(e===void 0)return`undefined`;if(typeof e==`string`)return JSON.stringify(e);if(typeof e==`number`||typeof e==`boolean`)return String(e);if(Array.isArray(e)){let n=` `.repeat(t+2);return`[\n${e.map(e=>`${n}${this.stringifyResources(e,t+2)}`).join(`, `)}\n${` `.repeat(t)}]`}if(typeof e==`object`&&e){let n=` `.repeat(t+2),r=Object.entries(e);return r.length===0?`{}`:`{\n${r.map(([e,r])=>{let i=this.needsQuotes(e)?JSON.stringify(e):e;return`${n}${i}: ${this.stringifyResources(r,t+2)}`}).join(`, `)}\n${` `.repeat(t)}}`}return String(e)}needsQuotes(e){return!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(e)}async generateLocales(e=!1){if(this.isGenerating&&this.generationPromise&&!e){this.logDebug(`Waiting for existing locale generation...`);let e=await this.generationPromise;if(e)return e}let t=`locales`,n=this.cache.get(t);if(e)this.cache.delete(t),this.isGenerating=!1,this.generationPromise=null;else{let e=this.options.hmr?1e3:5e3;if(n&&Date.now()-n.timestamp<e)return n.content}this.isGenerating=!0,this.generationPromise=(async()=>{try{let e=await this.parseLocales();try{await this.updateDeclarationFile(e,this.isBuildMode)}catch(e){if(this.isBuildMode)this.logError(`⚠️ Build-time type file update failed. Type checking may fail. Error: ${e instanceof Error?e.message:String(e)}`),this.logError(`💡 Tip: Try restarting the TypeScript server or check file permissions.`);else throw e}let n=this.generateTypeScriptExports(e);return this.cache.set(t,{content:n,timestamp:Date.now(),localeCount:Object.keys(e).length}),this.logDebug(`Generated ${Object.keys(e).length} locales with TypeScript types`),n}catch(e){this.logError(`Failed to generate locales: ${e instanceof Error?e.message:String(e)}`);return}finally{this.isGenerating=!1,this.generationPromise=null}})();let r=await this.generationPromise;if(r===void 0)throw Error(`Failed to generate locales.`);return r}dispose(){this.cache.clear(),this.server=void 0}};function l(e={}){let t=new c(e);return[{name:`@gulibs/react-vintl`,enforce:`pre`,async configResolved(e){t.setRootPath(e.root||process.cwd()),t.setBuildMode(e.command===`build`),e.command===`serve`&&await t.initialize()},configureServer(e){t.setupServer(e)},resolveId(e){if(o.includes(e))return`${a}?id=${e}`;if(e.includes(`react-vintl-locales`)&&(e.includes(`dist/`)||e.includes(`node_modules`))){let t=o.find(t=>{let n=t.replace(`@gulibs/`,``);return e.includes(n)||e.includes(`react-vintl-locales`)});if(t)return`${a}?id=${t}`}if(e.endsWith(`react-vintl-locales`)||e.endsWith(`react-vintl-locales.js`))return`${a}?id=${o[0]}`},async load(e){let{moduleId:n,pageId:r}=t.parseRequest(e);if(n===a&&r&&o.includes(r))return await t.generateLocales()},async buildEnd(){t.dispose()}}]}exports.createReactVintlPlugin=l;