Skip to content

Internationalization

shipdot.config.json
{
"defaultLocale": "en",
"locales": ["en", "es", "pt"]
}

defaultLocale is the language of your main entry files. locales lists every language you translate into (the default one is added automatically).

Create a sibling file with the locale before .md. It contains only the translated title and body:

.shipdot/entries/2026-10-14-export-excel.es.md
---
title: Exportar a Excel
---
Ahora puedes exportar cualquier reporte a Excel con un clic.

date, type, target, link and image always come from the main file. shipdot build puts translations in the entry’s i18n map:

{
"id": "2026-10-14-export-excel",
"title": "Export to Excel",
"html": "<p>You can now export…</p>",
"i18n": { "es": { "title": "Exportar a Excel", "html": "<p>Ahora puedes…</p>" } }
}

shipdot validate reports translations for undeclared locales, translations without a main file (orphans), translations of the default locale, and translated keys that can’t be translated.

  1. The locale attribute, if set: <ship-dot locale="es">.
  2. Otherwise navigator.languages, in the user’s order of preference.

Each candidate is matched against the locales present in the feed: exact match first (pt-BR), then the base language (pt-BR → pt), then any regional variant (pt → pt-BR). If nothing matches, the feed’s defaultLocale is used.

Per-entry fallback: an entry without a translation for the chosen locale is shown in the default locale, so a partially translated feed never has holes.

Dates are formatted with Intl.DateTimeFormat in the chosen locale.

The widget’s own labels (“What’s new”, “Got it”, “Learn more”, “New/Improved/Fixed”, the unread count) ship in English, Spanish and Portuguese. Other languages fall back to English. Adding a language is a small, welcome pull request to packages/shipdot/src/strings.ts.

In LLM mode, shipdot draft writes the translation files for every configured locale along with the draft. Review them in the pull request like any other text.