Add initial implementation of sidebar keys

This commit is contained in:
sebastien 2025-06-27 15:45:09 +02:00
parent 222a077d85
commit 3a5010a83a
9 changed files with 349 additions and 34 deletions

View file

@ -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", "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5",
"message": "Fifth doc translatable", "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": { "version.label": {
"description": "The label for version current", "description": "The label for version current",
"message": "current label", "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", "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5",
"message": "Fifth doc translatable", "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": { "version.label": {
"description": "The label for version 2.0.0", "description": "The label for version 2.0.0",
"message": "2.0.0 label", "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", "description": "The label for the doc item Fifth doc translatable in sidebar otherSidebar, linking to the doc doc5",
"message": "Fifth doc translatable", "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": { "version.label": {
"description": "The label for version 1.0.0", "description": "The label for version 1.0.0",
"message": "1.0.0 label", "message": "1.0.0 label",
@ -274,6 +346,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = `
"type": "ref", "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/", "tagsPath": "/tags/",
"versionName": "current", "versionName": "current",
@ -445,6 +563,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = `
"type": "ref", "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/", "tagsPath": "/tags/",
"versionName": "2.0.0", "versionName": "2.0.0",
@ -616,6 +780,52 @@ exports[`translateLoadedContent returns translated loaded content 1`] = `
"type": "ref", "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/", "tagsPath": "/tags/",
"versionName": "1.0.0", "versionName": "1.0.0",

View file

@ -16,6 +16,7 @@ import type {
LoadedContent, LoadedContent,
LoadedVersion, LoadedVersion,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
import type {SidebarItem} from '../sidebars/types';
function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata { function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
return { return {
@ -40,6 +41,59 @@ function createSampleDoc(doc: Pick<DocMetadata, 'id'>): 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( function createSampleVersion(
version: Pick<LoadedVersion, 'versionName'>, version: Pick<LoadedVersion, 'versionName'>,
): LoadedVersion { ): LoadedVersion {
@ -116,6 +170,10 @@ function createSampleVersion(
translatable: true, translatable: true,
}, },
], ],
sidebarWithDuplicates: createSampleSidebarWithDuplicates({
withUniqueKeys: true,
}),
}, },
...version, ...version,
}; };

View file

@ -238,6 +238,7 @@ Available doc IDs:
return { return {
type: 'category', type: 'category',
key: categoryMetadata?.key,
label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename, label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename,
collapsible: categoryMetadata?.collapsible, collapsible: categoryMetadata?.collapsible,
collapsed: categoryMetadata?.collapsed, collapsed: categoryMetadata?.collapsed,

View file

@ -19,6 +19,7 @@ import type {Slugger} from '@docusaurus/utils';
type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]}; type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]};
export type SidebarItemBase = { export type SidebarItemBase = {
key?: string;
className?: string; className?: string;
customProps?: {[key: string]: unknown}; customProps?: {[key: string]: unknown};
}; };
@ -215,6 +216,7 @@ export type PropSidebarBreadcrumbsItem =
| PropSidebarItemCategory; | PropSidebarItemCategory;
export type CategoryMetadataFile = { export type CategoryMetadataFile = {
key?: string;
label?: string; label?: string;
position?: number; position?: number;
description?: string; description?: string;

View file

@ -28,6 +28,7 @@ import type {
// in normalization // in normalization
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({ const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
key: Joi.string().optional(),
className: Joi.string(), className: Joi.string(),
customProps: Joi.object().unknown(), customProps: Joi.object().unknown(),
}); });
@ -166,6 +167,7 @@ export function validateSidebars(sidebars: {
} }
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({ const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
key: Joi.string(),
label: Joi.string(), label: Joi.string(),
description: Joi.string(), description: Joi.string(),
position: Joi.number(), position: Joi.number(),

View file

@ -7,6 +7,7 @@
import _ from 'lodash'; import _ from 'lodash';
import {mergeTranslations} from '@docusaurus/utils'; import {mergeTranslations} from '@docusaurus/utils';
import logger from '@docusaurus/logger';
import {CURRENT_VERSION_NAME} from './constants'; import {CURRENT_VERSION_NAME} from './constants';
import { import {
collectSidebarCategories, collectSidebarCategories,
@ -40,20 +41,50 @@ function getVersionFileName(versionName: string): string {
return `version-${versionName}`; 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( function getSidebarTranslationFileContent(
sidebar: Sidebar, sidebar: Sidebar,
sidebarName: string, sidebarName: string,
): TranslationFileContent { ): TranslationFileContent {
type TranslationMessageEntry = [string, TranslationMessage];
const categories = collectSidebarCategories(sidebar); const categories = collectSidebarCategories(sidebar);
const categoryEntries: TranslationMessageEntry[] = categories.flatMap( const categoryEntries: TranslationMessageEntry[] = categories.flatMap(
(category) => { (category) => {
const entries: TranslationMessageEntry[] = []; const entries: TranslationMessageEntry[] = [];
const categoryKey = category.key ?? category.label;
entries.push([ entries.push([
`sidebar.${sidebarName}.category.${category.label}`, `sidebar.${sidebarName}.category.${categoryKey}`,
{ {
message: category.label, message: category.label,
description: `The label for category ${category.label} in sidebar ${sidebarName}`, 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?.type === 'generated-index') {
if (category.link.title) { if (category.link.title) {
entries.push([ entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`, `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.title`,
{ {
message: category.link.title, message: category.link.title,
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`, description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
@ -72,7 +103,7 @@ function getSidebarTranslationFileContent(
} }
if (category.link.description) { if (category.link.description) {
entries.push([ entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`, `sidebar.${sidebarName}.category.${categoryKey}.link.generated-index.description`,
{ {
message: category.link.description, message: category.link.description,
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`, 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 links = collectSidebarLinks(sidebar);
const linksContent: TranslationFileContent = Object.fromEntries( const linksEntries: TranslationMessageEntry[] = links.map((link) => {
links.map((link) => [ const linkKey = link.key ?? link.label;
`sidebar.${sidebarName}.link.${link.label}`, return [
`sidebar.${sidebarName}.link.${linkKey}`,
{ {
message: link.label, message: link.label,
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`, description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
}, },
]), ];
); });
const docs = collectSidebarDocItems(sidebar) const docs = collectSidebarDocItems(sidebar)
.concat(collectSidebarRefs(sidebar)) .concat(collectSidebarRefs(sidebar))
.filter((item) => item.translatable); .filter((item) => item.translatable);
const docLinksContent: TranslationFileContent = Object.fromEntries( const docLinksEntries: TranslationMessageEntry[] = docs.map((doc) => {
docs.map((doc) => [ const docKey = doc.key ?? doc.label!;
`sidebar.${sidebarName}.doc.${doc.label!}`, return [
`sidebar.${sidebarName}.doc.${docKey}`,
{ {
message: doc.label!, message: doc.label!,
description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${ description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${
doc.id doc.id
}`, }`,
}, },
]), ];
); });
return mergeTranslations([categoryContent, linksContent, docLinksContent]); const allEntries = [...categoryEntries, ...linksEntries, ...docLinksEntries];
ensureNoSidebarDuplicateEntries(allEntries);
return Object.fromEntries(allEntries);
} }
function translateSidebar({ function translateSidebar({
@ -162,27 +185,30 @@ function translateSidebar({
return transformSidebarItems(sidebar, (item) => { return transformSidebarItems(sidebar, (item) => {
if (item.type === 'category') { if (item.type === 'category') {
const link = transformSidebarCategoryLink(item); const link = transformSidebarCategoryLink(item);
const categoryKey = item.key ?? item.label;
return { return {
...item, ...item,
label: label:
sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`] sidebarsTranslations[`sidebar.${sidebarName}.category.${categoryKey}`]
?.message ?? item.label, ?.message ?? item.label,
...(link && {link}), ...(link && {link}),
}; };
} }
if (item.type === 'link') { if (item.type === 'link') {
const linkKey = item.key ?? item.label;
return { return {
...item, ...item,
label: label:
sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`] sidebarsTranslations[`sidebar.${sidebarName}.link.${linkKey}`]
?.message ?? item.label, ?.message ?? item.label,
}; };
} }
if ((item.type === 'doc' || item.type === 'ref') && item.translatable) { if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
const docKey = item.key ?? item.label!;
return { return {
...item, ...item,
label: label:
sidebarsTranslations[`sidebar.${sidebarName}.doc.${item.label!}`] sidebarsTranslations[`sidebar.${sidebarName}.doc.${docKey}`]
?.message ?? item.label, ?.message ?? item.label,
}; };
} }

View file

@ -1,4 +1,5 @@
{ {
"key": "CategoryLabelTest alpha",
"label": "CategoryLabelTest", "label": "CategoryLabelTest",
"link": { "link": {
"type": "generated-index", "type": "generated-index",

View file

@ -1,4 +1,5 @@
{ {
"key": "CategoryLabelTest beta",
"label": "CategoryLabelTest", "label": "CategoryLabelTest",
"link": { "link": {
"type": "generated-index", "type": "generated-index",

View file

@ -8,6 +8,7 @@
/** /**
* @typedef {import('@docusaurus/plugin-content-docs').SidebarsConfig} SidebarsConfig * @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').SidebarItemConfig} SidebarItemConfig
* @typedef {import('@docusaurus/plugin-content-docs/lib/sidebars/types').SidebarItemCategoryConfig} SidebarItemCategoryConfig
*/ */
/** @type {SidebarsConfig} */ /** @type {SidebarsConfig} */
@ -63,7 +64,8 @@ const sidebars = {
items: [ items: [
{ {
type: 'link', type: 'link',
label: 'Link ', label: 'Link',
key: 'link-key-1',
href: 'https://docusaurus.io', href: 'https://docusaurus.io',
}, },
], ],
@ -76,6 +78,7 @@ const sidebars = {
{ {
type: 'link', type: 'link',
label: 'Link ', label: 'Link ',
key: 'link-key-2',
href: 'https://docusaurus.io', href: 'https://docusaurus.io',
}, },
], ],
@ -182,14 +185,16 @@ function generateHugeSidebarItems() {
/** /**
* @param {number} maxLevel * @param {number} maxLevel
* @param {number} currentLevel * @param {number} currentLevel
* @param {string} parentKey
* @returns {SidebarItemConfig[]} * @returns {SidebarItemConfig[]}
*/ */
function generateRecursive(maxLevel, currentLevel = 0) { function generateRecursive(maxLevel, currentLevel = 0, parentKey = 'ROOT') {
if (currentLevel === maxLevel) { if (currentLevel === maxLevel) {
return [ return [
{ {
type: 'link', type: 'link',
href: '/', href: '/',
key: `link-${parentKey}-maxLevel`,
label: `Link (level ${currentLevel + 1})`, label: `Link (level ${currentLevel + 1})`,
}, },
]; ];
@ -198,14 +203,23 @@ function generateHugeSidebarItems() {
const linkItems = Array.from(Array(linksCount).keys()).map((index) => ({ const linkItems = Array.from(Array(linksCount).keys()).map((index) => ({
type: 'link', type: 'link',
href: '/', href: '/',
key: `link-${parentKey}-${index}`,
label: `Link ${index} (level ${currentLevel + 1})`, label: `Link ${index} (level ${currentLevel + 1})`,
})); }));
const categoryItems = Array.from(Array(categoriesCount).keys()).map( const categoryItems = Array.from(Array(categoriesCount).keys()).map(
/**
* @returns {SidebarItemCategoryConfig}
*/
(index) => ({ (index) => ({
type: 'category', type: 'category',
label: `Category ${index} (level ${currentLevel + 1})`, label: `Category ${index} (level ${currentLevel + 1})`,
items: generateRecursive(maxLevel, currentLevel + 1), key: `category-${parentKey}-${index}`,
items: generateRecursive(
maxLevel,
currentLevel + 1,
`${parentKey}-${index}`,
),
}), }),
); );