mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-01 10:22:30 +02:00
feat(v2): infer default i18n locale config from locale code (#4449)
* improve locale default config * remove localeConfigs from i18n tutorial * better i18n types, tests and Intl.DisplayNames integration
This commit is contained in:
parent
5e73c72f26
commit
806fdbaf27
10 changed files with 189 additions and 110 deletions
|
@ -39,6 +39,7 @@ export default function LocaleDropdownNavbarItem({
|
|||
target: '_self',
|
||||
autoAddBaseUrl: false,
|
||||
className: locale === currentLocale ? 'dropdown__link--active' : '',
|
||||
style: {textTransform: 'capitalize'},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
7
packages/docusaurus-types/src/index.d.ts
vendored
7
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -101,11 +101,14 @@ export type I18nLocaleConfig = {
|
|||
export type I18nConfig = {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
localeConfigs: Record<string, I18nLocaleConfig>;
|
||||
localeConfigs: Record<string, Partial<I18nLocaleConfig>>;
|
||||
};
|
||||
|
||||
export type I18n = I18nConfig & {
|
||||
export type I18n = {
|
||||
defaultLocale: string;
|
||||
locales: [string, ...string[]];
|
||||
currentLocale: string;
|
||||
localeConfigs: Record<string, I18nLocaleConfig>;
|
||||
};
|
||||
|
||||
export interface DocusaurusContext {
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"react-router-config": "^5.1.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"rtl-detect": "^1.0.2",
|
||||
"semver": "^7.3.4",
|
||||
"serve-handler": "^6.1.3",
|
||||
"shelljs": "^0.8.4",
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
export const NODE_MAJOR_VERSION = parseInt(
|
||||
process.versions.node.split('.')[0],
|
||||
10,
|
||||
);
|
||||
|
||||
// Can be overridden with cli option --out-dir
|
||||
export const DEFAULT_BUILD_DIR_NAME = 'build';
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`loadI18n should throw when trying to load undeclared locale 1`] = `
|
||||
"It is not possible to load Docusaurus with locale=\\"it\\".
|
||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[en,fr,de]
|
||||
Note: Docusaurus only support running one locale at a time."
|
||||
`;
|
|
@ -5,25 +5,100 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {loadI18n, localizePath, defaultLocaleConfig} from '../i18n';
|
||||
import {
|
||||
loadI18n,
|
||||
localizePath,
|
||||
getDefaultLocaleConfig,
|
||||
shouldWarnAboutNodeVersion,
|
||||
} from '../i18n';
|
||||
import {DEFAULT_I18N_CONFIG} from '../configValidation';
|
||||
import path from 'path';
|
||||
import {chain, identity} from 'lodash';
|
||||
import {I18nConfig} from '@docusaurus/types';
|
||||
|
||||
function testLocaleConfigsFor(locales: string[]) {
|
||||
return chain(locales).keyBy(identity).mapValues(defaultLocaleConfig).value();
|
||||
return chain(locales)
|
||||
.keyBy(identity)
|
||||
.mapValues(getDefaultLocaleConfig)
|
||||
.value();
|
||||
}
|
||||
|
||||
function loadI18nTest(i18nConfig: I18nConfig, locale?: string) {
|
||||
return loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: i18nConfig,
|
||||
},
|
||||
{locale},
|
||||
);
|
||||
}
|
||||
|
||||
describe('defaultLocaleConfig', () => {
|
||||
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||
const canComputeLabel = typeof Intl.DisplayNames !== 'undefined';
|
||||
|
||||
test('returns correct labels', () => {
|
||||
expect(getDefaultLocaleConfig('fr')).toEqual({
|
||||
label: canComputeLabel ? 'français' : 'fr',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('fr-FR')).toEqual({
|
||||
label: canComputeLabel ? 'français (France)' : 'fr-FR',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('en')).toEqual({
|
||||
label: canComputeLabel ? 'English' : 'en',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('en-US')).toEqual({
|
||||
label: canComputeLabel ? 'American English' : 'en-US',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('zh')).toEqual({
|
||||
label: canComputeLabel ? '中文' : 'zh',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('zh-CN')).toEqual({
|
||||
label: canComputeLabel ? '中文(中国)' : 'zh-CN',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('en-US')).toEqual({
|
||||
label: canComputeLabel ? 'American English' : 'en-US',
|
||||
direction: 'ltr',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('fa')).toEqual({
|
||||
label: canComputeLabel ? 'فارسی' : 'fa',
|
||||
direction: 'rtl',
|
||||
});
|
||||
expect(getDefaultLocaleConfig('fa-IR')).toEqual({
|
||||
label: canComputeLabel ? 'فارسی (ایران)' : 'fa-IR',
|
||||
direction: 'rtl',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldWarnAboutNodeVersion', () => {
|
||||
test('warns for old NodeJS version and [en,fr]', () => {
|
||||
expect(shouldWarnAboutNodeVersion(12, ['en', 'fr'])).toEqual(true);
|
||||
});
|
||||
|
||||
test('not warn for old NodeJS version and [en]', () => {
|
||||
expect(shouldWarnAboutNodeVersion(12, ['en'])).toEqual(false);
|
||||
});
|
||||
|
||||
test('not warn for recent NodeJS version and [en,fr]', () => {
|
||||
expect(shouldWarnAboutNodeVersion(14, ['en', 'fr'])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadI18n', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
beforeEach(() => {
|
||||
consoleSpy.mockClear();
|
||||
});
|
||||
|
||||
test('should load I18n for default config', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: DEFAULT_I18N_CONFIG,
|
||||
},
|
||||
),
|
||||
).resolves.toEqual({
|
||||
await expect(loadI18nTest(DEFAULT_I18N_CONFIG)).resolves.toEqual({
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
currentLocale: 'en',
|
||||
|
@ -33,16 +108,11 @@ describe('loadI18n', () => {
|
|||
|
||||
test('should load I18n for multi-lang config', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
},
|
||||
),
|
||||
loadI18nTest({
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
|
@ -53,16 +123,13 @@ describe('loadI18n', () => {
|
|||
|
||||
test('should load I18n for multi-locale config with specified locale', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
loadI18nTest(
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
{locale: 'de'},
|
||||
'de',
|
||||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
|
@ -74,19 +141,16 @@ describe('loadI18n', () => {
|
|||
|
||||
test('should load I18n for multi-locale config with some xcustom locale configs', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
loadI18nTest(
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {
|
||||
fr: {label: 'Français'},
|
||||
// @ts-expect-error: empty on purpose
|
||||
en: {},
|
||||
},
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {
|
||||
fr: {label: 'Français'},
|
||||
en: {},
|
||||
},
|
||||
},
|
||||
{locale: 'de'},
|
||||
'de',
|
||||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
|
@ -94,26 +158,24 @@ describe('loadI18n', () => {
|
|||
currentLocale: 'de',
|
||||
localeConfigs: {
|
||||
fr: {label: 'Français', direction: 'ltr'},
|
||||
en: defaultLocaleConfig('en'),
|
||||
de: defaultLocaleConfig('de'),
|
||||
en: getDefaultLocaleConfig('en'),
|
||||
de: getDefaultLocaleConfig('de'),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should throw when trying to load undeclared locale', async () => {
|
||||
await expect(
|
||||
loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: {
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
},
|
||||
{locale: 'it'},
|
||||
),
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
test('should warn when trying to load undeclared locale', async () => {
|
||||
await loadI18nTest(
|
||||
{
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
},
|
||||
'it',
|
||||
);
|
||||
expect(consoleSpy.mock.calls[0][0]).toMatch(
|
||||
/The locale=it was not found in your site configuration/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,45 +7,79 @@
|
|||
import {I18n, DocusaurusConfig, I18nLocaleConfig} from '@docusaurus/types';
|
||||
import path from 'path';
|
||||
import {normalizeUrl} from '@docusaurus/utils';
|
||||
import {getLangDir} from 'rtl-detect';
|
||||
import {NODE_MAJOR_VERSION} from '../constants';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export function defaultLocaleConfig(locale: string): I18nLocaleConfig {
|
||||
function getDefaultLocaleLabel(locale: string) {
|
||||
// Intl.DisplayNames is ES2021 - Node14+
|
||||
// https://v8.dev/features/intl-displaynames
|
||||
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||
if (typeof Intl.DisplayNames !== 'undefined') {
|
||||
// @ts-expect-error: wait for TS support of ES2021 feature
|
||||
return new Intl.DisplayNames([locale], {type: 'language'}).of(locale);
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
|
||||
return {
|
||||
label: locale,
|
||||
direction: 'ltr',
|
||||
label: getDefaultLocaleLabel(locale),
|
||||
direction: getLangDir(locale),
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldWarnAboutNodeVersion(version: number, locales: string[]) {
|
||||
const isOnlyEnglish = locales.length === 1 && locales.includes('en');
|
||||
const isOlderNodeVersion = version < 14;
|
||||
return isOlderNodeVersion && !isOnlyEnglish;
|
||||
}
|
||||
|
||||
export async function loadI18n(
|
||||
config: DocusaurusConfig,
|
||||
options: {locale?: string} = {},
|
||||
): Promise<I18n> {
|
||||
const i18nConfig = config.i18n;
|
||||
const {i18n: i18nConfig} = config;
|
||||
|
||||
const currentLocale = options.locale ?? i18nConfig.defaultLocale;
|
||||
|
||||
if (currentLocale && !i18nConfig.locales.includes(currentLocale)) {
|
||||
throw new Error(
|
||||
`It is not possible to load Docusaurus with locale="${currentLocale}".
|
||||
This locale is not in the available locales of your site configuration: config.i18n.locales=[${i18nConfig.locales.join(
|
||||
',',
|
||||
)}]
|
||||
if (!i18nConfig.locales.includes(currentLocale)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`The locale=${currentLocale} was not found in your site configuration: config.i18n.locales=[${i18nConfig.locales.join(
|
||||
',',
|
||||
)}]
|
||||
Note: Docusaurus only support running one locale at a time.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const locales = i18nConfig.locales.includes(currentLocale)
|
||||
? i18nConfig.locales
|
||||
: (i18nConfig.locales.concat(currentLocale) as [string, ...string[]]);
|
||||
|
||||
if (shouldWarnAboutNodeVersion(NODE_MAJOR_VERSION, locales)) {
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`To use Docusaurus i18n, it is strongly advised to use NodeJS >= 14 (instead of ${NODE_MAJOR_VERSION})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function getLocaleConfig(locale: string): I18nLocaleConfig {
|
||||
// User provided values
|
||||
const localeConfigOptions: Partial<I18nLocaleConfig> =
|
||||
i18nConfig.localeConfigs[locale];
|
||||
|
||||
return {...defaultLocaleConfig(locale), ...localeConfigOptions};
|
||||
return {
|
||||
...getDefaultLocaleConfig(locale),
|
||||
...i18nConfig.localeConfigs[locale],
|
||||
};
|
||||
}
|
||||
|
||||
const localeConfigs = i18nConfig.locales.reduce((acc, locale) => {
|
||||
const localeConfigs = locales.reduce((acc, locale) => {
|
||||
return {...acc, [locale]: getLocaleConfig(locale)};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...i18nConfig,
|
||||
defaultLocale: i18nConfig.defaultLocale,
|
||||
locales,
|
||||
currentLocale,
|
||||
localeConfigs,
|
||||
};
|
||||
|
|
|
@ -24,14 +24,6 @@ module.exports = {
|
|||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'fr'],
|
||||
localeConfigs: {
|
||||
en: {
|
||||
label: 'English',
|
||||
},
|
||||
fr: {
|
||||
label: 'Français',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
|
|
@ -46,26 +46,6 @@ const isVersioningDisabled = !!process.env.DISABLE_VERSIONING;
|
|||
// https://docusaurus-i18n-staging.netlify.app/
|
||||
const isI18nStaging = process.env.I18N_STAGING === 'true';
|
||||
|
||||
const LocaleConfigs = isI18nStaging
|
||||
? // Staging locales (https://docusaurus-i18n-staging.netlify.app/)
|
||||
{
|
||||
en: {
|
||||
label: 'English',
|
||||
},
|
||||
'zh-CN': {
|
||||
label: '简体中文',
|
||||
},
|
||||
}
|
||||
: // Production locales
|
||||
{
|
||||
en: {
|
||||
label: 'English',
|
||||
},
|
||||
fr: {
|
||||
label: 'Français',
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
(module.exports = {
|
||||
title: 'Docusaurus',
|
||||
|
@ -77,8 +57,11 @@ const LocaleConfigs = isI18nStaging
|
|||
url: 'https://v2.docusaurus.io',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: Object.keys(LocaleConfigs),
|
||||
localeConfigs: LocaleConfigs,
|
||||
locales: isI18nStaging
|
||||
? // Staging locales (https://docusaurus-i18n-staging.netlify.app/)
|
||||
['en', 'zh-CN']
|
||||
: // Production locales
|
||||
['en', 'fr'],
|
||||
},
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
|
|
@ -17724,6 +17724,11 @@ rsvp@^4.8.4:
|
|||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
|
||||
|
||||
rtl-detect@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.2.tgz#8eca316f5c6563d54df4e406171dd7819adda67f"
|
||||
integrity sha512-5X1422hvphzg2a/bo4tIDbjFjbJUOaPZwqE6dnyyxqwFqfR+tBcvfqapJr0o0VygATVCGKiODEewhZtKF+90AA==
|
||||
|
||||
rtlcss@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-2.6.2.tgz#55b572b52c70015ba6e03d497e5c5cb8137104b4"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue