From d17203de9236ff1198db9c5d8bfce3d9b61f4be1 Mon Sep 17 00:00:00 2001 From: Tobias Bleckert Date: Mon, 8 Dec 2025 10:40:00 +0100 Subject: [PATCH 1/6] Custom i18n solution to replace paraglide --- astro.config.mjs | 17 +- messages/da.json | 1 - messages/en.json | 1 - messages/es.json | 1 - messages/fi.json | 1 - messages/fr.json | 1 - messages/nb.json | 1 - messages/sl.json | 1 - messages/sv.json | 1 - package.json | 5 +- project.inlang/.gitignore | 1 - project.inlang/project_id | 1 - project.inlang/settings.json | 24 --- scripts/generate-messages.ts | 182 ++++++++++++++++++ scripts/messages-plugin.ts | 78 ++++++++ src/layouts/BaseLayout.astro | 4 +- src/lib/components/Alert/Alert.svelte | 2 +- src/lib/components/DomainTest/Advanced.svelte | 2 +- .../components/DomainTest/DomainTest.svelte | 6 +- src/lib/components/DomainTest/General.svelte | 2 +- src/lib/components/DomainTest/History.svelte | 6 +- .../components/DomainTest/Nameservers.svelte | 2 +- src/lib/components/DomainTest/Records.svelte | 2 +- .../components/DomainTest/ResultInfo.svelte | 2 +- .../ProgramVersions/ProgramVersions.svelte | 2 +- src/messages/.gitignore | 2 + src/middleware.ts | 12 ++ src/pages/[lang]/faq.astro | 2 +- src/pages/[lang]/result/[...id].astro | 2 +- src/pages/[lang]/result/index.astro | 2 +- src/themes/default/Article.astro | 2 +- src/themes/default/Faq.astro | 2 +- src/themes/default/Footer.astro | 2 +- src/themes/default/LanguageSwitcher.astro | 6 +- src/themes/default/Logo.astro | 4 +- src/themes/default/MobileMenuButton.astro | 2 +- src/themes/default/Navigation.astro | 6 +- src/themes/default/Result.astro | 2 +- src/themes/default/StartTest.astro | 2 +- 39 files changed, 317 insertions(+), 77 deletions(-) delete mode 100644 project.inlang/.gitignore delete mode 100644 project.inlang/project_id delete mode 100644 project.inlang/settings.json create mode 100644 scripts/generate-messages.ts create mode 100644 scripts/messages-plugin.ts create mode 100644 src/messages/.gitignore create mode 100644 src/middleware.ts diff --git a/astro.config.mjs b/astro.config.mjs index 593baaef..22f8894e 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,6 +1,5 @@ // @ts-check import { defineConfig } from 'astro/config'; -import paraglide from '@inlang/paraglide-astro'; import remarkGfm from 'remark-gfm'; import { remarkDefinitionList, @@ -9,6 +8,7 @@ import { import svelte from '@astrojs/svelte'; import node from '@astrojs/node'; import config from './src/config.js'; +import messagesPlugin, { messagesIntegration } from './scripts/messages-plugin.ts'; // https://astro.build/config export default defineConfig({ @@ -33,18 +33,21 @@ export default defineConfig({ }, integrations: [ - paraglide({ - // recommended settings - project: './project.inlang', - outdir: './src/paraglide' - }), svelte({ compilerOptions: { customElement: true } - }) + }), + messagesIntegration() ], + vite: { + plugins: [messagesPlugin({ + defaultLanguage: config.defaultLanguage, + enabledLanguages: config.enabledLanguages, + })], + }, + adapter: process.env.NODE_ENV === 'production' ? undefined : node({ mode: 'standalone' }), diff --git a/messages/da.json b/messages/da.json index 0ee3e966..3bb325d9 100644 --- a/messages/da.json +++ b/messages/da.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Kør test", "faqNav": "FAQ", "welcome": "Velkommen til Zonemaster", diff --git a/messages/en.json b/messages/en.json index dc34717d..d26c003a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Start test", "faqNav": "FAQ", "welcome": "Welcome to Zonemaster", diff --git a/messages/es.json b/messages/es.json index 311dc8b2..c21058e1 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Iniciar prueba", "faqNav": "Preguntas frecuentes", "welcome": "Bienvenido a Zonemaster", diff --git a/messages/fi.json b/messages/fi.json index 8b41352c..c8c3ac0a 100644 --- a/messages/fi.json +++ b/messages/fi.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Aloita testi", "faqNav": "UKK", "welcome": "Tervetuloa Zonemasteriin", diff --git a/messages/fr.json b/messages/fr.json index 06216b95..153a8bd1 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Démarrer un test", "faqNav": "FAQ", "welcome": "Bienvenue sur Zonemaster", diff --git a/messages/nb.json b/messages/nb.json index 7a8d051f..ad9c3abc 100644 --- a/messages/nb.json +++ b/messages/nb.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Start sjekk", "faqNav": "FAQ", "welcome": "Velkommen til Zonemaster", diff --git a/messages/sl.json b/messages/sl.json index 80b4d2b7..aad4efd5 100644 --- a/messages/sl.json +++ b/messages/sl.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Začni test", "faqNav": "Pogosta vprašanja", "welcome": "Dobrodošli v Zonemaster", diff --git a/messages/sv.json b/messages/sv.json index 4a2ff5bf..0831cc59 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -1,5 +1,4 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", "startTestNav": "Kör test", "faqNav": "Vanliga frågor", "welcome": "Välkommen till Zonemaster", diff --git a/package.json b/package.json index 3627a491..0f7b93fb 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,12 @@ "scripts": { "start": "npm run dev", "dev": "astro dev", - "build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro build", + "build": "astro build", "release": "npm run build && node scripts/create_release.js", "preview": "astro preview", "astro": "astro", "lib:build": "cd src/lib && vite build -c ./vite.config.js --outDir ../../dist --emptyOutDir", "lib:demo": "vite serve demo -c src/lib/vite.config.js", - "postinstall": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide", "e2e": "playwright --version && playwright test", "e2e:update": "playwright --version && playwright test --update-snapshots", "e2e:install": "playwright install chromium", @@ -22,7 +21,6 @@ "dependencies": { "@astrojs/node": "^9.4.3", "@astrojs/svelte": "^7.1.1", - "@inlang/paraglide-astro": "^0.4.1", "astro": "^5.13.7", "punycode": "^2.3.1", "remark-definition-list": "^2.0.0", @@ -30,7 +28,6 @@ "svelte": "^5.38.9" }, "devDependencies": { - "@inlang/paraglide-js": "1.11.8", "@playwright/test": "^1.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.0", "@tsconfig/svelte": "^5.0.5", diff --git a/project.inlang/.gitignore b/project.inlang/.gitignore deleted file mode 100644 index 5e465967..00000000 --- a/project.inlang/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cache \ No newline at end of file diff --git a/project.inlang/project_id b/project.inlang/project_id deleted file mode 100644 index bda1c429..00000000 --- a/project.inlang/project_id +++ /dev/null @@ -1 +0,0 @@ -dd895364bbd879a5341f84299cef445ede6b51ac31ab10dd96219bd7711a9d83 \ No newline at end of file diff --git a/project.inlang/settings.json b/project.inlang/settings.json deleted file mode 100644 index fc45dfbe..00000000 --- a/project.inlang/settings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "https://inlang.com/schema/project-settings", - "sourceLanguageTag": "en", - "languageTags": [ - "en", - "da", - "es", - "fi", - "fr", - "nb", - "sv", - "sl" - ], - "modules": [ - "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js", - "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js" - ], - "plugin.inlang.messageFormat": { - "pathPattern": "./messages/{languageTag}.json" - } -} diff --git a/scripts/generate-messages.ts b/scripts/generate-messages.ts new file mode 100644 index 00000000..eff5a832 --- /dev/null +++ b/scripts/generate-messages.ts @@ -0,0 +1,182 @@ +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 getTimestamp(): string { + return new Date().toISOString(); +} + +function getFileHeader(): string { + return `/* Auto-generated by vite-plugin-messages - DO NOT EDIT */\n/* Generated: ${getTimestamp()} */\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`); + if (fs.existsSync(filePath)) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + + allMessages[lang] = JSON.parse(content); + } catch (e) { + console.error(`❌ Failed to parse ${filePath}`, e); + } + } else { + console.warn(`⚠️ Language file not found: ${filePath}`); + 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) { + const value = messages[key]; + const placeholders = extractPlaceholders(value); + + if (placeholders.length > 0) { + const paramsType = placeholders.map(p => `${p}: string | number`).join(', '); + const templateString = value.replace(placeholderRegex, '${params.$1}'); + functions.push( + `export const ${key} = (params: { ${paramsType} }): string => \`${escapeTemplateString(templateString)}\`;` + ); + } else { + functions.push( + `export const ${key} = (): string => \`${escapeTemplateString(value)}\`;` + ); + } + } 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(); + 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 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); + + if (placeholders.length > 0) { + const paramsType = placeholders.map(p => `${p}: string | number`).join(', '); + messageExports.push( + `export const ${key} = (params: { ${paramsType} }): string => { + return allMessages[getLocale()].${key}(params); +};` + ); + } else { + messageExports.push( + `export const ${key} = (): string => { + return allMessages[getLocale()].${key}(); +};` + ); + } + } + + 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 setLocale = (locale: unknown): void => { + if (enabledLanguages.includes(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'); +} diff --git a/scripts/messages-plugin.ts b/scripts/messages-plugin.ts new file mode 100644 index 00000000..2a86604e --- /dev/null +++ b/scripts/messages-plugin.ts @@ -0,0 +1,78 @@ +import path from 'path'; +import { generateMessages } from './generate-messages.ts'; + +interface MessagesPluginConfig { + defaultLanguage: string; + enabledLanguages: string[]; +} + +function validateConfig(config: any): asserts config is MessagesPluginConfig { + if (!config || typeof config !== 'object') { + throw new Error('messagesPlugin: config is required and must be an object'); + } + if (!config.defaultLanguage || typeof config.defaultLanguage !== 'string') { + throw new Error('messagesPlugin: config.defaultLanguage is required and must be a string'); + } + if (!Array.isArray(config.enabledLanguages) || config.enabledLanguages.length === 0) { + throw new Error('messagesPlugin: config.enabledLanguages is required and must be a non-empty array'); + } + if (!config.enabledLanguages.every((lang: any) => typeof lang === 'string')) { + throw new Error('messagesPlugin: config.enabledLanguages must contain only strings'); + } +} + +export function messagesIntegration() { + return { + name: 'messages-integration', + hooks: { + 'astro:config:setup': ({ injectScript }: any) => { + injectScript( + 'before-hydration', + ` + import { setLocale } from '@/messages'; + setLocale(document.documentElement.lang); + ` + ); + }, + }, + }; +} + +export default function messagesPlugin(config: MessagesPluginConfig) { + validateConfig(config); + + const langDir = path.resolve(process.cwd(), 'messages'); + const outDir = path.resolve(process.cwd(), 'src/messages'); + + const regenerate = () => { + try { + console.log('🔄 Generating message files...'); + generateMessages(langDir, outDir, config); + } catch (error) { + console.error('❌ Failed to generate message files:', error); + throw error; + } + }; + + return { + name: 'vite-plugin-messages', + + buildStart() { + regenerate(); + }, + + configureServer(server: any) { + server.watcher.add(langDir); + }, + + handleHotUpdate({ file, server }: { file: string; server: any }) { + if (file.startsWith(langDir) && file.endsWith('.json')) { + console.log('♻️ Language file changed:', path.basename(file)); + regenerate(); + // Trigger a full reload to pick up the new messages + server.ws.send({ type: 'full-reload' }); + return []; + } + }, + }; +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 32bc1850..54df4675 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,5 +1,5 @@ --- -import { languageTag } from '@/paraglide/runtime'; +import { getLocale } from '@/messages'; import '@theme/styles/styles.css'; import '@/assets/fonts/bootstrap-icons.min.css'; import '@/lib/styles/style.css'; @@ -13,7 +13,7 @@ const title = defaultTitle ? `${defaultTitle} – Zonemaster` : `Zonemaster`; --- - + diff --git a/src/lib/components/Alert/Alert.svelte b/src/lib/components/Alert/Alert.svelte index fdd528c6..79c45b68 100644 --- a/src/lib/components/Alert/Alert.svelte +++ b/src/lib/components/Alert/Alert.svelte @@ -2,7 +2,7 @@ import type { Snippet } from 'svelte'; import type { Severity } from '@/lib/alert.svelte.ts'; import Button from '../Button/Button.svelte'; - import * as m from '@/paraglide/messages'; + import * as m from '@/messages'; type Props = { severity: Severity; diff --git a/src/lib/components/DomainTest/Advanced.svelte b/src/lib/components/DomainTest/Advanced.svelte index 46408cd6..6c92c610 100644 --- a/src/lib/components/DomainTest/Advanced.svelte +++ b/src/lib/components/DomainTest/Advanced.svelte @@ -1,5 +1,5 @@