UNPKG

@vyxos/astro-i18next

Version:

I18next integration for Astro with dynamic namespace loading.

329 lines (314 loc) 10.1 kB
'use strict'; var fs = require('fs'); var pathe = require('pathe'); // src/config.ts function createBaseConfig(options) { return { lng: options.defaultLocale, fallbackLng: options.defaultLocale, supportedLngs: options.locales, defaultNS: options.defaultNamespace, fallbackNS: options.defaultNamespace, ns: [options.defaultNamespace] }; } // src/scripts.ts function generateServerScript(baseConfig, allTranslations, options) { return ` import i18next from "i18next"; const resources = ${JSON.stringify(allTranslations)}; globalThis.__astroI18nConfig = ${JSON.stringify(options)}; i18next.init({ ...${JSON.stringify(baseConfig)}, resources, initImmediate: false }).catch(err => console.error('[i18next] Server initialization failed:', err)); `; } function generateClientScript(baseConfig, options) { return ` import i18next from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; window.__astroI18nConfig = ${JSON.stringify(options)}; const dynamicBackend = { type: 'backend', init: function() {}, read: async function(language, namespace, callback) { try { const { loadTranslation } = await import('virtual:i18n-loader'); const data = await loadTranslation(language, namespace); callback(null, data); } catch (err) { console.warn(\`[i18next] Failed to load \${language}/\${namespace}:\`, err); callback(null, {}); } } }; // Global namespace loader for route-based loading window.__i18nLoadNamespaces = async function(namespaces) { const currentLang = i18next.language || '${baseConfig.lng}'; const promises = namespaces.map(ns => new Promise((resolve) => { if (i18next.hasResourceBundle(currentLang, ns)) { resolve(null); } else { i18next.loadNamespaces(ns, resolve); } }) ); await Promise.all(promises); }; i18next .use(LanguageDetector) .use(dynamicBackend) .init({ ...${JSON.stringify(baseConfig)}, initImmediate: false, detection: { order: ["htmlTag", "path"], lookupFromPathIndex: 0, caches: [] }, load: 'currentOnly', partialBundledLanguages: true, ns: [], // Start with empty namespaces defaultNS: false // Disable default namespace preloading }) .catch(err => console.error('[i18next] Client initialization failed:', err)); `; } function loadTranslation(srcDir, translationsDir, locale, namespace) { const filePath = pathe.resolve( srcDir, translationsDir, `${locale}/${namespace}.json` ); if (!fs.existsSync(filePath)) { console.warn( `[astro-i18next] Translation file not found: ${filePath} Expected structure: ${translationsDir}/${locale}/${namespace}.json` ); return {}; } try { const content = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(content); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { console.error( `[astro-i18next] Invalid translation file format: ${filePath} Expected JSON object, got ${typeof parsed}` ); return {}; } return parsed; } catch (error) { console.error( `[astro-i18next] Failed to parse translation file: ${filePath}`, error instanceof Error ? error.message : String(error) ); return {}; } } function loadAllTranslations(srcDir, options) { const allTranslations = {}; for (const locale of options.locales) { allTranslations[locale] = {}; for (const namespace of options.namespaces) { allTranslations[locale][namespace] = loadTranslation( srcDir, options.translationsDir, locale, namespace ); } } return allTranslations; } // src/validation.ts var I18nConfigError = class extends Error { constructor(message, field) { super(message); this.field = field; this.name = "I18nConfigError"; } }; function validateOptions(options) { if (!options) { throw new I18nConfigError("Integration options are required"); } if (!options.locales || !Array.isArray(options.locales) || options.locales.length === 0) { throw new I18nConfigError( "At least one locale must be specified", "locales" ); } if (!options.defaultLocale || typeof options.defaultLocale !== "string") { throw new I18nConfigError( "Default locale is required and must be a string", "defaultLocale" ); } if (!options.locales.includes(options.defaultLocale)) { throw new I18nConfigError( `Default locale "${options.defaultLocale}" must be included in locales array`, "defaultLocale" ); } if (!options.namespaces || !Array.isArray(options.namespaces) || options.namespaces.length === 0) { throw new I18nConfigError( "At least one namespace must be specified", "namespaces" ); } if (!options.defaultNamespace || typeof options.defaultNamespace !== "string") { throw new I18nConfigError( "Default namespace is required and must be a string", "defaultNamespace" ); } if (!options.namespaces.includes(options.defaultNamespace)) { throw new I18nConfigError( `Default namespace "${options.defaultNamespace}" must be included in namespaces array`, "defaultNamespace" ); } if (!options.translationsDir || typeof options.translationsDir !== "string") { throw new I18nConfigError( "Translations directory is required and must be a string", "translationsDir" ); } const invalidLocales = options.locales.filter( (locale) => typeof locale !== "string" || !/^[a-z]{2}(-[A-Z]{2})?$/.test(locale) ); if (invalidLocales.length > 0) { throw new I18nConfigError( `Invalid locale format: ${invalidLocales.join(", ")}. Use format: "en", "en-US"`, "locales" ); } const invalidNamespaces = options.namespaces.filter( (ns) => typeof ns !== "string" || !/^[a-zA-Z0-9_-]+$/.test(ns) ); if (invalidNamespaces.length > 0) { throw new I18nConfigError( `Invalid namespace format: ${invalidNamespaces.join(", ")}. Use only letters, numbers, hyphens, and underscores`, "namespaces" ); } } // src/vite-plugin.ts function createI18nVitePlugin(options, srcDir) { return { name: "i18n-virtual-modules", resolveId(id) { if (id === "virtual:i18n-config") return id; if (id === "virtual:i18n-loader") return id; const match = id.match(/^virtual:i18n-translation:(.+)\/(.+)$/); if (match) return id; const virtualMatch = id.match(/^\.?\/virtual-i18n-(.+)-(.+)\.js$/); if (virtualMatch) return `virtual:i18n-translation:${virtualMatch[1]}/${virtualMatch[2]}`; }, load(id) { if (id === "virtual:i18n-config") { return `export const i18nConfig = ${JSON.stringify(options)};`; } if (id === "virtual:i18n-loader") { return generateDynamicTranslationLoader(options); } const match = id.match(/^virtual:i18n-translation:(.+)\/(.+)$/); if (match) { const [, locale, namespace] = match; const translation = loadTranslation( srcDir, options.translationsDir, locale, namespace ); return `export default ${JSON.stringify(translation)};`; } } }; } function generateDynamicTranslationLoader(options) { const importMap = []; const caseStatements = []; options.locales.forEach((locale) => { options.namespaces.forEach((namespace) => { const importVar = `${locale}_${namespace}`.replace(/[^a-zA-Z0-9_]/g, "_"); importMap.push( `const ${importVar} = () => import('./virtual-i18n-${locale}-${namespace}.js');` ); caseStatements.push( ` case '${locale}/${namespace}': return (await ${importVar}()).default || {};` ); }); }); return ` ${importMap.join("\n")} export async function loadTranslation(locale, namespace) { try { const key = \`\${locale}/\${namespace}\`; switch (key) { ${caseStatements.join("\n")} default: console.warn(\`[i18next] Unknown translation: \${locale}/\${namespace}\`); return {}; } } catch (err) { console.warn(\`[i18next] Failed to load translation \${locale}/\${namespace}:\`, err); return {}; } } // Helper to preload specific namespaces export async function preloadNamespaces(locale, namespaces) { const promises = namespaces.map(ns => loadTranslation(locale, ns)); return Promise.all(promises); } // Available locales and namespaces for validation export const availableLocales = ${JSON.stringify(options.locales)}; export const availableNamespaces = ${JSON.stringify(options.namespaces)}; `; } // src/integration.ts function i18nIntegration(options) { return { name: "astro-i18next", hooks: { "astro:config:setup": async ({ config, injectScript, updateConfig }) => { try { validateOptions(options); const baseConfig = createBaseConfig(options); const allTranslations = loadAllTranslations( config.srcDir.pathname, options ); updateConfig({ vite: { plugins: [createI18nVitePlugin(options, config.srcDir.pathname)] } }); injectScript( "page-ssr", generateServerScript(baseConfig, allTranslations, options) ); injectScript( "before-hydration", generateClientScript(baseConfig, options) ); } catch (error) { if (error instanceof Error) { throw new Error( `[astro-i18next] Configuration error: ${error.message}` ); } throw error; } } } }; } exports.i18nIntegration = i18nIntegration; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map