mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat: allow customizing localization path of each locale (#7624)
This commit is contained in:
parent
39e3e3715e
commit
1b9bec1042
24 changed files with 254 additions and 30 deletions
|
@ -26,6 +26,7 @@ const DefaultI18N: I18n = {
|
|||
direction: 'ltr',
|
||||
htmlLang: 'en',
|
||||
calendar: 'gregory',
|
||||
path: 'en',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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: '/',
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[{ "type": "autogenerated", "dirName": "." }]
|
|
@ -66,3 +66,12 @@ exports[`docsVersion second docs instance versioning 1`] = `
|
|||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`docsVersion works with custom i18n paths 1`] = `
|
||||
[
|
||||
{
|
||||
"dirName": ".",
|
||||
"type": "autogenerated",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
Binary file not shown.
|
@ -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
|
||||
|
|
6
packages/docusaurus-types/src/index.d.ts
vendored
6
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -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 = {
|
||||
|
|
|
@ -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/');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
17
packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/docusaurus.config.js
generated
Normal file
17
packages/docusaurus/src/server/__tests__/__fixtures__/custom-i18n-site/docusaurus.config.js
generated
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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'),
|
||||
|
|
45
packages/docusaurus/src/server/__tests__/index.test.ts
Normal file
45
packages/docusaurus/src/server/__tests__/index.test.ts
Normal 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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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})) ?? {};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: () => {},
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue