From 06a205e0e673f505bbb87dfcfcb0f35b051677e9 Mon Sep 17 00:00:00 2001 From: Yan Thomas Date: Thu, 10 Aug 2023 08:08:41 -0300 Subject: Include built-in UI translation for regional docs (#475) Co-authored-by: Chris Swithinbank Co-authored-by: Chris Swithinbank <357379+delucis@users.noreply.github.com> --- .changeset/two-experts-rest.md | 5 +++ docs/src/content/docs/reference/configuration.md | 2 +- .../basics/translations-with-user-config.test.ts | 17 ---------- .../__tests__/basics/translations.test.ts | 32 ------------------ packages/starlight/__tests__/i18n/config.test.ts | 2 +- packages/starlight/__tests__/i18n/routing.test.ts | 2 +- .../i18n/translations-with-user-config.test.ts | 26 +++++++++++++++ .../starlight/__tests__/i18n/translations.test.ts | 38 ++++++++++++++++++++++ packages/starlight/__tests__/i18n/vitest.config.ts | 1 + packages/starlight/utils/translations.ts | 19 +++++++++-- 10 files changed, 89 insertions(+), 55 deletions(-) create mode 100644 .changeset/two-experts-rest.md delete mode 100644 packages/starlight/__tests__/basics/translations-with-user-config.test.ts delete mode 100644 packages/starlight/__tests__/basics/translations.test.ts create mode 100644 packages/starlight/__tests__/i18n/translations-with-user-config.test.ts create mode 100644 packages/starlight/__tests__/i18n/translations.test.ts diff --git a/.changeset/two-experts-rest.md b/.changeset/two-experts-rest.md new file mode 100644 index 00000000..550fe900 --- /dev/null +++ b/.changeset/two-experts-rest.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Locales whose language tag includes a regional subtag now use built-in UI translations for their base language. For example, a locale with a language of `pt-BR` will use our `pt` UI translations. diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md index c0d77d7f..593f4593 100644 --- a/docs/src/content/docs/reference/configuration.md +++ b/docs/src/content/docs/reference/configuration.md @@ -255,7 +255,7 @@ The label for this language to show to users, for example in the language switch **type:** `string` -The BCP-47 tag for this language, e.g. `"en"`, `"ar"`, or `"zh-CN"`. If not set, the language’s directory name will be used by default. +The BCP-47 tag for this language, e.g. `"en"`, `"ar"`, or `"zh-CN"`. If not set, the language’s directory name will be used by default. Language tags with regional subtags (e.g. `"pt-BR"` or `"en-US"`) will use built-in UI translations for their base language if no region-specific translations are found. ##### `dir` diff --git a/packages/starlight/__tests__/basics/translations-with-user-config.test.ts b/packages/starlight/__tests__/basics/translations-with-user-config.test.ts deleted file mode 100644 index 57c0afe9..00000000 --- a/packages/starlight/__tests__/basics/translations-with-user-config.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, test, vi } from 'vitest'; -import translations from '../../translations'; -import { useTranslations } from '../../utils/translations'; - -vi.mock('astro:content', async () => - (await import('../test-utils')).mockedAstroContent({ - i18n: [['en', { 'page.editLink': 'Modify this doc!' }]], - }) -); - -describe('useTranslations()', () => { - test('uses user-defined translations', () => { - const t = useTranslations(undefined); - expect(t('page.editLink')).toBe('Modify this doc!'); - expect(t('page.editLink')).not.toBe(translations.en?.['page.editLink']); - }); -}); diff --git a/packages/starlight/__tests__/basics/translations.test.ts b/packages/starlight/__tests__/basics/translations.test.ts deleted file mode 100644 index bd67ef9c..00000000 --- a/packages/starlight/__tests__/basics/translations.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, test, vi } from 'vitest'; -import translations from '../../translations'; -import { useTranslations } from '../../utils/translations'; - -describe('built-in translations', () => { - test('includes English', () => { - expect(translations).toHaveProperty('en'); - }); -}); - -describe('useTranslations()', () => { - test('works when no i18n collection is available', () => { - const t = useTranslations(undefined); - expect(t).toBeTypeOf('function'); - expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); - }); - - test('returns default locale for unknown language', () => { - const locale = 'xx'; - expect(translations).not.toHaveProperty(locale); - const t = useTranslations(locale); - expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); - }); - - test('returns a pick method for filtering by key', () => { - const t = useTranslations('en'); - expect(t.pick('tableOfContents.')).toEqual({ - 'tableOfContents.onThisPage': 'On this page', - 'tableOfContents.overview': 'Overview', - }); - }); -}); diff --git a/packages/starlight/__tests__/i18n/config.test.ts b/packages/starlight/__tests__/i18n/config.test.ts index 93f9659c..0e8e9c8d 100644 --- a/packages/starlight/__tests__/i18n/config.test.ts +++ b/packages/starlight/__tests__/i18n/config.test.ts @@ -7,7 +7,7 @@ test('test suite is using correct env', () => { test('config.isMultilingual is true with multiple locales', () => { expect(config.isMultilingual).toBe(true); - expect(config.locales).keys('fr', 'en', 'ar'); + expect(config.locales).keys('fr', 'en', 'ar', 'pt-br'); }); test('config.defaultLocale is populated from the user’s chosen default', () => { diff --git a/packages/starlight/__tests__/i18n/routing.test.ts b/packages/starlight/__tests__/i18n/routing.test.ts index e0402b54..18d8ed89 100644 --- a/packages/starlight/__tests__/i18n/routing.test.ts +++ b/packages/starlight/__tests__/i18n/routing.test.ts @@ -38,7 +38,7 @@ test('routes have locale data added', () => { expect(lang).toBe('ar'); expect(dir).toBe('rtl'); expect(locale).toBe('ar'); - } else { + } else if (id.startsWith('fr')) { expect(lang).toBe('fr'); expect(dir).toBe('ltr'); expect(locale).toBe('fr'); diff --git a/packages/starlight/__tests__/i18n/translations-with-user-config.test.ts b/packages/starlight/__tests__/i18n/translations-with-user-config.test.ts new file mode 100644 index 00000000..4335aa03 --- /dev/null +++ b/packages/starlight/__tests__/i18n/translations-with-user-config.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test, vi } from 'vitest'; +import translations from '../../translations'; +import { useTranslations } from '../../utils/translations'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + i18n: [ + ['en-US', { 'page.editLink': 'Modify this doc!' }], + ['pt-BR', { 'page.editLink': 'Modifique esse doc!' }], + ], + }) +); + +describe('useTranslations()', () => { + test('uses user-defined translations', () => { + const t = useTranslations(undefined); + expect(t('page.editLink')).toBe('Modify this doc!'); + expect(t('page.editLink')).not.toBe(translations.en?.['page.editLink']); + }); + + test('uses user-defined regional translations when available', () => { + const t = useTranslations('pt-br'); + expect(t('page.editLink')).toBe('Modifique esse doc!'); + expect(t('page.editLink')).not.toBe(translations.pt?.['page.editLink']); + }); +}); diff --git a/packages/starlight/__tests__/i18n/translations.test.ts b/packages/starlight/__tests__/i18n/translations.test.ts new file mode 100644 index 00000000..a8442166 --- /dev/null +++ b/packages/starlight/__tests__/i18n/translations.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test, vi } from 'vitest'; +import translations from '../../translations'; +import { useTranslations } from '../../utils/translations'; + +describe('built-in translations', () => { + test('includes English', () => { + expect(translations).toHaveProperty('en'); + }); +}); + +describe('useTranslations()', () => { + test('works when no i18n collection is available', () => { + const t = useTranslations(undefined); + expect(t).toBeTypeOf('function'); + expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); + }); + + test('returns default locale for unknown language', () => { + const locale = 'xx'; + expect(translations).not.toHaveProperty(locale); + const t = useTranslations(locale); + expect(t('page.editLink')).toBe(translations.en?.['page.editLink']); + }); + + test('returns a pick method for filtering by key', () => { + const t = useTranslations('en'); + expect(t.pick('tableOfContents.')).toEqual({ + 'tableOfContents.onThisPage': 'On this page', + 'tableOfContents.overview': 'Overview', + }); + }); + + test('uses built-in translations for regional variants', () => { + const t = useTranslations('pt-br'); + expect(t('page.nextLink')).toBe(translations.pt?.['page.nextLink']); + expect(t('page.nextLink')).not.toBe(translations.en?.['page.nextLink']); + }); +}); diff --git a/packages/starlight/__tests__/i18n/vitest.config.ts b/packages/starlight/__tests__/i18n/vitest.config.ts index da62f01a..36212fc7 100644 --- a/packages/starlight/__tests__/i18n/vitest.config.ts +++ b/packages/starlight/__tests__/i18n/vitest.config.ts @@ -7,5 +7,6 @@ export default defineVitestConfig({ fr: { label: 'French' }, en: { label: 'English', lang: 'en-US' }, ar: { label: 'Arabic', dir: 'rtl' }, + 'pt-br': { label: 'Brazilian Portuguese', lang: 'pt-BR' }, }, }); diff --git a/packages/starlight/utils/translations.ts b/packages/starlight/utils/translations.ts index da69bb1e..b4c6da4b 100644 --- a/packages/starlight/utils/translations.ts +++ b/packages/starlight/utils/translations.ts @@ -19,10 +19,20 @@ try { const defaults = buildDictionary( builtinTranslations.en!, userTranslations.en, - builtinTranslations[defaultLocale], + builtinTranslations[defaultLocale] || builtinTranslations[stripLangRegion(defaultLocale)], userTranslations[defaultLocale] ); +/** + * Strips the region subtag from a BCP-47 lang string. + * @param {string} [lang] + * @example + * const lang = stripLangRegion('en-GB'); // => 'en' + */ +export function stripLangRegion(lang: string) { + return lang.replace(/-[a-zA-Z]{2}/, ''); +} + /** * Generate a utility function that returns UI strings for the given `locale`. * @param {string | undefined} [locale] @@ -31,9 +41,12 @@ const defaults = buildDictionary( * const label = t('search.label'); // => 'Search' */ export function useTranslations(locale: string | undefined) { - // TODO: Use better mapping, e.g. so that `en-GB` matches `en`. const lang = localeToLang(locale); - const dictionary = buildDictionary(defaults, builtinTranslations[lang], userTranslations[lang]); + const dictionary = buildDictionary( + defaults, + builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)], + userTranslations[lang] + ); const t = (key: K) => dictionary[key]; t.pick = (startOfKey: string) => Object.fromEntries(Object.entries(dictionary).filter(([k]) => k.startsWith(startOfKey))); -- cgit