Documentación · Referencia · El formato de un proyecto
15. Multiidioma
Un fichero por idioma: direcciones, traducciones, datos, plantillas y fechas.
Versión 0.16
Un sitio puede estar en varios idiomas. Cada página de cada idioma es su propio fichero, en el mismo árbol que las demás, y el idioma va en el nombre:
contenido/
index.md portada en castellano
index.eu.md portada en euskera
instalaciones.md
instalaciones.eu.md con url: /instalazioak/
blog/
un-articulo.md solo en castellano
Un sitio en un solo idioma no escribe nada de esto y no cambia en nada, salvo
que el punto en el nombre de un fichero está reservado siempre (§15.2): un
servicios.yml junto a servicios.md ya no se copia a salida/ (§15.7), sea
cual sea el número de idiomas.
15.1 Idiomas en sitio.yml¶
idiomas:
- codigo: es
nombre: Castellano
- codigo: eu
nombre: Euskara
El primero es el predeterminado: sus páginas van sin prefijo en la URL, y las
de los demás, con el suyo (/eu/). codigo son dos o tres letras minúsculas,
como en ISO 639 (es, eu, en), y es lo que va en el nombre de los
ficheros, en el prefijo y en el hreflang. nombre es el nombre del idioma
para el selector, escrito en ese idioma; si falta, se usa el código. Un código
con región (pt-br, es-mx) no se admite y detiene el build: esa forma queda
reservada para más adelante (decisión 138).
Si idiomas no es una lista, si está vacía, si una entrada no tiene un código
válido o si un código se repite, el build se detiene: sin los idiomas no se
sabe qué ficheros se leen ni qué URL lleva cada página. Cualquier otro campo
de una entrada se avisa y se ignora.
Con idiomas, idioma sobra: si se escribe y no es el primero de la lista,
se avisa, y sitio.idioma vale siempre el del predeterminado. Un sitio en un
solo idioma lo dice con idioma (§2), y para el motor es un sitio con un solo
idioma en la lista.
15.2 Un fichero por idioma¶
El idioma de una página es el código que lleva antes de la extensión:
contacto.eu.md está en euskera. Un fichero sin código (contacto.md) está
en el idioma predeterminado, así que un sitio pasa a ser bilingüe sin renombrar
nada: basta con añadir los ficheros del otro idioma. Escribir el código del
predeterminado (contacto.es.md) es lo mismo; tener a la vez contacto.md y
contacto.es.md, o contacto.md y contacto.twig, detiene el build, porque
serían dos veces la misma página en el mismo idioma.
Las traducciones de una página son los ficheros de la misma carpeta con el
mismo nombre sin el código: contacto.md y contacto.eu.md son la misma
página. No hay que declararlo en ningún sitio, y un editor puede mostrarlas
como pestañas de idioma sobre la misma página.
Un fichero con un código que el sitio no declara (contacto.fr.md en un sitio
sin francés) no se lee: se salta con un aviso, porque puede ser una traducción
preparada de antemano. El punto en el nombre de una página, de un fichero de
datos/ o de un _datos.yml está reservado para el idioma en todos los
sitios, también en los de un solo idioma: un punto que no es el de un código de
dos o tres letras minúsculas (guia.v2.md, 2024.10.01-titulo.md,
guia.antigua.es.md) detiene el build con un error que nombra el fichero,
porque saltárselo haría que el despliegue borrara la página del sitio publicado
(decisión 135). Un nombre con la extensión de un fichero que se quiere generar
(robots.txt.twig, sitemap.xml.twig, llms.txt.md) también lo detiene, y el
error dice cómo hacerlo: una página robots.twig con url: /robots.txt
(decisión 143). Los ficheros que se copian tal cual (jquery.min.js) llevan los
puntos que quieran.
El idioma sale del nombre del fichero, no del front matter. idioma es un
campo reservado: si se escribe en una página, en un _datos.yml o en los
campos comunes de una página (§15.7), se avisa y se ignora.
15.3 URL¶
Las páginas del idioma predeterminado tienen la URL de siempre (§5). Las de los
demás llevan delante el prefijo de su idioma, y el motor lo pone siempre,
también a la URL que se escribe en url y a la que sale de un patrón:
contenido/index.eu.md → /eu/
contenido/instalaciones.eu.md → /eu/instalaciones/
contenido/instalaciones.eu.md con url: /instalazioak/ → /eu/instalazioak/
contenido/404.eu.md con url: /404.html → /eu/404.html
Así el patrón url: "/blog/{{ titulo|slug }}/" de un _datos.yml vale para
todos los idiomas, y en la URL de una traducción solo se escribe lo que se
traduce. Una página que no es del predeterminado no puede salir de su prefijo.
Si su url ya empieza por él (url: /eu/instalazioak/), se avisa: saldría
/eu/eu/instalazioak/.
Si la raíz de un idioma (/eu/) no es la URL de ninguna página, se avisa: el
selector de idioma llevaría a una dirección que no existe (§15.8). Solo en los
sitios que declaran idiomas, aunque sea con uno solo: con idioma no se avisa.
15.4 Traducción parcial¶
El caso normal no es un sitio duplicado: es una empresa que traduce portada,
quiénes somos y contacto, y mantiene el blog solo en castellano. Con un fichero
por idioma, eso sale solo: existe lo que existe. Si hay contacto.md y
contacto.en.md, la página está en dos idiomas; si un artículo solo tiene
un-articulo.md, solo existe en castellano y no se genera nada en inglés. No
se avisa de las traducciones que faltan.
Reglas que se derivan de eso:
- Sin respaldo automático. Una página sin traducir no se publica en ese
idioma con el texto del otro. Servir castellano bajo una URL inglesa es
contenido duplicado y un
hreflangque miente. hreflangsolo entre las traducciones que existen, y cada página se apunta también a sí misma (§15.8).- El selector de idioma, como se escribe en §15.8, nunca enlaza a una URL que no existe. El motor no escribe selectores: es una propiedad de la plantilla. Si la página actual no está traducida, lleva a la portada de ese idioma, y la plantilla puede distinguir los dos casos para avisar al visitante.
15.5 Colecciones¶
Las colecciones están acotadas al idioma de la página que las consulta: un
listado de blog en inglés nunca saca artículos en castellano. También todo.
Otro idioma se pide con un segundo argumento, que es un código o todos:
{% for articulo in coleccion('blog') %} {# en el idioma de la página #}
{% for articulo in coleccion('blog', 'eu') %} {# solo los que están en euskera #}
{% for articulo in coleccion('blog', idioma: 'todos') %} {# todos #}
Un código que el sitio no declara avisa y da una lista vacía. orden y donde
ven también idioma (§6.2). Una página con agrupar (§6.3) agrupa las del
idioma de su fichero.
15.6 Datos por idioma¶
Los menús no son la traducción uno a uno del mismo árbol: si el blog solo existe en castellano, el menú inglés tiene una entrada menos. Lo mismo pasa con el pie y a veces con secciones enteras. Así que los datos siguen la misma convención que el contenido:
datos/menus.yml común a todos los idiomas
datos/menus.es.yml castellano
datos/menus.en.yml inglés
Al contrario que con las páginas, un fichero de datos sin código no es del
predeterminado: es común a todos. En una página, datos.menus es el fichero
común con el de su idioma encima, mezclados por sus claves de primer nivel,
igual que un _datos.yml sobre el de su carpeta (§3): gana el del idioma, y
las listas y los mapas se sustituyen enteros. Un cliente.en.yml que solo
tiene horario traduce el horario y deja los teléfonos y la dirección del
común. Un menus.en.yml con su lista principal es otro menú, con sus
propias entradas, no una tabla de traducciones. Si uno de los dos no es un
mapa, el del idioma sustituye entero al común. Un dato que solo existe en un
idioma (textos.en.yml sin textos.yml) solo está en las páginas de ese
idioma. El atajo dato (§8.2) lee lo mismo que la página en la que está.
Aquí sí hay respaldo al fichero común, al contrario que con las páginas: un menú que falta deja el sitio sin navegación, mientras que una página que falta simplemente no está.
Los _datos.yml de carpeta admiten lo mismo: en cada carpeta, _datos.en.yml
se pone encima de _datos.yml para las páginas en inglés, antes de pasar a la
subcarpeta, donde el _datos.yml vuelve a ganar por estar más cerca. Sirve
para cambiar por idioma la plantilla o el patrón de URL de una sección.
15.7 Lo no traducible de una página¶
Junto a una página puede haber un .yml con su mismo nombre y lo que no
cambia de idioma: la imagen principal, el icono, el orden, los destinos de los
enlaces.
contenido/servicios.yml imagen, icono, orden
contenido/servicios.md título y texto en castellano
contenido/servicios.eu.md título y texto en euskera
Sus campos van encima de la cascada y debajo del front matter de cada idioma,
que gana si repite alguno; etiquetas, css y js se suman, como en toda la
cascada (§3). Así la misma imagen no se escribe una vez por idioma, sin nada
que obligue a que coincidan. Vale también en un sitio de un solo idioma.
Ese .yml no se copia a salida/: es parte de la página, no un fichero
publicado. Un .yml sin una página con su nombre en la misma carpeta se copia
como cualquier otro fichero (§4). Es también donde un editor guarda los
campos de una página que no son de cada traducción (§11.2).
15.8 Plantillas¶
Todas las páginas, también las que dan coleccion(), llevan:
pagina.idioma, el código de su idioma. En un sitio de un solo idioma es el deidioma, así que<html lang="{{ pagina.idioma }}">vale para todos.pagina.traducciones, sus versiones publicadas, ella incluida, por código y en el orden deidiomas:pagina.traducciones.eu.url. Cada una tiene la misma forma que cualquier otra página. Un borrador o una traducción conpublicaren el futuro no están. Una página generada poragruparo porpaginar, salvo la primera de esta, solo tiene como traducción a sí misma (§6.3, §6.4).
sitio.idiomas es la lista de idiomas, cada uno con codigo, nombre y
url, la raíz de ese idioma: / para el predeterminado y /eu/ para los
demás. En un sitio de un solo idioma tiene una sola entrada.
Con eso, la plantilla escribe los hreflang y el selector de idioma; el motor no
los escribe:
{% for codigo, traduccion in pagina.traducciones %}
<link rel="alternate" hreflang="{{ codigo }}" href="{{ sitio.url }}{{ traduccion.url }}">
{% endfor %}
{% for idioma in sitio.idiomas %}
{% set traduccion = pagina.traducciones[idioma.codigo] %}
<a href="{{ traduccion ? traduccion.url : idioma.url }}" hreflang="{{ idioma.codigo }}">{{ idioma.nombre }}</a>
{% endfor %}
activo('/eu/') solo es cierto en la portada en euskera, igual que / solo lo
es en la del predeterminado (§7.3).
15.9 Fechas¶
fecha() (§7.3) escribe los meses y los días en el idioma de la página. El
motor trae castellano, euskera, inglés y alemán, cada uno con su formato por
defecto:
| Idioma | fecha() |
fecha('l j F Y') |
|---|---|---|
es |
18 de marzo de 2025 | martes 18 marzo 2025 |
eu |
2025eko martxoaren 18a | asteartea 18 martxoa 2025 |
en |
18 March 2025 | Tuesday 18 March 2025 |
de |
18. März 2025 | Dienstag 18 März 2025 |
El formato por defecto del euskera no se puede escribir con letras: el sufijo
del año es -ko o -eko según cómo se lee el número (2026ko, 2025eko), y
lo calcula el motor. Con las letras, el mes sale como se nombra suelto
(martxoa); fecha('F\\r\\e\\n j\\a') da martxoaren 18a.
Para un idioma que el motor no trae (fr), se avisa una vez, al leer
sitio.yml, y las fechas salen con los nombres en inglés, que son los de PHP, y
con el formato por defecto del inglés, j F Y (18 March 2025). Solo las
letras l, D, F y M se escriben en el idioma de la página; las demás
salen como en PHP (S, a y A, en inglés).
15.10 Sitemap y feed¶
El sitemap es uno solo, con las páginas de todos los idiomas (§10.1) y sin la
404.html de ninguno. No lleva alternativas de idioma: los hreflang van en
el HTML de cada página.
El feed (§10.2) sale de la misma colección en cada idioma, cada uno en la raíz
de su idioma y con su xml:lang: /feed.xml en el predeterminado y
/eu/feed.xml en euskera. El del predeterminado se genera siempre, como en un
sitio de un solo idioma; el de los demás, solo si tienen alguna entrada. Todos
llevan el mismo título. Como en §10, un /eu/feed.xml del proyecto gana al
del motor.
15.11 Por qué un fichero por idioma¶
La razón de elegir un fichero por idioma, y las otras dos formas que se descartaron, están en la decisión 4.