feat(markdown): support per-locale markdown strings

Strings baked into pages by the markdown renderer can now be overridden
per locale via `locales.<index>.markdown` in the site config: container
titles (including GitHub-flavored alerts and custom containers) and the
code copy button title. Values resolve against `env.localeIndex` at
render time and fall back to the root markdown options, so the single
shared renderer keeps working unchanged.

close #4431

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5335/head
Divyansh Singh 2 months ago
parent e90178997a
commit faaa4a124e

@ -3,14 +3,19 @@ import {
disposeMdItInstance, disposeMdItInstance,
type MarkdownOptions type MarkdownOptions
} from 'node/markdown/markdown' } from 'node/markdown/markdown'
import type { MarkdownEnv } from 'node/shared'
async function render(src: string, options: MarkdownOptions = {}) { async function render(
src: string,
options: MarkdownOptions = {},
env?: Partial<MarkdownEnv>
) {
disposeMdItInstance() disposeMdItInstance()
const md = await createMarkdownRenderer('.', { const md = await createMarkdownRenderer('.', {
highlight: (code) => code, highlight: (code) => code,
...options ...options
}) })
return md.renderAsync(src) return md.renderAsync(src, env)
} }
describe('node/markdown/plugins/containers', () => { describe('node/markdown/plugins/containers', () => {
@ -479,3 +484,103 @@ describe('node/markdown/plugins/containers (github alerts)', () => {
`) `)
}) })
}) })
describe('node/markdown/plugins/containers (locales)', () => {
const options: MarkdownOptions = {
container: {
tipLabel: 'ROOT TIP',
customContainers: { success: 'SUCCESS' }
},
locales: {
zh: {
container: {
tipLabel: '提示',
detailsLabel: '详细信息',
customContainers: { success: '成功' }
}
}
}
}
test('resolves container titles for the active locale', async () => {
const src =
'::: tip\n内容\n:::\n\n::: details\n内容\n:::\n\n::: success\n内容\n:::'
expect(await render(src, options, { localeIndex: 'zh' }))
.toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default"></p>
<p></p>
</div>
<details class="details custom-block"><summary></summary>
<p></p>
</details>
<div class="success custom-block"><p class="custom-block-title custom-block-title-default"></p>
<p></p>
</div>
"
`)
})
test('falls back to root titles for other locales', async () => {
const src = '::: tip\ncontent\n:::'
const root = await render(src, options)
expect(await render(src, options, { localeIndex: 'es' })).toBe(root)
expect(root).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title custom-block-title-default">ROOT TIP</p>
<p>content</p>
</div>
"
`)
})
test('explicit titles win over locale defaults', async () => {
expect(
await render('::: tip Custom Title\n内容\n:::', options, {
localeIndex: 'zh'
})
).toMatchInlineSnapshot(`
"<div class="tip custom-block"><p class="custom-block-title">Custom Title</p>
<p></p>
</div>
"
`)
})
test('resolves alert titles for the active locale', async () => {
expect(
await render('> [!TIP]\n> 内容\n\n> [!SUCCESS]\n> 内容', options, {
localeIndex: 'zh'
})
).toMatchInlineSnapshot(`
"<div class="tip custom-block github-alert"><p class="custom-block-title"></p>
<p></p>
</div>
<div class="success custom-block github-alert"><p class="custom-block-title"></p>
<p></p>
</div>
"
`)
})
test('rejects locale titles for unregistered containers', async () => {
await expect(
render('text', {
locales: { zh: { container: { customContainers: { nope: 'X' } } } }
})
).rejects.toThrow('is not registered in the root markdown config')
})
test('resolves the code copy button title for the active locale', async () => {
const src = '```js\nconst a = 1\n```'
const opts: MarkdownOptions = {
codeCopyButtonTitle: 'Copy Code',
locales: { zh: { codeCopyButtonTitle: '复制代码' } }
}
expect(await render(src, opts, { localeIndex: 'zh' })).toContain(
'title="复制代码"'
)
expect(await render(src, opts, { localeIndex: 'es' })).toContain(
'title="Copy Code"'
)
expect(await render(src, opts)).toContain('title="Copy Code"')
})
})

@ -57,6 +57,34 @@ Refer [`DefaultTheme.Config`](https://github.com/vuejs/vitepress/blob/main/types
**Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`. **Pro tip:** Config file can be stored at `docs/.vitepress/config/index.ts` too. It might help you organize stuff by creating a configuration file per locale and then merge and export them from `index.ts`.
## Per-locale Markdown Strings
Strings baked into pages by the markdown renderer - the default titles of [custom containers](./markdown#custom-containers) and [GitHub-flavored alerts](./markdown#github-flavored-alerts), and the tooltip of the code copy button - can be overridden per locale with the `markdown` key of a locale entry:
```ts [docs/.vitepress/config.ts]
import { defineConfig } from 'vitepress'
export default defineConfig({
locales: {
root: { label: 'English', lang: 'en' },
zh: {
label: '简体中文',
lang: 'zh-Hans',
markdown: {
container: {
tipLabel: '提示',
warningLabel: '警告'
// ...the other labels, and titles of `customContainers`
},
codeCopyButtonTitle: '复制代码'
}
}
}
})
```
Values fall back to the root-level `markdown` options when a locale leaves them unset. Locale entries can only override the titles of containers registered at the root level - registering new containers per locale is not supported. Also note that since the markdown renderer is created once for the whole site, these can only be declared in the main config file, not in additional configs.
## Separate directory for each locale ## Separate directory for each locale
The following is a perfectly fine structure: The following is a perfectly fine structure:

@ -243,6 +243,8 @@ export default defineConfig({
}) })
``` ```
On multilingual sites, these labels can also be overridden per locale - see [Per-locale Markdown Strings](./i18n#per-locale-markdown-strings).
### Registering New Containers ### Registering New Containers
Beyond the built-in types, you can register additional containers by mapping their names to their default titles: Beyond the built-in types, you can register additional containers by mapping their names to their default titles:

@ -4,7 +4,10 @@ import path from 'node:path'
import pMap from 'p-map' import pMap from 'p-map'
import { normalizePath } from 'vite' import { normalizePath } from 'vite'
import type { SiteConfig } from './config' import type { SiteConfig } from './config'
import { createMarkdownRenderer } from './markdown/markdown' import {
createMarkdownRenderer,
mergeMarkdownLocales
} from './markdown/markdown'
import type { Awaitable } from './shared' import type { Awaitable } from './shared'
import { glob, normalizeGlob, type GlobOptions } from './utils/glob' import { glob, normalizeGlob, type GlobOptions } from './utils/glob'
@ -102,7 +105,7 @@ export function createContentLoader<T = ContentData[]>(
const md = await createMarkdownRenderer( const md = await createMarkdownRenderer(
config.srcDir, config.srcDir,
config.markdown, mergeMarkdownLocales(config.markdown, config.site.locales),
config.site.base, config.site.base,
config.logger, config.logger,
config.publicDir config.publicDir

@ -17,4 +17,9 @@ export * from './server'
export * from './utils/getGitTimestamp' export * from './utils/getGitTimestamp'
// shared types // shared types
export type { HeadConfig, Header, SiteData } from '../../types/shared' export type {
HeadConfig,
Header,
MarkdownLocaleOptions,
SiteData
} from '../../types/shared'

@ -35,7 +35,7 @@ import mditCjkFriendly from 'markdown-it-cjk-friendly'
import path from 'node:path' import path from 'node:path'
import type { BuiltinLanguage, BuiltinTheme, Highlighter } from 'shiki' import type { BuiltinLanguage, BuiltinTheme, Highlighter } from 'shiki'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import type { Awaitable } from '../shared' import type { Awaitable, LocaleConfig, MarkdownLocaleOptions } from '../shared'
import { import {
containerPlugin, containerPlugin,
gitHubAlertsPlugin, gitHubAlertsPlugin,
@ -91,6 +91,13 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
* @default { target: '_blank', rel: 'noreferrer' } * @default { target: '_blank', rel: 'noreferrer' }
*/ */
externalLinks?: Record<string, string> externalLinks?: Record<string, string>
/**
* Per-locale overrides for build-time markdown strings (container titles
* and the code copy button title), keyed by locale index. Populated
* automatically from `locales.<index>.markdown` in the site config - pass
* directly only when using `createMarkdownRenderer` standalone.
*/
locales?: Record<string, MarkdownLocaleOptions>
/* ==================== Syntax Highlighting ==================== */ /* ==================== Syntax Highlighting ==================== */
@ -298,6 +305,22 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
export type MarkdownRenderer = MarkdownItAsync export type MarkdownRenderer = MarkdownItAsync
// folds `locales.<index>.markdown` entries from the site config into
// `MarkdownOptions.locales` so per-locale strings reach the renderer -
// site config entries win over directly passed ones
export function mergeMarkdownLocales(
options: MarkdownOptions = {},
locales?: LocaleConfig
): MarkdownOptions {
const entries = Object.entries(locales ?? {}).filter(([, l]) => l?.markdown)
if (!entries.length) return options
const merged = { ...options.locales }
for (const [index, { markdown }] of entries) {
merged[index] = { ...merged[index], ...markdown }
}
return { ...options, locales: merged }
}
// highlight is marked as any to avoid type conflicts with plugins expecting // highlight is marked as any to avoid type conflicts with plugins expecting
// regular markdown-it which has sync highlight function. Such plugins will fail // regular markdown-it which has sync highlight function. Such plugins will fail
// if they access highlight directly but currently none of the ones we use do that. // if they access highlight directly but currently none of the ones we use do that.
@ -350,7 +373,8 @@ export async function createMarkdownRenderer(
if (options.preWrapper !== false) { if (options.preWrapper !== false) {
preWrapperPlugin(md, { preWrapperPlugin(md, {
codeCopyButtonTitle, codeCopyButtonTitle,
languageLabel: options.languageLabel languageLabel: options.languageLabel,
locales: options.locales
}) })
// must be applied after preWrapper as it augments its output // must be applied after preWrapper as it augments its output
lineNumberPlugin(md, options.lineNumbers) lineNumberPlugin(md, options.lineNumbers)
@ -358,9 +382,12 @@ export async function createMarkdownRenderer(
if (options.snippet !== false) { if (options.snippet !== false) {
snippetPlugin(md, srcDir) snippetPlugin(md, srcDir)
} }
containerPlugin(md, options.container, options.attrs !== false) containerPlugin(md, options.container, {
attrs: options.attrs !== false,
locales: options.locales
})
if (options.gfmAlerts !== false) { if (options.gfmAlerts !== false) {
gitHubAlertsPlugin(md, options.container) gitHubAlertsPlugin(md, options.container, { locales: options.locales })
} }
if (options.image !== false) { if (options.image !== false) {
imagePlugin(md, publicDir, normalizePluginOptions(options.image)) imagePlugin(md, publicDir, normalizePluginOptions(options.image))

@ -2,32 +2,31 @@ import { container } from '@mdit/plugin-container'
import type { MarkdownItAsync } from 'markdown-it-async' import type { MarkdownItAsync } from 'markdown-it-async'
import type { RenderRule } from 'markdown-it/lib/renderer.mjs' import type { RenderRule } from 'markdown-it/lib/renderer.mjs'
import type Token from 'markdown-it/lib/token.mjs' import type Token from 'markdown-it/lib/token.mjs'
import type { MarkdownEnv } from '../../shared' import type {
ContainerOptions,
MarkdownEnv,
MarkdownLocaleOptions
} from '../../shared'
import { extractTitle } from './preWrapper' import { extractTitle } from './preWrapper'
export interface ContainerOptions { export type { ContainerOptions } from '../../shared'
infoLabel?: string
noteLabel?: string export interface ContainerPluginOptions {
tipLabel?: string /**
warningLabel?: string * Whether fence-line `{...}` attributes are parsed. Mirrors the state of
dangerLabel?: string * the `markdown.attrs` option.
detailsLabel?: string */
importantLabel?: string attrs?: boolean
cautionLabel?: string
/** /**
* Additional containers to register, mapping the container name to its * Per-locale overrides for container titles, keyed by locale index.
* default title. Registered names work both as `::: name` blocks and as
* GitHub-style alerts (`> [!NAME]`), and are styleable in the theme via
* `.custom-block.name`. Names must be lowercase and may only contain
* letters, numbers, hyphens, and underscores.
*/ */
customContainers?: Record<string, string> locales?: Record<string, MarkdownLocaleOptions | undefined>
} }
export const containerPlugin = ( export const containerPlugin = (
md: MarkdownItAsync, md: MarkdownItAsync,
options?: ContainerOptions, options?: ContainerOptions,
attrs = true { attrs = true, locales }: ContainerPluginOptions = {}
) => { ) => {
md md
// explicitly escape Vue syntax // explicitly escape Vue syntax
@ -47,25 +46,47 @@ export const containerPlugin = (
closeRender: () => `</div></div>\n` closeRender: () => `</div></div>\n`
}) })
for (const [name, defaultTitle] of Object.entries(resolveTitles(options))) { const titles = resolveTitlesByLocale(options, locales)
for (const name of Object.keys(titles.base)) {
md.use(container, { md.use(container, {
name, name,
openRender: createOpenRender(md, name, defaultTitle, attrs), openRender: createOpenRender(md, name, titles, attrs),
closeRender: () => (name === 'details' ? `</details>\n` : `</div>\n`) closeRender: () => (name === 'details' ? `</details>\n` : `</div>\n`)
}) })
} }
} }
function resolveTitles(options?: ContainerOptions): Record<string, string> { interface LocaleTitles {
const titles: Record<string, string> = { base: Record<string, string>
tip: options?.tipLabel || 'TIP', byLocale: Record<string, Record<string, string>>
info: options?.infoLabel || 'INFO', }
warning: options?.warningLabel || 'WARNING',
danger: options?.dangerLabel || 'DANGER', function titlesFor(
details: options?.detailsLabel || 'Details', titles: LocaleTitles,
note: options?.noteLabel || 'NOTE', localeIndex: string | undefined
important: options?.importantLabel || 'IMPORTANT', ): Record<string, string> {
caution: options?.cautionLabel || 'CAUTION' return (localeIndex && titles.byLocale[localeIndex]) || titles.base
}
const containerLabels = [
['tip', 'tipLabel', 'TIP'],
['info', 'infoLabel', 'INFO'],
['warning', 'warningLabel', 'WARNING'],
['danger', 'dangerLabel', 'DANGER'],
['details', 'detailsLabel', 'Details'],
['note', 'noteLabel', 'NOTE'],
['important', 'importantLabel', 'IMPORTANT'],
['caution', 'cautionLabel', 'CAUTION']
] as const
function resolveTitlesByLocale(
options?: ContainerOptions,
locales?: Record<string, MarkdownLocaleOptions | undefined>
): LocaleTitles {
const base: Record<string, string> = {}
for (const [name, key, defaultTitle] of containerLabels) {
base[name] = options?.[key] || defaultTitle
} }
for (const [name, title] of Object.entries(options?.customContainers ?? {})) { for (const [name, title] of Object.entries(options?.customContainers ?? {})) {
if ( if (
@ -76,15 +97,38 @@ function resolveTitles(options?: ContainerOptions): Record<string, string> {
`Invalid custom container name: "${name}". Names must be lowercase ` + `Invalid custom container name: "${name}". Names must be lowercase ` +
`([a-z0-9_-]) and cannot be "v-pre", "raw", or "code-group".` `([a-z0-9_-]) and cannot be "v-pre", "raw", or "code-group".`
) )
titles[name] = title base[name] = title
} }
return titles
const byLocale: Record<string, Record<string, string>> = {}
for (const [localeIndex, localeOptions] of Object.entries(locales ?? {})) {
const overrides = localeOptions?.container
if (!overrides) continue
const titles = { ...base }
for (const [name, key] of containerLabels) {
if (overrides[key]) titles[name] = overrides[key]
}
for (const [name, title] of Object.entries(
overrides.customContainers ?? {}
)) {
if (!Object.hasOwn(base, name))
throw new Error(
`Custom container "${name}" in locale "${localeIndex}" is not ` +
`registered in the root markdown config. Locales can only ` +
`override titles of existing containers.`
)
titles[name] = title
}
byLocale[localeIndex] = titles
}
return { base, byLocale }
} }
function createOpenRender( function createOpenRender(
md: MarkdownItAsync, md: MarkdownItAsync,
name: string, name: string,
defaultTitle: string, titles: LocaleTitles,
attrs: boolean attrs: boolean
): RenderRule { ): RenderRule {
return (tokens, idx, _options, env: MarkdownEnv & { references?: any }) => { return (tokens, idx, _options, env: MarkdownEnv & { references?: any }) => {
@ -96,9 +140,10 @@ function createOpenRender(
token.attrJoin('class', `${name} custom-block`) token.attrJoin('class', `${name} custom-block`)
const renderedAttrs = md.renderer.renderAttrs(token) const renderedAttrs = md.renderer.renderAttrs(token)
if (noTitle) return `<div ${renderedAttrs}>\n` if (noTitle) return `<div ${renderedAttrs}>\n`
const title = md.renderInline(info || defaultTitle, { const title = md.renderInline(
references: env.references info || titlesFor(titles, env.localeIndex)[name],
}) { references: env.references }
)
if (name === 'details') if (name === 'details')
return `<details ${renderedAttrs}><summary>${title}</summary>\n` return `<details ${renderedAttrs}><summary>${title}</summary>\n`
const titleClass = const titleClass =
@ -172,11 +217,10 @@ const alertMarkerRE = /^\[!([\w-]+)\]([^\n\r]*)/
export const gitHubAlertsPlugin = ( export const gitHubAlertsPlugin = (
md: MarkdownItAsync, md: MarkdownItAsync,
options?: ContainerOptions options?: ContainerOptions,
{ locales }: Pick<ContainerPluginOptions, 'locales'> = {}
) => { ) => {
const titles = resolveTitles(options) const titles = resolveTitlesByLocale(options, locales)
// details makes no sense as a blockquote-style alert
delete titles.details
md.core.ruler.after('block', 'github-alerts', (state) => { md.core.ruler.after('block', 'github-alerts', (state) => {
const tokens = state.tokens const tokens = state.tokens
@ -200,8 +244,11 @@ export const gitHubAlertsPlugin = (
const match = firstContent.content.match(alertMarkerRE) const match = firstContent.content.match(alertMarkerRE)
if (!match) continue if (!match) continue
const type = match[1].toLowerCase() const type = match[1].toLowerCase()
if (!Object.hasOwn(titles, type)) continue // details makes no sense as a blockquote-style alert
const title = match[2].trim() || titles[type] if (type === 'details' || !Object.hasOwn(titles.base, type)) continue
const title =
match[2].trim() ||
titlesFor(titles, (state.env as MarkdownEnv)?.localeIndex)[type]
firstContent.content = firstContent.content firstContent.content = firstContent.content
.slice(match[0].length) .slice(match[0].length)
.trimStart() .trimStart()

@ -1,8 +1,13 @@
import type { MarkdownItAsync } from 'markdown-it-async' import type { MarkdownItAsync } from 'markdown-it-async'
import type { MarkdownEnv, MarkdownLocaleOptions } from '../../shared'
export interface Options { export interface Options {
codeCopyButtonTitle: string codeCopyButtonTitle: string
languageLabel?: Record<string, string> languageLabel?: Record<string, string>
/**
* Per-locale overrides for the copy button title, keyed by locale index.
*/
locales?: Record<string, MarkdownLocaleOptions | undefined>
} }
export function preWrapperPlugin(md: MarkdownItAsync, options: Options) { export function preWrapperPlugin(md: MarkdownItAsync, options: Options) {
@ -13,7 +18,7 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) {
const fence = md.renderer.rules.fence! const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => { md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args const [tokens, idx, , env] = args
const token = tokens[idx] const token = tokens[idx]
// remove title from info // remove title from info
@ -25,9 +30,14 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) {
const lang = extractLang(token.info) const lang = extractLang(token.info)
const label = langLabel[lang.toLowerCase()] || lang.replace(/_/g, ' ') const label = langLabel[lang.toLowerCase()] || lang.replace(/_/g, ' ')
const { localeIndex } = (env ?? {}) as MarkdownEnv
const codeCopyButtonTitle =
(localeIndex && options.locales?.[localeIndex]?.codeCopyButtonTitle) ||
options.codeCopyButtonTitle
return ( return (
`<div class="language-${lang}${active}">` + `<div class="language-${lang}${active}">` +
`<button title="${options.codeCopyButtonTitle}" class="copy"></button>` + `<button title="${codeCopyButtonTitle}" class="copy"></button>` +
`<span class="lang">${label}</span>` + `<span class="lang">${label}</span>` +
fence(...args) + fence(...args) +
'</div>' '</div>'

@ -6,6 +6,7 @@ import { createDebug } from 'obug'
import type { SiteConfig } from './config' import type { SiteConfig } from './config'
import { import {
createMarkdownRenderer, createMarkdownRenderer,
mergeMarkdownLocales,
type MarkdownOptions, type MarkdownOptions,
type MarkdownRenderer type MarkdownRenderer
} from './markdown/markdown' } from './markdown/markdown'
@ -94,7 +95,7 @@ export async function createMarkdownToVueRenderFn(
) { ) {
const md = await createMarkdownRenderer( const md = await createMarkdownRenderer(
srcDir, srcDir,
options, mergeMarkdownLocales(options, siteConfig?.site.locales),
base, base,
siteConfig?.logger, siteConfig?.logger,
siteConfig?.publicDir siteConfig?.publicDir

@ -10,12 +10,14 @@ export type {
AdditionalConfigDict, AdditionalConfigDict,
AdditionalConfigLoader, AdditionalConfigLoader,
Awaitable, Awaitable,
ContainerOptions,
DefaultTheme, DefaultTheme,
HeadConfig, HeadConfig,
Header, Header,
LocaleConfig, LocaleConfig,
LocaleSpecificConfig, LocaleSpecificConfig,
MarkdownEnv, MarkdownEnv,
MarkdownLocaleOptions,
PageData, PageData,
PageDataPayload, PageDataPayload,
Route, Route,
@ -109,7 +111,8 @@ export function resolveSiteDataByRoute(
relativePath: string relativePath: string
): SiteData { ): SiteData {
const localeIndex = getLocaleForPath(siteData, relativePath) const localeIndex = getLocaleForPath(siteData, relativePath)
const { label, link, ...localeConfig } = siteData.locales[localeIndex] ?? {} const { label, link, markdown, ...localeConfig } =
siteData.locales[localeIndex] ?? {}
Object.assign(localeConfig, { localeIndex }) Object.assign(localeConfig, { localeIndex })
const additionalConfigs = resolveAdditionalConfig(siteData, relativePath) const additionalConfigs = resolveAdditionalConfig(siteData, relativePath)

45
types/shared.d.ts vendored

@ -208,9 +208,52 @@ export interface LocaleSpecificConfig<ThemeConfig = any> {
themeConfig?: DeepPartial<ThemeConfig> themeConfig?: DeepPartial<ThemeConfig>
} }
export interface ContainerOptions {
infoLabel?: string
noteLabel?: string
tipLabel?: string
warningLabel?: string
dangerLabel?: string
detailsLabel?: string
importantLabel?: string
cautionLabel?: string
/**
* Additional containers to register, mapping the container name to its
* default title. Registered names work both as `::: name` blocks and as
* GitHub-style alerts (`> [!NAME]`), and are styleable in the theme via
* `.custom-block.name`. Names must be lowercase and may only contain
* letters, numbers, hyphens, and underscores.
*
* In locale-specific overrides only the titles of containers registered
* at the root level can be changed - new names cannot be added there.
*/
customContainers?: Record<string, string>
}
/**
* Build-time markdown strings that can be overridden per locale. Set them
* under `locales.<index>.markdown` in the site config; values fall back to
* the root `markdown` options when a locale leaves them unset.
*/
export interface MarkdownLocaleOptions {
/**
* Locale-specific labels for containers (`::: tip` etc.) and
* GitHub-flavored alerts.
*/
container?: ContainerOptions
/**
* Locale-specific tooltip text for the copy button in code blocks.
*/
codeCopyButtonTitle?: string
}
export type LocaleConfig<ThemeConfig = any> = Record< export type LocaleConfig<ThemeConfig = any> = Record<
string, string,
LocaleSpecificConfig<ThemeConfig> & { label: string; link?: string } LocaleSpecificConfig<ThemeConfig> & {
label: string
link?: string
markdown?: MarkdownLocaleOptions
}
> >
export type AdditionalConfig<ThemeConfig = any> = export type AdditionalConfig<ThemeConfig = any> =

Loading…
Cancel
Save