feat(core): allow customizing the i18n directory path (#7386)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
Joshua Chen 2022-06-02 23:37:14 +08:00 committed by GitHub
parent c07a514730
commit abe5450526
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 147 additions and 166 deletions

View file

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

View file

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

View file

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

View file

@ -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: {},

View file

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

View file

@ -60,6 +60,7 @@ Note: Docusaurus only support running one locale at a time.`;
return {
defaultLocale: i18nConfig.defaultLocale,
locales,
path: i18nConfig.path,
currentLocale,
localeConfigs,
};

View file

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

View file

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

View file

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

View file

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