diff options
author | HiDeoo | 2024-09-18 09:55:37 +0200 |
---|---|---|
committer | GitHub | 2024-09-18 09:55:37 +0200 |
commit | 5269aad928773ae08b35ba8e19c0f2832d0d2c89 (patch) | |
tree | 087e60ed8561925978b13bc06ea2afb6ddc27e3f | |
parent | d7a295e5f63171c7eee9fc11333157d8c7e6c803 (diff) | |
download | IT.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>
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 +``` @@ -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 <img />'); + }); + + 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 } } |