feat: add full Zonemaster stack with Docker and Spanish UI
- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI) - Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend - Dockerfile.gui: Astro static build served via nginx - docker-compose.yml: backend (internal) + frontend (port 5353) - nginx.conf: root redirects to /es/, /api/ proxied to backend - zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
zonemaster-gui/scripts/generate-messages.ts
Normal file
166
zonemaster-gui/scripts/generate-messages.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Regex to find placeholders like {variableName}
|
||||
const placeholderRegex = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
||||
|
||||
interface Messages {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface AllMessages {
|
||||
[lang: string]: Messages;
|
||||
}
|
||||
|
||||
interface GenerateConfig {
|
||||
defaultLanguage: string;
|
||||
enabledLanguages: string[];
|
||||
}
|
||||
|
||||
function getFileHeader(): string {
|
||||
return `/* Auto-generated by vite-plugin-messages - DO NOT EDIT */\n/* Generated: ${new Date().toISOString()} */\n`;
|
||||
}
|
||||
|
||||
export function generateMessages(langDir: string, outDir: string, config: GenerateConfig) {
|
||||
const { defaultLanguage, enabledLanguages } = config;
|
||||
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
}
|
||||
|
||||
const allMessages: AllMessages = {};
|
||||
|
||||
for (const lang of enabledLanguages) {
|
||||
const filePath = path.join(langDir, `${lang}.json`);
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
allMessages[lang] = JSON.parse(content);
|
||||
} catch (e) {
|
||||
console.warn(`⚠️ Failed to load ${lang}.json:`, e instanceof Error ? e.message : String(e));
|
||||
allMessages[lang] = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(allMessages).length === 0) {
|
||||
console.warn('⚠️ No language files found');
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultMessages = allMessages[defaultLanguage] || {};
|
||||
const allKeys = Object.keys(defaultMessages);
|
||||
|
||||
for (const lang of enabledLanguages) {
|
||||
const messages = allMessages[lang] || {};
|
||||
const functions: string[] = [];
|
||||
const reExports: string[] = [];
|
||||
|
||||
for (const key of allKeys) {
|
||||
const hasTranslation = key in messages;
|
||||
|
||||
if (hasTranslation) {
|
||||
functions.push(createMessageFunction(key, messages[key]));
|
||||
} else if (lang !== defaultLanguage) {
|
||||
reExports.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
let content = getFileHeader();
|
||||
|
||||
if (reExports.length > 0) {
|
||||
content += `\nexport { ${reExports.join(', ')} } from './${defaultLanguage}.ts';\n`;
|
||||
}
|
||||
|
||||
if (functions.length > 0) {
|
||||
content += `\n${functions.join('\n\n')}\n`;
|
||||
}
|
||||
|
||||
const filePath = path.join(outDir, `${lang}.ts`);
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log(`✅ Generated messages/${lang}.ts (${functions.length} translated, ${reExports.length} re-exported)`);
|
||||
}
|
||||
|
||||
generateIndexFile(outDir, allKeys, defaultMessages, config);
|
||||
}
|
||||
|
||||
function extractPlaceholders(value: string): string[] {
|
||||
const matches = value.matchAll(placeholderRegex);
|
||||
const placeholders = new Set<string>();
|
||||
for (const match of matches) {
|
||||
placeholders.add(match[1]);
|
||||
}
|
||||
return Array.from(placeholders);
|
||||
}
|
||||
|
||||
function escapeTemplateString(value: string): string {
|
||||
// Escape backticks and ${} that are not our placeholders
|
||||
return value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`');
|
||||
}
|
||||
|
||||
function createMessageFunction(key: string, value: string): string {
|
||||
const placeholders = extractPlaceholders(value);
|
||||
const templateString = value.replace(placeholderRegex, '${params.$1}');
|
||||
const escapedTemplate = escapeTemplateString(templateString);
|
||||
|
||||
if (placeholders.length > 0) {
|
||||
const paramsType = placeholders.map(p => `${p}: string | number`).join(', ');
|
||||
return `export const ${key} = (params: { ${paramsType} }): string => \`${escapedTemplate}\`;`;
|
||||
}
|
||||
return `export const ${key} = (): string => \`${escapedTemplate}\`;`;
|
||||
}
|
||||
|
||||
function generateIndexFile(outDir: string, allKeys: string[], defaultMessages: Messages, config: GenerateConfig) {
|
||||
const { enabledLanguages } = config;
|
||||
|
||||
const langImports = enabledLanguages
|
||||
.map(lang => `import * as ${lang} from './${lang}.ts';`)
|
||||
.join('\n');
|
||||
|
||||
const langObject = enabledLanguages.join(', ');
|
||||
const languageType = enabledLanguages.map(l => `'${l}'`).join(' | ');
|
||||
const messageExports: string[] = [];
|
||||
|
||||
for (const key of allKeys) {
|
||||
const value = defaultMessages[key] || '';
|
||||
const placeholders = extractPlaceholders(value);
|
||||
const paramsType = placeholders.length > 0
|
||||
? `params: { ${placeholders.map(p => `${p}: string | number`).join(', ')} }`
|
||||
: '';
|
||||
const paramsArg = paramsType ? 'params' : '';
|
||||
|
||||
messageExports.push(
|
||||
`export const ${key} = (${paramsType}): string => allMessages[getLocale()].${key}(${paramsArg});`
|
||||
);
|
||||
}
|
||||
|
||||
const content = `${getFileHeader()}
|
||||
import config from '@/config';
|
||||
|
||||
${langImports}
|
||||
|
||||
const allMessages = { ${langObject} } as const;
|
||||
|
||||
export type Language = ${languageType};
|
||||
|
||||
export const defaultLanguage: Language = '${config.defaultLanguage}';
|
||||
export const enabledLanguages: readonly Language[] = ${JSON.stringify(config.enabledLanguages)} as const;
|
||||
|
||||
let _locale: Language = defaultLanguage;
|
||||
|
||||
export const getLocale = (): Language => _locale;
|
||||
export const isValidLocale = (locale: unknown): locale is Language => enabledLanguages.includes(locale as Language);
|
||||
export const setLocale = (locale: unknown): void => {
|
||||
if (isValidLocale(locale)) {
|
||||
_locale = locale as Language;
|
||||
}
|
||||
};
|
||||
|
||||
// Message functions
|
||||
${messageExports.join('\n\n')}
|
||||
`;
|
||||
|
||||
const filePath = path.join(outDir, 'index.ts');
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log('✅ Generated messages/index.ts');
|
||||
}
|
||||
Reference in New Issue
Block a user