UNPKG

@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
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