import { defineConfig, loadEnv, Plugin } from 'vite'; import react from '@vitejs/plugin-react'; import { createHtmlPlugin } from 'vite-plugin-html'; import type { IncomingMessage, ServerResponse } from 'http'; import * as fs from 'fs'; import * as path from 'path'; type I18nEntry = { context: string; key: string; value: string }; /** Find the nearest enclosing function/component name above a given character offset. */ function _findEnclosingComponent(content: string, charOffset: number): string { const reFunc = /(?:export\s+)?(?:const|function)\s+([A-Z_]\w*)/g; let best = ''; let m: RegExpExecArray | null; while ((m = reFunc.exec(content)) !== null) { if (m.index > charOffset) break; best = m[1]; } return best; } /** Scan all .ts/.tsx under srcRoot for t() calls and return structured entries with context. */ function _scanTKeys(srcRoot: string): I18nEntry[] { const keyMap = new Map>(); const reSingle = /\bt\(\s*'((?:\\.|[^'])+)'\s*(?:,|\))/g; const reDouble = /\bt\(\s*"((?:\\.|[^"])+)"\s*(?:,|\))/g; const _addKey = (key: string, relFile: string, content: string, offset: number): void => { const comp = _findEnclosingComponent(content, offset); const ctx = comp ? `${relFile} > ${comp}` : relFile; if (!keyMap.has(key)) keyMap.set(key, new Set()); keyMap.get(key)!.add(ctx); }; const walk = (dir: string): void => { if (!fs.existsSync(dir)) return; for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, ent.name); if (ent.isDirectory()) { walk(full); } else if (/\.(tsx?)$/i.test(ent.name)) { const content = fs.readFileSync(full, 'utf-8'); const relFile = path.relative(srcRoot, full).replace(/\\/g, '/'); let m: RegExpExecArray | null; reSingle.lastIndex = 0; while ((m = reSingle.exec(content)) !== null) { const raw = m[1].replace(/\\'/g, "'").replace(/\\\\/g, '\\'); if (raw) _addKey(raw, relFile, content, m.index); } reDouble.lastIndex = 0; while ((m = reDouble.exec(content)) !== null) { const raw = m[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\'); if (raw) _addKey(raw, relFile, content, m.index); } } } }; walk(srcRoot); const entries: I18nEntry[] = []; for (const [key, contexts] of [...keyMap.entries()].sort((a, b) => a[0].localeCompare(b[0], 'de'))) { entries.push({ context: 'ui', key, value: [...contexts].sort().join(' | ') }); } return entries; } function extractI18nKeys(): Plugin { return { name: 'extract-i18n-keys', configureServer(server) { server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => { const url = req.url?.split('?')[0] ?? ''; if (!url.endsWith('/i18n-keys.json')) { next(); return; } const srcDir = path.join(process.cwd(), 'src'); const payload = JSON.stringify(_scanTKeys(srcDir), null, 2); res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.end(payload); }); }, writeBundle(options) { const outDir = options.dir ?? path.resolve(process.cwd(), 'dist'); const srcDir = path.join(process.cwd(), 'src'); const entries = _scanTKeys(srcDir); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } fs.writeFileSync(path.join(outDir, 'i18n-keys.json'), `${JSON.stringify(entries)}\n`, 'utf-8'); }, }; } // Custom plugin to serve static HTML files from public directory BEFORE SPA fallback function serveStaticHtml(): Plugin { return { name: 'serve-static-html', enforce: 'pre', configureServer(server) { // Directly adding middleware runs BEFORE internal Vite middlewares server.middlewares.use((req, res, next) => { // Check if request is for a static HTML file in public const url = req.url?.split('?')[0]; // Remove query string if (url && url.endsWith('.html') && url !== '/' && url !== '/index.html') { const filePath = path.join(process.cwd(), 'public', url); if (fs.existsSync(filePath)) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.statusCode = 200; res.end(fs.readFileSync(filePath, 'utf-8')); return; } } next(); }); }, }; } export default defineConfig(({ mode }) => { // Load env file based on mode const env = loadEnv(mode, process.cwd(), ''); return { plugins: [ // Serve static HTML files serveStaticHtml(), extractI18nKeys(), react(), createHtmlPlugin({ // Only process main index.html, not public static files pages: [ { entry: 'src/main.tsx', filename: 'index.html', template: 'index.html', injectOptions: { data: { VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn', }, }, }, ], }), ], envPrefix: 'VITE_', css: { modules: { scopeBehaviour: 'local', // Default behavior for CSS modules }, }, // Ensure public files are served correctly as static publicDir: 'public', }; });