mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-23 19:48:54 +02:00
feat(docs): sidebar item key
attribute - fix docs translations key conflicts (#11228)
This commit is contained in:
parent
d9d7e855c2
commit
da08536816
23 changed files with 632 additions and 59 deletions
|
@ -5,7 +5,12 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {toSidebarDocItemLinkProp, toTagDocListProp} from '../props';
|
||||
import {fromPartial} from '@total-typescript/shoehorn';
|
||||
import {
|
||||
toSidebarDocItemLinkProp,
|
||||
toSidebarsProp,
|
||||
toTagDocListProp,
|
||||
} from '../props';
|
||||
|
||||
describe('toTagDocListProp', () => {
|
||||
type Params = Parameters<typeof toTagDocListProp>[0];
|
||||
|
@ -132,3 +137,123 @@ describe('toSidebarDocItemLinkProp', () => {
|
|||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSidebarsProp', () => {
|
||||
type Params = Parameters<typeof toSidebarsProp>[0];
|
||||
|
||||
it('works', () => {
|
||||
const params: Params = {
|
||||
docs: [
|
||||
fromPartial({
|
||||
id: 'doc-id-1',
|
||||
permalink: '/doc-1',
|
||||
title: 'Doc 1 title',
|
||||
frontMatter: {},
|
||||
}),
|
||||
],
|
||||
sidebars: {
|
||||
mySidebar: [
|
||||
{
|
||||
type: 'link',
|
||||
label: 'Example link',
|
||||
key: 'link-example-key',
|
||||
href: 'https://example.com',
|
||||
},
|
||||
{
|
||||
type: 'ref',
|
||||
label: 'Doc 1 ref',
|
||||
key: 'ref-with-doc-id-1',
|
||||
id: 'doc-id-1',
|
||||
},
|
||||
{
|
||||
type: 'ref',
|
||||
id: 'doc-id-1',
|
||||
// no label/key on purpose
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'My category',
|
||||
key: 'my-category-key',
|
||||
collapsible: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Doc 1',
|
||||
key: 'doc-id-1',
|
||||
id: 'doc-id-1',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'doc-id-1',
|
||||
// no label/key on purpose
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = toSidebarsProp(params);
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"mySidebar": [
|
||||
{
|
||||
"href": "https://example.com",
|
||||
"key": "link-example-key",
|
||||
"label": "Example link",
|
||||
"type": "link",
|
||||
},
|
||||
{
|
||||
"className": undefined,
|
||||
"customProps": undefined,
|
||||
"docId": "doc-id-1",
|
||||
"href": "/doc-1",
|
||||
"key": "ref-with-doc-id-1",
|
||||
"label": "Doc 1 ref",
|
||||
"type": "link",
|
||||
"unlisted": undefined,
|
||||
},
|
||||
{
|
||||
"className": undefined,
|
||||
"customProps": undefined,
|
||||
"docId": "doc-id-1",
|
||||
"href": "/doc-1",
|
||||
"label": "Doc 1 title",
|
||||
"type": "link",
|
||||
"unlisted": undefined,
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"collapsible": false,
|
||||
"items": [
|
||||
{
|
||||
"className": undefined,
|
||||
"customProps": undefined,
|
||||
"docId": "doc-id-1",
|
||||
"href": "/doc-1",
|
||||
"key": "doc-id-1",
|
||||
"label": "Doc 1",
|
||||
"type": "link",
|
||||
"unlisted": undefined,
|
||||
},
|
||||
{
|
||||
"className": undefined,
|
||||
"customProps": undefined,
|
||||
"docId": "doc-id-1",
|
||||
"href": "/doc-1",
|
||||
"label": "Doc 1 title",
|
||||
"type": "link",
|
||||
"unlisted": undefined,
|
||||
},
|
||||
],
|
||||
"key": "my-category-key",
|
||||
"label": "My category",
|
||||
"type": "category",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import type {
|
|||
LoadedContent,
|
||||
LoadedVersion,
|
||||
} from '@docusaurus/plugin-content-docs';
|
||||
import type {Sidebar} from '../sidebars/types';
|
||||
|
||||
function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
||||
return {
|
||||
|
@ -41,7 +42,7 @@ function createSampleDoc(doc: Pick<DocMetadata, 'id'>): DocMetadata {
|
|||
}
|
||||
|
||||
function createSampleVersion(
|
||||
version: Pick<LoadedVersion, 'versionName'>,
|
||||
version: Pick<LoadedVersion, 'versionName'> & Partial<LoadedVersion>,
|
||||
): LoadedVersion {
|
||||
return {
|
||||
label: `${version.versionName} label`,
|
||||
|
@ -152,6 +153,150 @@ describe('getLoadedContentTranslationFiles', () => {
|
|||
it('returns translation files', () => {
|
||||
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('translation key conflicts', () => {
|
||||
function runTest({withUniqueKeys}: {withUniqueKeys: boolean}) {
|
||||
const sidebarWithConflicts: Sidebar = [
|
||||
{
|
||||
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'}),
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
label: 'COMMON LABEL',
|
||||
...(withUniqueKeys && {key: 'key-link1'}),
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
href: 'https://example.com',
|
||||
label: 'COMMON LABEL',
|
||||
...(withUniqueKeys && {key: 'key-link2'}),
|
||||
},
|
||||
];
|
||||
|
||||
const version = createSampleVersion({
|
||||
versionName: CURRENT_VERSION_NAME,
|
||||
sidebars: {
|
||||
sidebarWithConflicts,
|
||||
},
|
||||
});
|
||||
return getLoadedContentTranslationFiles({
|
||||
loadedVersions: [version],
|
||||
});
|
||||
}
|
||||
|
||||
it('works on sidebar with translation key conflicts resolved by unique sidebar item keys', () => {
|
||||
expect(runTest({withUniqueKeys: true})).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"content": {
|
||||
"sidebar.sidebarWithConflicts.category.key-cat1": {
|
||||
"description": "The label for category COMMON LABEL in sidebar sidebarWithConflicts",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.category.key-cat2": {
|
||||
"description": "The label for category COMMON LABEL in sidebar sidebarWithConflicts",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.doc.key-doc4": {
|
||||
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.doc.key-doc5": {
|
||||
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.doc.key-ref4": {
|
||||
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.doc.key-ref5": {
|
||||
"description": "The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.link.key-link1": {
|
||||
"description": "The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"sidebar.sidebarWithConflicts.link.key-link2": {
|
||||
"description": "The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com",
|
||||
"message": "COMMON LABEL",
|
||||
},
|
||||
"version.label": {
|
||||
"description": "The label for version current",
|
||||
"message": "current label",
|
||||
},
|
||||
},
|
||||
"path": "current",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws on sidebar translation key conflicts', () => {
|
||||
expect(() => runTest({withUniqueKeys: false}))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Multiple docs sidebar items produce the same translation key.
|
||||
- \`sidebar.sidebarWithConflicts.category.COMMON LABEL\`: 2 duplicates found:
|
||||
- COMMON LABEL (The label for category COMMON LABEL in sidebar sidebarWithConflicts)
|
||||
- COMMON LABEL (The label for category COMMON LABEL in sidebar sidebarWithConflicts)
|
||||
|
||||
- \`sidebar.sidebarWithConflicts.link.COMMON LABEL\`: 2 duplicates found:
|
||||
- COMMON LABEL (The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com)
|
||||
- COMMON LABEL (The label for link COMMON LABEL in sidebar sidebarWithConflicts, linking to https://example.com)
|
||||
|
||||
- \`sidebar.sidebarWithConflicts.doc.COMMON LABEL\`: 4 duplicates found:
|
||||
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4)
|
||||
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5)
|
||||
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc4)
|
||||
- COMMON LABEL (The label for the doc item COMMON LABEL in sidebar sidebarWithConflicts, linking to the doc doc5)
|
||||
|
||||
To avoid translation key conflicts, use the \`key\` attribute on the sidebar items above to uniquely identify them.
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateLoadedContent', () => {
|
||||
|
|
|
@ -41,6 +41,7 @@ export function toSidebarDocItemLinkProp({
|
|||
const {id, title, permalink, frontMatter, unlisted} = doc;
|
||||
return {
|
||||
type: 'link',
|
||||
...(item.key && {key: item.key}),
|
||||
href: permalink,
|
||||
// Front Matter data takes precedence over sidebars.json
|
||||
label: frontMatter.sidebar_label ?? item.label ?? title,
|
||||
|
@ -51,7 +52,9 @@ export function toSidebarDocItemLinkProp({
|
|||
};
|
||||
}
|
||||
|
||||
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
|
||||
export function toSidebarsProp(
|
||||
loadedVersion: Pick<LoadedVersion, 'docs' | 'sidebars'>,
|
||||
): PropSidebars {
|
||||
const docsById = createDocsByIdIndex(loadedVersion.docs);
|
||||
|
||||
function getDocById(docId: string): DocMetadata {
|
||||
|
|
|
@ -47,6 +47,7 @@ exports[`DefaultSidebarItemsGenerator generates complex nested sidebar 1`] = `
|
|||
"type": "doc",
|
||||
},
|
||||
],
|
||||
"key": "SubGuides-category-unique-key",
|
||||
"label": "SubGuides (metadata file label)",
|
||||
"link": {
|
||||
"description": "subGuides-description",
|
||||
|
|
|
@ -129,6 +129,7 @@ describe('DefaultSidebarItemsGenerator', () => {
|
|||
},
|
||||
'02-Guides/01-SubGuides': {
|
||||
label: 'SubGuides (metadata file label)',
|
||||
key: 'SubGuides-category-unique-key',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'subGuides-generated-index-slug',
|
||||
|
|
|
@ -29,10 +29,12 @@ describe('validateSidebars', () => {
|
|||
it('accept valid values', () => {
|
||||
const sidebars: SidebarsConfig = {
|
||||
sidebar1: [
|
||||
{type: 'doc', id: 'doc1'},
|
||||
{type: 'doc', id: 'doc1', key: 'key-doc1'},
|
||||
{type: 'doc', id: 'doc2'},
|
||||
{type: 'ref', id: 'doc2', key: 'ref-doc2'},
|
||||
{
|
||||
type: 'category',
|
||||
key: 'key-cat',
|
||||
label: 'Category',
|
||||
items: [{type: 'doc', id: 'doc3'}],
|
||||
},
|
||||
|
@ -68,6 +70,33 @@ describe('validateSidebars', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('sidebar category wrong key', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
key: '',
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
"type": "category",
|
||||
"items": [
|
||||
{
|
||||
"type": "doc",
|
||||
"id": "doc1"
|
||||
}
|
||||
],
|
||||
"key" [1]: ""
|
||||
}
|
||||
|
||||
[1] "key" is not allowed to be empty"
|
||||
`);
|
||||
});
|
||||
|
||||
it('sidebars link wrong label', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
|
@ -90,6 +119,28 @@ describe('validateSidebars', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('sidebars link wrong key', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
docs: [
|
||||
{
|
||||
type: 'link',
|
||||
key: false,
|
||||
href: 'https://github.com',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
"type": "link",
|
||||
"href": "https://github.com",
|
||||
"key" [1]: false
|
||||
}
|
||||
|
||||
[1] "key" must be a string"
|
||||
`);
|
||||
});
|
||||
|
||||
it('sidebars link wrong href', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
|
@ -188,6 +239,35 @@ describe('validateSidebars', () => {
|
|||
`);
|
||||
});
|
||||
|
||||
it('sidebars category wrong key', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
label: 'category',
|
||||
key: 42,
|
||||
items: [],
|
||||
},
|
||||
|
||||
{
|
||||
type: 'ref',
|
||||
id: 'hello',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
"type": "category",
|
||||
"label": "category",
|
||||
"items": [],
|
||||
"key" [1]: 42
|
||||
}
|
||||
|
||||
[1] "key" must be a string"
|
||||
`);
|
||||
});
|
||||
|
||||
it('sidebar category wrong items', () => {
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
|
@ -230,8 +310,8 @@ describe('validateSidebars', () => {
|
|||
const sidebars: SidebarsConfig = {
|
||||
sidebar1: [
|
||||
{
|
||||
// @ts-expect-error - test missing value
|
||||
type: 'html',
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -251,6 +331,7 @@ describe('validateSidebars', () => {
|
|||
sidebar1: [
|
||||
{
|
||||
type: 'html',
|
||||
key: 'html-key',
|
||||
value: '<p>Hello, World!</p>',
|
||||
defaultStyle: true,
|
||||
className: 'foo',
|
||||
|
@ -262,8 +343,6 @@ describe('validateSidebars', () => {
|
|||
});
|
||||
|
||||
describe('validateCategoryMetadataFile', () => {
|
||||
// TODO add more tests
|
||||
|
||||
it('throw for bad value', () => {
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile(42),
|
||||
|
@ -279,6 +358,7 @@ describe('validateCategoryMetadataFile', () => {
|
|||
const content: CategoryMetadataFile = {
|
||||
className: 'className',
|
||||
label: 'Category Label',
|
||||
key: 'category-key',
|
||||
description: 'Category Description',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
|
@ -293,24 +373,70 @@ describe('validateCategoryMetadataFile', () => {
|
|||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
it('rejects permalink', () => {
|
||||
const content: CategoryMetadataFile = {
|
||||
className: 'className',
|
||||
label: 'Category Label',
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
// @ts-expect-error: rejected on purpose
|
||||
permalink: 'somePermalink',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
position: 3,
|
||||
};
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile(content),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""link.permalink" is not allowed"`);
|
||||
describe('label', () => {
|
||||
it('accepts valid label', () => {
|
||||
const content: CategoryMetadataFile = {label: 'Category label'};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
it('throws for number label', () => {
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile({label: 42}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""label" must be a string"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('key', () => {
|
||||
it('accepts valid key', () => {
|
||||
const content: CategoryMetadataFile = {key: 'Category key'};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
it('throws for number key', () => {
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile({key: 42}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""key" must be a string"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('className', () => {
|
||||
it('accepts valid className', () => {
|
||||
const content: CategoryMetadataFile = {className: 'category-className'};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
it('throws for number key', () => {
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile({className: 42}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""className" must be a string"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link', () => {
|
||||
it('accepts valid link', () => {
|
||||
const content: CategoryMetadataFile = {
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
title: 'title',
|
||||
description: 'desc',
|
||||
},
|
||||
};
|
||||
expect(validateCategoryMetadataFile(content)).toEqual(content);
|
||||
});
|
||||
|
||||
it('rejects link permalink', () => {
|
||||
const content: CategoryMetadataFile = {
|
||||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'slug',
|
||||
// @ts-expect-error: rejected on purpose
|
||||
permalink: 'somePermalink',
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
validateCategoryMetadataFile(content),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""link.permalink" is not allowed"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -252,6 +252,7 @@ Available doc IDs:
|
|||
...(categoryMetadata?.description && {
|
||||
description: categoryMetadata?.description,
|
||||
}),
|
||||
...(categoryMetadata?.key && {key: categoryMetadata?.key}),
|
||||
...(link && {link}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {Slugger} from '@docusaurus/utils';
|
|||
type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]};
|
||||
|
||||
export type SidebarItemBase = {
|
||||
key?: string;
|
||||
className?: string;
|
||||
customProps?: {[key: string]: unknown};
|
||||
};
|
||||
|
@ -28,8 +29,9 @@ export type SidebarItemDoc = SidebarItemBase & {
|
|||
label?: string;
|
||||
id: string;
|
||||
/**
|
||||
* This is an internal marker. Items with labels defined in the config needs
|
||||
* to be translated with JSON
|
||||
* This is an internal marker set during the sidebar normalization process.
|
||||
* Docs with labels defined in the config need to be translated with JSON.
|
||||
* Otherwise, it's preferable to translate the MDX doc title or front matter.
|
||||
*/
|
||||
translatable?: true;
|
||||
};
|
||||
|
@ -215,6 +217,7 @@ export type PropSidebarBreadcrumbsItem =
|
|||
| PropSidebarItemCategory;
|
||||
|
||||
export type CategoryMetadataFile = {
|
||||
key?: string;
|
||||
label?: string;
|
||||
position?: number;
|
||||
description?: string;
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
// in normalization
|
||||
|
||||
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
|
||||
key: Joi.string(),
|
||||
className: Joi.string(),
|
||||
customProps: Joi.object().unknown(),
|
||||
});
|
||||
|
@ -166,6 +167,7 @@ export function validateSidebars(sidebars: {
|
|||
}
|
||||
|
||||
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
|
||||
key: Joi.string(),
|
||||
label: Joi.string(),
|
||||
description: Joi.string(),
|
||||
position: Joi.number(),
|
||||
|
|
|
@ -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,53 @@ 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 `${logger.code(translationKey)}: ${logger.num(
|
||||
entries.length,
|
||||
)} duplicates found:\n - ${entries
|
||||
.map((duplicate) => {
|
||||
const desc = duplicate[1].description;
|
||||
return `${logger.name(duplicate[1].message)} ${
|
||||
desc ? `(${logger.subdue(desc)})` : ''
|
||||
}`;
|
||||
})
|
||||
.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 categoryContent: TranslationFileContent = Object.fromEntries(
|
||||
categories.flatMap((category) => {
|
||||
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 +97,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 +106,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}`,
|
||||
|
@ -82,36 +116,40 @@ function getSidebarTranslationFileContent(
|
|||
}
|
||||
|
||||
return entries;
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
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({
|
||||
|
@ -150,27 +188,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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue