mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat(core): allow customizing the i18n directory path (#7386)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
c07a514730
commit
abe5450526
26 changed files with 147 additions and 166 deletions
|
@ -19,6 +19,7 @@ const DefaultI18N: I18n = {
|
|||
currentLocale: 'en',
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
path: '1i8n',
|
||||
localeConfigs: {
|
||||
en: {
|
||||
label: 'English',
|
||||
|
|
|
@ -47,6 +47,7 @@ function getI18n(locale: string): I18n {
|
|||
currentLocale: locale,
|
||||
locales: [locale],
|
||||
defaultLocale: locale,
|
||||
path: 'i18n',
|
||||
localeConfigs: {
|
||||
[locale]: {
|
||||
calendar: 'gregory',
|
||||
|
@ -70,6 +71,7 @@ const getPlugin = async (
|
|||
i18n: I18n = DefaultI18N,
|
||||
) => {
|
||||
const generatedFilesDir: string = path.resolve(siteDir, '.docusaurus');
|
||||
const localizationDir = path.join(siteDir, i18n.path, i18n.currentLocale);
|
||||
const siteConfig = {
|
||||
title: 'Hello',
|
||||
baseUrl: '/',
|
||||
|
@ -81,6 +83,7 @@ const getPlugin = async (
|
|||
siteConfig,
|
||||
generatedFilesDir,
|
||||
i18n,
|
||||
localizationDir,
|
||||
} as LoadContext,
|
||||
validateOptions({
|
||||
validate: normalizePluginOptions as Validate<
|
||||
|
|
|
@ -59,6 +59,7 @@ export default async function pluginContentBlog(
|
|||
siteDir,
|
||||
siteConfig,
|
||||
generatedFilesDir,
|
||||
localizationDir,
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
const {onBrokenMarkdownLinks, baseUrl} = siteConfig;
|
||||
|
@ -66,8 +67,7 @@ export default async function pluginContentBlog(
|
|||
const contentPaths: BlogContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
siteDir,
|
||||
locale: currentLocale,
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-blog',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
|
|
Binary file not shown.
|
@ -85,13 +85,15 @@ 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);
|
||||
// Copy docs files.
|
||||
const docsDir =
|
||||
locale === i18n.defaultLocale
|
||||
? path.resolve(siteDir, docsPath)
|
||||
: getDocsDirPathLocalized({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginId,
|
||||
versionName: CURRENT_VERSION_NAME,
|
||||
});
|
||||
|
@ -114,8 +116,7 @@ export async function cliDocsVersionCommand(
|
|||
locale === i18n.defaultLocale
|
||||
? getVersionDocsDirPath(siteDir, pluginId, version)
|
||||
: getDocsDirPathLocalized({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginId,
|
||||
versionName: version,
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
|||
} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
const DefaultI18N: I18n = {
|
||||
path: 'i18n',
|
||||
currentLocale: 'en',
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
|
@ -37,6 +38,7 @@ describe('readVersionsMetadata', () => {
|
|||
siteDir: simpleSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
localizationDir: path.join(simpleSiteDir, 'i18n/en'),
|
||||
} as LoadContext;
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
|
@ -198,6 +200,7 @@ describe('readVersionsMetadata', () => {
|
|||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
localizationDir: path.join(versionedSiteDir, 'i18n/en'),
|
||||
} as LoadContext;
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
|
@ -636,6 +639,7 @@ describe('readVersionsMetadata', () => {
|
|||
siteDir: versionedSiteDir,
|
||||
baseUrl: '/',
|
||||
i18n: DefaultI18N,
|
||||
localizationDir: path.join(versionedSiteDir, 'i18n/en'),
|
||||
} as LoadContext;
|
||||
|
||||
const vCurrent: VersionMetadata = {
|
||||
|
|
|
@ -55,19 +55,16 @@ export function getVersionSidebarsPath(
|
|||
}
|
||||
|
||||
export function getDocsDirPathLocalized({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginId,
|
||||
versionName,
|
||||
}: {
|
||||
siteDir: string;
|
||||
locale: string;
|
||||
localizationDir: string;
|
||||
pluginId: string;
|
||||
versionName: string;
|
||||
}): string {
|
||||
return getPluginI18nPath({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-docs',
|
||||
pluginId,
|
||||
subPaths: [
|
||||
|
@ -175,8 +172,7 @@ export async function getVersionMetadataPaths({
|
|||
> {
|
||||
const isCurrent = versionName === CURRENT_VERSION_NAME;
|
||||
const contentPathLocalized = getDocsDirPathLocalized({
|
||||
siteDir: context.siteDir,
|
||||
locale: context.i18n.currentLocale,
|
||||
localizationDir: context.localizationDir,
|
||||
pluginId: options.id,
|
||||
versionName,
|
||||
});
|
||||
|
|
|
@ -49,8 +49,7 @@ function getVersionEditUrls({
|
|||
const editDirPath = options.editCurrentVersion ? options.path : contentPath;
|
||||
const editDirPathLocalized = options.editCurrentVersion
|
||||
? getDocsDirPathLocalized({
|
||||
siteDir: context.siteDir,
|
||||
locale: context.i18n.currentLocale,
|
||||
localizationDir: context.localizationDir,
|
||||
versionName: CURRENT_VERSION_NAME,
|
||||
pluginId: options.id,
|
||||
})
|
||||
|
|
|
@ -55,19 +55,19 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
|
|||
exports[`docusaurus-plugin-content-pages loads simple pages with french translations 1`] = `
|
||||
[
|
||||
{
|
||||
"permalink": "/",
|
||||
"permalink": "/fr/",
|
||||
"source": "@site/src/pages/index.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"permalink": "/typescript",
|
||||
"permalink": "/fr/typescript",
|
||||
"source": "@site/src/pages/typescript.tsx",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"description": "Markdown index page",
|
||||
"frontMatter": {},
|
||||
"permalink": "/hello/",
|
||||
"permalink": "/fr/hello/",
|
||||
"source": "@site/src/pages/hello/index.md",
|
||||
"title": "Index",
|
||||
"type": "mdx",
|
||||
|
@ -78,26 +78,26 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
"description": "my mdx page",
|
||||
"title": "mdx page",
|
||||
},
|
||||
"permalink": "/hello/mdxPage",
|
||||
"permalink": "/fr/hello/mdxPage",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "mdx page",
|
||||
"type": "mdx",
|
||||
},
|
||||
{
|
||||
"permalink": "/hello/translatedJs",
|
||||
"permalink": "/fr/hello/translatedJs",
|
||||
"source": "@site/i18n/fr/docusaurus-plugin-content-pages/hello/translatedJs.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
{
|
||||
"description": "translated markdown page (fr)",
|
||||
"frontMatter": {},
|
||||
"permalink": "/hello/translatedMd",
|
||||
"permalink": "/fr/hello/translatedMd",
|
||||
"source": "@site/i18n/fr/docusaurus-plugin-content-pages/hello/translatedMd.md",
|
||||
"title": undefined,
|
||||
"type": "mdx",
|
||||
},
|
||||
{
|
||||
"permalink": "/hello/world",
|
||||
"permalink": "/fr/hello/world",
|
||||
"source": "@site/src/pages/hello/world.js",
|
||||
"type": "jsx",
|
||||
},
|
||||
|
|
|
@ -32,15 +32,9 @@ describe('docusaurus-plugin-content-pages', () => {
|
|||
|
||||
it('loads simple pages with french translations', async () => {
|
||||
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
||||
const context = await loadContext({siteDir});
|
||||
const context = await loadContext({siteDir, locale: 'fr'});
|
||||
const plugin = pluginContentPages(
|
||||
{
|
||||
...context,
|
||||
i18n: {
|
||||
...context.i18n,
|
||||
currentLocale: 'fr',
|
||||
},
|
||||
},
|
||||
context,
|
||||
validateOptions({
|
||||
validate: normalizePluginOptions,
|
||||
options: {
|
||||
|
|
|
@ -48,18 +48,12 @@ export default function pluginContentPages(
|
|||
[admonitions, options.admonitions],
|
||||
]);
|
||||
}
|
||||
const {
|
||||
siteConfig,
|
||||
siteDir,
|
||||
generatedFilesDir,
|
||||
i18n: {currentLocale},
|
||||
} = context;
|
||||
const {siteConfig, siteDir, generatedFilesDir, localizationDir} = context;
|
||||
|
||||
const contentPaths: PagesContentPaths = {
|
||||
contentPath: path.resolve(siteDir, options.path),
|
||||
contentPathLocalized: getPluginI18nPath({
|
||||
siteDir,
|
||||
locale: currentLocale,
|
||||
localizationDir,
|
||||
pluginName: 'docusaurus-plugin-content-pages',
|
||||
pluginId: options.id,
|
||||
}),
|
||||
|
|
11
packages/docusaurus-types/src/index.d.ts
vendored
11
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -70,6 +70,11 @@ export type I18nConfig = {
|
|||
* 3. Will be used for the `<link hrefLang="x-default">` tag
|
||||
*/
|
||||
defaultLocale: string;
|
||||
/**
|
||||
* Root folder which all locale folders are relative to. Can be absolute or
|
||||
* relative to the config file. e.g. `i18n`
|
||||
*/
|
||||
path: string;
|
||||
/** List of locales deployed on your site. Must contain `defaultLocale`. */
|
||||
locales: [string, ...string[]];
|
||||
/** Individual options for each locale. */
|
||||
|
@ -416,6 +421,12 @@ export type LoadContext = {
|
|||
siteConfig: DocusaurusConfig;
|
||||
siteConfigPath: string;
|
||||
outDir: string;
|
||||
/**
|
||||
* Directory where all source translations for the current locale can be found
|
||||
* in. Constructed with `i18n.path` + `i18n.currentLocale.path` (e.g.
|
||||
* `<siteDir>/i18n/en`)
|
||||
*/
|
||||
localizationDir: string;
|
||||
/**
|
||||
* Duplicated from `siteConfig.baseUrl`, but probably worth keeping. We mutate
|
||||
* `siteConfig` to make `baseUrl` there localized as well, but that's mostly
|
||||
|
|
|
@ -65,34 +65,33 @@ describe('getPluginI18nPath', () => {
|
|||
it('gets correct path', () => {
|
||||
expect(
|
||||
getPluginI18nPath({
|
||||
siteDir: __dirname,
|
||||
locale: 'zh-Hans',
|
||||
localizationDir: '<SITE_DIR>/i18n/zh-Hans',
|
||||
pluginName: 'plugin-content-docs',
|
||||
pluginId: 'community',
|
||||
subPaths: ['foo'],
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<PROJECT_ROOT>/packages/docusaurus-utils/src/__tests__/i18n/zh-Hans/plugin-content-docs-community/foo"`,
|
||||
`"<SITE_DIR>/i18n/zh-Hans/plugin-content-docs-community/foo"`,
|
||||
);
|
||||
});
|
||||
it('gets correct path for default plugin', () => {
|
||||
expect(
|
||||
getPluginI18nPath({
|
||||
siteDir: __dirname,
|
||||
locale: 'zh-Hans',
|
||||
localizationDir: '<SITE_DIR>/i18n/zh-Hans',
|
||||
pluginName: 'plugin-content-docs',
|
||||
subPaths: ['foo'],
|
||||
}).replace(__dirname, ''),
|
||||
).toMatchInlineSnapshot(`"/i18n/zh-Hans/plugin-content-docs/foo"`);
|
||||
}),
|
||||
).toMatchInlineSnapshot(
|
||||
`"<SITE_DIR>/i18n/zh-Hans/plugin-content-docs/foo"`,
|
||||
);
|
||||
});
|
||||
it('gets correct path when no sub-paths', () => {
|
||||
expect(
|
||||
getPluginI18nPath({
|
||||
siteDir: __dirname,
|
||||
locale: 'zh-Hans',
|
||||
localizationDir: '<SITE_DIR>/i18n/zh-Hans',
|
||||
pluginName: 'plugin-content-docs',
|
||||
}).replace(__dirname, ''),
|
||||
).toMatchInlineSnapshot(`"/i18n/zh-Hans/plugin-content-docs"`);
|
||||
}),
|
||||
).toMatchInlineSnapshot(`"<SITE_DIR>/i18n/zh-Hans/plugin-content-docs"`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -104,6 +103,7 @@ describe('localizePath', () => {
|
|||
path: '/baseUrl',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: {},
|
||||
|
@ -120,6 +120,7 @@ describe('localizePath', () => {
|
|||
path: '/baseFsPath',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: {},
|
||||
|
@ -136,6 +137,7 @@ describe('localizePath', () => {
|
|||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {},
|
||||
|
@ -152,6 +154,7 @@ describe('localizePath', () => {
|
|||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {},
|
||||
|
@ -167,6 +170,7 @@ describe('localizePath', () => {
|
|||
path: '/baseUrl/',
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr'],
|
||||
currentLocale: 'en',
|
||||
localeConfigs: {},
|
||||
|
|
|
@ -75,7 +75,7 @@ export const THEME_PATH = `${SRC_DIR_NAME}/theme`;
|
|||
* All translation-related data live here, relative to site directory. Content
|
||||
* will be namespaced by locale.
|
||||
*/
|
||||
export const I18N_DIR_NAME = 'i18n';
|
||||
export const DEFAULT_I18N_DIR_NAME = 'i18n';
|
||||
|
||||
/**
|
||||
* Translations for React code.
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
import {DEFAULT_PLUGIN_ID, I18N_DIR_NAME} from './constants';
|
||||
import {DEFAULT_PLUGIN_ID} from './constants';
|
||||
import {normalizeUrl} from './urlUtils';
|
||||
import type {
|
||||
TranslationFileContent,
|
||||
|
@ -46,24 +46,18 @@ export function updateTranslationFileMessages(
|
|||
* expect everything it needs for translations to be found under this path.
|
||||
*/
|
||||
export function getPluginI18nPath({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginName,
|
||||
pluginId = DEFAULT_PLUGIN_ID,
|
||||
subPaths = [],
|
||||
}: {
|
||||
siteDir: string;
|
||||
locale: string;
|
||||
localizationDir: string;
|
||||
pluginName: string;
|
||||
pluginId?: string | undefined;
|
||||
subPaths?: string[];
|
||||
}): string {
|
||||
return path.join(
|
||||
siteDir,
|
||||
I18N_DIR_NAME,
|
||||
// Namespace first by locale: convenient to work in a single folder for a
|
||||
// translator
|
||||
locale,
|
||||
localizationDir,
|
||||
// Make it convenient to use for single-instance
|
||||
// ie: return "docs", not "docs-default" nor "docs/default"
|
||||
`${pluginName}${pluginId === DEFAULT_PLUGIN_ID ? '' : `-${pluginId}`}`,
|
||||
|
|
|
@ -17,7 +17,7 @@ export {
|
|||
DEFAULT_STATIC_DIR_NAME,
|
||||
OUTPUT_STATIC_ASSETS_DIR_NAME,
|
||||
THEME_PATH,
|
||||
I18N_DIR_NAME,
|
||||
DEFAULT_I18N_DIR_NAME,
|
||||
CODE_TRANSLATIONS_FILE_NAME,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
|
|
|
@ -25,7 +25,6 @@ import {
|
|||
getHttpsConfig,
|
||||
} from '../webpack/utils';
|
||||
import {getHostPort, type HostPortOptions} from '../server/getHostPort';
|
||||
import {getTranslationsLocaleDirPath} from '../server/translations/translations';
|
||||
|
||||
export type StartCLIOptions = HostPortOptions &
|
||||
Pick<LoadContextOptions, 'locale' | 'config'> & {
|
||||
|
@ -82,7 +81,7 @@ export async function start(
|
|||
logger.error(err.stack);
|
||||
});
|
||||
}, 500);
|
||||
const {siteConfig, plugins} = props;
|
||||
const {siteConfig, plugins, localizationDir} = props;
|
||||
|
||||
const normalizeToSiteDir = (filepath: string) => {
|
||||
if (filepath && path.isAbsolute(filepath)) {
|
||||
|
@ -96,14 +95,7 @@ export async function start(
|
|||
.filter(Boolean)
|
||||
.map(normalizeToSiteDir);
|
||||
|
||||
const pathsToWatch = [
|
||||
...pluginPaths,
|
||||
props.siteConfigPath,
|
||||
getTranslationsLocaleDirPath({
|
||||
siteDir,
|
||||
locale: props.i18n.currentLocale,
|
||||
}),
|
||||
];
|
||||
const pathsToWatch = [...pluginPaths, props.siteConfigPath, localizationDir];
|
||||
|
||||
const pollingOptions = {
|
||||
usePolling: !!cliOptions.poll,
|
||||
|
|
|
@ -47,14 +47,12 @@ async function getExtraSourceCodeFilePaths(): Promise<string[]> {
|
|||
}
|
||||
|
||||
async function writePluginTranslationFiles({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
plugin,
|
||||
locale,
|
||||
options,
|
||||
}: {
|
||||
siteDir: string;
|
||||
localizationDir: string;
|
||||
plugin: InitializedPlugin;
|
||||
locale: string;
|
||||
options: WriteTranslationsOptions;
|
||||
}) {
|
||||
if (plugin.getTranslationFiles) {
|
||||
|
@ -66,10 +64,9 @@ async function writePluginTranslationFiles({
|
|||
await Promise.all(
|
||||
translationFiles.map(async (translationFile) => {
|
||||
await writePluginTranslations({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
plugin,
|
||||
translationFile,
|
||||
locale,
|
||||
options,
|
||||
});
|
||||
}),
|
||||
|
@ -86,6 +83,7 @@ export async function writeTranslations(
|
|||
config: options.config,
|
||||
locale: options.locale,
|
||||
});
|
||||
const {localizationDir} = context;
|
||||
const plugins = await initPlugins(context);
|
||||
|
||||
const locale = options.locale ?? context.i18n.defaultLocale;
|
||||
|
@ -116,11 +114,11 @@ Available locales are: ${context.i18n.locales.join(',')}.`,
|
|||
defaultCodeMessages,
|
||||
});
|
||||
|
||||
await writeCodeTranslations({siteDir, locale}, codeTranslations, options);
|
||||
await writeCodeTranslations({localizationDir}, codeTranslations, options);
|
||||
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
await writePluginTranslationFiles({siteDir, plugin, locale, options});
|
||||
await writePluginTranslationFiles({localizationDir, plugin, options});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ exports[`loadSiteConfig website with .cjs siteConfig 1`] = `
|
|||
"locales": [
|
||||
"en",
|
||||
],
|
||||
"path": "i18n",
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -49,6 +50,7 @@ exports[`loadSiteConfig website with valid async config 1`] = `
|
|||
"locales": [
|
||||
"en",
|
||||
],
|
||||
"path": "i18n",
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -87,6 +89,7 @@ exports[`loadSiteConfig website with valid async config creator function 1`] = `
|
|||
"locales": [
|
||||
"en",
|
||||
],
|
||||
"path": "i18n",
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -125,6 +128,7 @@ exports[`loadSiteConfig website with valid config creator function 1`] = `
|
|||
"locales": [
|
||||
"en",
|
||||
],
|
||||
"path": "i18n",
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
@ -166,6 +170,7 @@ exports[`loadSiteConfig website with valid siteConfig 1`] = `
|
|||
"locales": [
|
||||
"en",
|
||||
],
|
||||
"path": "i18n",
|
||||
},
|
||||
"noIndex": false,
|
||||
"onBrokenLinks": "throw",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import {jest} from '@jest/globals';
|
||||
import {loadI18n, getDefaultLocaleConfig} from '../i18n';
|
||||
import {DEFAULT_I18N_CONFIG} from '../configValidation';
|
||||
import type {I18nConfig} from '@docusaurus/types';
|
||||
import type {DocusaurusConfig, I18nConfig} from '@docusaurus/types';
|
||||
|
||||
function testLocaleConfigsFor(locales: string[]) {
|
||||
return Object.fromEntries(
|
||||
|
@ -18,10 +18,9 @@ function testLocaleConfigsFor(locales: string[]) {
|
|||
|
||||
function loadI18nTest(i18nConfig: I18nConfig, locale?: string) {
|
||||
return loadI18n(
|
||||
// @ts-expect-error: enough for this test
|
||||
{
|
||||
i18n: i18nConfig,
|
||||
},
|
||||
} as DocusaurusConfig,
|
||||
{locale},
|
||||
);
|
||||
}
|
||||
|
@ -101,6 +100,7 @@ describe('loadI18n', () => {
|
|||
|
||||
it('loads I18n for default config', async () => {
|
||||
await expect(loadI18nTest(DEFAULT_I18N_CONFIG)).resolves.toEqual({
|
||||
path: 'i18n',
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
currentLocale: 'en',
|
||||
|
@ -111,12 +111,14 @@ describe('loadI18n', () => {
|
|||
it('loads I18n for multi-lang config', async () => {
|
||||
await expect(
|
||||
loadI18nTest({
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'fr',
|
||||
localeConfigs: testLocaleConfigsFor(['en', 'fr', 'de']),
|
||||
|
@ -127,6 +129,7 @@ describe('loadI18n', () => {
|
|||
await expect(
|
||||
loadI18nTest(
|
||||
{
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
|
@ -135,6 +138,7 @@ describe('loadI18n', () => {
|
|||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'de',
|
||||
localeConfigs: testLocaleConfigsFor(['en', 'fr', 'de']),
|
||||
|
@ -145,6 +149,7 @@ describe('loadI18n', () => {
|
|||
await expect(
|
||||
loadI18nTest(
|
||||
{
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {
|
||||
|
@ -156,6 +161,7 @@ describe('loadI18n', () => {
|
|||
),
|
||||
).resolves.toEqual({
|
||||
defaultLocale: 'fr',
|
||||
path: 'i18n',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
currentLocale: 'de',
|
||||
localeConfigs: {
|
||||
|
@ -174,6 +180,7 @@ describe('loadI18n', () => {
|
|||
it('warns when trying to load undeclared locale', async () => {
|
||||
await loadI18nTest(
|
||||
{
|
||||
path: 'i18n',
|
||||
defaultLocale: 'fr',
|
||||
locales: ['en', 'fr', 'de'],
|
||||
localeConfigs: {},
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {DEFAULT_STATIC_DIR_NAME} from '@docusaurus/utils';
|
||||
import {
|
||||
DEFAULT_STATIC_DIR_NAME,
|
||||
DEFAULT_I18N_DIR_NAME,
|
||||
} from '@docusaurus/utils';
|
||||
import {Joi, URISchema, printWarning} from '@docusaurus/utils-validation';
|
||||
import type {DocusaurusConfig, I18nConfig} from '@docusaurus/types';
|
||||
|
||||
|
@ -13,6 +16,7 @@ const DEFAULT_I18N_LOCALE = 'en';
|
|||
|
||||
export const DEFAULT_I18N_CONFIG: I18nConfig = {
|
||||
defaultLocale: DEFAULT_I18N_LOCALE,
|
||||
path: DEFAULT_I18N_DIR_NAME,
|
||||
locales: [DEFAULT_I18N_LOCALE],
|
||||
localeConfigs: {},
|
||||
};
|
||||
|
@ -135,6 +139,7 @@ const LocaleConfigSchema = Joi.object({
|
|||
|
||||
const I18N_CONFIG_SCHEMA = Joi.object<I18nConfig>({
|
||||
defaultLocale: Joi.string().required(),
|
||||
path: Joi.string().default(DEFAULT_I18N_CONFIG.path),
|
||||
locales: Joi.array().items().min(1).items(Joi.string().required()).required(),
|
||||
localeConfigs: Joi.object()
|
||||
.pattern(/.*/, LocaleConfigSchema)
|
||||
|
|
|
@ -60,6 +60,7 @@ Note: Docusaurus only support running one locale at a time.`;
|
|||
return {
|
||||
defaultLocale: i18nConfig.defaultLocale,
|
||||
locales,
|
||||
path: i18nConfig.path,
|
||||
currentLocale,
|
||||
localeConfigs,
|
||||
};
|
||||
|
|
|
@ -85,11 +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 codeTranslationFileContent =
|
||||
(await readCodeTranslationFileContent({
|
||||
siteDir,
|
||||
locale: i18n.currentLocale,
|
||||
})) ?? {};
|
||||
(await readCodeTranslationFileContent({localizationDir})) ?? {};
|
||||
|
||||
// We only need key->message for code translations
|
||||
const codeTranslations = _.mapValues(
|
||||
|
@ -100,6 +100,7 @@ export async function loadContext(
|
|||
return {
|
||||
siteDir,
|
||||
generatedFilesDir,
|
||||
localizationDir,
|
||||
siteConfig,
|
||||
siteConfigPath,
|
||||
outDir,
|
||||
|
@ -125,6 +126,7 @@ export async function load(options: LoadContextOptions): Promise<Props> {
|
|||
outDir,
|
||||
baseUrl,
|
||||
i18n,
|
||||
localizationDir,
|
||||
codeTranslations: siteCodeTranslations,
|
||||
} = context;
|
||||
const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context);
|
||||
|
@ -246,6 +248,7 @@ ${Object.entries(registry)
|
|||
outDir,
|
||||
baseUrl,
|
||||
i18n,
|
||||
localizationDir,
|
||||
generatedFilesDir,
|
||||
routes: pluginsRouteConfigs,
|
||||
routesPaths,
|
||||
|
|
|
@ -56,8 +56,7 @@ export async function loadPlugins(context: LoadContext): Promise<{
|
|||
const translationFiles = await Promise.all(
|
||||
rawTranslationFiles.map((translationFile) =>
|
||||
localizePluginTranslationFile({
|
||||
locale: context.i18n.currentLocale,
|
||||
siteDir: context.siteDir,
|
||||
localizationDir: context.localizationDir,
|
||||
translationFile,
|
||||
plugin,
|
||||
}),
|
||||
|
|
|
@ -44,8 +44,10 @@ async function createTmpTranslationFile(
|
|||
}
|
||||
|
||||
return {
|
||||
siteDir,
|
||||
readFile: () => fs.readJSON(filePath),
|
||||
localizationDir: path.join(siteDir, 'i18n/en'),
|
||||
readFile() {
|
||||
return fs.readJSON(filePath);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -58,9 +60,9 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('creates new translation file', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile(null);
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
|
@ -80,9 +82,9 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('creates new translation file with prefix', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile(null);
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
|
@ -104,14 +106,14 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('appends missing translations', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
key2: {message: 'key2 message'},
|
||||
key3: {message: 'key3 message'},
|
||||
});
|
||||
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
|
@ -133,12 +135,12 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('appends missing.* translations with prefix', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
|
@ -158,12 +160,12 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('overrides missing translations', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
|
@ -183,12 +185,12 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('overrides missing translations with prefix', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message'},
|
||||
});
|
||||
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message new'},
|
||||
key2: {message: 'key2 message new'},
|
||||
|
@ -209,14 +211,14 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('always overrides message description', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile({
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile({
|
||||
key1: {message: 'key1 message', description: 'key1 desc'},
|
||||
key2: {message: 'key2 message', description: 'key2 desc'},
|
||||
key3: {message: 'key3 message', description: undefined},
|
||||
});
|
||||
|
||||
await writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message new', description: undefined},
|
||||
key2: {message: 'key2 message new', description: 'key2 desc new'},
|
||||
|
@ -236,9 +238,9 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('does not create empty translation files', async () => {
|
||||
const {siteDir, readFile} = await createTmpTranslationFile(null);
|
||||
const {localizationDir, readFile} = await createTmpTranslationFile(null);
|
||||
|
||||
await writeCodeTranslations({siteDir, locale: 'en'}, {}, {});
|
||||
await writeCodeTranslations({localizationDir}, {}, {});
|
||||
|
||||
await expect(readFile()).rejects.toThrowError(
|
||||
/ENOENT: no such file or directory, open /,
|
||||
|
@ -247,14 +249,14 @@ describe('writeCodeTranslations', () => {
|
|||
});
|
||||
|
||||
it('throws for invalid content', async () => {
|
||||
const {siteDir} = await createTmpTranslationFile(
|
||||
const {localizationDir} = await createTmpTranslationFile(
|
||||
// @ts-expect-error: bad content on purpose
|
||||
{bad: 'content'},
|
||||
);
|
||||
|
||||
await expect(() =>
|
||||
writeCodeTranslations(
|
||||
{siteDir, locale: 'en'},
|
||||
{localizationDir},
|
||||
{
|
||||
key1: {message: 'key1 message'},
|
||||
},
|
||||
|
@ -269,19 +271,16 @@ describe('writeCodeTranslations', () => {
|
|||
|
||||
describe('writePluginTranslations', () => {
|
||||
it('writes plugin translations', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
const localizationDir = await createTmpSiteDir();
|
||||
|
||||
const filePath = path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
localizationDir,
|
||||
'my-plugin-name',
|
||||
'my/translation/file.json',
|
||||
);
|
||||
|
||||
await writePluginTranslations({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
localizationDir,
|
||||
translationFile: {
|
||||
path: 'my/translation/file',
|
||||
content: {
|
||||
|
@ -306,12 +305,10 @@ describe('writePluginTranslations', () => {
|
|||
});
|
||||
|
||||
it('writes plugin translations consecutively with different options', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
const localizationDir = await createTmpSiteDir();
|
||||
|
||||
const filePath = path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
localizationDir,
|
||||
'my-plugin-name-my-plugin-id',
|
||||
'my/translation/file.json',
|
||||
);
|
||||
|
@ -321,7 +318,7 @@ describe('writePluginTranslations', () => {
|
|||
options?: WriteTranslationsOptions,
|
||||
) {
|
||||
return writePluginTranslations({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
locale: 'fr',
|
||||
translationFile: {
|
||||
path: 'my/translation/file',
|
||||
|
@ -381,12 +378,11 @@ describe('writePluginTranslations', () => {
|
|||
});
|
||||
|
||||
it('throws with explicit extension', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
const localizationDir = await createTmpSiteDir();
|
||||
|
||||
await expect(() =>
|
||||
writePluginTranslations({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
localizationDir,
|
||||
translationFile: {
|
||||
path: 'my/translation/file.json',
|
||||
content: {},
|
||||
|
@ -409,7 +405,7 @@ describe('writePluginTranslations', () => {
|
|||
|
||||
describe('localizePluginTranslationFile', () => {
|
||||
it('does not localize if localized file does not exist', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
const localizationDir = await createTmpSiteDir();
|
||||
|
||||
const translationFile: TranslationFile = {
|
||||
path: 'my/translation/file',
|
||||
|
@ -421,8 +417,7 @@ describe('localizePluginTranslationFile', () => {
|
|||
};
|
||||
|
||||
const localizedTranslationFile = await localizePluginTranslationFile({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
localizationDir,
|
||||
translationFile,
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
|
@ -434,16 +429,10 @@ describe('localizePluginTranslationFile', () => {
|
|||
});
|
||||
|
||||
it('normalizes partially localized translation files', async () => {
|
||||
const siteDir = await createTmpSiteDir();
|
||||
const localizationDir = await createTmpSiteDir();
|
||||
|
||||
await fs.outputJSON(
|
||||
path.join(
|
||||
siteDir,
|
||||
'i18n',
|
||||
'fr',
|
||||
'my-plugin-name',
|
||||
'my/translation/file.json',
|
||||
),
|
||||
path.join(localizationDir, 'my-plugin-name', 'my/translation/file.json'),
|
||||
{
|
||||
key2: {message: 'key2 message localized'},
|
||||
key4: {message: 'key4 message localized'},
|
||||
|
@ -460,8 +449,7 @@ describe('localizePluginTranslationFile', () => {
|
|||
};
|
||||
|
||||
const localizedTranslationFile = await localizePluginTranslationFile({
|
||||
siteDir,
|
||||
locale: 'fr',
|
||||
localizationDir,
|
||||
translationFile,
|
||||
plugin: {
|
||||
name: 'my-plugin-name',
|
||||
|
@ -486,13 +474,13 @@ describe('localizePluginTranslationFile', () => {
|
|||
|
||||
describe('readCodeTranslationFileContent', () => {
|
||||
async function testReadTranslation(val: TranslationFileContent) {
|
||||
const {siteDir} = await createTmpTranslationFile(val);
|
||||
return readCodeTranslationFileContent({siteDir, locale: 'en'});
|
||||
const {localizationDir} = await createTmpTranslationFile(val);
|
||||
return readCodeTranslationFileContent({localizationDir});
|
||||
}
|
||||
|
||||
it("returns undefined if file does't exist", async () => {
|
||||
await expect(
|
||||
readCodeTranslationFileContent({siteDir: 'foo', locale: 'en'}),
|
||||
readCodeTranslationFileContent({localizationDir: 'foo'}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import logger from '@docusaurus/logger';
|
|||
import {
|
||||
getPluginI18nPath,
|
||||
toMessageRelativeFilePath,
|
||||
I18N_DIR_NAME,
|
||||
CODE_TRANSLATIONS_FILE_NAME,
|
||||
} from '@docusaurus/utils';
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
|
@ -29,8 +28,7 @@ export type WriteTranslationsOptions = {
|
|||
};
|
||||
|
||||
type TranslationContext = {
|
||||
siteDir: string;
|
||||
locale: string;
|
||||
localizationDir: string;
|
||||
};
|
||||
|
||||
const TranslationFileContentSchema = Joi.object<TranslationFileContent>()
|
||||
|
@ -143,18 +141,8 @@ Maybe you should remove them? ${unknownKeys}`;
|
|||
}
|
||||
}
|
||||
|
||||
// Should we make this configurable?
|
||||
export function getTranslationsLocaleDirPath(
|
||||
context: TranslationContext,
|
||||
): string {
|
||||
return path.join(context.siteDir, I18N_DIR_NAME, context.locale);
|
||||
}
|
||||
|
||||
function getCodeTranslationsFilePath(context: TranslationContext): string {
|
||||
return path.join(
|
||||
getTranslationsLocaleDirPath(context),
|
||||
CODE_TRANSLATIONS_FILE_NAME,
|
||||
);
|
||||
return path.join(context.localizationDir, CODE_TRANSLATIONS_FILE_NAME);
|
||||
}
|
||||
|
||||
export async function readCodeTranslationFileContent(
|
||||
|
@ -187,17 +175,15 @@ function addTranslationFileExtension(translationFilePath: string) {
|
|||
}
|
||||
|
||||
function getPluginTranslationFilePath({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFilePath,
|
||||
}: TranslationContext & {
|
||||
plugin: InitializedPlugin;
|
||||
translationFilePath: string;
|
||||
}): string {
|
||||
const dirPath = getPluginI18nPath({
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
pluginName: plugin.name,
|
||||
pluginId: plugin.options.id,
|
||||
});
|
||||
|
@ -206,9 +192,8 @@ function getPluginTranslationFilePath({
|
|||
}
|
||||
|
||||
export async function writePluginTranslations({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFile,
|
||||
options,
|
||||
}: TranslationContext & {
|
||||
|
@ -218,8 +203,7 @@ export async function writePluginTranslations({
|
|||
}): Promise<void> {
|
||||
const filePath = getPluginTranslationFilePath({
|
||||
plugin,
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
translationFilePath: translationFile.path,
|
||||
});
|
||||
await writeTranslationFileContent({
|
||||
|
@ -230,9 +214,8 @@ export async function writePluginTranslations({
|
|||
}
|
||||
|
||||
export async function localizePluginTranslationFile({
|
||||
siteDir,
|
||||
localizationDir,
|
||||
plugin,
|
||||
locale,
|
||||
translationFile,
|
||||
}: TranslationContext & {
|
||||
plugin: InitializedPlugin;
|
||||
|
@ -240,8 +223,7 @@ export async function localizePluginTranslationFile({
|
|||
}): Promise<TranslationFile> {
|
||||
const filePath = getPluginTranslationFilePath({
|
||||
plugin,
|
||||
siteDir,
|
||||
locale,
|
||||
localizationDir,
|
||||
translationFilePath: translationFile.path,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue