summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-09-18 09:55:37 +0200
committerGitHub2024-09-18 09:55:37 +0200
commit5269aad928773ae08b35ba8e19c0f2832d0d2c89 (patch)
tree087e60ed8561925978b13bc06ea2afb6ddc27e3f
parentd7a295e5f63171c7eee9fc11333157d8c7e6c803 (diff)
downloadIT.starlight-5269aad928773ae08b35ba8e19c0f2832d0d2c89.tar.gz
IT.starlight-5269aad928773ae08b35ba8e19c0f2832d0d2c89.tar.bz2
IT.starlight-5269aad928773ae08b35ba8e19c0f2832d0d2c89.zip
Use `i18next` for UI strings and add new `injectTranslations` plugin 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>
-rw-r--r--.changeset/cool-experts-sort.md19
-rw-r--r--.changeset/eighty-beds-attack.md11
-rw-r--r--.changeset/thirty-dodos-drop.md11
-rw-r--r--.npmrc1
-rw-r--r--docs/src/content/docs/guides/i18n.mdx112
-rw-r--r--docs/src/content/docs/reference/overrides.md6
-rw-r--r--docs/src/content/docs/reference/plugins.md69
-rw-r--r--package.json10
-rw-r--r--packages/docsearch/DocSearch.astro19
-rw-r--r--packages/docsearch/package.json5
-rw-r--r--packages/markdoc/package.json2
-rw-r--r--packages/starlight/__tests__/basics/route-data.test.ts19
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data.test.ts11
-rw-r--r--packages/starlight/__tests__/basics/translations.test.ts149
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts19
-rw-r--r--packages/starlight/__tests__/i18n-single-root-locale/route-data.test.ts19
-rw-r--r--packages/starlight/__tests__/i18n/route-data.test.ts32
-rw-r--r--packages/starlight/__tests__/i18n/translations-fs.test.ts28
-rw-r--r--packages/starlight/__tests__/i18n/translations.test.ts8
-rw-r--r--packages/starlight/__tests__/plugins/translations.test.ts48
-rw-r--r--packages/starlight/__tests__/plugins/vitest.config.ts22
-rw-r--r--packages/starlight/__tests__/test-config.ts23
-rw-r--r--packages/starlight/__tests__/test-utils.ts13
-rw-r--r--packages/starlight/components/DraftContentNotice.astro4
-rw-r--r--packages/starlight/components/EditLink.astro4
-rw-r--r--packages/starlight/components/FallbackContentNotice.astro4
-rw-r--r--packages/starlight/components/Footer.astro2
-rw-r--r--packages/starlight/components/LanguageSelect.astro4
-rw-r--r--packages/starlight/components/LastUpdated.astro4
-rw-r--r--packages/starlight/components/MobileMenuToggle.astro4
-rw-r--r--packages/starlight/components/MobileTableOfContents.astro4
-rw-r--r--packages/starlight/components/PageFrame.astro4
-rw-r--r--packages/starlight/components/Pagination.astro6
-rw-r--r--packages/starlight/components/Search.astro18
-rw-r--r--packages/starlight/components/SidebarRestorePoint.astro2
-rw-r--r--packages/starlight/components/SkipLink.astro4
-rw-r--r--packages/starlight/components/TableOfContents.astro4
-rw-r--r--packages/starlight/components/ThemeSelect.astro10
-rw-r--r--packages/starlight/i18n.d.ts18
-rw-r--r--packages/starlight/index.ts36
-rw-r--r--packages/starlight/integrations/asides.ts2
-rw-r--r--packages/starlight/integrations/virtual-user-config.ts5
-rw-r--r--packages/starlight/locals.d.ts17
-rw-r--r--packages/starlight/locals.ts8
-rw-r--r--packages/starlight/package.json4
-rw-r--r--packages/starlight/utils/createTranslationSystem.ts93
-rw-r--r--packages/starlight/utils/i18n.ts20
-rw-r--r--packages/starlight/utils/plugins.ts69
-rw-r--r--packages/starlight/utils/route-data.ts9
-rw-r--r--packages/starlight/utils/starlight-page.ts21
-rw-r--r--packages/starlight/utils/translations-fs.ts7
-rw-r--r--packages/starlight/utils/translations.ts13
-rw-r--r--packages/starlight/utils/types.ts15
-rw-r--r--packages/starlight/virtual.d.ts5
-rw-r--r--packages/starlight/vitest.config.ts8
-rw-r--r--packages/tailwind/package.json1
-rw-r--r--pnpm-lock.yaml121
-rw-r--r--tsconfig.json1
58 files changed, 925 insertions, 282 deletions
diff --git a/.changeset/cool-experts-sort.md b/.changeset/cool-experts-sort.md
new file mode 100644
index 00000000..6de9fd8c
--- /dev/null
+++ b/.changeset/cool-experts-sort.md
@@ -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');
+---
+```
diff --git a/.changeset/eighty-beds-attack.md b/.changeset/eighty-beds-attack.md
new file mode 100644
index 00000000..000fa3e9
--- /dev/null
+++ b/.changeset/eighty-beds-attack.md
@@ -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
+```
diff --git a/.changeset/thirty-dodos-drop.md b/.changeset/thirty-dodos-drop.md
new file mode 100644
index 00000000..4fef44db
--- /dev/null
+++ b/.changeset/thirty-dodos-drop.md
@@ -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
+```
diff --git a/.npmrc b/.npmrc
index 901b4e86..11cb30ee 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,3 +1,4 @@
prefer-workspace-packages=true
link-workspace-packages=true
shell-emulator=true
+auto-install-peers=false
diff --git a/docs/src/content/docs/guides/i18n.mdx b/docs/src/content/docs/guides/i18n.mdx
index 2fe235b2..a320fbab 100644
--- a/docs/src/content/docs/guides/i18n.mdx
+++ b/docs/src/content/docs/guides/i18n.mdx
@@ -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.
diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md
index 9293654b..8e5ed012 100644
--- a/docs/src/content/docs/reference/overrides.md
+++ b/docs/src/content/docs/reference/overrides.md
@@ -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
diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md
index 7f44f60b..e22d146a 100644
--- a/docs/src/content/docs/reference/plugins.md
+++ b/docs/src/content/docs/reference/plugins.md
@@ -27,6 +27,7 @@ interface StarlightPlugin {
command: 'dev' | 'build' | 'preview';
isRestart: boolean;
logger: AstroIntegrationLogger;
+ injectTranslations: (Record<string, Record<string, string>>) => void;
}) => void | Promise<void>;
};
}
@@ -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 {}
+}
+```
diff --git a/package.json b/package.json
index e328ac87..41c2ad50 100644
--- a/package.json
+++ b/package.json
@@ -42,5 +42,13 @@
"limit": "14.5 kB",
"gzip": true
}
- ]
+ ],
+ "pnpm": {
+ "peerDependencyRules": {
+ "ignoreMissing": [
+ "@algolia/client-search",
+ "search-insights"
+ ]
+ }
+ }
}
diff --git a/packages/docsearch/DocSearch.astro b/packages/docsearch/DocSearch.astro
index 12db6b46..467192af 100644
--- a/packages/docsearch/DocSearch.astro
+++ b/packages/docsearch/DocSearch.astro
@@ -4,8 +4,6 @@ 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'
@@ -13,15 +11,18 @@ type DocSearchTranslationProps = Pick<
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.'),
@@ -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
@@ -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>
diff --git a/packages/docsearch/package.json b/packages/docsearch/package.json
index d4ec221e..8826ac25 100644
--- a/packages/docsearch/package.json
+++ b/packages/docsearch/package.json
@@ -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:*"
}
}
diff --git a/packages/markdoc/package.json b/packages/markdoc/package.json
index 81e79cd8..8b5898c4 100644
--- a/packages/markdoc/package.json
+++ b/packages/markdoc/package.json
@@ -17,6 +17,8 @@
"./components": "./components.ts"
},
"devDependencies": {
+ "@astrojs/markdoc": "^0.11.4",
+ "@astrojs/starlight": "workspace:*",
"vitest": "^1.6.0"
},
"peerDependencies": {
diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts
index d59ce013..f3293259 100644
--- a/packages/starlight/__tests__/basics/route-data.test.ts
+++ b/packages/starlight/__tests__/basics/route-data.test.ts
@@ -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({
@@ -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();
+ }
});
diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
index 63ac6efd..c3510a65 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
@@ -467,13 +467,18 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla
expect(data.hasSidebar).toBe(false);
});
-test('includes localized labels', async () => {
+test('throws when accessing a label using the deprecated `labels` prop', async () => {
const data = await generateStarlightPageRouteData({
props: starlightPageProps,
url: starlightPageUrl,
});
- expect(data.labels).toBeDefined();
- expect(data.labels['skipLink.label']).toBe('Skip to content');
+ 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"
+ `);
});
test('uses provided edit URL if any', async () => {
diff --git a/packages/starlight/__tests__/basics/translations.test.ts b/packages/starlight/__tests__/basics/translations.test.ts
new file mode 100644
index 00000000..abada3e4
--- /dev/null
+++ b/packages/starlight/__tests__/basics/translations.test.ts
@@ -0,0 +1,149 @@
+import { describe, expect, test, vi } from 'vitest';
+import { useTranslations } from '../../utils/translations';
+import translations from '../../translations';
+
+describe('useTranslations()', () => {
+ test('includes localized UI strings', () => {
+ const t = useTranslations(undefined);
+ expect(t).toBeTypeOf('function');
+ expect(t('skipLink.label')).toBe('Skip to content');
+ });
+});
+
+describe('t()', async () => {
+ // The mocked user-defined translations are scoped to this `describe` block so that they do not
+ // affect other tests (`vi.mock` → `vi.doMock`).
+ vi.doMock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ i18n: [
+ [
+ 'en',
+ {
+ 'test.interpolation': '{{subject}} is {{adjective}}',
+ 'test.dataModel': 'Powered by {{integration.name}}',
+ 'test.escape': 'The tag is {{tag}}',
+ 'test.unescape': 'The tag is {{- tag}}',
+ 'test.currency': 'The price is {{price, currency(USD)}}',
+ 'test.list': '{{subjects, list}} are awesome',
+ 'test.count_one': '{{count}} project',
+ 'test.count_other': '{{count}} projects',
+ 'test.nesting1': '$t(test.nesting2) is nested',
+ 'test.nesting2': 'this UI string',
+ },
+ // We do not strip unknown translations in this test so that user-defined translations can
+ // override plugin translations like it would in a real- world scenario were the plugin
+ // would have provided a custom schema to extend the translations.
+ { stripUnknown: false },
+ ],
+ ],
+ })
+ );
+ // Reset the modules registry so that re-importing `../../utils/translations` re-evaluates the
+ // module and re-computes `useTranslations`. Re-importing the module is necessary because
+ // top-level imports cannot be re-evaluated.
+ vi.resetModules();
+ const { useTranslations } = await import('../../utils/translations');
+ const t = useTranslations(undefined);
+
+ test('supports using interpolation', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.interpolation', { subject: 'Starlight', adjective: 'amazing' })).toBe(
+ 'Starlight is amazing'
+ );
+ });
+
+ test('supports using data models', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.dataModel', { integration: { name: 'Starlight' } })).toBe(
+ 'Powered by Starlight'
+ );
+ });
+
+ test('escapes by default', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.escape', { tag: '<img />' })).toBe('The tag is &lt;img &#x2F;&gt;');
+ });
+
+ test('supports unescaped strings', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.unescape', { tag: '<img />' })).toBe('The tag is <img />');
+ });
+
+ test('supports currencies', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.currency', { price: 1000 })).toBe('The price is $1,000.00');
+ });
+
+ test('supports lists', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.list', { subjects: ['Astro', 'Starlight', 'Astro DB'] })).toBe(
+ 'Astro, Starlight, and Astro DB are awesome'
+ );
+ });
+
+ test('supports counts', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.count', { count: 1 })).toBe('1 project');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.count', { count: 20 })).toBe('20 projects');
+ });
+
+ test('supports nesting', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a mocked translation key.
+ expect(t('test.nesting1')).toBe('this UI string is nested');
+ });
+
+ test('returns the UI string key if the translation is missing', () => {
+ expect(t).toBeTypeOf('function');
+ // @ts-expect-error - using a missing translation key.
+ expect(t('test.unknown')).toBe('test.unknown');
+ });
+});
+
+describe('t.all()', async () => {
+ // See the `t()` tests for an explanation of how the user-defined translations are mocked.
+ vi.doMock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ i18n: [['en', { 'test.foo': 'bar' }, { stripUnknown: false }]],
+ })
+ );
+ vi.resetModules();
+ const { useTranslations } = await import('../../utils/translations');
+ const t = useTranslations(undefined);
+
+ test('returns all translations including custom ones', () => {
+ expect(t.all).toBeTypeOf('function');
+ expect(t.all()).toEqual({ ...translations.en, 'test.foo': 'bar' });
+ });
+});
+
+describe('t.exists()', async () => {
+ // See the `t()` tests for an explanation of how the user-defined translations are mocked.
+ vi.doMock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ i18n: [['en', { 'test.foo': 'bar' }, { stripUnknown: false }]],
+ })
+ );
+ vi.resetModules();
+ const { useTranslations } = await import('../../utils/translations');
+ const t = useTranslations(undefined);
+
+ test('returns `true` for existing translations', () => {
+ expect(t.exists).toBeTypeOf('function');
+ expect(t.exists('skipLink.label')).toBe(true);
+ expect(t.exists('test.foo')).toBe(true);
+ });
+
+ test('returns `false` for unknown translations', () => {
+ expect(t.exists).toBeTypeOf('function');
+ expect(t.exists('test.unknown')).toBe(false);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts
deleted file mode 100644
index dbf5d354..00000000
--- a/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
-import { routes } from '../../utils/routing';
-
-vi.mock('astro:content', async () =>
- (await import('../test-utils')).mockedAstroContent({
- docs: [['fr/index.mdx', { title: 'Accueil' }]],
- })
-);
-
-test('includes localized labels (fr)', () => {
- 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('Aller au contenu');
-});
diff --git a/packages/starlight/__tests__/i18n-single-root-locale/route-data.test.ts b/packages/starlight/__tests__/i18n-single-root-locale/route-data.test.ts
deleted file mode 100644
index 654ba1b7..00000000
--- a/packages/starlight/__tests__/i18n-single-root-locale/route-data.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
-import { routes } from '../../utils/routing';
-
-vi.mock('astro:content', async () =>
- (await import('../test-utils')).mockedAstroContent({
- docs: [['index.mdx', { title: 'Accueil' }]],
- })
-);
-
-test('includes localized labels (fr)', () => {
- 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('Aller au contenu');
-});
diff --git a/packages/starlight/__tests__/i18n/route-data.test.ts b/packages/starlight/__tests__/i18n/route-data.test.ts
deleted file mode 100644
index 57beed0c..00000000
--- a/packages/starlight/__tests__/i18n/route-data.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { expect, test, vi } from 'vitest';
-import { generateRouteData } from '../../utils/route-data';
-import { routes } from '../../utils/routing';
-
-vi.mock('astro:content', async () =>
- (await import('../test-utils')).mockedAstroContent({
- docs: [
- ['fr/index.mdx', { title: 'Accueil' }],
- ['pt-br/index.mdx', { title: 'Pagina inicial' }],
- ],
- })
-);
-
-test('includes localized labels (fr)', () => {
- 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('Aller au contenu');
-});
-
-test('includes localized labels (pt-br)', () => {
- const route = routes[1]!;
- 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('Pular para o conteúdo');
-});
diff --git a/packages/starlight/__tests__/i18n/translations-fs.test.ts b/packages/starlight/__tests__/i18n/translations-fs.test.ts
index 5b9025aa..0115f1d3 100644
--- a/packages/starlight/__tests__/i18n/translations-fs.test.ts
+++ b/packages/starlight/__tests__/i18n/translations-fs.test.ts
@@ -73,4 +73,32 @@ describe('createTranslationSystemFromFs', () => {
)
).toThrow(SyntaxError);
});
+
+ test('creates a translation system that uses custom strings injected by plugins', () => {
+ const useTranslations = createTranslationSystemFromFs(
+ {
+ locales: { en: { label: 'English', dir: 'ltr' } },
+ defaultLocale: { label: 'English', locale: 'en', dir: 'ltr' },
+ },
+ // Using non-existent `_src/` to ignore custom files in this test fixture.
+ { srcDir: new URL('./_src/', import.meta.url) },
+ { en: { 'page.editLink': 'Make this page even more different' } }
+ );
+ const t = useTranslations('en');
+ expect(t('page.editLink')).toMatchInlineSnapshot('"Make this page even more different"');
+ });
+
+ test('creates a translation system that prioritizes user translations over plugin translations', () => {
+ const useTranslations = createTranslationSystemFromFs(
+ {
+ locales: { en: { label: 'English', dir: 'ltr' } },
+ defaultLocale: { label: 'English', locale: 'en', dir: 'ltr' },
+ },
+ // Using `src/` to load custom files in this test fixture.
+ { srcDir: new URL('./src/', import.meta.url) },
+ { en: { 'page.editLink': 'Make this page even more different' } }
+ );
+ const t = useTranslations('en');
+ expect(t('page.editLink')).toMatchInlineSnapshot('"Make this page different"');
+ });
});
diff --git a/packages/starlight/__tests__/i18n/translations.test.ts b/packages/starlight/__tests__/i18n/translations.test.ts
index 35cd9491..e0c0ccbe 100644
--- a/packages/starlight/__tests__/i18n/translations.test.ts
+++ b/packages/starlight/__tests__/i18n/translations.test.ts
@@ -28,3 +28,11 @@ describe('useTranslations()', () => {
expect(t('page.nextLink')).not.toBe(translations.en?.['page.nextLink']);
});
});
+
+describe('t.dir()', async () => {
+ test('returns text directions', () => {
+ expect(useTranslations(undefined).dir()).toBe('ltr');
+ expect(useTranslations('fr').dir()).toBe('ltr');
+ expect(useTranslations('ar').dir()).toBe('rtl');
+ });
+});
diff --git a/packages/starlight/__tests__/plugins/translations.test.ts b/packages/starlight/__tests__/plugins/translations.test.ts
new file mode 100644
index 00000000..3f6574f7
--- /dev/null
+++ b/packages/starlight/__tests__/plugins/translations.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, test, vi } from 'vitest';
+import { useTranslations } from '../../utils/translations';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ // We do not strip unknown translations in this test so that user-defined translations can
+ // override plugin translations like it would in a real- world scenario were the plugin would
+ // have provided a custom schema to extend the translations.
+ i18n: [['ar', { 'testPlugin3.doThing': 'افعل الشيء' }, { stripUnknown: false }]],
+ })
+);
+
+describe('useTranslations()', () => {
+ test('includes UI strings injected by plugins for the default locale', () => {
+ const t = useTranslations(undefined);
+ expect(t).toBeTypeOf('function');
+ // Include the default locale strings.
+ expect(t('skipLink.label')).toBe('Skip to content');
+ // Include a built-in translation overriden by a plugin.
+ expect(t('search.label')).toBe('Search the thing');
+ // Include a translation injected by a plugin.
+ // @ts-expect-error - translation key injected by a test plugin.
+ expect(t('testPlugin3.doThing')).toBe('Do the Plugin 3 thing');
+ });
+
+ test('includes UI strings injected by plugins', () => {
+ const t = useTranslations('fr');
+ // Include the default locale strings.
+ expect(t('skipLink.label')).toBe('Aller au contenu');
+ // Include a built-in translation overriden by a plugin.
+ expect(t('search.label')).toBe('Rechercher le truc');
+ // Include a translation injected by a plugin.
+ // @ts-expect-error - translation key injected by a test plugin.
+ expect(t('testPlugin3.doThing')).toBe('Faire la chose du plugin 3');
+ });
+
+ test('uses user-defined translations for untranslated strings injected by plugins', () => {
+ const t = useTranslations('pt-br');
+ // @ts-expect-error - translation key injected by a test plugin.
+ expect(t('testPlugin3.doThing')).toBe('Do the Plugin 3 thing');
+ });
+
+ test('prefers user-defined translations over plugin translations', () => {
+ const t = useTranslations('ar');
+ // @ts-expect-error - translation key injected by a test plugin.
+ expect(t('testPlugin3.doThing')).toBe('افعل الشيء');
+ });
+});
diff --git a/packages/starlight/__tests__/plugins/vitest.config.ts b/packages/starlight/__tests__/plugins/vitest.config.ts
index a6eaa3b2..78c4c254 100644
--- a/packages/starlight/__tests__/plugins/vitest.config.ts
+++ b/packages/starlight/__tests__/plugins/vitest.config.ts
@@ -3,6 +3,13 @@ import { defineVitestConfig } from '../test-config';
export default defineVitestConfig({
title: 'Plugins',
sidebar: [{ label: 'Getting Started', link: 'getting-started' }],
+ defaultLocale: 'en',
+ locales: {
+ en: { label: 'English', lang: 'en' },
+ fr: { label: 'French' },
+ ar: { label: 'Arabic', dir: 'rtl' },
+ 'pt-br': { label: 'Brazilian Portuguese', lang: 'pt-BR' },
+ },
plugins: [
{
name: 'test-plugin-1',
@@ -37,11 +44,24 @@ export default defineVitestConfig({
{
name: 'test-plugin-3',
hooks: {
- async setup({ config, updateConfig }) {
+ async setup({ config, updateConfig, injectTranslations }) {
await Promise.resolve();
updateConfig({
description: `${config.description} - plugin 3`,
});
+ injectTranslations({
+ en: {
+ 'search.label': 'Search the thing',
+ 'testPlugin3.doThing': 'Do the Plugin 3 thing',
+ },
+ fr: {
+ 'search.label': 'Rechercher le truc',
+ 'testPlugin3.doThing': 'Faire la chose du plugin 3',
+ },
+ ar: {
+ 'testPlugin3.doThing': 'قم بعمل المكون الإضافي 3',
+ },
+ });
},
},
},
diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts
index b1a34440..47ab267d 100644
--- a/packages/starlight/__tests__/test-config.ts
+++ b/packages/starlight/__tests__/test-config.ts
@@ -20,15 +20,24 @@ export async function defineVitestConfig(
const trailingSlash = opts?.trailingSlash ?? 'ignore';
const command = opts?.command ?? 'dev';
- const { starlightConfig } = await runPlugins(config, plugins, createTestPluginContext());
+ const { starlightConfig, pluginTranslations } = await runPlugins(
+ config,
+ plugins,
+ createTestPluginContext()
+ );
return getViteConfig({
plugins: [
- vitePluginStarlightUserConfig(command, starlightConfig, {
- root,
- srcDir,
- build,
- trailingSlash,
- }),
+ vitePluginStarlightUserConfig(
+ command,
+ starlightConfig,
+ {
+ root,
+ srcDir,
+ build,
+ trailingSlash,
+ },
+ pluginTranslations
+ ),
],
test: {
snapshotSerializers: ['./snapshot-serializer-astro-error.ts'],
diff --git a/packages/starlight/__tests__/test-utils.ts b/packages/starlight/__tests__/test-utils.ts
index 2e821544..69b01eda 100644
--- a/packages/starlight/__tests__/test-utils.ts
+++ b/packages/starlight/__tests__/test-utils.ts
@@ -37,8 +37,17 @@ function mockDoc(
};
}
-function mockDict(id: string, data: z.input<ReturnType<typeof i18nSchema>>) {
- return { id, data: i18nSchema().parse(data) };
+function mockDict(
+ id: string,
+ data: z.input<ReturnType<typeof i18nSchema>>,
+ { stripUnknown } = { stripUnknown: true }
+) {
+ return {
+ id,
+ data: stripUnknown
+ ? i18nSchema().parse(data)
+ : i18nSchema().and(z.record(z.string())).parse(data),
+ };
}
export async function mockedAstroContent({
diff --git a/packages/starlight/components/DraftContentNotice.astro b/packages/starlight/components/DraftContentNotice.astro
index 67cd0466..70d0d4fa 100644
--- a/packages/starlight/components/DraftContentNotice.astro
+++ b/packages/starlight/components/DraftContentNotice.astro
@@ -1,8 +1,6 @@
---
import ContentNotice from './ContentNotice.astro';
import type { Props } from '../props';
-
-const { labels } = Astro.props;
---
-<ContentNotice icon="warning" label={labels['page.draft']} />
+<ContentNotice icon="warning" label={Astro.locals.t('page.draft')} />
diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro
index 74be4c8f..711d3999 100644
--- a/packages/starlight/components/EditLink.astro
+++ b/packages/starlight/components/EditLink.astro
@@ -2,14 +2,14 @@
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-const { editUrl, labels } = Astro.props;
+const { editUrl } = Astro.props;
---
{
editUrl && (
<a href={editUrl} class="sl-flex">
<Icon name="pencil" size="1.2em" />
- {labels['page.editLink']}
+ {Astro.locals.t('page.editLink')}
</a>
)
}
diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro
index b3474fb2..616d06fe 100644
--- a/packages/starlight/components/FallbackContentNotice.astro
+++ b/packages/starlight/components/FallbackContentNotice.astro
@@ -1,8 +1,6 @@
---
import ContentNotice from './ContentNotice.astro';
import type { Props } from '../props';
-
-const { labels } = Astro.props;
---
-<ContentNotice icon="warning" label={labels['i18n.untranslatedContent']} />
+<ContentNotice icon="warning" label={Astro.locals.t('i18n.untranslatedContent')} />
diff --git a/packages/starlight/components/Footer.astro b/packages/starlight/components/Footer.astro
index f75b5e40..0bba6825 100644
--- a/packages/starlight/components/Footer.astro
+++ b/packages/starlight/components/Footer.astro
@@ -18,7 +18,7 @@ import { Icon } from '../components';
{
config.credits && (
<a class="kudos sl-flex" href="https://starlight.astro.build">
- <Icon name={'starlight'} /> {Astro.props.labels['builtWithStarlight.label']}
+ <Icon name={'starlight'} /> {Astro.locals.t('builtWithStarlight.label')}
</a>
)
}
diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro
index c85292dc..42999eac 100644
--- a/packages/starlight/components/LanguageSelect.astro
+++ b/packages/starlight/components/LanguageSelect.astro
@@ -10,8 +10,6 @@ import type { Props } from '../props';
function localizedPathname(locale: string | undefined): string {
return localizedUrl(Astro.url, locale).pathname;
}
-
-const { labels } = Astro.props;
---
{
@@ -19,7 +17,7 @@ const { labels } = Astro.props;
<starlight-lang-select>
<Select
icon="translate"
- label={labels['languageSelect.accessibleLabel']}
+ label={Astro.locals.t('languageSelect.accessibleLabel')}
value={localizedPathname(Astro.props.locale)}
options={Object.entries(config.locales).map(([code, locale]) => ({
value: localizedPathname(code),
diff --git a/packages/starlight/components/LastUpdated.astro b/packages/starlight/components/LastUpdated.astro
index 6e5fb841..74c28210 100644
--- a/packages/starlight/components/LastUpdated.astro
+++ b/packages/starlight/components/LastUpdated.astro
@@ -1,13 +1,13 @@
---
import type { Props } from '../props';
-const { labels, lang, lastUpdated } = Astro.props;
+const { lang, lastUpdated } = Astro.props;
---
{
lastUpdated && (
<p>
- {labels['page.lastUpdated']}{' '}
+ {Astro.locals.t('page.lastUpdated')}{' '}
<time datetime={lastUpdated.toISOString()}>
{lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium', timeZone: 'UTC' })}
</time>
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index b48e1230..56d36e4b 100644
--- a/packages/starlight/components/MobileMenuToggle.astro
+++ b/packages/starlight/components/MobileMenuToggle.astro
@@ -1,14 +1,12 @@
---
import type { Props } from '../props';
import Icon from '../user-components/Icon.astro';
-
-const { labels } = Astro.props;
---
<starlight-menu-button>
<button
aria-expanded="false"
- aria-label={labels['menuButton.accessibleLabel']}
+ aria-label={Astro.locals.t('menuButton.accessibleLabel')}
aria-controls="starlight__sidebar"
class="sl-flex md:sl-hidden"
>
diff --git a/packages/starlight/components/MobileTableOfContents.astro b/packages/starlight/components/MobileTableOfContents.astro
index 74342e3c..506ee20b 100644
--- a/packages/starlight/components/MobileTableOfContents.astro
+++ b/packages/starlight/components/MobileTableOfContents.astro
@@ -3,7 +3,7 @@ import Icon from '../user-components/Icon.astro';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
import type { Props } from '../props';
-const { labels, toc } = Astro.props;
+const { toc } = Astro.props;
---
{
@@ -13,7 +13,7 @@ const { labels, toc } = Astro.props;
<details id="starlight__mobile-toc">
<summary id="starlight__on-this-page--mobile" class="sl-flex">
<div class="toggle sl-flex">
- {labels['tableOfContents.onThisPage']}
+ {Astro.locals.t('tableOfContents.onThisPage')}
<Icon name={'right-caret'} class="caret" size="1rem" />
</div>
<span class="display-current" />
diff --git a/packages/starlight/components/PageFrame.astro b/packages/starlight/components/PageFrame.astro
index 33980343..3a9f0d27 100644
--- a/packages/starlight/components/PageFrame.astro
+++ b/packages/starlight/components/PageFrame.astro
@@ -2,14 +2,14 @@
import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle';
import type { Props } from '../props';
-const { hasSidebar, labels } = Astro.props;
+const { hasSidebar } = Astro.props;
---
<div class="page sl-flex">
<header class="header"><slot name="header" /></header>
{
hasSidebar && (
- <nav class="sidebar" aria-label={labels['sidebarNav.accessibleLabel']}>
+ <nav class="sidebar" aria-label={Astro.locals.t('sidebarNav.accessibleLabel')}>
<MobileMenuToggle {...Astro.props} />
<div id="starlight__sidebar" class="sidebar-pane">
<div class="sidebar-content sl-flex">
diff --git a/packages/starlight/components/Pagination.astro b/packages/starlight/components/Pagination.astro
index 0c92c31e..9ae74521 100644
--- a/packages/starlight/components/Pagination.astro
+++ b/packages/starlight/components/Pagination.astro
@@ -2,7 +2,7 @@
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-const { dir, labels, pagination } = Astro.props;
+const { dir, pagination } = Astro.props;
const { prev, next } = pagination;
const isRtl = dir === 'rtl';
---
@@ -13,7 +13,7 @@ const isRtl = dir === 'rtl';
<a href={prev.href} rel="prev">
<Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
<span>
- {labels['page.previousLink']}
+ {Astro.locals.t('page.previousLink')}
<br />
<span class="link-title">{prev.label}</span>
</span>
@@ -25,7 +25,7 @@ const isRtl = dir === 'rtl';
<a href={next.href} rel="next">
<Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
<span>
- {labels['page.nextLink']}
+ {Astro.locals.t('page.nextLink')}
<br />
<span class="link-title">{next.label}</span>
</span>
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index 713e9415..c07c6cb6 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -4,12 +4,10 @@ import Icon from '../user-components/Icon.astro';
import project from 'virtual:starlight/project-context';
import type { Props } from '../props';
-const { labels } = Astro.props;
-
const pagefindTranslations = {
- placeholder: labels['search.label'],
+ placeholder: Astro.locals.t('search.label'),
...Object.fromEntries(
- Object.entries(labels)
+ Object.entries(Astro.locals.t.all())
.filter(([key]) => key.startsWith('pagefind.'))
.map(([key, value]) => [key.replace('pagefind.', ''), value])
),
@@ -23,28 +21,28 @@ const pagefindTranslations = {
<button
data-open-modal
disabled
- aria-label={labels['search.label']}
+ aria-label={Astro.locals.t('search.label')}
aria-keyshortcuts="Control+K"
>
<Icon name="magnifier" />
- <span class="sl-hidden md:sl-block" aria-hidden="true">{labels['search.label']}</span>
+ <span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
<kbd class="sl-hidden md:sl-flex" style="display: none;">
- <kbd>{labels['search.ctrlKey']}</kbd><kbd>K</kbd>
+ <kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
</kbd>
</button>
- <dialog style="padding:0" aria-label={labels['search.label']}>
+ <dialog style="padding:0" aria-label={Astro.locals.t('search.label')}>
<div class="dialog-frame sl-flex">
{
/* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
}
<button data-close-modal class="sl-flex md:sl-hidden">
- {labels['search.cancelLabel']}
+ {Astro.locals.t('search.cancelLabel')}
</button>
{
import.meta.env.DEV ? (
<div style="margin: auto; text-align: center; white-space: pre-line;" dir="ltr">
- <p>{labels['search.devWarning']}</p>
+ <p>{Astro.locals.t('search.devWarning')}</p>
</div>
) : (
<div class="search-container">
diff --git a/packages/starlight/components/SidebarRestorePoint.astro b/packages/starlight/components/SidebarRestorePoint.astro
index d3d96939..5da9b8b2 100644
--- a/packages/starlight/components/SidebarRestorePoint.astro
+++ b/packages/starlight/components/SidebarRestorePoint.astro
@@ -1,7 +1,7 @@
---
/** Unique symbol for storing a running index in `locals`. */
const currentGroupIndexSymbol = Symbol.for('starlight-sidebar-group-index');
-const locals = Astro.locals as Record<typeof currentGroupIndexSymbol, number>;
+const locals = Astro.locals as App.Locals & { [currentGroupIndexSymbol]: number };
/** The current sidebar group’s index retrieved from `locals` if set, starting at `0`. */
const index = locals[currentGroupIndexSymbol] || 0;
diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro
index 307c1ef9..72d18c17 100644
--- a/packages/starlight/components/SkipLink.astro
+++ b/packages/starlight/components/SkipLink.astro
@@ -1,11 +1,9 @@
---
import { PAGE_TITLE_ID } from '../constants';
import type { Props } from '../props';
-
-const { labels } = Astro.props;
---
-<a href={`#${PAGE_TITLE_ID}`}>{labels['skipLink.label']}</a>
+<a href={`#${PAGE_TITLE_ID}`}>{Astro.locals.t('skipLink.label')}</a>
<style>
a {
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index eafd3d86..3d8d9e37 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -2,14 +2,14 @@
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
import type { Props } from '../props';
-const { labels, toc } = Astro.props;
+const { toc } = Astro.props;
---
{
toc && (
<starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
<nav aria-labelledby="starlight__on-this-page">
- <h2 id="starlight__on-this-page">{labels['tableOfContents.onThisPage']}</h2>
+ <h2 id="starlight__on-this-page">{Astro.locals.t('tableOfContents.onThisPage')}</h2>
<TableOfContentsList toc={toc.items} />
</nav>
</starlight-toc>
diff --git a/packages/starlight/components/ThemeSelect.astro b/packages/starlight/components/ThemeSelect.astro
index 97015602..7db1aff7 100644
--- a/packages/starlight/components/ThemeSelect.astro
+++ b/packages/starlight/components/ThemeSelect.astro
@@ -1,20 +1,18 @@
---
import Select from './Select.astro';
import type { Props } from '../props';
-
-const { labels } = Astro.props;
---
<starlight-theme-select>
{/* TODO: Can we give this select a width that works well for each language’s strings? */}
<Select
icon="laptop"
- label={labels['themeSelect.accessibleLabel']}
+ label={Astro.locals.t('themeSelect.accessibleLabel')}
value="auto"
options={[
- { label: labels['themeSelect.dark'], selected: false, value: 'dark' },
- { label: labels['themeSelect.light'], selected: false, value: 'light' },
- { label: labels['themeSelect.auto'], selected: true, value: 'auto' },
+ { label: Astro.locals.t('themeSelect.dark'), selected: false, value: 'dark' },
+ { label: Astro.locals.t('themeSelect.light'), selected: false, value: 'light' },
+ { label: Astro.locals.t('themeSelect.auto'), selected: true, value: 'auto' },
]}
width="6.25em"
/>
diff --git a/packages/starlight/i18n.d.ts b/packages/starlight/i18n.d.ts
new file mode 100644
index 00000000..8cb82217
--- /dev/null
+++ b/packages/starlight/i18n.d.ts
@@ -0,0 +1,18 @@
+/*
+ * This file imports the original `i18next` types and extends them to configure the
+ * Starlight namespace.
+ *
+ * Note that the top-level `import` makes this module non-ambient, so can’t be
+ * combined with other `.d.ts` files such as `locals.d.ts`.
+ */
+
+import 'i18next';
+
+declare module 'i18next' {
+ interface CustomTypeOptions {
+ defaultNS: typeof import('./utils/createTranslationSystem').I18nextNamespace;
+ resources: {
+ starlight: Record<import('./utils/createTranslationSystem').I18nKeys, string>;
+ };
+ }
+}
diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts
index d2a5e572..50b1a87e 100644
--- a/packages/starlight/index.ts
+++ b/packages/starlight/index.ts
@@ -1,3 +1,11 @@
+/**
+ * These triple-slash directives defines dependencies to various declaration files that will be
+ * loaded when a user imports the Starlight integration in their Astro configuration file. These
+ * directives must be first at the top of the file and can only be preceded by this comment.
+ */
+/// <reference path="./locals.d.ts" />
+/// <reference path="./i18n.d.ts" />
+
import mdx from '@astrojs/mdx';
import type { AstroIntegration } from 'astro';
import { spawn } from 'node:child_process';
@@ -9,7 +17,12 @@ import { starlightSitemap } from './integrations/sitemap';
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
import { createTranslationSystemFromFs } from './utils/translations-fs';
-import { runPlugins, type StarlightUserConfigWithPlugins } from './utils/plugins';
+import {
+ injectPluginTranslationsTypes,
+ runPlugins,
+ type PluginTranslations,
+ type StarlightUserConfigWithPlugins,
+} from './utils/plugins';
import { processI18nConfig } from './utils/i18n';
import type { StarlightConfig } from './types';
@@ -18,10 +31,12 @@ export default function StarlightIntegration({
...opts
}: StarlightUserConfigWithPlugins): AstroIntegration {
let userConfig: StarlightConfig;
+ let pluginTranslations: PluginTranslations = {};
return {
name: '@astrojs/starlight',
hooks: {
'astro:config:setup': async ({
+ addMiddleware,
command,
config,
injectRoute,
@@ -42,10 +57,17 @@ export default function StarlightIntegration({
config.i18n
);
- const { integrations } = pluginResult;
+ const integrations = pluginResult.integrations;
+ pluginTranslations = pluginResult.pluginTranslations;
userConfig = starlightConfig;
- const useTranslations = createTranslationSystemFromFs(starlightConfig, config);
+ const useTranslations = createTranslationSystemFromFs(
+ starlightConfig,
+ config,
+ pluginTranslations
+ );
+
+ addMiddleware({ entrypoint: '@astrojs/starlight/locals', order: 'pre' });
if (!starlightConfig.disable404Route) {
injectRoute({
@@ -91,7 +113,9 @@ export default function StarlightIntegration({
updateConfig({
vite: {
- plugins: [vitePluginStarlightUserConfig(command, starlightConfig, config)],
+ plugins: [
+ vitePluginStarlightUserConfig(command, starlightConfig, config, pluginTranslations),
+ ],
},
markdown: {
remarkPlugins: [
@@ -112,6 +136,10 @@ export default function StarlightIntegration({
});
},
+ 'astro:config:done': ({ injectTypes }) => {
+ injectPluginTranslationsTypes(pluginTranslations, injectTypes);
+ },
+
'astro:build:done': ({ dir }) => {
if (!userConfig.pagefind) return;
const targetDir = fileURLToPath(dir);
diff --git a/packages/starlight/integrations/asides.ts b/packages/starlight/integrations/asides.ts
index 7c4dfb74..00ca11a6 100644
--- a/packages/starlight/integrations/asides.ts
+++ b/packages/starlight/integrations/asides.ts
@@ -166,7 +166,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
// children with the `directiveLabel` property set to true. We want to pass it as the title
// prop to <Aside>, so when we find a directive label, we store it for the title prop and
// remove the paragraph from the container’s children.
- let title = t(`aside.${variant}`);
+ let title: string = t(`aside.${variant}`);
let titleNode: PhrasingContent[] = [{ type: 'text', value: title }];
const firstChild = node.children[0];
if (
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index 92207e0c..52f60bad 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { StarlightConfig } from '../utils/user-config';
import { getAllNewestCommitDate } from '../utils/git';
+import type { PluginTranslations } from '../utils/plugins';
function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
return `\0${id}`;
@@ -19,7 +20,8 @@ export function vitePluginStarlightUserConfig(
trailingSlash,
}: Pick<AstroConfig, 'root' | 'srcDir' | 'trailingSlash'> & {
build: Pick<AstroConfig['build'], 'format'>;
- }
+ },
+ pluginTranslations: PluginTranslations
): NonNullable<ViteUserConfig['plugins']>[number] {
/**
* Resolves module IDs to a usable format:
@@ -80,6 +82,7 @@ export function vitePluginStarlightUserConfig(
userCollections = (await import(${resolveId('./content/config.ts', srcDir)})).collections;
} catch {}
export const collections = userCollections;`,
+ 'virtual:starlight/plugin-translations': `export default ${JSON.stringify(pluginTranslations)}`,
...virtualComponentModules,
} satisfies Record<string, string>;
diff --git a/packages/starlight/locals.d.ts b/packages/starlight/locals.d.ts
new file mode 100644
index 00000000..160b5eae
--- /dev/null
+++ b/packages/starlight/locals.d.ts
@@ -0,0 +1,17 @@
+/**
+ * This namespace is reserved for Starlight (only used for i18n at the moment).
+ * It can be extended by plugins using module augmentation and interface merging.
+ * For an example, see: https://starlight.astro.build/reference/plugins/#injecttranslations
+ */
+declare namespace StarlightApp {
+ interface I18n {}
+}
+
+/**
+ * Extending Astro’s `App.Locals` interface registers types for the middleware added by Starlight.
+ */
+declare namespace App {
+ interface Locals {
+ t: import('./utils/createTranslationSystem').I18nT;
+ }
+}
diff --git a/packages/starlight/locals.ts b/packages/starlight/locals.ts
new file mode 100644
index 00000000..481a8e01
--- /dev/null
+++ b/packages/starlight/locals.ts
@@ -0,0 +1,8 @@
+import { defineMiddleware } from 'astro:middleware';
+import { useTranslations } from './utils/translations';
+
+export const onRequest = defineMiddleware((context, next) => {
+ context.locals.t = useTranslations(context.currentLocale);
+
+ return next();
+});
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index f1b4c1d0..e154d8e6 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -26,6 +26,7 @@
"type": "module",
"exports": {
".": "./index.ts",
+ "./locals": "./locals.ts",
"./components": "./components.ts",
"./components/LanguageSelect.astro": {
"types": "./components/LanguageSelect.astro.tsx",
@@ -171,7 +172,7 @@
"./style/markdown.css": "./style/markdown.css"
},
"peerDependencies": {
- "astro": "^4.8.6"
+ "astro": "^4.14.0"
},
"devDependencies": {
"@astrojs/markdown-remark": "^5.1.0",
@@ -194,6 +195,7 @@
"hast-util-select": "^6.0.2",
"hast-util-to-string": "^3.0.0",
"hastscript": "^9.0.0",
+ "i18next": "^23.11.5",
"mdast-util-directive": "^3.0.0",
"mdast-util-to-markdown": "^2.1.0",
"mdast-util-to-string": "^4.0.0",
diff --git a/packages/starlight/utils/createTranslationSystem.ts b/packages/starlight/utils/createTranslationSystem.ts
index c65c7f6b..8bbd13dc 100644
--- a/packages/starlight/utils/createTranslationSystem.ts
+++ b/packages/starlight/utils/createTranslationSystem.ts
@@ -1,27 +1,60 @@
+import i18next, { type ExistsFunction, type TFunction } from 'i18next';
import type { i18nSchemaOutput } from '../schemas/i18n';
import builtinTranslations from '../translations/index';
import { BuiltInDefaultLocale } from './i18n';
import type { StarlightConfig } from './user-config';
+import type { UserI18nKeys, UserI18nSchema } from './translations';
+
+/**
+ * The namespace for i18next resources used by Starlight.
+ * All translations handled by Starlight are stored in the same namespace and Starlight always use
+ * a new instance of i18next configured for this namespace.
+ */
+export const I18nextNamespace = 'starlight' as const;
export function createTranslationSystem<T extends i18nSchemaOutput>(
+ config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
userTranslations: Record<string, T>,
- config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>
+ pluginTranslations: Record<string, T> = {}
) {
- /** User-configured default locale. */
- const defaultLocale = config.defaultLocale?.locale || 'root';
+ const defaultLocale =
+ config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang;
- /** Default map of UI strings based on Starlight and user-configured defaults. */
- const defaults = buildDictionary(
- builtinTranslations.en!,
- userTranslations.en,
- builtinTranslations[defaultLocale] || builtinTranslations[stripLangRegion(defaultLocale)],
- userTranslations[defaultLocale]
- );
+ const translations = {
+ [defaultLocale]: buildResources(
+ builtinTranslations[defaultLocale],
+ builtinTranslations[stripLangRegion(defaultLocale)],
+ pluginTranslations[defaultLocale],
+ userTranslations[defaultLocale]
+ ),
+ };
+
+ if (config.locales) {
+ for (const locale in config.locales) {
+ const lang = localeToLang(locale, config.locales, config.defaultLocale);
+
+ translations[lang] = buildResources(
+ builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
+ pluginTranslations[lang],
+ userTranslations[lang]
+ );
+ }
+ }
+
+ const i18n = i18next.createInstance();
+ i18n.init({
+ resources: translations,
+ fallbackLng:
+ config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang,
+ });
/**
* Generate a utility function that returns UI strings for the given `locale`.
*
- * Also includes an `all()` method for getting the entire dictionary.
+ * Also includes a few utility methods:
+ * - `all()` method for getting the entire dictionary.
+ * - `exists()` method for checking if a key exists in the dictionary.
+ * - `dir()` method for getting the text direction of the locale.
*
* @param {string | undefined} [locale]
* @example
@@ -30,16 +63,19 @@ export function createTranslationSystem<T extends i18nSchemaOutput>(
* // => 'Search'
* const dictionary = t.all();
* // => { 'skipLink.label': 'Skip to content', 'search.label': 'Search', ... }
+ * const exists = t.exists('search.label');
+ * // => true
+ * const dir = t.dir();
+ * // => 'ltr'
*/
- return function useTranslations(locale: string | undefined) {
+ return (locale: string | undefined) => {
const lang = localeToLang(locale, config.locales, config.defaultLocale);
- const dictionary = buildDictionary(
- defaults,
- builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
- userTranslations[lang]
- );
- const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
- t.all = () => dictionary;
+
+ const t = i18n.getFixedT(lang, I18nextNamespace) as I18nT;
+ t.all = () => i18n.getResourceBundle(lang, I18nextNamespace);
+ t.exists = (key, options) => i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options });
+ t.dir = (dirLang = lang) => i18n.dir(dirLang);
+
return t;
};
}
@@ -70,12 +106,11 @@ function localeToLang(
type BuiltInStrings = (typeof builtinTranslations)['en'];
-/** Build a dictionary by layering preferred translation sources. */
-function buildDictionary<T extends Record<string, string | undefined>>(
- base: BuiltInStrings,
+/** Build an i18next resources dictionary by layering preferred translation sources. */
+function buildResources<T extends Record<string, string | undefined>>(
...dictionaries: (T | BuiltInStrings | undefined)[]
-): BuiltInStrings & T {
- const dictionary = { ...base };
+): { [I18nextNamespace]: BuiltInStrings & T } {
+ const dictionary: Partial<BuiltInStrings> = {};
// Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
for (const dict of dictionaries) {
for (const key in dict) {
@@ -83,5 +118,13 @@ function buildDictionary<T extends Record<string, string | undefined>>(
if (value) dictionary[key as keyof typeof dictionary] = value;
}
}
- return dictionary as BuiltInStrings & T;
+ return { [I18nextNamespace]: dictionary as BuiltInStrings & T };
}
+
+export type I18nKeys = UserI18nKeys | keyof StarlightApp.I18n;
+
+export type I18nT = TFunction<'starlight', undefined> & {
+ all: () => UserI18nSchema;
+ exists: ExistsFunction;
+ dir: (lang?: string) => 'ltr' | 'rtl';
+};
diff --git a/packages/starlight/utils/i18n.ts b/packages/starlight/utils/i18n.ts
index 6c9816d0..68d4f84c 100644
--- a/packages/starlight/utils/i18n.ts
+++ b/packages/starlight/utils/i18n.ts
@@ -3,6 +3,26 @@ import { AstroError } from 'astro/errors';
import type { StarlightConfig } from './user-config';
/**
+ * A proxy object that throws an error when a user tries to access the deprecated `labels` prop in
+ * a component override.
+ *
+ * @todo Remove in a future release once people have updated — no later than v1.
+ */
+export const DeprecatedLabelsPropProxy = new Proxy<Record<string, never>>(
+ {},
+ {
+ get(_, key) {
+ const label = String(key);
+ throw new AstroError(
+ `The \`labels\` prop in component overrides has been removed.`,
+ `Replace \`Astro.props.labels["${label}"]\` with \`Astro.locals.t("${label}")\` instead.\n` +
+ 'For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations'
+ );
+ },
+ }
+);
+
+/**
* A list of well-known right-to-left languages used as a fallback when determining the text
* direction of a locale is not supported by the `Intl.Locale` API in the current environment.
*
diff --git a/packages/starlight/utils/plugins.ts b/packages/starlight/utils/plugins.ts
index e0df8ceb..1eaf337e 100644
--- a/packages/starlight/utils/plugins.ts
+++ b/packages/starlight/utils/plugins.ts
@@ -1,8 +1,9 @@
-import type { AstroIntegration } from 'astro';
+import type { AstroIntegration, HookParameters } from 'astro';
import { z } from 'astro/zod';
import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
import { parseWithFriendlyErrors } from '../utils/error-map';
import { AstroError } from 'astro/errors';
+import type { UserI18nSchema } from './translations';
/**
* Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -32,6 +33,8 @@ export async function runPlugins(
// A list of Astro integrations added by the various plugins.
const integrations: AstroIntegration[] = [];
+ // A list of translations injected by the various plugins keyed by locale.
+ const pluginTranslations: PluginTranslations = {};
for (const {
name,
@@ -70,6 +73,13 @@ export async function runPlugins(
command: context.command,
isRestart: context.isRestart,
logger: context.logger.fork(name),
+ injectTranslations(translations) {
+ // Merge the translations injected by the plugin.
+ for (const [locale, localeTranslations] of Object.entries(translations)) {
+ pluginTranslations[locale] ??= {};
+ Object.assign(pluginTranslations[locale]!, localeTranslations);
+ }
+ },
});
}
@@ -81,7 +91,34 @@ export async function runPlugins(
);
}
- return { integrations, starlightConfig };
+ return { integrations, starlightConfig, pluginTranslations };
+}
+
+export function injectPluginTranslationsTypes(
+ translations: PluginTranslations,
+ injectTypes: HookParameters<'astro:config:done'>['injectTypes']
+) {
+ const allKeys = new Set<string>();
+
+ for (const localeTranslations of Object.values(translations)) {
+ for (const key of Object.keys(localeTranslations)) {
+ allKeys.add(key);
+ }
+ }
+
+ // If there are no translations to inject, we don't need to generate any types or cleanup
+ // previous ones as they will not be referenced anymore.
+ if (allKeys.size === 0) return;
+
+ injectTypes({
+ filename: 'i18n-plugins.d.ts',
+ content: `declare namespace StarlightApp {
+ type PluginUIStringKeys = {
+ ${[...allKeys].map((key) => `'${key}': string;`).join('\n\t\t')}
+ };
+ interface I18n extends PluginUIStringKeys {}
+}`,
+ });
}
// https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113
@@ -192,6 +229,32 @@ const starlightPluginSchema = baseStarlightPluginSchema.extend({
* @see https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger
*/
logger: z.any() as z.Schema<StarlightPluginContext['logger']>,
+ /**
+ * A callback function to add or update translations strings.
+ *
+ * @see https://starlight.astro.build/guides/i18n/#extend-translation-schema
+ *
+ * @example
+ * {
+ * name: 'My Starlight Plugin',
+ * hooks: {
+ * setup({ injectTranslations }) {
+ * injectTranslations({
+ * en: {
+ * 'myPlugin.doThing': 'Do the thing',
+ * },
+ * fr: {
+ * 'myPlugin.doThing': 'Faire le truc',
+ * },
+ * });
+ * }
+ * }
+ * }
+ */
+ injectTranslations: z.function(
+ z.tuple([z.record(z.string(), z.record(z.string(), z.string()))]),
+ z.void()
+ ),
}),
]),
z.union([z.void(), z.promise(z.void())])
@@ -222,3 +285,5 @@ export type StarlightPluginContext = Pick<
Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0],
'command' | 'config' | 'isRestart' | 'logger'
>;
+
+export type PluginTranslations = Record<string, UserI18nSchema & Record<string, string>>;
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
index 6a3e3a2a..c64571f9 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/route-data.ts
@@ -7,8 +7,9 @@ import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
import { ensureTrailingSlash } from './path';
import type { Route } from './routing';
import { localizedId } from './slugs';
-import { useTranslations } from './translations';
import { formatPath } from './format-path';
+import { useTranslations } from './translations';
+import { DeprecatedLabelsPropProxy } from './i18n';
export interface PageProps extends Route {
headings: MarkdownHeading[];
@@ -33,8 +34,8 @@ export interface StarlightRouteData extends Route {
lastUpdated: Date | undefined;
/** URL object for the address where this page can be edited if enabled. */
editUrl: URL | undefined;
- /** Record of UI strings localized for the current page. */
- labels: ReturnType<ReturnType<typeof useTranslations>['all']>;
+ /** @deprecated Use `Astro.locals.t()` instead. */
+ labels: Record<string, never>;
}
export function generateRouteData({
@@ -57,7 +58,7 @@ export function generateRouteData({
toc: getToC(props),
lastUpdated: getLastUpdated(props),
editUrl: getEditUrl(props),
- labels: useTranslations(locale).all(),
+ labels: DeprecatedLabelsPropProxy,
};
}
diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts
index b45d088f..a25a000f 100644
--- a/packages/starlight/utils/starlight-page.ts
+++ b/packages/starlight/utils/starlight-page.ts
@@ -13,8 +13,9 @@ import {
import type { StarlightDocsEntry } from './routing';
import { slugToLocaleData, urlToSlug } from './slugs';
import { getPrevNextLinks, getSidebarFromConfig } from './navigation';
-import { useTranslations } from './translations';
import { docsSchema } from '../schema';
+import type { Prettify, RemoveIndexSignature } from './types';
+import { DeprecatedLabelsPropProxy } from './i18n';
import { SidebarItemSchema } from '../schemas/sidebar';
import type { StarlightConfig, StarlightUserConfig } from './user-config';
@@ -151,7 +152,7 @@ export async function generateStarlightPageRouteData({
entryMeta,
hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash',
headings,
- labels: useTranslations(localeData.locale).all(),
+ labels: DeprecatedLabelsPropProxy,
lastUpdated,
pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
sidebar,
@@ -214,19 +215,3 @@ async function getUserDocsSchema(): Promise<
const userCollections = (await import('virtual:starlight/collection-config')).collections;
return userCollections?.docs.schema ?? docsSchema();
}
-
-// https://stackoverflow.com/a/66252656/1945960
-type RemoveIndexSignature<T> = {
- [K in keyof T as string extends K
- ? never
- : number extends K
- ? never
- : symbol extends K
- ? never
- : K]: T[K];
-};
-
-// https://www.totaltypescript.com/concepts/the-prettify-helper
-type Prettify<T> = {
- [K in keyof T]: T[K];
-} & {};
diff --git a/packages/starlight/utils/translations-fs.ts b/packages/starlight/utils/translations-fs.ts
index d218f19e..cd927aa5 100644
--- a/packages/starlight/utils/translations-fs.ts
+++ b/packages/starlight/utils/translations-fs.ts
@@ -11,9 +11,10 @@ import type { AstroConfig } from 'astro';
*
* @see [`./translations.ts`](./translations.ts)
*/
-export function createTranslationSystemFromFs(
+export function createTranslationSystemFromFs<T extends i18nSchemaOutput>(
opts: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
- { srcDir }: Pick<AstroConfig, 'srcDir'>
+ { srcDir }: Pick<AstroConfig, 'srcDir'>,
+ pluginTranslations: Record<string, T> = {}
) {
/** All translation data from the i18n collection, keyed by `id`, which matches locale. */
let userTranslations: Record<string, i18nSchemaOutput> = {};
@@ -40,5 +41,5 @@ export function createTranslationSystemFromFs(
}
}
- return createTranslationSystem(userTranslations, opts);
+ return createTranslationSystem(opts, userTranslations, pluginTranslations);
}
diff --git a/packages/starlight/utils/translations.ts b/packages/starlight/utils/translations.ts
index bbcbc45e..a2dcea44 100644
--- a/packages/starlight/utils/translations.ts
+++ b/packages/starlight/utils/translations.ts
@@ -1,13 +1,16 @@
import { getCollection, type CollectionEntry, type DataCollectionKey } from 'astro:content';
import config from 'virtual:starlight/user-config';
+import pluginTranslations from 'virtual:starlight/plugin-translations';
import type { i18nSchemaOutput } from '../schemas/i18n';
import { createTranslationSystem } from './createTranslationSystem';
+import type { RemoveIndexSignature } from './types';
-type UserI18nSchema = 'i18n' extends DataCollectionKey
+export type UserI18nSchema = 'i18n' extends DataCollectionKey
? CollectionEntry<'i18n'> extends { data: infer T }
- ? T
+ ? i18nSchemaOutput & T
: i18nSchemaOutput
: i18nSchemaOutput;
+export type UserI18nKeys = keyof RemoveIndexSignature<UserI18nSchema>;
/** Get all translation data from the i18n collection, keyed by `id`, which matches locale. */
async function loadTranslations() {
@@ -34,4 +37,8 @@ async function loadTranslations() {
* const t = useTranslations('en');
* const label = t('search.label'); // => 'Search'
*/
-export const useTranslations = createTranslationSystem(await loadTranslations(), config);
+export const useTranslations = createTranslationSystem(
+ config,
+ await loadTranslations(),
+ pluginTranslations
+);
diff --git a/packages/starlight/utils/types.ts b/packages/starlight/utils/types.ts
new file mode 100644
index 00000000..7598d6e8
--- /dev/null
+++ b/packages/starlight/utils/types.ts
@@ -0,0 +1,15 @@
+// https://stackoverflow.com/a/66252656/1945960
+export type RemoveIndexSignature<T> = {
+ [K in keyof T as string extends K
+ ? never
+ : number extends K
+ ? never
+ : symbol extends K
+ ? never
+ : K]: T[K];
+};
+
+// https://www.totaltypescript.com/concepts/the-prettify-helper
+export type Prettify<T> = {
+ [K in keyof T]: T[K];
+} & {};
diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts
index de3ba265..f7475aa8 100644
--- a/packages/starlight/virtual.d.ts
+++ b/packages/starlight/virtual.d.ts
@@ -28,6 +28,11 @@ declare module 'virtual:starlight/user-images' {
};
}
+declare module 'virtual:starlight/plugin-translations' {
+ const PluginTranslations: import('./utils/plugins').PluginTranslations;
+ export default PluginTranslations;
+}
+
declare module 'virtual:starlight/collection-config' {
export const collections: import('astro:content').ContentConfig['collections'] | undefined;
}
diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts
index e15ce187..47ca99be 100644
--- a/packages/starlight/vitest.config.ts
+++ b/packages/starlight/vitest.config.ts
@@ -21,10 +21,10 @@ export default defineConfig({
],
thresholds: {
autoUpdate: true,
- lines: 89.53,
- functions: 94.08,
- branches: 93.07,
- statements: 89.53,
+ lines: 89.18,
+ functions: 92.7,
+ branches: 93.04,
+ statements: 89.18,
},
},
},
diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json
index f5638d3c..2787840b 100644
--- a/packages/tailwind/package.json
+++ b/packages/tailwind/package.json
@@ -25,6 +25,7 @@
"devDependencies": {
"@vitest/coverage-v8": "^1.6.0",
"postcss": "^8.4.38",
+ "tailwindcss": "^3.3.3",
"vitest": "^1.6.0"
},
"peerDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ac9b53a9..5233b638 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,7 +1,7 @@
lockfileVersion: '6.0'
settings:
- autoInstallPeers: true
+ autoInstallPeers: false
excludeLinksFromLockfile: false
importers:
@@ -123,15 +123,16 @@ importers:
packages/docsearch:
dependencies:
- '@astrojs/starlight':
- specifier: '>=0.14.0'
- version: link:../starlight
'@docsearch/css':
specifier: ^3.6.0
version: 3.6.0
'@docsearch/js':
specifier: ^3.6.0
- version: 3.6.0(@algolia/client-search@4.20.0)(search-insights@2.11.0)
+ version: 3.6.0
+ devDependencies:
+ '@astrojs/starlight':
+ specifier: workspace:*
+ version: link:../starlight
packages/file-icons-generator:
dependencies:
@@ -147,14 +148,13 @@ importers:
version: 1.3.8
packages/markdoc:
- dependencies:
+ devDependencies:
'@astrojs/markdoc':
specifier: ^0.11.4
version: 0.11.4(astro@4.15.3)
'@astrojs/starlight':
- specifier: '>=0.23.0'
+ specifier: workspace:*
version: link:../starlight
- devDependencies:
vitest:
specifier: ^1.6.0
version: 1.6.0(@types/node@18.16.19)
@@ -194,6 +194,9 @@ importers:
hastscript:
specifier: ^9.0.0
version: 9.0.0
+ i18next:
+ specifier: ^23.11.5
+ version: 23.11.5
mdast-util-directive:
specifier: ^3.0.0
version: 3.0.0
@@ -287,16 +290,6 @@ importers:
version: 4.15.3(@types/node@18.16.19)(typescript@5.4.5)
packages/tailwind:
- dependencies:
- '@astrojs/starlight':
- specifier: '>=0.9.0'
- version: link:../starlight
- '@astrojs/tailwind':
- specifier: ^5.0.0
- version: 5.1.0(astro@4.15.3)(tailwindcss@3.4.4)
- tailwindcss:
- specifier: ^3.3.3
- version: 3.4.4
devDependencies:
'@vitest/coverage-v8':
specifier: ^1.6.0
@@ -304,53 +297,62 @@ importers:
postcss:
specifier: ^8.4.38
version: 8.4.45
+ tailwindcss:
+ specifier: ^3.3.3
+ version: 3.4.4
vitest:
specifier: ^1.6.0
version: 1.6.0(@types/node@18.16.19)
packages:
- /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0):
+ /@algolia/autocomplete-core@1.9.3(algoliasearch@4.20.0):
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
dependencies:
- '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0)
- '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
+ '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(algoliasearch@4.20.0)
+ '@algolia/autocomplete-shared': 1.9.3(algoliasearch@4.20.0)
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
- search-insights
dev: false
- /@algolia/autocomplete-plugin-algolia-insights@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0):
+ /@algolia/autocomplete-plugin-algolia-insights@1.9.3(algoliasearch@4.20.0):
resolution: {integrity: sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==}
peerDependencies:
search-insights: '>= 1 < 3'
+ peerDependenciesMeta:
+ search-insights:
+ optional: true
dependencies:
- '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
- search-insights: 2.11.0
+ '@algolia/autocomplete-shared': 1.9.3(algoliasearch@4.20.0)
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
dev: false
- /@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0):
+ /@algolia/autocomplete-preset-algolia@1.9.3(algoliasearch@4.20.0):
resolution: {integrity: sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
+ peerDependenciesMeta:
+ '@algolia/client-search':
+ optional: true
dependencies:
- '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
- '@algolia/client-search': 4.20.0
+ '@algolia/autocomplete-shared': 1.9.3(algoliasearch@4.20.0)
algoliasearch: 4.20.0
dev: false
- /@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0):
+ /@algolia/autocomplete-shared@1.9.3(algoliasearch@4.20.0):
resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==}
peerDependencies:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
+ peerDependenciesMeta:
+ '@algolia/client-search':
+ optional: true
dependencies:
- '@algolia/client-search': 4.20.0
algoliasearch: 4.20.0
dev: false
@@ -447,7 +449,6 @@ packages:
/@alloc/quick-lru@5.2.0:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
- dev: false
/@ampproject/remapping@2.2.1:
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
@@ -545,7 +546,6 @@ packages:
- '@types/react'
- react
- supports-color
- dev: false
/@astrojs/markdown-remark@5.2.0:
resolution: {integrity: sha512-vWGM24KZXz11jR3JO+oqYU3T2qpuOi4uGivJ9SQLCAI01+vEkHC60YJMRvHPc+hwd60F7euNs1PeOEixIIiNQw==}
@@ -812,6 +812,13 @@ packages:
regenerator-runtime: 0.13.11
dev: true
+ /@babel/runtime@7.24.7:
+ resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ regenerator-runtime: 0.14.1
+ dev: false
+
/@babel/template@7.25.0:
resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==}
engines: {node: '>=6.9.0'}
@@ -1075,10 +1082,10 @@ packages:
resolution: {integrity: sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==}
dev: false
- /@docsearch/js@3.6.0(@algolia/client-search@4.20.0)(search-insights@2.11.0):
+ /@docsearch/js@3.6.0:
resolution: {integrity: sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==}
dependencies:
- '@docsearch/react': 3.6.0(@algolia/client-search@4.20.0)(search-insights@2.11.0)
+ '@docsearch/react': 3.6.0
preact: 10.18.2
transitivePeerDependencies:
- '@algolia/client-search'
@@ -1088,7 +1095,7 @@ packages:
- search-insights
dev: false
- /@docsearch/react@3.6.0(@algolia/client-search@4.20.0)(search-insights@2.11.0):
+ /@docsearch/react@3.6.0:
resolution: {integrity: sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==}
peerDependencies:
'@types/react': '>= 16.8.0 < 19.0.0'
@@ -1105,11 +1112,10 @@ packages:
search-insights:
optional: true
dependencies:
- '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0)
- '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
+ '@algolia/autocomplete-core': 1.9.3(algoliasearch@4.20.0)
+ '@algolia/autocomplete-preset-algolia': 1.9.3(algoliasearch@4.20.0)
'@docsearch/css': 3.6.0
algoliasearch: 4.20.0
- search-insights: 2.11.0
transitivePeerDependencies:
- '@algolia/client-search'
dev: false
@@ -1664,7 +1670,6 @@ packages:
optional: true
optionalDependencies:
'@types/markdown-it': 12.2.3
- dev: false
/@mdx-js/mdx@3.0.1:
resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==}
@@ -1995,7 +2000,6 @@ packages:
/@types/linkify-it@5.0.0:
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
requiresBuild: true
- dev: false
optional: true
/@types/markdown-it@12.2.3:
@@ -2004,7 +2008,6 @@ packages:
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
- dev: false
optional: true
/@types/mdast@4.0.4:
@@ -2015,7 +2018,6 @@ packages:
/@types/mdurl@2.0.0:
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
requiresBuild: true
- dev: false
optional: true
/@types/mdx@2.0.5:
@@ -2318,7 +2320,6 @@ packages:
/any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
- dev: false
/anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
@@ -2673,7 +2674,6 @@ packages:
/camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
- dev: false
/camelcase-keys@6.2.2:
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
@@ -2901,7 +2901,6 @@ packages:
/commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
- dev: false
/commander@6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
@@ -3136,7 +3135,6 @@ packages:
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
- dev: false
/diff-sequences@29.6.3:
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
@@ -3757,7 +3755,6 @@ packages:
engines: {node: '>=10.13.0'}
dependencies:
is-glob: 4.0.3
- dev: false
/glob@7.1.6:
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
@@ -3769,7 +3766,6 @@ packages:
minimatch: 3.1.2
once: 1.4.0
path-is-absolute: 1.0.1
- dev: false
/glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
@@ -4204,6 +4200,12 @@ packages:
engines: {node: '>=16.17.0'}
dev: true
+ /i18next@23.11.5:
+ resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
+ dependencies:
+ '@babel/runtime': 7.24.7
+ dev: false
+
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -4603,7 +4605,6 @@ packages:
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
- dev: false
/lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
@@ -5400,7 +5401,6 @@ packages:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
- dev: false
/nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
@@ -5515,7 +5515,6 @@ packages:
/object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
- dev: false
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
@@ -5872,7 +5871,6 @@ packages:
/pirates@4.0.6:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
- dev: false
/pkg-dir@4.2.0:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
@@ -5914,7 +5912,6 @@ packages:
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.8
- dev: false
/postcss-js@4.0.1(postcss@8.4.45):
resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==}
@@ -5924,7 +5921,6 @@ packages:
dependencies:
camelcase-css: 2.0.1
postcss: 8.4.45
- dev: false
/postcss-load-config@4.0.2(postcss@8.4.45):
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
@@ -5941,7 +5937,6 @@ packages:
lilconfig: 3.1.2
postcss: 8.4.45
yaml: 2.3.4
- dev: false
/postcss-nested@6.0.1(postcss@8.4.45):
resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==}
@@ -5951,7 +5946,6 @@ packages:
dependencies:
postcss: 8.4.45
postcss-selector-parser: 6.0.13
- dev: false
/postcss-selector-parser@6.0.13:
resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==}
@@ -5959,11 +5953,9 @@ packages:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
- dev: false
/postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- dev: false
/postcss@8.4.45:
resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==}
@@ -6158,7 +6150,6 @@ packages:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
dependencies:
pify: 2.3.0
- dev: false
/read-pkg-up@7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
@@ -6215,6 +6206,10 @@ packages:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: true
+ /regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+ dev: false
+
/regexp.prototype.flags@1.5.0:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
@@ -6492,10 +6487,6 @@ packages:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
- /search-insights@2.11.0:
- resolution: {integrity: sha512-Uin2J8Bpm3xaZi9Y8QibSys6uJOFZ+REMrf42v20AA3FUDUrshKkMEP6liJbMAHCm71wO6ls4mwAf7a3gFVxLw==}
- dev: false
-
/section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
@@ -6994,7 +6985,6 @@ packages:
mz: 2.7.0
pirates: 4.0.6
ts-interface-checker: 0.1.13
- dev: false
/suf-log@2.5.3:
resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==}
@@ -7048,7 +7038,6 @@ packages:
sucrase: 3.34.0
transitivePeerDependencies:
- ts-node
- dev: false
/tar-fs@2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
@@ -7103,13 +7092,11 @@ packages:
engines: {node: '>=0.8'}
dependencies:
thenify: 3.3.1
- dev: false
/thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
dependencies:
any-promise: 1.3.0
- dev: false
/through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -7179,7 +7166,6 @@ packages:
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
- dev: false
/tsconfck@3.1.3(typescript@5.4.5):
resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==}
@@ -7867,7 +7853,6 @@ packages:
/yaml@2.3.4:
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
engines: {node: '>= 14'}
- dev: false
/yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
diff --git a/tsconfig.json b/tsconfig.json
index f3dbc75e..38ec1b74 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,6 @@
"exclude": ["**/dist/**", "**/__coverage__/**"],
"compilerOptions": {
"jsx": "preserve",
- "allowJs": true,
"checkJs": true
}
}