@vyxos/astro-i18next
Version:
I18next integration for Astro with dynamic namespace loading.
329 lines (314 loc) • 10.1 kB
JavaScript
;
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