i18n Type-Safe: Extracción de Claves Profundas en TypeScript
El problema con la mayoría de librerías i18n es que las claves son strings sueltos:
t('nav.home') // Sin autocompletado, sin verificación de tipos
t('nav.hoem') // Un typo — error silencioso en runtime
Lo aprendí de las malas. Un typo en una clave de traducción no te explota en build time — simplemente muestra texto vacío o la clave cruda al usuario. Armé un sistema type-safe para que eso no pase.
El tipo DeepKeyOf
La base es un tipo recursivo que extrae todas las rutas posibles de un objeto anidado:
type DeepKeyOf<T> = T extends object
? {
[K in Extract<keyof T, string>]: T[K] extends object
? T[K] extends Array<unknown>
? `${K}` // No recursar en arrays
: `${K}` | `${K}.${DeepKeyOf<T[K]>}`
: `${K}`
}[Extract<keyof T, string>]
: never
Dado esto:
const translations = {
nav: {
home: 'Inicio',
about: 'Acerca de',
nested: {
deep: 'Valor profundo'
}
},
footer: {
copyright: '© 2026'
}
}
DeepKeyOf<typeof translations> te genera la unión completa: 'nav', 'nav.home', 'nav.about', 'nav.nested', 'nav.nested.deep', 'footer', 'footer.copyright'. TypeScript te va a autocompletar cada una de esas.
La función t()
Con el tipo armado, construís la función de traducción:
// Define claves válidas basadas en traducciones en inglés
type NestedKeys = DeepKeyOf<(typeof resources)['en']>
// Helper para obtener valores anidados de forma segura
const get = (obj: unknown, path: string, defaultValue = ''): string => {
const keys = path.split('.')
let result: unknown = obj
for (const key of keys) {
if (result == null || typeof result !== 'object') {
return defaultValue
}
result = (result as Record<string, unknown>)[key]
}
return result == null ? defaultValue : String(result)
}
// La función de traducción type-safe
export function useTranslations(lang: SupportedLanguages) {
return function t(key: NestedKeys) {
return get(
resources,
`${lang}.${key}`,
get(resources, `${defaultLang}.${key}`, key as string)
)
}
}
La cadena de fallback es importante: intenta el idioma actual, después el idioma por defecto, y si no encuentra nada te devuelve la clave. Así nunca mostrás una pantalla rota.
Usándolo en componentes
---
import { getLangFromUrl, useTranslations } from '@i18n/utils'
const lang = getLangFromUrl(Astro.url)
const t = useTranslations(lang)
---
<nav>
<a href="/">{t('nav.home')}</a> <!-- ¡El autocompletado funciona! -->
<a href="/about">{t('nav.about')}</a>
<!-- t('nav.hoem') sería un error de TypeScript -->
</nav>
Traducción de rutas
Para las URLs también conviene tener tipado. Usá un mapa de rutas separado:
// routes.ts
export const routes = {
notebook: {
en: 'notebook',
es: 'cuaderno',
},
contact: {
en: 'contact',
es: 'contactame',
},
} as const
export type RouteNames = keyof typeof routes
Y el helper para traducirlas:
export function useTranslatedPath(lang: SupportedLanguages) {
const translatePath = (
path: RouteNames,
targetLang: SupportedLanguages = lang,
slug?: string
) => {
const basePath = routes[path][targetLang]
if (targetLang === defaultLang) {
return slug ? `/${basePath}/${slug}` : `/${basePath}`
}
return slug ? `/${targetLang}/${basePath}/${slug}` : `/${targetLang}/${basePath}`
}
return { translatePath }
}
Entonces:
const { translatePath } = useTranslatedPath('en')
translatePath('notebook', 'en') // '/notebook'
translatePath('notebook', 'es') // '/es/cuaderno'
La inversión inicial en configurar esto se paga sola. Cualquier typo en una clave de traducción se convierte en error de compilación, y refactorizar es seguro. Eso es todo por ahora.




Comments for typsf1