Saltar al contenido principal

Manejo de idiomas

Este apartado es bastante sencillo. Para empezar, es importante haber leído las secciones anteriores Estructura y categorización de errores y Centralización de errores. Con eso en mente, podemos empezar.

🇺🇳 Agregando nuevos idiomas​

Lo único que nos tocaría hacer es transformar todos los textos de language natural a la siguiente estructura:

interface IMultiLanguage {
es: string;
en: string;
// More languages as needed
}

Tomando como ejemplo los errores de usuario que vimos previamente, quedaría de la siguiente manera:

// amazing-project/apps/api/src/plugins/errors/dictionaries/users.ts

const userErrors = {
'not-found': {
status: 404,
type: 'NOT_FOUND_ERROR',
code: 'USER_NOT_FOUND',
title: {
es: 'Usuario no encontrado',
en: 'User not found',
},
message: {
es: 'El usuario que buscas no fue encontrado',
en: 'The desired user could not be found',
},
},
suspended: {
status: 403,
type: 'AUTHORIZATION_ERROR',
code: 'SUSPENDED_USER',
title: {
es: 'Usuario suspendido',
en: 'User suspended',
},
message: {
es: 'Tu cuenta se encuentra suspendida',
en: 'Your account was suspended',
},
},
};

✅ Resolviendo el idioma​

Ya teniendo la nueva estructura, lo que nos queda por hacer es saber cual idioma debemos retornar al usuario, puesto que el frontend seguirá recibiendo la estructura anterior donde el título y el mensaje son strings. Este análisis lo hacemos en base a un header estándar llamado accept-language donde se reciben los idiomas deseados por el cliente (normalmente tomados automáticamente de la configuración del usuario).

Ejemplo del header​

Accept-Language: en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7,es-VE;q=0.6,es;q=0.5

Para poder leer este header, debemos dividirlo en pequeñas partes de izquierda a derecha. Para esto dividimos por las comas:

  • en-US: el idioma preferido del usuario
  • en;q=0.9: segundo idioma, en caso de no tener alguna traducción específica para inglés estadounidense, cualquier respuesta en inglés funciona (británico, australiano, etc.)
  • fr-FR;q=0.8: tercer idioma, francés.
  • fr;q=0.7: cuarto idioma, cualquier traducción al francés disponible.
  • es-VE;q=0.6: quinto idioma, español de Venezuela.
  • es;q=0.5: sexto idioma, cualquier traducción al español disponible.

El negociador​

Para efectos de nuestros sistemas, podemos decir que este usuario prefiere respuestas en inglés, francés o español (en ese orden). Con este conocimiento, procedemos a desarrollar al negociador de idiomas:

import { FastifyRequest } from 'fastify';

const availableLanguages = ['en', 'es'] as const;
export type AvailableLanguages = (typeof availableLanguages)[number];

function processLanguages(acceptLanguageHeader: string) {
const languages = acceptLanguageHeader.split(',').map((lang) => lang.trim());
return languages;
}

function negotiate(requestedLanguages: string[]): AvailableLanguages {
const negotiationResult = requestedLanguages.reduce(
(acc, lang: string) => {
// If already selected, skip further checks
if (acc.selected) return acc;

// Check if the language is available
const language = lang.split(';')[0].trim();
if (!availableLanguages.includes(language as any)) return acc;
return { language, selected: true };
},
{ language: availableLanguages[0], selected: false }
);

return negotiationResult.language as AvailableLanguages;
}

export function languageNegotiation(
request: FastifyRequest
): AvailableLanguages {
const acceptLanguages = request?.headers['accept-language'] || 'en';
const requestedLanguages = processLanguages(acceptLanguages);
const finalLanguage = negotiate(requestedLanguages);
return finalLanguage as AvailableLanguages;
}

Este código lo colocamos en su propio archivo y lo implementamos en el handler general que ya desarrollamos para entonces obtener:

import type { FastifyReply, FastifyRequest } from 'fastify';
import { generateCleanStackTrace } from './stack';
import { Exception } from './exception';
import { languageNegotiation } from './language';
import { errorRegistry } from './dictionaries';
import { logError } from './logs';

function injectParams(detail: string, params: Record<string, any>) {
return Object.entries(params).reduce((acc, [key, value]) => {
return acc.replace(`<${key}>`, value);
}, detail);
}

const productionEnv = process.env.APP_ENV === 'production';

export function handleError(
error: Error | Exception,
request: FastifyRequest,
reply: FastifyReply
) {
const date = new Date();
const language = languageNegotiation(request); // <-- Negociador agregado!
const stack = generateCleanStackTrace(error);

let title = 'Unhandled server error';
let status = 500;
let message = 'An unexpected error appeared';
let type = 'default';
let silent = false;

if (error instanceof Exception) {
silent = error.silent && productionEnv;
const data = error.data;
title = error.data.title[language]; // <-- Idioma agregado!
status = error.data.status;
type = error.data.type;
message = injectParams(data.message[language], error.params); // <-- Idioma agregado!
}

const genericError = errorRegistry.getError('internal', 'default');
const response = {
title: silent ? genericError.title[language] : title, // <-- Idioma agregado!
message: silent ? genericError.message[language] : message, // <-- Idioma agregado!
status: silent ? genericError.status : status,
stack: !productionEnv && status === 500 ? stack : undefined,
};

// Send error response
reply.status(status).send(response);
return reply;
}