You translate an Astro page by wrapping its text in t() and its internal links in href(), both read from Astro.locals. Everything runs on the server, and one page file serves every language.
Before you start
- The integration is installed and
<UniversallyHead />is in your layout. See Install the Astro integration. - The project has at least one target language with its Live switch on.
Translate text with t()
- Read
tfromAstro.localsin the page's frontmatter. - Pass each piece of source text to it.
---
const { t } = Astro.locals;
---
<h1>{t('About us')}</h1>
<p>{t('We translate websites.')}</p>
t() takes your source text and returns the text for the language being rendered. The source text is the key, so the string in your code has to match the source string in the dashboard character for character. An extra space or a changed comma makes it a different string.
A string with no translation yet returns the source text, so a page never breaks while a translation is on its way. The string is recorded once and reported after the response. On source language pages, t() returns its argument with no lookup and no report.
Never call t(''). The translator refuses an empty string, and that report fails with a logged error.
Localize links with href()
- Read
hreffromAstro.locals. - Wrap every internal path.
---
const { t, href } = Astro.locals;
---
<a href={href('/pricing')}>{t('See pricing')}</a>
On a French page, /pricing becomes /fr/pricing, and the query string and hash are kept. On the source language the path comes back unchanged, because the source language is served without a prefix.
href() also returns these unchanged:
- an empty string
- protocol relative URLs that start with
// - anything that does not start with
/:https:andmailto:links,tel:links,#fragment,?query, and relative paths - a path that already carries the prefix, such as
/fror/fr/pricing, so calling it twice does no harm
Read the current language
Astro.locals.lang holds the variant code of the language being rendered, in lowercase: fr for French (France), es-419 for Spanish (Mexico), and en-us on an English (US) source page. Astro.locals holds lang, t, and href, and nothing else.
Keep inline HTML inside t()
Plain <strong> and <em> tags can stay inside the string. Render the result with set:html:
---
const { t } = Astro.locals;
---
<p set:html={t('Wrap text in <strong>t()</strong> and keep your <em>markup</em> intact.')} />
A string that contains a tag is normalized before translation: attributes on inline tags are replaced by markers, and img and svg elements are emptied. Keep strings to plain text, or to <strong> and <em> without attributes.
Keep <a> outside t():
---
const { t, href } = Astro.locals;
---
<p>{t('Compare every plan.')} <a href={href('/pricing')}>{t('See pricing')}</a></p>
The link's URL belongs to a language, so href() has to build it. A URL inside the source text also makes the key different on every page that links somewhere else, which splits one translation into many.
Pass translated text to islands
There is no client side t(). A component that hydrates in the browser gets translated text only as props from the .astro file:
---
import Counter from '../components/Counter.tsx';
const { t } = Astro.locals;
---
<Counter client:load label={t('Add one')} />
Translate a static site
With output: 'static', translation happens during the build:
- Run the build. It loads every language, renders each page once, then sends every string it did not have in one call.
- Read the end of the log. If anything was new, it prints a line like
12 new strings translated. Rebuild to include them. - Run the build again. The new strings are now in the HTML.
A static site has no server code running after the build. Dashboard edits reach it only when you rebuild and redeploy, as described in Translation updates in Astro.
Verify it worked
Open a page under a target prefix, such as /fr/about. Text you wrapped in t() appears translated after a reload, and links built with href() point to /fr/ paths. Text you did not wrap stays in your source language: nothing on the page is translated unless it goes through t().
If it does not work
- One string stays in the source language while the rest translate: its key does not match the dashboard. Compare the argument of
t()with the source string, character for character. - Every string stays in the source language: check the server log for
word limit reached. See Astro integration troubleshooting.