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:
Sébastien Lorber 2021-03-18 11:43:07 +01:00 committed by GitHub
parent 5e73c72f26
commit 806fdbaf27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 189 additions and 110 deletions

View file

@ -39,6 +39,7 @@ export default function LocaleDropdownNavbarItem({
target: '_self',
autoAddBaseUrl: false,
className: locale === currentLocale ? 'dropdown__link--active' : '',
style: {textTransform: 'capitalize'},
};
});

View file

@ -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 {

View file

@ -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",

View file

@ -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';

View file

@ -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."
`;

View file

@ -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/,
);
});
});

View file

@ -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,
};

View file

@ -24,14 +24,6 @@ module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
localeConfigs: {
en: {
label: 'English',
},
fr: {
label: 'Français',
},
},
},
};
```

View file

@ -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',

View file

@ -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"