From 3a5010a83a05c82cd814670bcaca68f5e6be53a7 Mon Sep 17 00:00:00 2001 From: sebastien Date: Fri, 27 Jun 2025 15:45:09 +0200 Subject: [PATCH] Add initial implementation of sidebar keys --- .../__snapshots__/translations.test.ts.snap | 210 ++++++++++++++++++ .../src/__tests__/translations.test.ts | 58 +++++ .../src/sidebars/generator.ts | 1 + .../src/sidebars/types.ts | 2 + .../src/sidebars/validation.ts | 2 + .../src/translations.ts | 88 +++++--- .../alpha/test/_category_.json | 1 + .../beta/test/_category_.json | 1 + website/_dogfooding/docs-tests-sidebars.js | 20 +- 9 files changed, 349 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/translations.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/translations.test.ts.snap index 7498d1ba22..85001a3509 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/translations.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/translations.test.ts.snap @@ -28,6 +28,30 @@ exports[`getLoadedContentTranslationFiles returns translation files 1`] = ` "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5", "message": "Fifth doc translatable", }, + "sidebar.sidebarWithDuplicates.category.key-cat1": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.category.key-cat2": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, "version.label": { "description": "The label for version current", "message": "current label", @@ -61,6 +85,30 @@ exports[`getLoadedContentTranslationFiles returns translation files 1`] = ` "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5", "message": "Fifth doc translatable", }, + "sidebar.sidebarWithDuplicates.category.key-cat1": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.category.key-cat2": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, "version.label": { "description": "The label for version 2.0.0", "message": "2.0.0 label", @@ -94,6 +142,30 @@ exports[`getLoadedContentTranslationFiles returns translation files 1`] = ` "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5", "message": "Fifth doc translatable", }, + "sidebar.sidebarWithDuplicates.category.key-cat1": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.category.key-cat2": { + "description": "The label for category COMMON LABEL in sidebar sidebarWithDuplicates", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-doc5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref4": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc4", + "message": "COMMON LABEL", + }, + "sidebar.sidebarWithDuplicates.doc.key-ref5": { + "description": "The label for the doc item COMMON LABEL in sidebar sidebarWithDuplicates, linking to the doc doc5", + "message": "COMMON LABEL", + }, "version.label": { "description": "The label for version 1.0.0", "message": "1.0.0 label", @@ -274,6 +346,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = ` "type": "ref", }, ], + "sidebarWithDuplicates": [ + { + "id": "doc4", + "key": "key-doc4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc5", + "key": "key-doc5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc4", + "key": "key-ref4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "id": "doc5", + "key": "key-ref5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat1", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat2", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + ], }, "tagsPath": "/tags/", "versionName": "current", @@ -445,6 +563,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = ` "type": "ref", }, ], + "sidebarWithDuplicates": [ + { + "id": "doc4", + "key": "key-doc4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc5", + "key": "key-doc5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc4", + "key": "key-ref4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "id": "doc5", + "key": "key-ref5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat1", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat2", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + ], }, "tagsPath": "/tags/", "versionName": "2.0.0", @@ -616,6 +780,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = ` "type": "ref", }, ], + "sidebarWithDuplicates": [ + { + "id": "doc4", + "key": "key-doc4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc5", + "key": "key-doc5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "doc", + }, + { + "id": "doc4", + "key": "key-ref4", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "id": "doc5", + "key": "key-ref5", + "label": "COMMON LABEL (translated)", + "translatable": true, + "type": "ref", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat1", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + { + "collapsed": false, + "collapsible": true, + "items": [], + "key": "key-cat2", + "label": "COMMON LABEL (translated)", + "type": "category", + }, + ], }, "tagsPath": "/tags/", "versionName": "1.0.0", diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/translations.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/translations.test.ts index 6175606158..56cc958fee 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/translations.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/translations.test.ts @@ -16,6 +16,7 @@ import type { LoadedContent, LoadedVersion, } from '@docusaurus/plugin-content-docs'; +import type {SidebarItem} from '../sidebars/types'; function createSampleDoc(doc: Pick): DocMetadata { return { @@ -40,6 +41,59 @@ function createSampleDoc(doc: Pick): DocMetadata { }; } +function createSampleSidebarWithDuplicates({ + withUniqueKeys, +}: { + withUniqueKeys: boolean; +}): SidebarItem[] { + return [ + { + type: 'doc', + id: 'doc4', + label: 'COMMON LABEL', + translatable: true, + ...(withUniqueKeys && {key: 'key-doc4'}), + }, + { + type: 'doc', + id: 'doc5', + label: 'COMMON LABEL', + translatable: true, + ...(withUniqueKeys && {key: 'key-doc5'}), + }, + { + type: 'ref', + id: 'doc4', + label: 'COMMON LABEL', + translatable: true, + ...(withUniqueKeys && {key: 'key-ref4'}), + }, + { + type: 'ref', + id: 'doc5', + label: 'COMMON LABEL', + translatable: true, + ...(withUniqueKeys && {key: 'key-ref5'}), + }, + { + type: 'category', + label: 'COMMON LABEL', + items: [], + collapsed: false, + collapsible: true, + ...(withUniqueKeys && {key: 'key-cat1'}), + }, + { + type: 'category', + label: 'COMMON LABEL', + items: [], + collapsed: false, + collapsible: true, + ...(withUniqueKeys && {key: 'key-cat2'}), + }, + ]; +} + function createSampleVersion( version: Pick, ): LoadedVersion { @@ -116,6 +170,10 @@ function createSampleVersion( translatable: true, }, ], + + sidebarWithDuplicates: createSampleSidebarWithDuplicates({ + withUniqueKeys: true, + }), }, ...version, }; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts index 20383b003a..c67c13070f 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/generator.ts @@ -238,6 +238,7 @@ Available doc IDs: return { type: 'category', + key: categoryMetadata?.key, label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename, collapsible: categoryMetadata?.collapsible, collapsed: categoryMetadata?.collapsed, diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts index 9dbd23415d..57f124e9c0 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/types.ts @@ -19,6 +19,7 @@ import type {Slugger} from '@docusaurus/utils'; type Expand = {[P in keyof T]: T[P]}; export type SidebarItemBase = { + key?: string; className?: string; customProps?: {[key: string]: unknown}; }; @@ -215,6 +216,7 @@ export type PropSidebarBreadcrumbsItem = | PropSidebarItemCategory; export type CategoryMetadataFile = { + key?: string; label?: string; position?: number; description?: string; diff --git a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts index 33492dc309..5660f4c9c1 100644 --- a/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts +++ b/packages/docusaurus-plugin-content-docs/src/sidebars/validation.ts @@ -28,6 +28,7 @@ import type { // in normalization const sidebarItemBaseSchema = Joi.object({ + key: Joi.string().optional(), className: Joi.string(), customProps: Joi.object().unknown(), }); @@ -166,6 +167,7 @@ export function validateSidebars(sidebars: { } const categoryMetadataFileSchema = Joi.object({ + key: Joi.string(), label: Joi.string(), description: Joi.string(), position: Joi.number(), diff --git a/packages/docusaurus-plugin-content-docs/src/translations.ts b/packages/docusaurus-plugin-content-docs/src/translations.ts index e64e1dde7b..42a89e218a 100644 --- a/packages/docusaurus-plugin-content-docs/src/translations.ts +++ b/packages/docusaurus-plugin-content-docs/src/translations.ts @@ -7,6 +7,7 @@ import _ from 'lodash'; import {mergeTranslations} from '@docusaurus/utils'; +import logger from '@docusaurus/logger'; import {CURRENT_VERSION_NAME} from './constants'; import { collectSidebarCategories, @@ -40,20 +41,50 @@ function getVersionFileName(versionName: string): string { return `version-${versionName}`; } +type TranslationMessageEntry = [string, TranslationMessage]; + +function ensureNoSidebarDuplicateEntries( + translationEntries: TranslationMessageEntry[], +): void { + const grouped = _.groupBy(translationEntries, (entry) => entry[0]); + const duplicates = Object.entries(grouped).filter( + (entry) => entry[1].length > 1, + ); + + if (duplicates.length > 0) { + throw new Error(`Multiple docs sidebar items produce the same translation key. +- ${duplicates + .map(([translationKey, entries]) => { + return `${translationKey}: ${ + entries.length + } duplicates found:\n - ${entries + .map((duplicate) => { + return logger.interpolate`name=${duplicate[1].message} (subdue=${duplicate[1].description})}`; + }) + .join('\n - ')}`; + }) + .join('\n\n- ')} + +To avoid translation key conflicts, use the ${logger.code( + 'key', + )} attribute on the sidebar items above to uniquely identify them. + `); + } +} + function getSidebarTranslationFileContent( sidebar: Sidebar, sidebarName: string, ): TranslationFileContent { - type TranslationMessageEntry = [string, TranslationMessage]; - const categories = collectSidebarCategories(sidebar); const categoryEntries: TranslationMessageEntry[] = categories.flatMap( (category) => { const entries: TranslationMessageEntry[] = []; + const categoryKey = category.key ?? category.label; entries.push([ - `sidebar.${sidebarName}.category.${category.label}`, + `sidebar.${sidebarName}.category.${categoryKey}`, { message: category.label, description: `The label for category ${category.label} in sidebar ${sidebarName}`, @@ -63,7 +94,7 @@ function getSidebarTranslationFileContent( if (category.link?.type === 'generated-index') { if (category.link.title) { entries.push([ - `sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`, + `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.title`, { message: category.link.title, description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`, @@ -72,7 +103,7 @@ function getSidebarTranslationFileContent( } if (category.link.description) { entries.push([ - `sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`, + `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.description`, { message: category.link.description, description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`, @@ -85,45 +116,37 @@ function getSidebarTranslationFileContent( }, ); - _.chain(categoryEntries) - .groupBy((entry) => entry[0]) - .forEach((group, key) => { - if (group.length > 1) { - console.warn(`KEY CONFLICT FOR key = ${key}`, group.length); - } - }) - .value(); - - const categoryContent: TranslationFileContent = - Object.fromEntries(categoryEntries); - const links = collectSidebarLinks(sidebar); - const linksContent: TranslationFileContent = Object.fromEntries( - links.map((link) => [ - `sidebar.${sidebarName}.link.${link.label}`, + const linksEntries: TranslationMessageEntry[] = links.map((link) => { + const linkKey = link.key ?? link.label; + return [ + `sidebar.${sidebarName}.link.${linkKey}`, { message: link.label, description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`, }, - ]), - ); + ]; + }); const docs = collectSidebarDocItems(sidebar) .concat(collectSidebarRefs(sidebar)) .filter((item) => item.translatable); - const docLinksContent: TranslationFileContent = Object.fromEntries( - docs.map((doc) => [ - `sidebar.${sidebarName}.doc.${doc.label!}`, + const docLinksEntries: TranslationMessageEntry[] = docs.map((doc) => { + const docKey = doc.key ?? doc.label!; + return [ + `sidebar.${sidebarName}.doc.${docKey}`, { message: doc.label!, description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${ doc.id }`, }, - ]), - ); + ]; + }); - return mergeTranslations([categoryContent, linksContent, docLinksContent]); + const allEntries = [...categoryEntries, ...linksEntries, ...docLinksEntries]; + ensureNoSidebarDuplicateEntries(allEntries); + return Object.fromEntries(allEntries); } function translateSidebar({ @@ -162,27 +185,30 @@ function translateSidebar({ return transformSidebarItems(sidebar, (item) => { if (item.type === 'category') { const link = transformSidebarCategoryLink(item); + const categoryKey = item.key ?? item.label; return { ...item, label: - sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`] + sidebarsTranslations[`sidebar.${sidebarName}.category.${categoryKey}`] ?.message ?? item.label, ...(link && {link}), }; } if (item.type === 'link') { + const linkKey = item.key ?? item.label; return { ...item, label: - sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`] + sidebarsTranslations[`sidebar.${sidebarName}.link.${linkKey}`] ?.message ?? item.label, }; } if ((item.type === 'doc' || item.type === 'ref') && item.translatable) { + const docKey = item.key ?? item.label!; return { ...item, label: - sidebarsTranslations[`sidebar.${sidebarName}.doc.${item.label!}`] + sidebarsTranslations[`sidebar.${sidebarName}.doc.${docKey}`] ?.message ?? item.label, }; } diff --git a/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/alpha/test/_category_.json b/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/alpha/test/_category_.json index 0a027d5781..dd99960965 100644 --- a/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/alpha/test/_category_.json +++ b/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/alpha/test/_category_.json @@ -1,4 +1,5 @@ { + "key": "CategoryLabelTest alpha", "label": "CategoryLabelTest", "link": { "type": "generated-index", diff --git a/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/beta/test/_category_.json b/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/beta/test/_category_.json index 668e049124..e0fad183c6 100644 --- a/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/beta/test/_category_.json +++ b/website/_dogfooding/_docs tests/tests/Conflicts/category-index-name/beta/test/_category_.json @@ -1,4 +1,5 @@ { + "key": "CategoryLabelTest beta", "label": "CategoryLabelTest", "link": { "type": "generated-index", diff --git a/website/_dogfooding/docs-tests-sidebars.js b/website/_dogfooding/docs-tests-sidebars.js index 67d4b80300..d8dac0c10e 100644 --- a/website/_dogfooding/docs-tests-sidebars.js +++ b/website/_dogfooding/docs-tests-sidebars.js @@ -8,6 +8,7 @@ /** * @typedef {import('@docusaurus/plugin-content-docs').SidebarsConfig} SidebarsConfig * @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemConfig} SidebarItemConfig + * @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemCategoryConfig} SidebarItemCategoryConfig */ /** @type {SidebarsConfig} */ @@ -63,7 +64,8 @@ const sidebars = { items: [ { type: 'link', - label: 'Link ', + label: 'Link', + key: 'link-key-1', href: 'https://docusaurus.io', }, ], @@ -76,6 +78,7 @@ const sidebars = { { type: 'link', label: 'Link ', + key: 'link-key-2', href: 'https://docusaurus.io', }, ], @@ -182,14 +185,16 @@ function generateHugeSidebarItems() { /** * @param {number} maxLevel * @param {number} currentLevel + * @param {string} parentKey * @returns {SidebarItemConfig[]} */ - function generateRecursive(maxLevel, currentLevel = 0) { + function generateRecursive(maxLevel, currentLevel = 0, parentKey = 'ROOT') { if (currentLevel === maxLevel) { return [ { type: 'link', href: '/', + key: `link-${parentKey}-maxLevel`, label: `Link (level ${currentLevel + 1})`, }, ]; @@ -198,14 +203,23 @@ function generateHugeSidebarItems() { const linkItems = Array.from(Array(linksCount).keys()).map((index) => ({ type: 'link', href: '/', + key: `link-${parentKey}-${index}`, label: `Link ${index} (level ${currentLevel + 1})`, })); const categoryItems = Array.from(Array(categoriesCount).keys()).map( + /** + * @returns {SidebarItemCategoryConfig} + */ (index) => ({ type: 'category', label: `Category ${index} (level ${currentLevel + 1})`, - items: generateRecursive(maxLevel, currentLevel + 1), + key: `category-${parentKey}-${index}`, + items: generateRecursive( + maxLevel, + currentLevel + 1, + `${parentKey}-${index}`, + ), }), );