@pulzar/core
Version:
Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support
403 lines • 14.7 kB
JavaScript
import i18next from "i18next";
import Backend from "i18next-fs-backend";
import { watch } from "chokidar";
import { join } from "path";
import { logger } from "../utils/logger";
export class I18nManager {
i18n;
options;
watcher;
initialized = false;
constructor(options = {}) {
this.options = {
defaultLanguage: "en",
supportedLanguages: ["en"],
localesPath: "src/i18n/locales",
fallbackLanguage: "en",
loadPath: "{{lng}}/{{ns}}.json",
addPath: "{{lng}}/{{ns}}.missing.json",
enableHotReload: process.env.NODE_ENV === "development",
detectionOrder: ["query", "header", "cookie", "session"],
cookieName: "i18n_language",
queryParameter: "lang",
headerName: "accept-language",
sessionKey: "language",
pathIndex: 0,
subdomainIndex: 0,
caching: true,
preload: [],
debug: process.env.NODE_ENV === "development",
...options,
};
this.i18n = i18next.createInstance();
}
/**
* Initialize the i18n system
*/
async initialize() {
if (this.initialized) {
throw new Error("I18nManager already initialized");
}
logger.info("Initializing i18n system", {
defaultLanguage: this.options.defaultLanguage,
supportedLanguages: this.options.supportedLanguages,
localesPath: this.options.localesPath,
});
await this.i18n.use(Backend).init({
lng: this.options.defaultLanguage,
fallbackLng: this.options.fallbackLanguage,
supportedLngs: this.options.supportedLanguages,
preload: this.options.preload.length > 0
? this.options.preload
: this.options.supportedLanguages,
debug: this.options.debug,
backend: {
loadPath: join(this.options.localesPath, this.options.loadPath),
addPath: join(this.options.localesPath, this.options.addPath),
},
interpolation: {
escapeValue: false, // React already does escaping
},
detection: {
order: this.options.detectionOrder,
lookupQuerystring: this.options.queryParameter,
lookupCookie: this.options.cookieName,
lookupHeader: this.options.headerName,
lookupSession: this.options.sessionKey,
lookupPath: this.options.pathIndex,
lookupSubdomain: this.options.subdomainIndex,
caches: this.options.caching ? ["cookie"] : [],
},
react: {
useSuspense: false,
},
});
if (this.options.enableHotReload) {
this.setupHotReload();
}
this.initialized = true;
logger.info("I18n system initialized", {
languages: this.i18n.languages,
loadedNamespaces: this.i18n.loadNamespaces,
});
}
/**
* Setup hot reload for locale files
*/
setupHotReload() {
const watchPath = join(this.options.localesPath, "**/*.json");
this.watcher = watch(watchPath, {
persistent: true,
ignoreInitial: true,
});
this.watcher.on("change", async (filePath) => {
logger.debug("Locale file changed, reloading", { filePath });
try {
// Extract language and namespace from file path
const pathParts = filePath
.replace(this.options.localesPath, "")
.split("/")
.filter(Boolean);
if (pathParts.length >= 2) {
const language = pathParts[0];
const namespace = pathParts[1]?.replace(".json", "");
// Reload the resource bundle
await this.i18n.reloadResources(language, namespace);
logger.info("Locale reloaded", { language, namespace });
}
}
catch (error) {
logger.error("Failed to reload locale", { filePath, error });
}
});
logger.info("I18n hot reload enabled", { watchPath });
}
/**
* Detect language from request
*/
detectLanguageFromRequest(request) {
// Check query parameter
if (this.options.detectionOrder.includes("query")) {
const queryLang = request.query?.[this.options.queryParameter];
if (queryLang && this.isLanguageSupported(queryLang)) {
return queryLang;
}
}
// Check headers
if (this.options.detectionOrder.includes("header")) {
const acceptLanguage = request.headers[this.options.headerName.toLowerCase()];
if (acceptLanguage) {
const detectedLang = this.parseAcceptLanguage(acceptLanguage);
if (detectedLang && this.isLanguageSupported(detectedLang)) {
return detectedLang;
}
}
}
// Check cookies
if (this.options.detectionOrder.includes("cookie")) {
const cookieLang = this.parseCookies(request.headers.cookie || "")?.[this.options.cookieName];
if (cookieLang && this.isLanguageSupported(cookieLang)) {
return cookieLang;
}
}
// Check session (if available)
if (this.options.detectionOrder.includes("session")) {
const sessionLang = request.session?.[this.options.sessionKey];
if (sessionLang && this.isLanguageSupported(sessionLang)) {
return sessionLang;
}
}
// Check URL path
if (this.options.detectionOrder.includes("path")) {
const pathSegments = request.url.split("/").filter(Boolean);
if (pathSegments.length > this.options.pathIndex) {
const pathLang = pathSegments[this.options.pathIndex];
if (pathLang && this.isLanguageSupported(pathLang)) {
return pathLang;
}
}
}
// Check subdomain
if (this.options.detectionOrder.includes("subdomain")) {
const hostname = request.headers.host || "";
const subdomains = hostname.split(".").reverse();
if (subdomains.length > this.options.subdomainIndex + 1) {
const subdomainLang = subdomains[subdomains.length - 1 - this.options.subdomainIndex];
if (subdomainLang && this.isLanguageSupported(subdomainLang)) {
return subdomainLang;
}
}
}
return this.options.defaultLanguage;
}
/**
* Parse Accept-Language header
*/
parseAcceptLanguage(acceptLanguage) {
const languages = acceptLanguage
.split(",")
.map((lang) => {
const [code, quality = "1"] = lang.trim().split(";q=");
return {
code: code?.split("-")[0] || code, // Take only language part, ignore region
quality: parseFloat(quality),
};
})
.sort((a, b) => b.quality - a.quality);
for (const lang of languages) {
if (lang.code && this.isLanguageSupported(lang.code)) {
return lang.code;
}
}
return null;
}
/**
* Parse cookies header
*/
parseCookies(cookieHeader) {
const cookies = {};
cookieHeader.split(";").forEach((cookie) => {
const [name, value] = cookie.trim().split("=");
if (name && value) {
cookies[name] = decodeURIComponent(value);
}
});
return cookies;
}
/**
* Check if language is supported
*/
isLanguageSupported(language) {
return this.options.supportedLanguages.includes(language);
}
/**
* Create i18n context for a request
*/
createContext(request) {
const language = this.detectLanguageFromRequest(request);
const clonedI18n = this.i18n.cloneInstance({ lng: language });
return {
language,
t: clonedI18n.t.bind(clonedI18n),
i18n: clonedI18n,
detectLanguage: () => this.detectLanguageFromRequest(request),
changeLanguage: async (newLanguage) => {
if (!this.isLanguageSupported(newLanguage)) {
throw new Error(`Language "${newLanguage}" is not supported`);
}
await clonedI18n.changeLanguage(newLanguage);
return clonedI18n.t.bind(clonedI18n);
},
getLanguages: () => this.options.supportedLanguages,
getResourceBundle: (lang, namespace = "translation") => {
return clonedI18n.getResourceBundle(lang, namespace);
},
};
}
/**
* Get main i18n instance
*/
getInstance() {
return this.i18n;
}
/**
* Get supported languages
*/
getSupportedLanguages() {
return this.options.supportedLanguages;
}
/**
* Get default language
*/
getDefaultLanguage() {
return this.options.defaultLanguage;
}
/**
* Add new language support
*/
async addLanguage(language, resources) {
if (!this.options.supportedLanguages.includes(language)) {
this.options.supportedLanguages.push(language);
}
if (resources) {
this.i18n.addResourceBundle(language, "translation", resources);
}
logger.info("Language added", { language });
}
/**
* Remove language support
*/
removeLanguage(language) {
const index = this.options.supportedLanguages.indexOf(language);
if (index > -1) {
this.options.supportedLanguages.splice(index, 1);
}
this.i18n.removeResourceBundle(language, "translation");
logger.info("Language removed", { language });
}
/**
* Shutdown i18n system
*/
async shutdown() {
if (this.watcher) {
await this.watcher.close();
this.watcher = undefined;
}
this.initialized = false;
logger.info("I18n system shutdown");
}
}
/**
* Fastify plugin for i18n
*/
export function createI18nPlugin(options = {}) {
return async function i18nPlugin(fastify) {
const i18nManager = new I18nManager(options);
await i18nManager.initialize();
// Add i18n to Fastify instance
fastify.decorate("i18n", i18nManager);
// Add hook to inject i18n context into requests
fastify.addHook("onRequest", async (request, reply) => {
const i18nContext = i18nManager.createContext(request);
// Add i18n context to request
request.i18n = i18nContext;
// Add helper methods to reply
reply.t = i18nContext.t;
reply.setLanguage = async (language) => {
await i18nContext.changeLanguage(language);
// Set language cookie
reply.cookie(options.cookieName || "i18n_language", language, {
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
httpOnly: false,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
};
});
// Cleanup on server close
fastify.addHook("onClose", async () => {
await i18nManager.shutdown();
});
};
}
/**
* Create default locale structure
*/
export function createDefaultLocales() {
return {
en: {
translation: {
common: {
welcome: "Welcome",
hello: "Hello",
goodbye: "Goodbye",
yes: "Yes",
no: "No",
save: "Save",
cancel: "Cancel",
delete: "Delete",
edit: "Edit",
create: "Create",
update: "Update",
search: "Search",
loading: "Loading...",
error: "Error",
success: "Success",
},
errors: {
notFound: "Not found",
unauthorized: "Unauthorized",
forbidden: "Forbidden",
internalError: "Internal server error",
validationFailed: "Validation failed",
required: "This field is required",
invalid: "This field is invalid",
},
messages: {
created: "Created successfully",
updated: "Updated successfully",
deleted: "Deleted successfully",
saved: "Saved successfully",
},
},
},
es: {
translation: {
common: {
welcome: "Bienvenido",
hello: "Hola",
goodbye: "Adiós",
yes: "Sí",
no: "No",
save: "Guardar",
cancel: "Cancelar",
delete: "Eliminar",
edit: "Editar",
create: "Crear",
update: "Actualizar",
search: "Buscar",
loading: "Cargando...",
error: "Error",
success: "Éxito",
},
errors: {
notFound: "No encontrado",
unauthorized: "No autorizado",
forbidden: "Prohibido",
internalError: "Error interno del servidor",
validationFailed: "Validación fallida",
required: "Este campo es obligatorio",
invalid: "Este campo no es válido",
},
messages: {
created: "Creado exitosamente",
updated: "Actualizado exitosamente",
deleted: "Eliminado exitosamente",
saved: "Guardado exitosamente",
},
},
},
};
}
export default I18nManager;
//# sourceMappingURL=i18n.js.map