fix(core): all plugin lifecycles should receive translated content (#7066)

This commit is contained in:
Joshua Chen 2022-03-30 11:57:13 +08:00 committed by GitHub
parent 3f33e90704
commit fd24bd180d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 112 deletions

View file

@ -125,10 +125,7 @@ export async function load(options: LoadContextOptions): Promise<Props> {
ssrTemplate, ssrTemplate,
codeTranslations: siteCodeTranslations, codeTranslations: siteCodeTranslations,
} = context; } = context;
const {plugins, pluginsRouteConfigs, globalData, themeConfigTranslated} = const {plugins, pluginsRouteConfigs, globalData} = await loadPlugins(context);
await loadPlugins(context);
// Side-effect to replace the untranslated themeConfig by the translated one
context.siteConfig.themeConfig = themeConfigTranslated;
const clientModules = loadClientModules(plugins); const clientModules = loadClientModules(plugins);
const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins); const {headTags, preBodyTags, postBodyTags} = loadHtmlTags(plugins);
const {registry, routesChunkNames, routesConfig, routesPaths} = const {registry, routesChunkNames, routesConfig, routesPaths} =

View file

@ -64,6 +64,5 @@ exports[`loadPlugins loads plugins 1`] = `
}, },
], ],
"pluginsRouteConfigs": [], "pluginsRouteConfigs": [],
"themeConfigTranslated": {},
} }
`; `;

View file

@ -13,7 +13,6 @@ import type {
RouteConfig, RouteConfig,
AllContent, AllContent,
GlobalData, GlobalData,
ThemeConfig,
LoadedPlugin, LoadedPlugin,
InitializedPlugin, InitializedPlugin,
PluginRouteContext, PluginRouteContext,
@ -28,13 +27,13 @@ import {applyRouteTrailingSlash, sortConfig} from './routeConfig';
/** /**
* Initializes the plugins, runs `loadContent`, `translateContent`, * Initializes the plugins, runs `loadContent`, `translateContent`,
* `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is * `contentLoaded`, and `translateThemeConfig`. Because `contentLoaded` is
* side-effect-ful (it generates temp files), so is this function. * side-effect-ful (it generates temp files), so is this function. This function
* would also mutate `context.siteConfig.themeConfig` to translate it.
*/ */
export async function loadPlugins(context: LoadContext): Promise<{ export async function loadPlugins(context: LoadContext): Promise<{
plugins: LoadedPlugin[]; plugins: LoadedPlugin[];
pluginsRouteConfigs: RouteConfig[]; pluginsRouteConfigs: RouteConfig[];
globalData: GlobalData; globalData: GlobalData;
themeConfigTranslated: ThemeConfig;
}> { }> {
// 1. Plugin Lifecycle - Initialization/Constructor. // 1. Plugin Lifecycle - Initialization/Constructor.
const plugins: InitializedPlugin[] = await initPlugins(context); const plugins: InitializedPlugin[] = await initPlugins(context);
@ -48,21 +47,15 @@ export async function loadPlugins(context: LoadContext): Promise<{
// Currently plugins run lifecycle methods in parallel and are not // Currently plugins run lifecycle methods in parallel and are not
// order-dependent. We could change this in future if there are plugins which // order-dependent. We could change this in future if there are plugins which
// need to run in certain order or depend on others for data. // need to run in certain order or depend on others for data.
// This would also translate theme config and content upfront, given the
// translation files that the plugin declares.
const loadedPlugins: LoadedPlugin[] = await Promise.all( const loadedPlugins: LoadedPlugin[] = await Promise.all(
plugins.map(async (plugin) => { plugins.map(async (plugin) => {
const content = await plugin.loadContent?.(); const content = await plugin.loadContent?.();
return {...plugin, content}; const rawTranslationFiles =
}), (await plugin?.getTranslationFiles?.({content})) ?? [];
); const translationFiles = await Promise.all(
rawTranslationFiles.map((translationFile) =>
const contentLoadedTranslatedPlugins = await Promise.all(
loadedPlugins.map(async (plugin) => {
const translationFiles =
(await plugin?.getTranslationFiles?.({
content: plugin.content,
})) ?? [];
const localizedTranslationFiles = await Promise.all(
translationFiles.map((translationFile) =>
localizePluginTranslationFile({ localizePluginTranslationFile({
locale: context.i18n.currentLocale, locale: context.i18n.currentLocale,
siteDir: context.siteDir, siteDir: context.siteDir,
@ -71,10 +64,17 @@ export async function loadPlugins(context: LoadContext): Promise<{
}), }),
), ),
); );
return { const translatedContent =
...plugin, plugin.translateContent?.({content, translationFiles}) ?? content;
translationFiles: localizedTranslationFiles, const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
}; themeConfig: context.siteConfig.themeConfig,
translationFiles,
});
// Side-effect to merge theme config translations. A plugin should only
// translate its own slice of theme config and should make no assumptions
// about other plugins' keys, so this is safe to run in parallel.
Object.assign(context.siteConfig.themeConfig, translatedThemeConfigSlice);
return {...plugin, content: translatedContent};
}), }),
); );
@ -90,86 +90,75 @@ export async function loadPlugins(context: LoadContext): Promise<{
// 3. Plugin Lifecycle - contentLoaded. // 3. Plugin Lifecycle - contentLoaded.
const pluginsRouteConfigs: RouteConfig[] = []; const pluginsRouteConfigs: RouteConfig[] = [];
const globalData: GlobalData = {}; const globalData: GlobalData = {};
await Promise.all( await Promise.all(
contentLoadedTranslatedPlugins.map( loadedPlugins.map(async ({content, ...plugin}) => {
async ({content, translationFiles, ...plugin}) => { if (!plugin.contentLoaded) {
if (!plugin.contentLoaded) { return;
return; }
} const pluginId = plugin.options.id;
const pluginId = plugin.options.id; // plugins data files are namespaced by pluginName/pluginId
// plugins data files are namespaced by pluginName/pluginId const dataDir = path.join(
const dataDir = path.join( context.generatedFilesDir,
context.generatedFilesDir, plugin.name,
plugin.name, pluginId,
pluginId, );
); // TODO this would be better to do all that in the codegen phase
// TODO this would be better to do all that in the codegen phase // TODO handle context for nested routes
// TODO handle context for nested routes const pluginRouteContext: PluginRouteContext = {
const pluginRouteContext: PluginRouteContext = { plugin: {name: plugin.name, id: pluginId},
plugin: {name: plugin.name, id: pluginId}, data: undefined, // TODO allow plugins to provide context data
data: undefined, // TODO allow plugins to provide context data };
}; const pluginRouteContextModulePath = path.join(
const pluginRouteContextModulePath = path.join( dataDir,
dataDir, `${docuHash('pluginRouteContextModule')}.json`,
`${docuHash('pluginRouteContextModule')}.json`, );
); await generate(
await generate( '/',
'/', pluginRouteContextModulePath,
pluginRouteContextModulePath, JSON.stringify(pluginRouteContext, null, 2),
JSON.stringify(pluginRouteContext, null, 2), );
);
const actions: PluginContentLoadedActions = { const actions: PluginContentLoadedActions = {
addRoute(initialRouteConfig) { addRoute(initialRouteConfig) {
// Trailing slash behavior is handled generically for all plugins // Trailing slash behavior is handled generically for all plugins
const finalRouteConfig = applyRouteTrailingSlash( const finalRouteConfig = applyRouteTrailingSlash(
initialRouteConfig, initialRouteConfig,
context.siteConfig, context.siteConfig,
); );
pluginsRouteConfigs.push({ pluginsRouteConfigs.push({
...finalRouteConfig, ...finalRouteConfig,
modules: { modules: {
...finalRouteConfig.modules, ...finalRouteConfig.modules,
__routeContextModule: pluginRouteContextModulePath, __routeContextModule: pluginRouteContextModulePath,
}, },
}); });
}, },
async createData(name, data) { async createData(name, data) {
const modulePath = path.join(dataDir, name); const modulePath = path.join(dataDir, name);
await generate(dataDir, name, data); await generate(dataDir, name, data);
return modulePath; return modulePath;
}, },
setGlobalData(data) { setGlobalData(data) {
globalData[plugin.name] ??= {}; globalData[plugin.name] ??= {};
globalData[plugin.name]![pluginId] = data; globalData[plugin.name]![pluginId] = data;
}, },
}; };
const translatedContent = await plugin.contentLoaded({content, actions, allContent});
plugin.translateContent?.({content, translationFiles}) ?? content; }),
await plugin.contentLoaded({
content: translatedContent,
actions,
allContent,
});
},
),
); );
// 4. Plugin Lifecycle - routesLoaded. // 4. Plugin Lifecycle - routesLoaded.
await Promise.all( await Promise.all(
contentLoadedTranslatedPlugins.map(async (plugin) => { loadedPlugins.map(async (plugin) => {
if (!plugin.routesLoaded) { if (!plugin.routesLoaded) {
return; return;
} }
// TODO remove this deprecated lifecycle soon // TODO alpha-60: remove this deprecated lifecycle soon
// deprecated since alpha-60 // 1 user reported usage of this lifecycle: https://github.com/facebook/docusaurus/issues/3918
// TODO, 1 user reported usage of this lifecycle! https://github.com/facebook/docusaurus/issues/3918
logger.error`Plugin code=${'routesLoaded'} lifecycle is deprecated. If you think we should keep this lifecycle, please report here: url=${'https://github.com/facebook/docusaurus/issues/3918'}`; logger.error`Plugin code=${'routesLoaded'} lifecycle is deprecated. If you think we should keep this lifecycle, please report here: url=${'https://github.com/facebook/docusaurus/issues/3918'}`;
await plugin.routesLoaded(pluginsRouteConfigs); await plugin.routesLoaded(pluginsRouteConfigs);
@ -180,25 +169,5 @@ export async function loadPlugins(context: LoadContext): Promise<{
// routes are always placed last. // routes are always placed last.
sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl); sortConfig(pluginsRouteConfigs, context.siteConfig.baseUrl);
// Apply each plugin one after the other to translate the theme config return {plugins: loadedPlugins, pluginsRouteConfigs, globalData};
const themeConfigTranslated = contentLoadedTranslatedPlugins.reduce(
(currentThemeConfig, plugin) => {
const translatedThemeConfigSlice = plugin.translateThemeConfig?.({
themeConfig: currentThemeConfig,
translationFiles: plugin.translationFiles,
});
return {
...currentThemeConfig,
...translatedThemeConfigSlice,
};
},
context.siteConfig.themeConfig,
);
return {
plugins: loadedPlugins,
pluginsRouteConfigs,
globalData,
themeConfigTranslated,
};
} }