feat: allow customizing localization path of each locale (#7624)

This commit is contained in:
Joshua Chen 2022-06-17 17:07:35 +08:00 committed by GitHub
parent 39e3e3715e
commit 1b9bec1042
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 254 additions and 30 deletions

View file

@ -26,6 +26,7 @@ const DefaultI18N: I18n = {
direction: 'ltr',
htmlLang: 'en',
calendar: 'gregory',
path: 'en',
},
},
};

View file

@ -54,6 +54,7 @@ function getI18n(locale: string): I18n {
label: locale,
htmlLang: locale,
direction: 'ltr',
path: locale,
},
},
};
@ -71,7 +72,11 @@ const getPlugin = async (
i18n: I18n = DefaultI18N,
) => {
const generatedFilesDir: string = path.resolve(siteDir, '.docusaurus');
const localizationDir = path.join(siteDir, i18n.path, i18n.currentLocale);
const localizationDir = path.join(
siteDir,
i18n.path,
i18n.localeConfigs[i18n.currentLocale]!.path,
);
const siteConfig = {
title: 'Hello',
baseUrl: '/',

View file

@ -0,0 +1 @@
[{ "type": "autogenerated", "dirName": "." }]

View file

@ -66,3 +66,12 @@ exports[`docsVersion second docs instance versioning 1`] = `
],
}
`;
exports[`docsVersion works with custom i18n paths 1`] = `
[
{
"dirName": ".",
"type": "autogenerated",
},
]
`;

View file

@ -85,9 +85,11 @@ export async function cliDocsVersionCommand(
await Promise.all(
i18n.locales.map(async (locale) => {
// TODO duplicated logic from core, so duplicate comment as well: we need
// to support customization per-locale in the future
const localizationDir = path.resolve(siteDir, i18n.path, locale);
const localizationDir = path.resolve(
siteDir,
i18n.path,
i18n.localeConfigs[locale]!.path,
);
// Copy docs files.
const docsDir =
locale === i18n.defaultLocale

View file

@ -61,6 +61,12 @@ export type I18nLocaleConfig = {
* or `en-US` (`en` means `en-US`).
*/
calendar: string;
/**
* Root folder that all plugin localization folders of this locale are
* relative to. Will be resolved against `i18n.path`. Defaults to the locale's
* name.
*/
path: string;
};
export type I18nConfig = {

View file

@ -123,7 +123,7 @@ describe('localizePath', () => {
path: 'i18n',
locales: ['en', 'fr'],
currentLocale: 'fr',
localeConfigs: {},
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
},
options: {localizePath: true},
}),
@ -140,7 +140,7 @@ describe('localizePath', () => {
path: 'i18n',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
},
options: {localizePath: true},
}),
@ -157,7 +157,7 @@ describe('localizePath', () => {
path: 'i18n',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
},
}),
).toBe('/baseUrl/');
@ -173,7 +173,7 @@ describe('localizePath', () => {
path: 'i18n',
locales: ['en', 'fr'],
currentLocale: 'en',
localeConfigs: {},
localeConfigs: {fr: {path: 'fr'}, en: {path: 'en'}},
},
}),
).toBe('/baseUrl/');

View file

@ -68,6 +68,9 @@ export function getPluginI18nPath({
/**
* Takes a path and returns a localized a version (which is basically `path +
* i18n.currentLocale`).
*
* This is used to resolve the `outDir` and `baseUrl` of each locale; it is NOT
* used to determine plugin localization file locations.
*/
export function localizePath({
pathType,
@ -94,13 +97,15 @@ export function localizePath({
};
}): string {
const shouldLocalizePath: boolean =
//
options.localizePath ?? i18n.currentLocale !== i18n.defaultLocale;
if (!shouldLocalizePath) {
return originalPath;
}
// FS paths need special care, for Windows support
// FS paths need special care, for Windows support. Note: we don't use the
// locale config's `path` here, because this function is used for resolving
// outDir, which must be the same as baseUrl. When we have the baseUrl config,
// we need to sync the two.
if (pathType === 'fs') {
return path.join(originalPath, i18n.currentLocale);
}

View file

@ -0,0 +1,17 @@
module.exports = {
title: 'Site',
url: 'https://example.com',
baseUrl: '/',
i18n: {
locales: ['en', 'zh-Hans'],
defaultLocale: 'en',
localeConfigs: {
en: {
path: 'en-custom'
},
'zh-Hans': {
path: 'zh-Hans-custom'
}
}
}
};

View file

@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`load loads props for site with custom i18n path 1`] = `
{
"baseUrl": "/",
"codeTranslations": {},
"generatedFilesDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/.docusaurus",
"headTags": "",
"i18n": {
"currentLocale": "en",
"defaultLocale": "en",
"localeConfigs": {
"en": {
"calendar": "gregory",
"direction": "ltr",
"htmlLang": "en",
"label": "English",
"path": "en-custom",
},
"zh-Hans": {
"calendar": "gregory",
"direction": "ltr",
"htmlLang": "zh-Hans",
"label": "简体中文",
"path": "zh-Hans-custom",
},
},
"locales": [
"en",
"zh-Hans",
],
"path": "i18n",
},
"localizationDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/i18n/en-custom",
"outDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/build",
"plugins": [
{
"content": undefined,
"getClientModules": [Function],
"injectHtmlTags": [Function],
"name": "docusaurus-bootstrap-plugin",
"options": {
"id": "default",
},
"path": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site",
"version": {
"type": "synthetic",
},
},
{
"configureWebpack": [Function],
"content": undefined,
"name": "docusaurus-mdx-fallback-plugin",
"options": {
"id": "default",
},
"path": ".",
"version": {
"type": "synthetic",
},
},
],
"postBodyTags": "",
"preBodyTags": "",
"routes": [],
"routesPaths": [
"/404.html",
],
"siteConfig": {
"baseUrl": "/",
"baseUrlIssueBanner": true,
"clientModules": [],
"customFields": {},
"i18n": {
"defaultLocale": "en",
"localeConfigs": {
"en": {
"direction": "ltr",
"path": "en-custom",
},
"zh-Hans": {
"direction": "ltr",
"path": "zh-Hans-custom",
},
},
"locales": [
"en",
"zh-Hans",
],
"path": "i18n",
},
"noIndex": false,
"onBrokenLinks": "throw",
"onBrokenMarkdownLinks": "warn",
"onDuplicateRoutes": "warn",
"plugins": [],
"presets": [],
"scripts": [],
"staticDirectories": [
"static",
],
"stylesheets": [],
"tagline": "",
"themeConfig": {},
"themes": [],
"title": "Site",
"titleDelimiter": "|",
"url": "https://example.com",
},
"siteConfigPath": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/docusaurus.config.js",
"siteDir": "<PROJECT_ROOT>/packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site",
"siteMetadata": {
"docusaurusVersion": "<CURRENT_VERSION>",
"pluginVersions": {},
"siteVersion": undefined,
},
}
`;

View file

@ -32,42 +32,49 @@ describe('defaultLocaleConfig', () => {
direction: 'ltr',
htmlLang: 'fr',
calendar: 'gregory',
path: 'fr',
});
expect(getDefaultLocaleConfig('fr-FR')).toEqual({
label: 'Français (France)',
direction: 'ltr',
htmlLang: 'fr-FR',
calendar: 'gregory',
path: 'fr-FR',
});
expect(getDefaultLocaleConfig('en')).toEqual({
label: 'English',
direction: 'ltr',
htmlLang: 'en',
calendar: 'gregory',
path: 'en',
});
expect(getDefaultLocaleConfig('en-US')).toEqual({
label: 'American English',
direction: 'ltr',
htmlLang: 'en-US',
calendar: 'gregory',
path: 'en-US',
});
expect(getDefaultLocaleConfig('zh')).toEqual({
label: '中文',
direction: 'ltr',
htmlLang: 'zh',
calendar: 'gregory',
path: 'zh',
});
expect(getDefaultLocaleConfig('zh-CN')).toEqual({
label: '中文(中国)',
direction: 'ltr',
htmlLang: 'zh-CN',
calendar: 'gregory',
path: 'zh-CN',
});
expect(getDefaultLocaleConfig('en-US')).toEqual({
label: 'American English',
direction: 'ltr',
htmlLang: 'en-US',
calendar: 'gregory',
path: 'en-US',
});
expect(getDefaultLocaleConfig('fa')).toEqual({
// cSpell:ignore فارسی
@ -75,6 +82,7 @@ describe('defaultLocaleConfig', () => {
direction: 'rtl',
htmlLang: 'fa',
calendar: 'gregory',
path: 'fa',
});
expect(getDefaultLocaleConfig('fa-IR')).toEqual({
// cSpell:ignore ایران فارسیا
@ -82,12 +90,14 @@ describe('defaultLocaleConfig', () => {
direction: 'rtl',
htmlLang: 'fa-IR',
calendar: 'gregory',
path: 'fa-IR',
});
expect(getDefaultLocaleConfig('en-US-u-ca-buddhist')).toEqual({
label: 'American English',
direction: 'ltr',
htmlLang: 'en-US-u-ca-buddhist',
calendar: 'buddhist',
path: 'en-US-u-ca-buddhist',
});
});
});
@ -170,6 +180,7 @@ describe('loadI18n', () => {
direction: 'ltr',
htmlLang: 'fr',
calendar: 'gregory',
path: 'fr',
},
en: getDefaultLocaleConfig('en'),
de: getDefaultLocaleConfig('de'),

View file

@ -0,0 +1,45 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import path from 'path';
import {mergeWithCustomize} from 'webpack-merge';
import {loadSetup} from './testUtils';
import type {Props} from '@docusaurus/types';
import type {DeepPartial} from 'utility-types';
describe('load', () => {
it('loads props for site with custom i18n path', async () => {
const props = await loadSetup('custom-i18n-site');
expect(props).toMatchSnapshot();
const props2 = await loadSetup('custom-i18n-site', {locale: 'zh-Hans'});
expect(props2).toEqual(
mergeWithCustomize<DeepPartial<Props>>({
customizeArray(a, b, key) {
return ['routesPaths', 'plugins'].includes(key) ? b : undefined;
},
})(props, {
baseUrl: '/zh-Hans/',
i18n: {
currentLocale: 'zh-Hans',
},
localizationDir: path.join(
__dirname,
'__fixtures__/custom-i18n-site/i18n/zh-Hans-custom',
),
outDir: path.join(
__dirname,
'__fixtures__/custom-i18n-site/build/zh-Hans',
),
routesPaths: ['/zh-Hans/404.html'],
siteConfig: {
baseUrl: '/zh-Hans/',
},
plugins: props2.plugins,
}),
);
});
});

View file

@ -6,20 +6,14 @@
*/
import path from 'path';
import {load} from '../index';
import {load, type LoadContextOptions} from '../index';
import type {Props} from '@docusaurus/types';
// Helper methods to setup dummy/fake projects.
export default async function loadSetup(name: string): Promise<Props> {
export async function loadSetup(
name: string,
options?: Partial<LoadContextOptions>,
): Promise<Props> {
const fixtures = path.join(__dirname, '__fixtures__');
const simpleSite = path.join(fixtures, 'simple-site');
const customSite = path.join(fixtures, 'custom-site');
switch (name) {
case 'custom':
return load({siteDir: customSite});
case 'simple':
default:
return load({siteDir: simpleSite});
}
return load({siteDir: path.join(fixtures, name), ...options});
}

View file

@ -135,6 +135,7 @@ const LocaleConfigSchema = Joi.object({
htmlLang: Joi.string(),
direction: Joi.string().equal('ltr', 'rtl').default('ltr'),
calendar: Joi.string(),
path: Joi.string(),
});
const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({

View file

@ -26,6 +26,7 @@ export function getDefaultLocaleConfig(locale: string): I18nLocaleConfig {
htmlLang: locale,
// If the locale name includes -u-ca-xxx the calendar will be defined
calendar: new Intl.Locale(locale).calendar ?? 'gregory',
path: locale,
};
}

View file

@ -85,8 +85,11 @@ export async function loadContext(
const siteConfig: DocusaurusConfig = {...initialSiteConfig, baseUrl};
// TODO allow customizing localizationDir per-locale
const localizationDir = path.resolve(siteDir, i18n.path, i18n.currentLocale);
const localizationDir = path.resolve(
siteDir,
i18n.path,
i18n.localeConfigs[i18n.currentLocale]!.path,
);
const codeTranslationFileContent =
(await readCodeTranslationFileContent({localizationDir})) ?? {};

View file

@ -8,18 +8,18 @@
import webpack from 'webpack';
import createClientConfig from '../client';
import loadSetup from '../../server/__tests__/testUtils';
import {loadSetup} from '../../server/__tests__/testUtils';
describe('webpack dev config', () => {
it('simple', async () => {
const props = await loadSetup('simple');
const props = await loadSetup('simple-site');
const config = await createClientConfig(props);
const errors = webpack.validate(config);
expect(errors).toBeUndefined();
});
it('custom', async () => {
const props = await loadSetup('custom');
const props = await loadSetup('custom-site');
const config = await createClientConfig(props);
const errors = webpack.validate(config);
expect(errors).toBeUndefined();

View file

@ -9,12 +9,12 @@ import {jest} from '@jest/globals';
import webpack from 'webpack';
import createServerConfig from '../server';
import loadSetup from '../../server/__tests__/testUtils';
import {loadSetup} from '../../server/__tests__/testUtils';
describe('webpack production config', () => {
it('simple', async () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('simple');
const props = await loadSetup('simple-site');
const config = await createServerConfig({
props,
onHeadTagsCollected: () => {},
@ -26,7 +26,7 @@ describe('webpack production config', () => {
it('custom', async () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
const props = await loadSetup('custom');
const props = await loadSetup('custom-site');
const config = await createServerConfig({
props,
onHeadTagsCollected: () => {},

View file

@ -130,18 +130,21 @@ module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fa'],
path: 'i18n',
localeConfigs: {
en: {
label: 'English',
direction: 'ltr',
htmlLang: 'en-US',
calendar: 'gregory',
path: 'en',
},
fa: {
label: 'فارسی',
direction: 'rtl',
htmlLang: 'fa-IR',
calendar: 'persian',
path: 'fa',
},
},
},
@ -150,11 +153,13 @@ module.exports = {
- `defaultLocale`: The locale that (1) does not have its name in the base URL (2) gets started with `docusaurus start` without `--locale` option (3) will be used for the `<link hrefLang="x-default">` tag
- `locales`: List of locales deployed on your site. Must contain `defaultLocale`.
- `path`: Root folder which all locale folders are relative to. Can be absolute or relative to the config file. Defaults to `i18n`.
- `localeConfigs`: Individual options for each locale.
- `label`: The label displayed for this locale in the locales dropdown.
- `direction`: `ltr` (default) or `rtl` (for [right-to-left languages](https://developer.mozilla.org/en-US/docs/Glossary/rtl) like Farsi, Arabic, Hebrew, etc.). Used to select the locale's CSS and HTML meta attribute.
- `htmlLang`: BCP 47 language tag to use in `<html lang="...">` and in `<link ... hreflang="...">`
- `calendar`: the [calendar](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar) used to calculate the date era. Note that it doesn't control the actual string displayed: `MM/DD/YYYY` and `DD/MM/YYYY` are both `gregory`. To choose the format (`DD/MM/YYYY` or `MM/DD/YYYY`), set your locale name to `en-GB` or `en-US` (`en` means `en-US`).
- `path`: Root folder that all plugin localization folders of this locale are relative to. Will be resolved against `i18n.path`. Defaults to the locale's name. Note: this has no effect on the locale's `baseUrl`—customization of base URL is a work-in-progress.
### `noIndex` {#noIndex}