Implementación de NestJS I18N para Traducciones

Resumen

Esta receta muestra cómo implementar la internacionalización (I18N) en un proyecto NestJS utilizando una serie de utilidades y adaptadores para realizar traducciones basadas en el contenido de las solicitudes.

Descripción

NestJS I18N es una solución para manejar traducciones en el backend de diversas plataformas. Consiste en una serie de utilidades y adaptadores que permiten traducir textos de acuerdo a la solicitud recibida. Estas herramientas están disponibles en la librería inlaze-common.

Prerrequisitos

  • Conocimientos básicos de NestJS.

  • Entorno de desarrollo configurado con NestJS.

  • Librería @inlaze_techlead/inlaze-common instalada:

    pnpm add @inlaze_techlead/inlaze-common

Casos de Uso

  • Internacionalización de mensajes de error, notificaciones y otros textos en el backend.

  • Personalización de contenido según el idioma preferido del usuario.

  • Soporte para múltiples idiomas en aplicaciones empresariales.

Instrucciones Paso a Paso

Paso 1: Preparación

  1. Instalar la librería inlaze-common:

    pnpm add @inlaze_techlead/inlaze-common
  2. Configurar el Adapter de Traducción:

    import { Inject, Injectable, Scope } from "@nestjs/common";
    import { REQUEST } from "@nestjs/core";
    import { Request } from "express";
    import { translate } from "../utils/translation.util";
    import { getMessageFromError } from "../utils/get-message-from-error.util";
    import { LoggerService } from "../logger/services/logger.service";
    
    @Injectable({ scope: Scope.REQUEST })
    export class TranslationAdapter {
      public constructor(
        private readonly _loggerService: LoggerService,
        @Inject(REQUEST) private readonly _request: Request,
      ) {}
    
      public translate(key: string, ...args: unknown[]): string {
        try {
          return translate(this._request, key, args);
        } catch (error) {
          this._loggerService.error({
            data: {
              message: `Error translating message, error: ${getMessageFromError(error)}`,
              context: TranslationAdapter.name,
            },
          });
          return key;
        }
      }
    }

Paso 2: Proceso Principal

  1. Implementar la Utilidad de Traducción:

    import { isIn } from "class-validator";
    import type { Request as ExpressReq } from "express";
    import { getRawProperty } from "./object.utils";
    import type { AllowedTranslateLanguages } from "../types/allowed-translate-message-languages.type";
    import { LanguagesEnum } from "../enums/languages.enum";
    import { ALLOWED_TRANSLATE_MESSAGE_LANGUAGES } from "../constants/allowed-translate-message-languages.constant";
    
    type LanguagesType = Record<AllowedTranslateLanguages, Record<string, Record<string, string>>>;
    const languages: Partial<LanguagesType> = {};
    
    export function setLanguages(newLanguages: Partial<LanguagesType>): void {
      Object.assign(languages, newLanguages);
    }
    
    export function getUserLanguage(
      languageOrReq?: ExpressReq | (LanguagesEnum | undefined) | (string | undefined),
    ): AllowedTranslateLanguages {
      const language =
        typeof languageOrReq === "string"
          ? languageOrReq
          : languageOrReq?.headers["language"] ??
            languageOrReq?.headers["accept-language"] ??
            (languageOrReq?.user as any)?.language ??
            LanguagesEnum.ENGLISH;
      return !isIn(language, ALLOWED_TRANSLATE_MESSAGE_LANGUAGES)
        ? LanguagesEnum.ENGLISH
        : (language as AllowedTranslateLanguages);
    }
    
    export function getTranslatedArgs(args: unknown[], locale: AllowedTranslateLanguages): string[] {
      return args.map((arg) => {
        if (typeof arg === "string" && arg.includes(".")) return arg;
        const prop: string = `${locale}.args.${arg}`;
        return getRawProperty<string | undefined>(prop, languages) ?? (arg as string);
      });
    }
    
    export function translate(
      languageOrReq: ExpressReq | (LanguagesEnum | undefined) | (string | undefined),
      key: string,
      ...args: unknown[]
    ): string {
      try {
        const locale: AllowedTranslateLanguages = getUserLanguage(languageOrReq);
        const translation: string | undefined = getRawProperty(`${locale}.${key}`, languages);
        if (!args.length) return translation ?? key;
        const translatedArgs: string[] = getTranslatedArgs(args.flat(), locale);
        return (
          translation?.replace(/{(\d+)}/g, (match, number) => {
            return typeof translatedArgs[number] !== "undefined" ? translatedArgs[number] : match;
          }) ?? key
        );
      } catch (error) {
        return key;
      }
    }
  2. Usar la Utilidad de Traducción en la Aplicación:

    import { NestFactory } from "@nestjs/core";
    import { Logger } from "@nestjs/common";
    import { ConfigService } from "@nestjs/config";
    import * as chalk from "chalk";
    import { AppModule } from "./app/app.module";
    import { getLocalIpUtil, setLanguages } from "@inlaze_techlead/inlaze-common";
    import * as languages from "@app/locales";
    
    async function bootstrap(): Promise<void> {
      setLanguages(languages);
      const app = await NestFactory.create(AppModule);
      const configService: ConfigService = app.get(ConfigService);
      const port: number = configService.get<number>("PORT") ?? 3001;
      const logger = new Logger();
      app.enableCors({ origin: true, credentials: true });
      await app.listen(port);
      logger.log(
        `Inlaze Affiliates is running on: ${chalk.blueBright(
          `http://${getLocalIpUtil()}:${port}`,
        )} or ${chalk.blueBright(await app.getUrl())}`,
        bootstrap.name,
      );
    }
    void bootstrap();
  3. Exportar los Lenguajes:

    import { loadJsonFile } from "@inlaze_techlead/inlaze-common";
    
    export const en = {
      default: loadJsonFile<Record<string, string>>("locales/en/default.json"),
      admin: loadJsonFile<Record<string, string>>("locales/en/admin.json"),
      exception: loadJsonFile<Record<string, string>>("locales/en/exception.json"),
      common: loadJsonFile<Record<string, string>>("locales/en/common.json"),
      args: loadJsonFile<Record<string, string>>("locales/en/args.json"),
    };
    export const es = {
      default: loadJsonFile<Record<string, string>>("locales/es/default.json"),
      admin: loadJsonFile<Record<string, string>>("locales/es/admin.json"),
      exception: loadJsonFile<Record<string, string>>("locales/es/exception.json"),
      common: loadJsonFile<Record<string, string>>("locales/es/common.json"),
      args: loadJsonFile<Record<string, string>>("locales/es/args.json"),
    };
    // Otros idiomas...

Paso 3: Validación

  1. Verificar Traducción:

    • Asegúrese de que los textos traducidos aparezcan correctamente en la aplicación.

    • Compruebe que los mensajes de error se traduzcan de acuerdo con el idioma del usuario.

Solución de Problemas

Problemas Comunes

  1. Error en la Traducción:

    • Descripción: La traducción no se muestra correctamente.

    • Solución: Verifique que el archivo de idioma esté cargado correctamente y que las claves de traducción existan.

  2. Idioma no Detectado:

    • Descripción: La aplicación no detecta el idioma del usuario.

    • Solución: Asegúrese de que los encabezados HTTP language o accept-language estén presentes en la solicitud.

Preguntas Frecuentes

  • ¿Cómo puedo agregar un nuevo idioma?

    • Añada un nuevo archivo JSON con las traducciones y actualice la configuración en setLanguages.

  • ¿Qué pasa si una clave de traducción no existe?

    • Si una clave no existe, se devuelve la clave original como texto.

Recursos Externos

Conclusión

Implementar la internacionalización en NestJS permite ofrecer una experiencia de usuario personalizada en múltiples idiomas. Siguiendo esta receta, se puede configurar fácilmente un sistema de traducción eficaz. Si tienes preguntas o sugerencias, no dudes en contactarnos.

Historial de Revisión


Versión
Fecha
Descripción
Autor

1.0

2024-07-06

Documento Inicial

Inyer Marin

Last updated