diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts index d7747d16300c..367872215282 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/feed.test.ts @@ -19,6 +19,7 @@ const DefaultI18N: I18n = { currentLocale: 'en', locales: ['en'], defaultLocale: 'en', + path: '1i8n', localeConfigs: { en: { label: 'English', diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts index 47d4b98fd4e0..4ec710ce0a39 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/index.test.ts @@ -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< diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 2b0355a0f3f1..f0c1f8cc24ed 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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, }), diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts index 194ce3c4e46e..d9ed52eea89d 100644 Binary files a/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts and b/packages/docusaurus-plugin-content-docs/src/__tests__/cli.test.ts differ diff --git a/packages/docusaurus-plugin-content-docs/src/cli.ts b/packages/docusaurus-plugin-content-docs/src/cli.ts index 65e4984affa0..1818a3f4e22e 100644 --- a/packages/docusaurus-plugin-content-docs/src/cli.ts +++ b/packages/docusaurus-plugin-content-docs/src/cli.ts @@ -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, }); diff --git a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/index.test.ts index 15bb3d06d720..a52ba1b7d186 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/__tests__/index.test.ts @@ -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 = { diff --git a/packages/docusaurus-plugin-content-docs/src/versions/files.ts b/packages/docusaurus-plugin-content-docs/src/versions/files.ts index 04c97efaebaa..2f045717c29e 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/files.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/files.ts @@ -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, }); diff --git a/packages/docusaurus-plugin-content-docs/src/versions/index.ts b/packages/docusaurus-plugin-content-docs/src/versions/index.ts index 369e0fd6efc2..d39bee56198d 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions/index.ts @@ -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, }) diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap index 818f94da35f5..bc0f5cef2947 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/__snapshots__/index.test.ts.snap @@ -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", }, diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts index 31706d3a4cd4..dd570260a417 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/index.test.ts @@ -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: { diff --git a/packages/docusaurus-plugin-content-pages/src/index.ts b/packages/docusaurus-plugin-content-pages/src/index.ts index a9051e4545e6..7480962d07cc 100644 --- a/packages/docusaurus-plugin-content-pages/src/index.ts +++ b/packages/docusaurus-plugin-content-pages/src/index.ts @@ -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, }), diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index ee4151930eee..2cae26753d5b 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -70,6 +70,11 @@ export type I18nConfig = { * 3. Will be used for the `` 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. + * `/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 diff --git a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts index c0a458d3a8ec..c0b45f358568 100644 --- a/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/i18nUtils.test.ts @@ -65,34 +65,33 @@ describe('getPluginI18nPath', () => { it('gets correct path', () => { expect( getPluginI18nPath({ - siteDir: __dirname, - locale: 'zh-Hans', + localizationDir: '/i18n/zh-Hans', pluginName: 'plugin-content-docs', pluginId: 'community', subPaths: ['foo'], }), ).toMatchInlineSnapshot( - `"/packages/docusaurus-utils/src/__tests__/i18n/zh-Hans/plugin-content-docs-community/foo"`, + `"/i18n/zh-Hans/plugin-content-docs-community/foo"`, ); }); it('gets correct path for default plugin', () => { expect( getPluginI18nPath({ - siteDir: __dirname, - locale: 'zh-Hans', + localizationDir: '/i18n/zh-Hans', pluginName: 'plugin-content-docs', subPaths: ['foo'], - }).replace(__dirname, ''), - ).toMatchInlineSnapshot(`"/i18n/zh-Hans/plugin-content-docs/foo"`); + }), + ).toMatchInlineSnapshot( + `"/i18n/zh-Hans/plugin-content-docs/foo"`, + ); }); it('gets correct path when no sub-paths', () => { expect( getPluginI18nPath({ - siteDir: __dirname, - locale: 'zh-Hans', + localizationDir: '/i18n/zh-Hans', pluginName: 'plugin-content-docs', - }).replace(__dirname, ''), - ).toMatchInlineSnapshot(`"/i18n/zh-Hans/plugin-content-docs"`); + }), + ).toMatchInlineSnapshot(`"/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: {}, diff --git a/packages/docusaurus-utils/src/constants.ts b/packages/docusaurus-utils/src/constants.ts index 9f8c452669bb..6ecba7aea0f5 100644 --- a/packages/docusaurus-utils/src/constants.ts +++ b/packages/docusaurus-utils/src/constants.ts @@ -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. diff --git a/packages/docusaurus-utils/src/i18nUtils.ts b/packages/docusaurus-utils/src/i18nUtils.ts index a6d08cf83749..8c706f1b8ce7 100644 --- a/packages/docusaurus-utils/src/i18nUtils.ts +++ b/packages/docusaurus-utils/src/i18nUtils.ts @@ -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}`}`, diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 520f5f73aab2..30549ba99c03 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -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, diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index f49874839d68..e41ae556cddf 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -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 & { @@ -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, diff --git a/packages/docusaurus/src/commands/writeTranslations.ts b/packages/docusaurus/src/commands/writeTranslations.ts index 14ae1e64adc6..d69014306fcf 100644 --- a/packages/docusaurus/src/commands/writeTranslations.ts +++ b/packages/docusaurus/src/commands/writeTranslations.ts @@ -47,14 +47,12 @@ async function getExtraSourceCodeFilePaths(): Promise { } 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}); }), ); } diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 2c20bb59c885..64dfeff98eea 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -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", diff --git a/packages/docusaurus/src/server/__tests__/i18n.test.ts b/packages/docusaurus/src/server/__tests__/i18n.test.ts index 84e1e83ada2d..8ef8fea9faf3 100644 --- a/packages/docusaurus/src/server/__tests__/i18n.test.ts +++ b/packages/docusaurus/src/server/__tests__/i18n.test.ts @@ -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: {}, diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 15a66776b19d..4027f84ca4d9 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -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({ 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) diff --git a/packages/docusaurus/src/server/i18n.ts b/packages/docusaurus/src/server/i18n.ts index cd9bbe4990ea..59b7b85a38f1 100644 --- a/packages/docusaurus/src/server/i18n.ts +++ b/packages/docusaurus/src/server/i18n.ts @@ -60,6 +60,7 @@ Note: Docusaurus only support running one locale at a time.`; return { defaultLocale: i18nConfig.defaultLocale, locales, + path: i18nConfig.path, currentLocale, localeConfigs, }; diff --git a/packages/docusaurus/src/server/index.ts b/packages/docusaurus/src/server/index.ts index 90908237b04f..27a21d5248cb 100644 --- a/packages/docusaurus/src/server/index.ts +++ b/packages/docusaurus/src/server/index.ts @@ -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 { 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, diff --git a/packages/docusaurus/src/server/plugins/index.ts b/packages/docusaurus/src/server/plugins/index.ts index 5a8dbb98d095..73fba212b289 100644 --- a/packages/docusaurus/src/server/plugins/index.ts +++ b/packages/docusaurus/src/server/plugins/index.ts @@ -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, }), diff --git a/packages/docusaurus/src/server/translations/__tests__/translations.test.ts b/packages/docusaurus/src/server/translations/__tests__/translations.test.ts index 1e1b59aad85f..a62aae849802 100644 --- a/packages/docusaurus/src/server/translations/__tests__/translations.test.ts +++ b/packages/docusaurus/src/server/translations/__tests__/translations.test.ts @@ -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(); }); diff --git a/packages/docusaurus/src/server/translations/translations.ts b/packages/docusaurus/src/server/translations/translations.ts index fd3fb0305523..83d19495bffc 100644 --- a/packages/docusaurus/src/server/translations/translations.ts +++ b/packages/docusaurus/src/server/translations/translations.ts @@ -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() @@ -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 { 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 { const filePath = getPluginTranslationFilePath({ plugin, - siteDir, - locale, + localizationDir, translationFilePath: translationFile.path, });