@gulibs/react-vintl
Version:
Type-safe i18n library for React with Vite plugin and automatic type inference
83 lines (71 loc) • 11.6 kB
JavaScript
const e=require(`./chunk-CUT6urMc.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.options={...s,...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(e){this.options.debug&&console.log(`[react-vintl] ${e}`)}logError(e){console.error(`[react-vintl] ${e}`)}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){if(!this.options.hmr){this.logDebug(`HMR disabled, skipping declaration file update.`);return}let r=this.collectAllResourceKeys(e),a=t.default.resolve(this.rootPath,`node_modules/@gulibs/react-vintl/react-vintl-locales.d.ts`),o=t.default.resolve(this.rootPath,`react-vintl-locales.d.ts`),s=(0,i.existsSync)(a)?a:o;try{let e;try{e=await n.default.readFile(s,`utf-8`)}catch{this.logDebug(`Declaration file not found, creating default template`),e=this.createDefaultDeclarationContent()}let t=r.length>0?r.map(e=>` | '${e}'`).join(`
`):` | string`,i=new Set;r.forEach(e=>{let t=e.indexOf(`.`);t>0&&i.add(e.substring(0,t))});let a=i.size>0?Array.from(i).sort().map(e=>` | '${e}'`).join(`
`):` | string`;e=e.replace(/export type I18nKeys =[\s\S]*?;/,`export type I18nKeys =\n${t};`),e=e.replace(/export type I18nNamespaces =[\s\S]*?;/,`export type I18nNamespaces =\n${a};`),await n.default.writeFile(s,e,`utf-8`),this.logDebug(`Updated declaration file with ${r.length} keys and ${i.size} namespaces`)}catch(e){this.logError(`Failed to update declaration file: ${e instanceof Error?e.message:String(e)}`)}}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();await this.updateDeclarationFile(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()),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()},buildEnd(){t.dispose()}}]}exports.createReactVintlPlugin=l;