Skip to content

Commit

Permalink
Use i18next for UI strings and add new injectTranslations plugin …
Browse files Browse the repository at this point in the history
…callback (#1923)

Co-authored-by: Chris Swithinbank <357379+delucis@users.noreply.github.com>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 18, 2024
1 parent d7a295e commit 5269aad
Show file tree
Hide file tree
Showing 58 changed files with 925 additions and 282 deletions.
19 changes: 19 additions & 0 deletions .changeset/cool-experts-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@astrojs/starlight': minor
---

Overhauls the built-in localization system which is now powered by the [`i18next`](https://www.i18next.com/) library and available to use anywhere in your documentation website.

See the [“Using UI translations”](https://starlight.astro.build/guides/i18n/#using-ui-translations) guide to learn more about how to access built-in UI labels or your own custom strings in your project. Plugin authors can also use the new [`injectTranslations()`](https://starlight.astro.build/reference/plugins/#injecttranslations) helper to add or update translation strings.

⚠️ **BREAKING CHANGE:** The `Astro.props.labels` props has been removed from the props passed down to custom component overrides.

If you are relying on `Astro.props.labels` (for example to read a built-in UI label), you will need to update your code to use the new [`Astro.locals.t()`](https://starlight.astro.build/guides/i18n/#using-ui-translations) helper instead.

```astro
---
import type { Props } from '@astrojs/starlight/props';
// The `search.label` UI label for this page’s language:
const searchLabel = Astro.locals.t('search.label');
---
```
11 changes: 11 additions & 0 deletions .changeset/eighty-beds-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@astrojs/starlight-docsearch': minor
---

⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now 0.28.0

Please use the `@astrojs/upgrade` command to upgrade your project:

```sh
npx @astrojs/upgrade
```
11 changes: 11 additions & 0 deletions .changeset/thirty-dodos-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@astrojs/starlight': minor
---

⚠️ **BREAKING CHANGE:** The minimum supported version of Astro is now 4.14.0

Please update Astro and Starlight together:

```sh
npx @astrojs/upgrade
```
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
prefer-workspace-packages=true
link-workspace-packages=true
shell-emulator=true
auto-install-peers=false
112 changes: 112 additions & 0 deletions docs/src/content/docs/guides/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,118 @@ export const collections = {
Learn more about content collection schemas in [“Defining a collection schema”](https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema) in the Astro docs.
## Using UI translations
You can access Starlight’s [built-in UI strings](/guides/i18n/#translate-starlights-ui) as well as [user-defined](/guides/i18n/#extend-translation-schema), and [plugin-provided](/reference/plugins/#injecttranslations) UI strings through a unified API powered by [i18next](https://www.i18next.com/).
This includes support for features like [interpolation](https://www.i18next.com/translation-function/interpolation) and [pluralization](https://www.i18next.com/translation-function/plurals).
In Astro components, this API is available as part of the [global `Astro` object](https://docs.astro.build/en/reference/api-reference/#astrolocals) as `Astro.locals.t`:
```astro title="example.astro"
<p dir={Astro.locals.t.dir()}>
{Astro.locals.t('404.text')}
</p>
```
You can also use the API in [endpoints](https://docs.astro.build/en/guides/endpoints/), where the `locals` object is available as part of the [endpoint context](https://docs.astro.build/en/reference/api-reference/#contextlocals):
```ts title="src/pages/404.ts"
export const GET = (context) => {
return new Response(context.locals.t('404.text'));
};
```
### Rendering a UI string
Render UI strings using the `locals.t()` function.
This is an instance of i18next’s `t()` function, which takes a UI string key as its first argument and returns the corresponding translation for the current language.
For example, given a custom translation file with the following content:
```json title="src/content/i18n/en.json"
{
"link.astro": "Astro documentation",
"link.astro.custom": "Astro documentation for {{feature}}"
}
```
The first UI string can be rendered by passing `'link.astro'` to the `t()` function:
```astro {3}
<!-- src/components/Example.astro -->
<a href="https://docs.astro.build/">
{Astro.locals.t('link.astro')}
</a>
<!-- Renders: <a href="...">Astro documentation</a> -->
```
The second UI string uses i18next’s [interpolation syntax](https://www.i18next.com/translation-function/interpolation) for the `{{feature}}` placeholder.
The value for `feature` must be set in an options object passed as the second argument to `t()`:
```astro {3}
<!-- src/components/Example.astro -->
<a href="https://docs.astro.build/en/guides/astro-db/">
{Astro.locals.t('link.astro.custom', { feature: 'Astro DB' })}
</a>
<!-- Renders: <a href="...">Astro documentation for Astro DB</a> -->
```
See the [i18next documentation](https://www.i18next.com/overview/api#t) for more information on how to use the `t()` function with interpolation, formatting, and more.
### Advanced APIs
#### `t.all()`
The `locals.t.all()` function returns an object containing all UI strings available for the current locale.
```astro
---
// src/components/Example.astro
const allStrings = Astro.locals.t.all();
// ^
// {
// "skipLink.label": "Skip to content",
// "search.label": "Search",
// …
// }
---
```
#### `t.exists()`
To check if a translation key exists for a locale, use the `locals.t.exists()` function with the translation key as first argument.
Pass an optional second argument if you need to override the current locale.
```astro
---
// src/components/Example.astro
const keyExistsInCurrentLocale = Astro.locals.t.exists('a.key');
// ^ true
const keyExistsInFrench = Astro.locals.t.exists('another.key', { lng: 'fr' });
// ^ false
---
```
See the [`exists()` reference in the i18next documentation](https://www.i18next.com/overview/api#exists) for more information.
#### `t.dir()`
The `locals.t.dir()` function returns the text direction of the current or a specific locale.
```astro
---
// src/components/Example.astro
const currentDirection = Astro.locals.t.dir();
// ^
// 'ltr'
const arabicDirection = Astro.locals.t.dir('ar');
// ^
// 'rtl'
---
```
See the [`dir()` reference in the i18next documentation](https://www.i18next.com/overview/api#dir) for more information.
## Accessing the current locale
You can use [`Astro.currentLocale`](https://docs.astro.build/en/reference/api-reference/#astrocurrentlocale) to read the current locale in `.astro` components.
Expand Down
6 changes: 0 additions & 6 deletions docs/src/content/docs/reference/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,6 @@ JavaScript `Date` object representing when this page was last updated if enabled

`URL` object for the address where this page can be edited if enabled.

#### `labels`

**Type:** `Record<string, string>`

An object containing UI strings localized for the current page. See the [“Translate Starlight’s UI”](/guides/i18n/#translate-starlights-ui) guide for a list of all the available keys.

---

## Components
Expand Down
69 changes: 69 additions & 0 deletions docs/src/content/docs/reference/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface StarlightPlugin {
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
logger: AstroIntegrationLogger;
injectTranslations: (Record<string, Record<string, string>>) => void;
}) => void | Promise<void>;
};
}
Expand Down Expand Up @@ -161,3 +162,71 @@ The example above will log a message that includes the provided info message:
```shell
[long-process-plugin] Starting long process…
```

#### `injectTranslations`

**type:** `(translations: Record<string, Record<string, string>>) => void`

A callback function to add or update translation strings used in Starlight’s [localization APIs](/guides/i18n/#using-ui-translations).

In the following example, a plugin injects translations for a custom UI string named `myPlugin.doThing` for the `en` and `fr` locales:

```ts {6-13} /(injectTranslations)[^(]/
// plugin.ts
export default {
name: 'plugin-with-translations',
hooks: {
setup({ injectTranslations }) {
injectTranslations({
en: {
'myPlugin.doThing': 'Do the thing',
},
fr: {
'myPlugin.doThing': 'Faire le truc',
},
});
},
},
};
```

To use the injected translations in your plugin UI, follow the [“Using UI translations” guide](/guides/i18n/#using-ui-translations).

Types for a plugin’s injected translation strings are generated automatically in a user’s project, but are not yet available when working in your plugin’s codebase.
To type the `locals.t` object in the context of your plugin, declare the following global namespaces in a TypeScript declaration file:

```ts
// env.d.ts
declare namespace App {
type StarlightLocals = import('@astrojs/starlight').StarlightLocals;
// Define the `locals.t` object in the context of a plugin.
interface Locals extends StarlightLocals {}
}

declare namespace StarlightApp {
// Define the additional plugin translations in the `I18n` interface.
interface I18n {
'myPlugin.doThing': string;
}
}
```

You can also infer the types for the `StarlightApp.I18n` interface from a source file if you have an object containing your translations.

For example, given the following source file:

```ts title="ui-strings.ts"
export const UIStrings = {
en: { 'myPlugin.doThing': 'Do the thing' },
fr: { 'myPlugin.doThing': 'Faire le truc' },
};
```

The following declaration would infer types from the English keys in the source file:

```ts title="env.d.ts"
declare namespace StarlightApp {
type UIStrings = typeof import('./ui-strings').UIStrings.en;
interface I18n extends UIStrings {}
}
```
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@
"limit": "14.5 kB",
"gzip": true
}
]
],
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"@algolia/client-search",
"search-insights"
]
}
}
}
19 changes: 12 additions & 7 deletions packages/docsearch/DocSearch.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,25 @@ import '@docsearch/css/dist/modal.css';
import type docsearch from '@docsearch/js';
import './variables.css';
const { labels } = Astro.props;
type DocSearchTranslationProps = Pick<
Parameters<typeof docsearch>[0],
'placeholder' | 'translations'
>;
const pick = (keyStart: string) =>
Object.fromEntries(
Object.entries(labels)
Object.entries(Astro.locals.t.all())
.filter(([key]) => key.startsWith(keyStart))
.map(([key, value]) => [key.replace(keyStart, ''), value])
);
const docsearchTranslations: DocSearchTranslationProps = {
placeholder: labels['search.label'],
placeholder: Astro.locals.t('search.label'),
translations: {
button: { buttonText: labels['search.label'], buttonAriaLabel: labels['search.label'] },
button: {
buttonText: Astro.locals.t('search.label'),
buttonAriaLabel: Astro.locals.t('search.label'),
},
modal: {
searchBox: pick('docsearch.searchBox.'),
startScreen: pick('docsearch.startScreen.'),
Expand All @@ -34,7 +35,11 @@ const docsearchTranslations: DocSearchTranslationProps = {
---

<sl-doc-search data-translations={JSON.stringify(docsearchTranslations)}>
<button type="button" class="DocSearch DocSearch-Button" aria-label={labels['search.label']}>
<button
type="button"
class="DocSearch DocSearch-Button"
aria-label={Astro.locals.t('search.label')}
>
<span class="DocSearch-Button-Container">
<svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20">
<path
Expand All @@ -45,7 +50,7 @@ const docsearchTranslations: DocSearchTranslationProps = {
stroke-linecap="round"
stroke-linejoin="round"></path>
</svg>
<span class="DocSearch-Button-Placeholder">{labels['search.label']}</span>
<span class="DocSearch-Button-Placeholder">{Astro.locals.t('search.label')}</span>
</span>
<span class="DocSearch-Button-Keys"></span>
</button>
Expand Down
5 changes: 4 additions & 1 deletion packages/docsearch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@
"./schema": "./schema.ts"
},
"peerDependencies": {
"@astrojs/starlight": ">=0.14.0"
"@astrojs/starlight": ">=0.28.0"
},
"dependencies": {
"@docsearch/css": "^3.6.0",
"@docsearch/js": "^3.6.0"
},
"devDependencies": {
"@astrojs/starlight": "workspace:*"
}
}
2 changes: 2 additions & 0 deletions packages/markdoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"./components": "./components.ts"
},
"devDependencies": {
"@astrojs/markdoc": "^0.11.4",
"@astrojs/starlight": "workspace:*",
"vitest": "^1.6.0"
},
"peerDependencies": {
Expand Down
19 changes: 16 additions & 3 deletions packages/starlight/__tests__/basics/route-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, test, vi } from 'vitest';
import { generateRouteData } from '../../utils/route-data';
import { routes } from '../../utils/routing';
import pkg from '../../package.json';

vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
Expand Down Expand Up @@ -87,12 +88,24 @@ test('uses explicit last updated date from frontmatter', () => {
expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated);
});

test('includes localized labels', () => {
test('throws when accessing a label using the deprecated `labels` prop in pre v1 versions', () => {
const isPreV1 = pkg.version[0] === '0';

const route = routes[0]!;
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
url: new URL('https://example.com'),
});
expect(data.labels).toBeDefined();
expect(data.labels['skipLink.label']).toBe('Skip to content');

if (isPreV1) {
expect(() => data.labels['any']).toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
The \`labels\` prop in component overrides has been removed.
Hint:
Replace \`Astro.props.labels["any"]\` with \`Astro.locals.t("any")\` instead.
For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations"
`);
} else {
expect(() => data.labels['any']).not.toThrow();
}
});
Loading

0 comments on commit 5269aad

Please sign in to comment.