Skip to content

Commit

Permalink
Add i18n Recipe (withastro#2895)
Browse files Browse the repository at this point in the history
Co-authored-by: Yan Thomas <[email protected]>
Co-authored-by: Lorenzo Lewis <[email protected]>
Co-authored-by: Bryce Russell <[email protected]>
Co-authored-by: Sarah Rainsberger <[email protected]>
Co-authored-by: Chris Swithinbank <[email protected]>
  • Loading branch information
6 people authored Mar 24, 2023
1 parent f78ee76 commit 49ee04e
Show file tree
Hide file tree
Showing 2 changed files with 346 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/components/tabs/StaticSsrTabs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import Tabs from './Tabs';
---

<Tabs client:visible sharedStore="deploy-type">
<Fragment slot="tab.1.static">Static</Fragment>
<Fragment slot="tab.2.ssr">SSR</Fragment>

<Fragment slot="panel.1.static">
<slot name="static" />
</Fragment>
<Fragment slot="panel.2.ssr">
<slot name="ssr" />
</Fragment>
</Tabs>
331 changes: 331 additions & 0 deletions src/content/docs/en/recipes/i18n.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
---
title: Add i18n features
description: Use dynamic routing and content collections to add internationalization support to your Astro site.
type: recipe
i18nReady: false
---
import FileTree from '~/components/FileTree.astro'
import StaticSsrTabs from '~/components/tabs/StaticSsrTabs.astro'

Astro doesn't have built-in internationalization (i18n) support, but you can build your own i18n solution.
In this recipe, you'll learn how to use content collections and dynamic routing to serve content for different languages.

This example serves each language at its own subpath, e.g. `example.com/en/blog` for English and `example.com/fr/blog` for French.

## Recipe

### Set up pages for each language

1. Create a directory for each language you want to support. For example, `en/` and `fr/` if you are supporting English and French:

<FileTree>
- src/
- pages/
- **en/**
- about.astro
- index.astro
- **fr/**
- about.astro
- index.astro
- index.astro
</FileTree>


2. Set up `src/pages/index.astro` to redirect to your default language.

<StaticSsrTabs>
<Fragment slot="static">
```astro
---
// src/pages/index.astro
---
<meta http-equiv="refresh" content="0;url=/en/" />
```

This approach uses a [meta refresh](https://en.wikipedia.org/wiki/Meta_refresh) and will work however you deploy your site. Some static hosts also let you configure server redirects with a custom configuration file. See your deploy platform’s documentation for more details.
</Fragment>

<Fragment slot ="ssr">
If you are using an SSR adapter, you can use [`Astro.redirect`](/en/guides/server-side-rendering/#astroredirect) to redirect to the default language on the server.

```astro
---
// src/pages/index.astro
return Astro.redirect('/en/');
---
```
</Fragment>
</StaticSsrTabs>

### Use collections for translated content

1. Create a folder in `src/content/` for each type of content you want to include and add subdirectories for each supported language. For example, to support English and French blog posts:

<FileTree>
- src/
- content/
- blog/
- **en/** Blog posts in English
- post-1.md
- post-2.md
- **fr/** Blog posts in French
- post-1.md
- post-2.md
</FileTree>

2. Create a `src/content/config.ts` file and export a collection for each type of content.

```ts
//src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
date: z.date()
})
});

export const collections = {
'blog': blogCollection
};

```
📚 Read more about [Content Collections](/en/guides/content-collections/).

3. Use [dynamic routes](/en/core-concepts/routing/#dynamic-routes) to fetch and render content based on a `lang` and a `slug` parameter.

<StaticSsrTabs>
<Fragment slot="static">
In static rendering mode, use `getStaticPaths` to map each content entry to a page:

```astro
//src/pages/[lang]/blog/[...slug].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const pages = await getCollection('blog')
const paths = pages.map(page => {
const [lang, ...slug] = page.slug.split('/');
return { params: { lang, slug: slug.join('/') || undefined }, props: page }
})
return paths;
}
const { lang, slug } = Astro.params;
const page = Astro.props;
const formattedDate = page.data.date.toLocaleString(lang);
const { Content } = await page.render();
---
<h1>{page.data.title}</h1>
<p>by {page.data.author} • {formattedDate}</p>
<Content/>
```
</Fragment>

<Fragment slot="ssr">
In [SSR mode](/en/guides/server-side-rendering/), fetch the requested entry directly:

```astro
//src/pages/[lang]/blog/[...slug].astro
---
import { getEntryBySlug } from 'astro:content';
const { lang, slug } = Astro.params;
const page = await getEntryBySlug('blog', `${lang}/${slug}`);
if (!page) {
return Astro.redirect('/404');
}
const formattedDate = page.data.date.toLocaleString(lang);
const { Content, headings } = await page.render();
---
<h1>{page.data.title}</h1>
<p>by {page.data.author} • {formattedDate}</p>
<Content/>
```
</Fragment>
</StaticSsrTabs>

📚 Read more about [dynamic routing](/en/core-concepts/routing/#dynamic-routes).

:::tip[Date formatting]
The example above uses the built-in [`toLocaleString()` date-formatting method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) to create a human-readable string from the frontmatter date.
This ensures the date and time are formatted to match the user’s language.
:::

### Translate UI strings

Create dictionaries of terms to translate the labels for UI elements around your site. This allows your visitors to experience your site fully in their language.

1. Create a `src/i18n/ui.ts` file to store your translation strings:

```ts
// src/i18n/ui.ts
export const languages = {
en: 'English',
fr: 'Français',
};

export const defaultLang = 'en';

export const ui = {
en: {
'nav.home': 'Home',
'nav.about': 'About',
'nav.twitter': 'Twitter',
},
fr: {
'nav.home': 'Acceuil',
'nav.about': 'À propos',
},
} as const;
```

2. Create two helper functions: one to detect the page language based on the current URL, and one to get translations strings for different parts of the UI in `src/i18n/utils.ts`:

```js
// src/i18n/utils.ts
import { ui, defaultLang } from './ui';

export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split('/');
if (lang in ui) return lang as keyof typeof ui;
return defaultLang;
}

export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof typeof ui[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key];
}
}
```

:::note[Did you notice?]
In step 1, the `nav.twitter` string was not translated to French. You may not want every term translated, such as proper names or common industry terms. The `useTranslations` helper will return the default language’s value if a key is not translated. In this example, French users will also see “Twitter” in the nav bar.
:::

3. Import the helpers where needed and use them to choose the UI string that corresponds to the current language. For example, a nav component might look like:

```astro
---
// src/components/Nav.astro
import { getLangFromUrl, useTranslations } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
const t = useTranslations(lang);
---
<ul>
<li>
<a href={`/${lang}/home/`}>
{t('nav.home')}
</a>
</li>
<li>
<a href={`/${lang}/about/`}>
{t('nav.about')}
</a>
</li>
<li>
<a href="https://twitter.com/astrodotbuild">
{t('nav.twitter')}
</a>
</li>
</ul>
```

4. Each page must have a `lang` attribute on the `<html>` element that matches the language on the page. In this example, a [reusable layout](/en/core-concepts/layouts/) extracts the language from the current route:

```astro
---
// src/layouts/Base.astro
import { getLangFromUrl } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
---
<html lang={lang}>
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
<slot />
</body>
</html>
```

You can then use this base layout to ensure that pages use the correct `lang` attribute automatically.

```astro
---
// src/pages/en/about.astro
import Base from "../../layouts/Base.astro"
---
<Base>
<h1>About me</h1>
...
</Base>
```

### Let users switch between languages

Create links to the different languages you support so users can choose the language they want to read your site in.

1. Create a component to show a link for each language:

```astro
---
// src/components/LanguagePicker.astro
import { languages } from '../i18n/ui';
---
<ul>
{Object.entries(languages).map(([lang, label]) => (
<li>
<a href={`/${lang}/`}>{label}</a>
</li>
))}
</ul>
```

2. Add `<LanguagePicker />` to your site so it is shown on every page. The example below adds it to the site footer in a base layout:

```astro ins={3,17-19}
---
// src/layouts/Base.astro
import LanguagePicker from '../components/LanguagePicker.astro';
import { getLangFromUrl } from '../i18n/utils';
const lang = getLangFromUrl(Astro.url);
---
<html lang={lang}>
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
<slot />
<footer>
<LanguagePicker />
</footer>
</body>
</html>
```

## Resources
- [Choosing a Language Tag](https://www.w3.org/International/questions/qa-choosing-language-tags)
- [Right-to-left Styling 101](https://rtlstyling.com/)

## Community libraries
- [astro-i18next](https://github.com/yassinedoghri/astro-i18next) — An Astro integration for i18next including some utility components.
- [astro-i18n](https://github.com/alexandre-fernandez/astro-i18n) — A TypeScript-first internationalization library for Astro.

0 comments on commit 49ee04e

Please sign in to comment.