Skip to content

How languages work

SwiftAgency ships in English, German and Indonesian. Every page exists in every language, visitors can switch language without losing their place, and search engines are told which page is which translation.

These are read from src/i18n/config.ts when the docs are built:

CodeLanguageURLsDatesOpen Graph
enEnglish/worken-GBen_GB
deDeutsch/de/workde-DEde_DE
idBahasa Indonesia/id/workid-IDid_ID

The default language has no prefix in the URL. The others live under their code.

Interface text is everything built into the pages: navigation, buttons, headings, the services and prices, form labels and errors. It lives in one dictionary per language in src/i18n/ui/:

src/i18n/ui/en.ts
const en = {
nav: {
work: 'Work',
services: 'Services',
// …
},
cta: {
contact: 'Start a project',
work: 'View work',
},
// …
};

Content is what you publish: case studies, articles, roles and legal pages. Translations sit in a language folder next to the original, with the same file name. See Translating content.

en.ts defines the shape; de.ts and id.ts are typed as Translation. If a key is missing from one of them, or misspelled, npm run check fails and names the file and key. You cannot ship a page with a blank label by accident.

Placeholders such as {count} are filled in by format():

format(dict.home.allProjects, { count: 8 }); // "All 8 projects"
---
import { localizePath, t, useLocale } from '@/i18n/utils';
const locale = useLocale(Astro);
const dict = t(locale);
---
<a href={localizePath('/work', locale)}>{dict.cta.work}</a>
  • useLocale(Astro) returns the language of the page being built.
  • t(locale) returns its dictionary.
  • localizePath(path, locale) adds the language prefix: /work becomes /de/work. External links and mailto: are left alone.
  • formatDate(date, locale) formats a date the local way: 21 May 2026, 21. Mai 2026, 21 Mei 2026.
  • A language switcher in the header and menu. It keeps the visitor on the same page in the other language.
  • <html lang> set per page, so screen readers use the right voice.
  • hreflang links to every translation plus an x-default, and og:locale with its alternates.
  • A sitemap that lists each page’s translations.
  • Structured data in the page’s language.
  • A 404 page that switches to the visitor’s language from the URL they tried.
  • Honest fallbacks. Content not yet translated is shown in English and marked with lang="en", so a German page never pretends English text is German.

To publish only in English, remove de and id from locales in src/i18n/config.ts, remove their imports from the dictionaries map in src/i18n/utils.ts, and delete src/i18n/ui/de.ts, src/i18n/ui/id.ts and the de/ and id/ folders in every collection in src/content/.

The language switcher hides itself when there is only one language, and URLs stay as they are.