From 5f3f8ee89be47f47d4325b7d21c88c5b320aa899 Mon Sep 17 00:00:00 2001 From: Hippo <6137925+hippotastic@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:20:18 +0200 Subject: [PATCH] Migrate code blocks to Expressive Code (#3644) * Migrate code blocks to Expressive Code * Update astro-expressive-code * Update to astro-expressive-code v0.9.0 * Fix code blocks in RTL langs using Expressive Code * Update to latest version * Move theme file, fix Firefox hover issue --- astro.config.ts | 9 +- integrations/astro-code-snippets.ts | 305 ------------ integrations/expressive-code.ts | 47 ++ .../syntax-highlighting-theme.ts | 5 +- package.json | 1 + pnpm-lock.yaml | 134 ++++++ public/index.css | 25 +- src/components/CodeSnippet/CodeSnippet.astro | 443 ------------------ src/components/CodeSnippet/color-contrast.ts | 29 -- src/components/CodeSnippet/copy-button.ts | 31 -- src/components/CodeSnippet/shiki-block.ts | 59 --- src/components/CodeSnippet/shiki-line.ts | 338 ------------- src/components/CodeSnippet/types.ts | 44 -- src/i18n/en/ui.ts | 9 +- src/i18n/es/ui.ts | 9 +- src/i18n/ja/ui.ts | 4 +- src/i18n/util.ts | 2 +- src/i18n/zh-cn/ui.ts | 9 +- 18 files changed, 205 insertions(+), 1298 deletions(-) delete mode 100644 integrations/astro-code-snippets.ts create mode 100644 integrations/expressive-code.ts rename syntax-highlighting-theme.ts => integrations/syntax-highlighting-theme.ts (99%) delete mode 100644 src/components/CodeSnippet/CodeSnippet.astro delete mode 100644 src/components/CodeSnippet/color-contrast.ts delete mode 100644 src/components/CodeSnippet/copy-button.ts delete mode 100644 src/components/CodeSnippet/shiki-block.ts delete mode 100644 src/components/CodeSnippet/shiki-line.ts delete mode 100644 src/components/CodeSnippet/types.ts diff --git a/astro.config.ts b/astro.config.ts index 5396bc67db49d..c524bfe6a8e11 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -8,31 +8,28 @@ import rehypeSlug from 'rehype-slug'; import remarkSmartypants from 'remark-smartypants'; import { asideAutoImport, astroAsides } from './integrations/astro-asides'; -import { astroCodeSnippets, codeSnippetAutoImport } from './integrations/astro-code-snippets'; +import { astroDocsExpressiveCode } from './integrations/expressive-code'; import { sitemap } from './integrations/sitemap'; import { autolinkConfig } from './plugins/rehype-autolink-config'; import { rehypei18nAutolinkHeadings } from './plugins/rehype-i18n-autolink-headings'; import { rehypeOptimizeStatic } from './plugins/rehype-optimize-static'; import { rehypeTasklistEnhancer } from './plugins/rehype-tasklist-enhancer'; import { remarkFallbackLang } from './plugins/remark-fallback-lang'; -import { theme } from './syntax-highlighting-theme'; // https://astro.build/config export default defineConfig({ site: 'https://docs.astro.build/', integrations: [ AutoImport({ - imports: [asideAutoImport, codeSnippetAutoImport], + imports: [asideAutoImport], }), preact({ compat: true }), sitemap(), astroAsides(), - astroCodeSnippets(), + astroDocsExpressiveCode(), mdx(), ], markdown: { - syntaxHighlight: 'shiki', - shikiConfig: { theme }, // Override with our own config smartypants: false, remarkPlugins: [ diff --git a/integrations/astro-code-snippets.ts b/integrations/astro-code-snippets.ts deleted file mode 100644 index 90aee62217da0..0000000000000 --- a/integrations/astro-code-snippets.ts +++ /dev/null @@ -1,305 +0,0 @@ -import type { AstroIntegration } from 'astro'; -import type { BlockContent, Parent, Root } from 'mdast'; -import type { Plugin, Transformer } from 'unified'; -import { visit } from 'unist-util-visit'; -import type { BuildVisitor } from 'unist-util-visit/complex-types'; -import { makeComponentNode } from './utils/makeComponentNode'; - -const CodeSnippetTagname = 'AutoImportedCodeSnippet'; -export const codeSnippetAutoImport: Record = { - '~/components/CodeSnippet/CodeSnippet.astro': [['default', CodeSnippetTagname]], -}; - -const LanguageGroups = { - code: ['astro', 'cjs', 'htm', 'html', 'js', 'jsx', 'mjs', 'svelte', 'ts', 'tsx', 'vue'], - data: ['env', 'json', 'yaml', 'yml'], - styles: ['css', 'less', 'sass', 'scss', 'styl', 'stylus'], - textContent: ['markdown', 'md', 'mdx'], -}; -const FileNameCommentRegExp = new RegExp( - [ - // Start of line - `^`, - // Optional whitespace - `\\s*`, - // Mandatory comment start (`//`, `#` or ``) - `(?:-->)?`, - // Optional whitespace - `\\s*`, - // End of line - `$`, - ].join('') -); - -export interface CodeSnippetWrapper extends Parent { - type: 'codeSnippetWrapper'; - children: BlockContent[]; -} - -declare module 'mdast' { - interface BlockContentMap { - codeSnippetWrapper: CodeSnippetWrapper; - } -} - -export function remarkCodeSnippets(): Plugin<[], Root> { - const visitor: BuildVisitor = (code, index, parent) => { - if (index === null || parent === null) return; - - // Parse optional meta information after the opening code fence, - // trying to get a meta title and an array of highlighted lines - const { title: metaTitle, lineMarkings, inlineMarkings } = parseMeta(code.meta || ''); - let title = metaTitle; - - // Preprocess the code - const { preprocessedCode, extractedFileName, removedLineIndex, removedLineCount } = - preprocessCode( - code.value, - code.lang || '', - // Only try to extract a file name from the code if no meta title was found above - title === undefined - ); - code.value = preprocessedCode; - if (extractedFileName) { - title = extractedFileName; - } - - // If there was no title in the meta information or in the code, check if the previous - // Markdown paragraph contains a file name that we can use as a title - if (title === undefined && index > 0) { - // Check the previous node to see if it matches our requirements - const prev = parent.children[index - 1]; - const strongContent = - // The previous node must be a paragraph... - prev.type === 'paragraph' && - // ...it must contain exactly one child with strong formatting... - prev.children.length === 1 && - prev.children[0].type === 'strong' && - // ...this child must also contain exactly one child - prev.children[0].children.length === 1 && - // ...which is the result of this expression - prev.children[0].children[0]; - - // Require the strong content to be either raw text or inline code and retrieve its value - const prevParaStrongTextValue = - strongContent && strongContent.type === 'text' && strongContent.value; - const prevParaStrongCodeValue = - strongContent && strongContent.type === 'inlineCode' && strongContent.value; - const potentialFileName = prevParaStrongTextValue || prevParaStrongCodeValue; - - // Check if it's a file name - const matches = potentialFileName && FileNameCommentRegExp.exec(`// ${potentialFileName}`); - if (matches) { - // Yes, store the file name and replace the paragraph with an empty node - title = matches[2]; - parent.children[index - 1] = { - type: 'html', - value: '', - }; - } - } - - const attributes = { - lang: code.lang, - title: encodeMarkdownStringProp(title), - removedLineIndex, - removedLineCount, - lineMarkings: encodeMarkdownStringArrayProp(lineMarkings), - inlineMarkings: encodeMarkdownStringArrayProp(inlineMarkings), - }; - - const codeSnippetWrapper = makeComponentNode(CodeSnippetTagname, { attributes }, code); - - parent.children.splice(index, 1, codeSnippetWrapper); - }; - - const transformer: Transformer = (tree) => { - visit(tree, 'code', visitor); - }; - - return function attacher() { - return transformer; - }; -} - -/** - * Parses the given meta information string and returns contained supported properties. - * - * Meta information is the string after the opening code fence and language name. - */ -function parseMeta(meta: string) { - // Try to find the meta property `title="..."` or `title='...'`, - // store its value and remove it from meta - let title: string | undefined; - meta = meta.replace(/(?:\s|^)title\s*=\s*(["'])(.*?)(? { - title = content; - return ''; - }); - - // Find line marking definitions inside curly braces, with an optional marker type prefix. - // - // Examples: - // - `{4-5,10}` (if no marker type prefix is given, it defaults to `mark`) - // - `mark={4-5,10}` - // - `del={4-5,10}` - // - `ins={4-5,10}` - const lineMarkings: string[] = []; - meta = meta.replace(/(?:\s|^)(?:([a-zA-Z]+)\s*=\s*)?({[0-9,\s-]*})/g, (_, prefix, range) => { - lineMarkings.push(`${prefix || 'mark'}=${range}`); - return ''; - }); - - // Find inline marking definitions inside single or double quotes (to match plaintext strings) - // or forward slashes (to match regular expressions), with an optional marker type prefix. - // - // Examples for plaintext strings: - // - `"Astro.props"` (if no marker type prefix is given, it defaults to `mark`) - // - `ins=" -

${this.tooltip}

-`; - } -} diff --git a/src/components/CodeSnippet/shiki-block.ts b/src/components/CodeSnippet/shiki-block.ts deleted file mode 100644 index 3dc4f6ccf02fa..0000000000000 --- a/src/components/CodeSnippet/shiki-block.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { CopyButton, CopyButtonArgs } from './copy-button'; -import { ShikiLine } from './shiki-line'; -import { InlineMarkingDefinition, LineMarkingDefinition, MarkerTypeOrder } from './types'; - -export class ShikiBlock { - private htmlBeforeFirstLine = ''; - private shikiLines: ShikiLine[] = []; - private htmlAfterLastLine = ''; - private copyButton: CopyButton | null = null; - - constructor(highlightedCodeHtml: string, copyButtonArgs: CopyButtonArgs) { - if (!highlightedCodeHtml) return; - - const codeBlockRegExp = /^\s*()([\s\S]*)(<\/code><\/pre>)\s*$/; - const matches = highlightedCodeHtml.match(codeBlockRegExp); - if (!matches) - throw new Error( - `Shiki-highlighted code block HTML did not match expected format. HTML code:\n${highlightedCodeHtml}` - ); - - this.htmlBeforeFirstLine = matches[1]; - const innerHtml = matches[2]; - this.htmlAfterLastLine = matches[3]; - - // Parse inner HTML code to ShikiLine instances - const innerHtmlLines = innerHtml.split(/\r?\n/); - this.shikiLines = innerHtmlLines.map((htmlLine) => new ShikiLine(htmlLine)); - - this.copyButton = new CopyButton(this.shikiLines, copyButtonArgs); - } - - applyMarkings(lineMarkings: LineMarkingDefinition[], inlineMarkings: InlineMarkingDefinition[]) { - if (!lineMarkings.length && !inlineMarkings.length) return; - - this.shikiLines.forEach((line, i) => { - // Determine line marker type (if any) - const matchingDefinitions = lineMarkings.filter((def) => def.lines.includes(i + 1)); - if (matchingDefinitions) { - const markerTypes = matchingDefinitions.map((def) => def.markerType); - markerTypes.sort((a, b) => MarkerTypeOrder.indexOf(a) - MarkerTypeOrder.indexOf(b)); - const highestPrioMarkerType = markerTypes[0]; - line.setLineMarkerType(highestPrioMarkerType); - } - - line.applyInlineMarkings(inlineMarkings); - }); - } - - renderToHtml() { - const linesHtml = this.shikiLines - .map((line) => { - line.ensureTokenColorContrast(); - return line.renderToHtml(); - }) - .join('\n'); - const copyButton = this.copyButton?.renderToHtml(); - return `${this.htmlBeforeFirstLine}${linesHtml}${this.htmlAfterLastLine}${copyButton}`; - } -} diff --git a/src/components/CodeSnippet/shiki-line.ts b/src/components/CodeSnippet/shiki-line.ts deleted file mode 100644 index d88814964c269..0000000000000 --- a/src/components/CodeSnippet/shiki-line.ts +++ /dev/null @@ -1,338 +0,0 @@ -import chroma from 'chroma-js'; -import { unescape } from '~/util/html-entities'; -import { ensureTextContrast } from './color-contrast'; -import { - InlineMarkingDefinition, - InlineToken, - InsertionPoint, - MarkedRange, - MarkerToken, - MarkerType, - MarkerTypeOrder, -} from './types'; - -export class ShikiLine { - readonly tokens: InlineToken[]; - readonly textLine: string; - - private beforeClassValue: string; - private classes: Set; - private afterClassValue: string; - private afterTokens: string; - - constructor(highlightedCodeLine: string) { - const lineRegExp = /^()(.*)(<\/span>)$/; - const lineMatches = highlightedCodeLine.match(lineRegExp); - if (!lineMatches) - throw new Error( - `Shiki-highlighted code line HTML did not match expected format. HTML code:\n${highlightedCodeLine}` - ); - - this.beforeClassValue = lineMatches[1]; - this.classes = new Set(lineMatches[2].split(' ')); - this.afterClassValue = lineMatches[3]; - const tokensHtml = lineMatches[4]; - this.afterTokens = lineMatches[5]; - - // Split line into inline tokens - const tokenRegExp = /(.*?)<\/span>/g; - const tokenMatches = tokensHtml.matchAll(tokenRegExp); - this.tokens = []; - this.textLine = ''; - for (const tokenMatch of tokenMatches) { - const [, color, otherStyles, innerHtml] = tokenMatch; - const text = unescape(innerHtml); - this.tokens.push({ - tokenType: 'syntax', - color, - otherStyles, - innerHtml, - text, - textStart: this.textLine.length, - textEnd: this.textLine.length + text.length, - }); - this.textLine += text; - } - } - - applyInlineMarkings(inlineMarkings: InlineMarkingDefinition[]) { - const markedRanges: MarkedRange[] = []; - - // Go through all definitions, find matches for their text or regExp in textLine, - // and fill markedRanges with their capture groups or entire matches - inlineMarkings.forEach((inlineMarking) => { - const matches = this.getInlineMarkingDefinitionMatches(inlineMarking); - markedRanges.push(...matches); - }); - - if (!markedRanges.length) return; - - // Flatten marked ranges to prevent any overlaps - const flattenedRanges = this.flattenMarkedRanges(markedRanges); - - // Build an array of marker elements to insert - const markerElements = flattenedRanges.map((range) => ({ - markerType: range.markerType, - opening: this.textPositionToTokenPosition(range.start), - closing: this.textPositionToTokenPosition(range.end), - })); - - // Mutate inline tokens in reverse direction (from end to start), - // inserting opening and closing marker tokens at the determined positions, - // optionally splitting syntax tokens if they only match partially - markerElements.reverse().forEach((markerElement) => { - const markerToken: MarkerToken = { - tokenType: 'marker', - markerType: markerElement.markerType, - }; - - this.insertMarkerTokenAtPosition(markerElement.closing, { ...markerToken, closing: true }); - this.insertMarkerTokenAtPosition(markerElement.opening, markerToken); - }); - } - - ensureTokenColorContrast() { - // Ensure proper color contrast of syntax tokens inside marked ranges - // (note that only the lightness of the background color is used) - const backgroundColor = chroma('#2e336b'); - const isLineMarked = this.getLineMarkerType() !== undefined; - let inInlineMarker = false; - this.tokens.forEach((token) => { - if (token.tokenType === 'marker') { - inInlineMarker = !token.closing; - return; - } - if (inInlineMarker || isLineMarked) { - const tokenColor = chroma(token.color); - const fixedTokenColor = ensureTextContrast(tokenColor, backgroundColor); - token.color = fixedTokenColor.hex(); - } - }); - } - - renderToHtml() { - const classValue = [...this.classes].join(' '); - - // Build the line's inner HTML code by rendering all contained tokens - let innerHtml = this.tokens - .map((token) => { - if (token.tokenType === 'marker') return `<${token.closing ? '/' : ''}${token.markerType}>`; - return `${token.innerHtml}`; - }) - .join(''); - - // Browsers don't seem render the background color of completely empty lines, - // so if the rendered inner HTML code is empty and we want to mark the line, - // we need to add some content to make the background color visible. - // To keep the copy & paste result unchanged at the same time, we add an empty span - // and attach a CSS class that displays a space inside a ::before pseudo-element. - if (!innerHtml && this.getLineMarkerType() !== undefined) - innerHtml = ''; - - return `${this.beforeClassValue}${classValue}${this.afterClassValue}${innerHtml}${this.afterTokens}`; - } - - getLineMarkerType(): MarkerType { - return MarkerTypeOrder.find( - (markerType) => markerType && this.classes.has(markerType.toString()) - ); - } - - setLineMarkerType(newType: MarkerType) { - // Remove all existing marker type classes (if any) - MarkerTypeOrder.forEach( - (markerType) => markerType && this.classes.delete(markerType.toString()) - ); - - if (newType === undefined) return; - this.classes.add(newType.toString()); - } - - private getInlineMarkingDefinitionMatches(inlineMarking: InlineMarkingDefinition) { - const markedRanges: MarkedRange[] = []; - - if (inlineMarking.text) { - let idx = this.textLine.indexOf(inlineMarking.text, 0); - while (idx > -1) { - markedRanges.push({ - markerType: inlineMarking.markerType, - start: idx, - end: idx + inlineMarking.text.length, - }); - idx = this.textLine.indexOf(inlineMarking.text, idx + inlineMarking.text.length); - } - return markedRanges; - } - - if (inlineMarking.regExp) { - const matches = this.textLine.matchAll(inlineMarking.regExp); - for (const match of matches) { - const fullMatchIndex = match.index as number; - // Read the start and end ranges from the `indices` property, - // which is made available through the RegExp flag `d` - // (and unfortunately not recognized by TypeScript) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let groupIndices = (match as any).indices as ([start: number, end: number] | null)[]; - // If accessing the group indices is unsupported, use fallback logic - if (!groupIndices || !groupIndices.length) { - // Try to find the position of each capture group match inside the full match - groupIndices = match.map((groupValue) => { - const groupIndex = groupValue ? match[0].indexOf(groupValue) : -1; - if (groupIndex === -1) return null; - const groupStart = fullMatchIndex + groupIndex; - const groupEnd = groupStart + groupValue.length; - return [groupStart, groupEnd]; - }); - } - // Remove null group indices - groupIndices = groupIndices.filter((range) => range); - // If there are no non-null indices, use the full match instead - if (!groupIndices.length) { - groupIndices = [[fullMatchIndex, fullMatchIndex + match[0].length]]; - } - // If there are multiple non-null indices, remove the first one - // as it is the full match and we only want to mark capture groups - if (groupIndices.length > 1) { - groupIndices.shift(); - } - // Create marked ranges from all remaining group indices - groupIndices.forEach((range) => { - if (!range) return; - markedRanges.push({ - markerType: inlineMarking.markerType, - start: range[0], - end: range[1], - }); - }); - } - return markedRanges; - } - - throw new Error(`Missing matching logic for inlineMarking=${JSON.stringify(inlineMarking)}`); - } - - private textPositionToTokenPosition(textPosition: number): InsertionPoint { - for (const [tokenIndex, token] of this.tokens.entries()) { - if (token.tokenType !== 'syntax') continue; - - if (textPosition === token.textStart) { - return { - tokenIndex, - innerHtmlOffset: 0, - }; - } - - // The text position is inside the current token - if (textPosition > token.textStart && textPosition < token.textEnd) { - // NOTE: We used to escape the string before `indexOf` as rehype would escape HTML entities - // at render-time, causing the text position to shift. However, with rehype-optimize-static, - // the HTML is preserved as is, so we don't have to anticipate for the shift anymore. - const innerHtmlOffset = ( - token.text.slice(0, textPosition - token.textStart) + - // Insert our special character at textPosition - '\n' + - token.text.slice(textPosition - token.textStart) - ).indexOf('\n'); - - return { - tokenIndex, - innerHtmlOffset, - }; - } - } - - // If we arrive here, the position is after the last token - return { - tokenIndex: this.tokens.length, - innerHtmlOffset: 0, - }; - } - - private insertMarkerTokenAtPosition(position: InsertionPoint, markerToken: MarkerToken) { - // Insert the new token inside the given token by splitting it - if (position.innerHtmlOffset > 0) { - const insideToken = this.tokens[position.tokenIndex]; - if (insideToken.tokenType !== 'syntax') - throw new Error( - `Cannot insert a marker token inside a token of type "${insideToken.tokenType}"!` - ); - - const newInnerHtmlBeforeMarker = insideToken.innerHtml.slice(0, position.innerHtmlOffset); - const tokenAfterMarker = { - ...insideToken, - innerHtml: insideToken.innerHtml.slice(position.innerHtmlOffset), - }; - insideToken.innerHtml = newInnerHtmlBeforeMarker; - const newTokens: InlineToken[] = [markerToken]; - // Only add the inside token if it still has contents after splitting - if (tokenAfterMarker.innerHtml.length) newTokens.push(tokenAfterMarker); - this.tokens.splice(position.tokenIndex + 1, 0, ...newTokens); - return; - } - - // Insert the new token before the given token - this.tokens.splice(position.tokenIndex, 0, markerToken); - } - - private flattenMarkedRanges(markedRanges: MarkedRange[]): MarkedRange[] { - const flattenedRanges: MarkedRange[] = []; - const sortedRanges = [...markedRanges].sort((a, b) => a.start - b.start); - const posInRange = (pos: number): { idx: number; range?: MarkedRange } => { - for (let idx = 0; idx < flattenedRanges.length; idx++) { - const range = flattenedRanges[idx]; - if (pos < range.end) - return { - idx, - range: pos >= range.start ? range : undefined, - }; - } - // After the last element - return { - idx: flattenedRanges.length, - }; - }; - - MarkerTypeOrder.forEach((markerType) => { - sortedRanges - .filter((range) => range.markerType === markerType) - .forEach((rangeToAdd) => { - // Clone range to avoid overriding values of the original object - rangeToAdd = { ...rangeToAdd }; - - // Get insertion position for the start and end of rangeToAdd - const posStart = posInRange(rangeToAdd.start); - const posEnd = posInRange(rangeToAdd.end); - - const newElements: MarkedRange[] = [rangeToAdd]; - - // rangeToAdd starts inside an existing range and their start points differ - if (posStart.range && rangeToAdd.start !== posStart.range.start) { - if (posStart.range.markerType === rangeToAdd.markerType) { - rangeToAdd.start = posStart.range.start; - } else { - newElements.unshift({ - ...posStart.range, - end: rangeToAdd.start, - }); - } - } - - // rangeToAdd ends inside an existing range and their end points differ - if (posEnd.range && rangeToAdd.end !== posEnd.range.end) { - if (posEnd.range.markerType === rangeToAdd.markerType) { - rangeToAdd.end = posEnd.range.end; - } else { - newElements.push({ - ...posEnd.range, - start: rangeToAdd.end, - }); - } - } - - flattenedRanges.splice(posStart.idx, posEnd.idx - posStart.idx + 1, ...newElements); - }); - }); - - return flattenedRanges; - } -} diff --git a/src/components/CodeSnippet/types.ts b/src/components/CodeSnippet/types.ts deleted file mode 100644 index 94aed687340b2..0000000000000 --- a/src/components/CodeSnippet/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type MarkerType = 'mark' | 'ins' | 'del' | undefined; - -/** When markers overlap, those with higher indices override lower ones. */ -export const MarkerTypeOrder: MarkerType[] = ['mark', 'del', 'ins']; - -export type LineMarkingDefinition = { - markerType: MarkerType; - lines: number[]; -}; - -export type InlineMarkingDefinition = { - markerType: MarkerType; - text?: string; - regExp?: RegExp; -}; - -export type MarkedRange = { - markerType: MarkerType; - start: number; - end: number; -}; - -export type SyntaxToken = { - tokenType: 'syntax'; - color: string; - otherStyles: string; - innerHtml: string; - text: string; - textStart: number; - textEnd: number; -}; - -export type MarkerToken = { - tokenType: 'marker'; - markerType: MarkerType; - closing?: boolean; -}; - -export type InlineToken = SyntaxToken | MarkerToken; - -export type InsertionPoint = { - tokenIndex: number; - innerHtmlOffset: number; -}; diff --git a/src/i18n/en/ui.ts b/src/i18n/en/ui.ts index c2bf4bbf1bc37..4df6f20cb9708 100644 --- a/src/i18n/en/ui.ts +++ b/src/i18n/en/ui.ts @@ -72,8 +72,6 @@ export default { 'aside.tip': 'Tip', 'aside.caution': 'Caution', 'aside.danger': 'Danger', - // `` vocabulary - 'codeSnippet.terminalCaption': 'Terminal window', // `` vocabulary 'languageSelect.label': 'Select language', // Integrations vocabulary @@ -112,9 +110,10 @@ export default { 'feedback.success': 'Thanks! We received your feedback.', // `` component 'fileTree.directoryLabel': 'Directory', - // CopyButton class in `` component - 'copyButton.title': 'Copy to clipboard', - 'copyButton.tooltip': 'Copied!', + // Code snippet vocabulary + 'expressiveCode.terminalWindowFallbackTitle': 'Terminal window', + 'expressiveCode.copyButtonTooltip': 'Copy to clipboard', + 'expressiveCode.copyButtonCopied': 'Copied!', // Backend Guides vocabulary 'backend.navTitle': 'More backend service guides', }; diff --git a/src/i18n/es/ui.ts b/src/i18n/es/ui.ts index ac61313b97e62..23ba431583bde 100644 --- a/src/i18n/es/ui.ts +++ b/src/i18n/es/ui.ts @@ -75,8 +75,6 @@ export default UIDictionary({ 'aside.tip': 'Consejo', 'aside.caution': 'Precaución', 'aside.danger': 'Peligro', - // Vocabulario de - 'codeSnippet.terminalCaption': 'Ventana de terminal', // Vocabulario de 'languageSelect.label': 'Seleccionar idioma', // Vocabulario de integraciones @@ -115,9 +113,10 @@ export default UIDictionary({ 'feedback.success': '¡Gracias! Hemos recibido tu opinión.', // Componente 'fileTree.directoryLabel': 'Directorio', - // Clase CopyButton en el componente - 'copyButton.title': 'Copiar al portapapeles', - 'copyButton.tooltip': '¡Copiado!', + // Code snippet vocabulary + 'expressiveCode.terminalWindowFallbackTitle': 'Ventana de terminal', + 'expressiveCode.copyButtonTooltip': 'Copiar al portapapeles', + 'expressiveCode.copyButtonCopied': '¡Copiado!', // Vocabulario de guías de backend 'backend.navTitle': 'Más guías de servicios backend', }); diff --git a/src/i18n/ja/ui.ts b/src/i18n/ja/ui.ts index 72239d0c66c98..2b6c09a860aea 100644 --- a/src/i18n/ja/ui.ts +++ b/src/i18n/ja/ui.ts @@ -71,8 +71,6 @@ export default UIDictionary({ 'aside.tip': 'ヒント', 'aside.caution': '注意', 'aside.danger': '危険', - // `` vocabulary - 'codeSnippet.terminalCaption': 'ターミナルウィンドウ', // `` vocabulary 'languageSelect.label': '言語の選択', // Integrations vocabulary @@ -111,6 +109,8 @@ export default UIDictionary({ 'feedback.success': 'ありがとうございますlフィードバックを受け取りました。', // `` component 'fileTree.directoryLabel': 'ディレクトリ', + // Code snippet vocabulary + 'expressiveCode.terminalWindowFallbackTitle': 'ターミナルウィンドウ', // Backend Guides vocabulary 'backend.navTitle': 'その他のバックエンドサービスガイド', }); diff --git a/src/i18n/util.ts b/src/i18n/util.ts index 035be201d831c..8806024fe9399 100644 --- a/src/i18n/util.ts +++ b/src/i18n/util.ts @@ -21,7 +21,7 @@ function mapDefaultExports(modules: Record) { return exportMap; } -const translations = mapDefaultExports(import.meta.glob('./*/ui.ts', { eager: true })); +export const translations = mapDefaultExports(import.meta.glob('./*/ui.ts', { eager: true })); const docsearchTranslations = mapDefaultExports( import.meta.glob('./*/docsearch.ts', { eager: true }) ); diff --git a/src/i18n/zh-cn/ui.ts b/src/i18n/zh-cn/ui.ts index 03719c4aeb4f5..e0adb0848c6c5 100644 --- a/src/i18n/zh-cn/ui.ts +++ b/src/i18n/zh-cn/ui.ts @@ -71,8 +71,6 @@ export default UIDictionary({ 'aside.tip': '提示', 'aside.caution': '警告', 'aside.danger': '危险', - // `` vocabulary - 'codeSnippet.terminalCaption': '终端窗口', // `` vocabulary 'languageSelect.label': '选择语言', // Integrations vocabulary @@ -111,9 +109,10 @@ export default UIDictionary({ 'feedback.success': '感谢!我们收到了你的反馈。', // `` component 'fileTree.directoryLabel': '目录', - // CopyButton class in `` component - 'copyButton.title': '复制到剪贴板', - 'copyButton.tooltip': '复制成功!', + // Code snippet vocabulary + 'expressiveCode.terminalWindowFallbackTitle': '终端窗口', + 'expressiveCode.copyButtonTooltip': '复制到剪贴板', + 'expressiveCode.copyButtonCopied': '复制成功!', // Backend Guides vocabulary 'backend.navTitle': '更多后端服务指南', });