diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0e9c5774ba3..1a230368719 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -51,6 +51,7 @@ jobs: filters: | docs: - '.github/workflows/deploy-docs.yml' + - 'crowdin.yml' - 'docs/**' - 'scripts/generate_docs_json.py' - 'invokeai/app/**' @@ -111,6 +112,10 @@ jobs: run: pnpm run check-docs-data working-directory: docs + - name: test localization helpers + run: pnpm run test:localization + working-directory: docs + - name: build docs run: pnpm build working-directory: docs diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..10d372206dc --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,23 @@ +preserve_hierarchy: true + +files: + - source: /docs/src/content/docs/index.mdx + translation: /docs/src/content/docs/%two_letters_code%/%original_file_name% + + - source: /docs/src/content/docs/start-here/**/*.{md,mdx} + translation: /docs/src/content/docs/%two_letters_code%/start-here/**/%original_file_name% + + - source: /docs/src/content/docs/configuration/**/*.{md,mdx} + translation: /docs/src/content/docs/%two_letters_code%/configuration/**/%original_file_name% + + - source: /docs/src/content/docs/concepts/**/*.{md,mdx} + translation: /docs/src/content/docs/%two_letters_code%/concepts/**/%original_file_name% + + - source: /docs/src/content/docs/features/**/*.{md,mdx} + translation: /docs/src/content/docs/%two_letters_code%/features/**/%original_file_name% + + - source: /docs/src/content/docs/troubleshooting/**/*.{md,mdx} + translation: /docs/src/content/docs/%two_letters_code%/troubleshooting/**/%original_file_name% + + - source: /docs/src/content/i18n/en.json + translation: /docs/src/content/i18n/%two_letters_code%.json diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index ebb59d36115..1984590b58a 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -7,6 +7,7 @@ import starlightLinksValidator from 'starlight-links-validator'; import starlightLlmsText from 'starlight-llms-txt'; import starlightChangelogs from 'starlight-changelogs'; import { rehypePrefixBaseToRootLinks } from './plugins/rehype-prefix-base-to-root-links.mjs'; +import { remarkLocalizeContent } from './plugins/remark-localize-content.mjs'; import starlightContextualMenu from 'starlight-contextual-menu'; // Configs @@ -34,6 +35,7 @@ export default defineConfig({ site, base: base || undefined, markdown: { + remarkPlugins: [[remarkLocalizeContent, { locales: ['de', 'es', 'hi'] }]], rehypePlugins: [[rehypePrefixBaseToRootLinks, { base }]], }, integrations: [ @@ -41,6 +43,9 @@ export default defineConfig({ // Content title: { en: 'InvokeAI Documentation', + de: 'InvokeAI-Dokumentation', + es: 'Documentación de InvokeAI', + hi: 'InvokeAI दस्तावेज़', }, logo: { src: './src/assets/invoke-icon-wide.svg', @@ -58,6 +63,18 @@ export default defineConfig({ label: 'English', lang: 'en', }, + de: { + label: 'Deutsch', + lang: 'de', + }, + es: { + label: 'Español', + lang: 'es', + }, + hi: { + label: 'हिन्दी', + lang: 'hi', + }, }, social: socialConfig, tableOfContents: { @@ -73,12 +90,16 @@ export default defineConfig({ ThemeProvider: './src/lib/components/ForceDarkTheme.astro', ThemeSelect: './src/lib/components/EmptyComponent.astro', Footer: './src/lib/components/Footer.astro', + EditLink: './src/lib/components/EditLink.astro', + MarkdownContent: './src/lib/components/MarkdownContent.astro', PageFrame: './src/layouts/PageFrameExtended.astro', }, plugins: [ starlightLinksValidator({ errorOnRelativeLinks: false, errorOnLocalLinks: false, + // The validator only knows content collection routes, not custom Astro pages. + exclude: ['/download/'], }), starlightLlmsText(), starlightChangelogs(), diff --git a/docs/package.json b/docs/package.json index 29d0786e8ea..23f700f22e5 100644 --- a/docs/package.json +++ b/docs/package.json @@ -9,6 +9,7 @@ "check-docs-data": "pnpm run generate-docs-data && git diff --exit-code -- src/generated", "check-deploy-output": "node ./scripts/verify-deploy-output.mjs", "check-redirects": "node ./scripts/validate-redirect-targets.mjs", + "test:localization": "node --test ./plugins/remark-localize-content.test.mjs", "build": "astro build", "preview": "astro preview", "astro": "astro" diff --git a/docs/plugins/remark-localize-content.mjs b/docs/plugins/remark-localize-content.mjs new file mode 100644 index 00000000000..86f22516963 --- /dev/null +++ b/docs/plugins/remark-localize-content.mjs @@ -0,0 +1,127 @@ +import { existsSync } from 'node:fs'; +import { dirname, relative, resolve, sep } from 'node:path'; + +const relativeImportPattern = /(from\s+|import\s*)(['"])(\.\.?\/[^'"]+)\2/g; + +export function remarkLocalizeContent(options = {}) { + const locales = new Set(options.locales ?? []); + + return (tree, file) => { + const context = getLocalizedFileContext(file?.path, locales); + + if (!context) { + return; + } + + walk(tree, (node) => { + if (node.type === 'link' && typeof node.url === 'string') { + node.url = localizeRootLink(node.url, context.locale); + } + + if (node.type === 'image' && typeof node.url === 'string') { + node.url = pointToSourceAsset(node.url, context); + } + + if (node.type === 'mdxjsEsm' && typeof node.value === 'string') { + node.value = node.value.replace(relativeImportPattern, (match, prefix, quote, specifier) => { + const localizedSpecifier = pointToSourceAsset(specifier, context); + return `${prefix}${quote}${localizedSpecifier}${quote}`; + }); + } + + if (node.type === 'mdxJsxAttribute' && typeof node.value === 'string') { + if (node.name === 'href' || node.name === 'link') { + node.value = localizeRootLink(node.value, context.locale); + } else if (node.name === 'src') { + node.value = pointToSourceAsset(node.value, context); + } + } + }); + }; +} + +function getLocalizedFileContext(filePath, locales) { + if (typeof filePath !== 'string') { + return undefined; + } + + const normalizedPath = filePath.split(sep).join('/'); + const docsMarker = '/src/content/docs/'; + const markerIndex = normalizedPath.lastIndexOf(docsMarker); + + if (markerIndex === -1) { + return undefined; + } + + const docsRoot = normalizedPath.slice(0, markerIndex + docsMarker.length - 1); + const pathWithinDocs = normalizedPath.slice(markerIndex + docsMarker.length); + const [locale, ...sourceSegments] = pathWithinDocs.split('/'); + + if (!locales.has(locale) || sourceSegments.length === 0) { + return undefined; + } + + return { + locale, + localizedDirectory: dirname(normalizedPath), + sourceDirectory: dirname(resolve(docsRoot, ...sourceSegments)), + }; +} + +function localizeRootLink(url, locale) { + if (!url.startsWith('/') || url.startsWith('//')) { + return url; + } + + if ( + url === `/${locale}` || + url.startsWith(`/${locale}/`) || + url === '/download' || + url.startsWith('/download/') + ) { + return url; + } + + return `/${locale}${url}`; +} + +function pointToSourceAsset(url, context) { + if (!url.startsWith('./') && !url.startsWith('../')) { + return url; + } + + const suffixIndex = url.search(/[?#]/); + const pathPart = suffixIndex === -1 ? url : url.slice(0, suffixIndex); + const suffix = suffixIndex === -1 ? '' : url.slice(suffixIndex); + const sourceTarget = resolve(context.sourceDirectory, pathPart); + + if (!existsSync(sourceTarget)) { + return url; + } + + const localizedPath = relative(context.localizedDirectory, sourceTarget).split(sep).join('/'); + const normalizedPath = localizedPath.startsWith('.') ? localizedPath : `./${localizedPath}`; + return `${normalizedPath}${suffix}`; +} + +function walk(node, visitor) { + if (!node || typeof node !== 'object') { + return; + } + + visitor(node); + + if (!Array.isArray(node.children)) { + return; + } + + for (const child of node.children) { + walk(child, visitor); + } +} + +export const testing = { + getLocalizedFileContext, + localizeRootLink, + pointToSourceAsset, +}; diff --git a/docs/plugins/remark-localize-content.test.mjs b/docs/plugins/remark-localize-content.test.mjs new file mode 100644 index 00000000000..010d2e4b502 --- /dev/null +++ b/docs/plugins/remark-localize-content.test.mjs @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +import { remarkLocalizeContent, testing } from './remark-localize-content.mjs'; + +const docsRoot = fileURLToPath(new URL('../src/content/docs/', import.meta.url)); + +test('localizes links and reuses source assets in translated MDX', () => { + const tree = { + type: 'root', + children: [ + { type: 'link', url: '/concepts/prompting-guide/', children: [] }, + { type: 'link', url: 'https://example.com/', children: [] }, + { type: 'image', url: './assets/gallery.png', children: [] }, + { + type: 'mdxJsxFlowElement', + children: [ + { type: 'mdxJsxAttribute', name: 'href', value: '/troubleshooting/faq/' }, + { type: 'mdxJsxAttribute', name: 'href', value: '/download/' }, + ], + }, + ], + }; + + remarkLocalizeContent({ locales: ['de', 'es', 'hi'] })(tree, { + path: `${docsRoot}es/features/gallery.mdx`, + }); + + assert.equal(tree.children[0].url, '/es/concepts/prompting-guide/'); + assert.equal(tree.children[1].url, 'https://example.com/'); + assert.equal(tree.children[2].url, '../../features/assets/gallery.png'); + assert.equal(tree.children[3].children[0].value, '/es/troubleshooting/faq/'); + assert.equal(tree.children[3].children[1].value, '/download/'); +}); + +test('rewrites relative MDX asset imports to the English source tree', () => { + const tree = { + type: 'root', + children: [ + { + type: 'mdxjsEsm', + value: "import splashImage from './assets/invoke-webui-canvas.png';", + children: [], + }, + ], + }; + + remarkLocalizeContent({ locales: ['de', 'es', 'hi'] })(tree, { + path: `${docsRoot}hi/index.mdx`, + }); + + assert.equal( + tree.children[0].value, + "import splashImage from '../assets/invoke-webui-canvas.png';", + ); +}); + +test('leaves English source files unchanged', () => { + const tree = { + type: 'root', + children: [{ type: 'link', url: '/configuration/docker/', children: [] }], + }; + + remarkLocalizeContent({ locales: ['de', 'es', 'hi'] })(tree, { + path: `${docsRoot}index.mdx`, + }); + + assert.equal(tree.children[0].url, '/configuration/docker/'); + assert.equal( + testing.getLocalizedFileContext(`${docsRoot}it/index.mdx`, new Set(['de', 'es', 'hi'])), + undefined, + ); +}); diff --git a/docs/scripts/verify-deploy-output.mjs b/docs/scripts/verify-deploy-output.mjs index 885d1f44baa..d99b2a8de4d 100644 --- a/docs/scripts/verify-deploy-output.mjs +++ b/docs/scripts/verify-deploy-output.mjs @@ -3,6 +3,8 @@ import { readFileSync } from 'node:fs'; const deployTarget = process.env.DEPLOY_TARGET ?? 'custom'; const base = deployTarget === 'ghpages' ? '/InvokeAI' : ''; const withBase = (path) => `${base}${path}`; +const siteUrl = (path) => + deployTarget === 'ghpages' ? `https://invoke-ai.github.io${base}${path}` : `https://invoke.ai${path}`; const expectations = [ { @@ -45,6 +47,24 @@ const expectations = [ }, ]; +for (const locale of ['de', 'es', 'hi']) { + expectations.push({ + file: `${locale}/start-here/installation/index.html`, + includes: [ + ` - - src/ - - content/ - - docs/ - - start-here/ - - installation.mdx - - i18n/ - - zh-CN Country code here - - start-here/ - - installation.mdx - - -We recommend simply copy/pasting the file and rewriting the text from there. - -Learn more about the intricacies of translating Astro Starlight docs [here](https://starlight.astro.build/guides/i18n). +Do not manually edit exported locale files. Make English documentation changes in GitHub and translation changes in Crowdin. Crowdin maintains a translation pull request, and the documentation workflow builds every locale before the PR is merged. ## Running a Build diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index a0c7e3e67c6..7cfbf5357b4 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -84,7 +84,7 @@ Whether you are looking to install the app, get support, train your own models, Ready to dive in? The [Invoke Launcher](/start-here/installation/) is the fastest way to get up and running on Windows, macOS, and Linux. For advanced setups, try [Docker](/configuration/docker/) or a [manual Python installation](/start-here/manual/). - + Get Invoke diff --git a/docs/src/content/i18n/en.json b/docs/src/content/i18n/en.json index 69333e3a0b2..e9f13c64db2 100644 --- a/docs/src/content/i18n/en.json +++ b/docs/src/content/i18n/en.json @@ -1,45 +1,32 @@ { - "skipLink.label": "Skip to content", - "search.label": "Search", - "search.ctrlKey": "Ctrl", - "search.cancelLabel": "Cancel", - "search.devWarning": "Search is only available in production builds. \nTry building and previewing the site to test it out locally.", - "themeSelect.accessibleLabel": "Select theme", - "themeSelect.dark": "Dark", - "themeSelect.light": "Light", - "themeSelect.auto": "Auto", - "languageSelect.accessibleLabel": "Select language", - "menuButton.accessibleLabel": "Menu", - "sidebarNav.accessibleLabel": "Main", - "tableOfContents.onThisPage": "On this page", - "tableOfContents.overview": "Overview", - "i18n.untranslatedContent": "This content is not available in your language yet.", - "page.editLink": "Edit page", - "page.lastUpdated": "Last updated:", - "page.previousLink": "Previous", - "page.nextLink": "Next", - "page.draft": "This content is a draft and will not be included in production builds.", - "404.text": "Page not found. Check the URL or try using the search bar.", - "aside.note": "Note", - "aside.tip": "Tip", - "aside.caution": "Caution", - "aside.danger": "Danger", - "fileTree.directory": "Directory", - "builtWithStarlight.label": "Built with Starlight", - "heading.anchorLabel": "Section titled “{{title}}”", - - "expressiveCode.copyButtonCopied": "Copied!", - "expressiveCode.copyButtonTooltip": "Copy to clipboard", - "expressiveCode.terminalWindowFallbackTitle": "Terminal window", - - "pagefind.clear_search": "Clear", - "pagefind.load_more": "Load more results", - "pagefind.search_label": "Search this site", - "pagefind.filters_label": "Filters", - "pagefind.zero_results": "No results for [SEARCH_TERM]", - "pagefind.many_results": "[COUNT] results for [SEARCH_TERM]", - "pagefind.one_result": "[COUNT] result for [SEARCH_TERM]", - "pagefind.alt_search": "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead", - "pagefind.search_suggestion": "No results for [SEARCH_TERM]. Try one of the following searches:", - "pagefind.searching": "Searching for [SEARCH_TERM]..." + "footer.builtBy": "This site was designed and developed by", + "page.translateLink": "Translate this page", + "download.windows.headline": "Download for Windows", + "download.windows.note": "Requires Windows 10 or later, and NVIDIA or AMD GPU.", + "download.windows.action": "Download EXE", + "download.macos.headline": "Download for macOS", + "download.macos.note": "Requires Apple Silicon (M-Series). Not compatible with Intel.", + "download.macos.action": "Download DMG", + "download.linux.headline": "Download for Linux", + "download.linux.note": "Requires NVIDIA or AMD GPU. Compatible with most distributions.", + "download.linux.action": "Download AppImage", + "download.github.headline": "Download from GitHub", + "download.github.description": "For advanced users who want to set up Invoke manually or contribute to the project.", + "download.docker.headline": "Run with Docker", + "download.docker.description": "For users who want to run Invoke without installing dependencies directly on their system.", + "download.hosted.heading": "Hosted Options", + "download.hosted.description": "For users who want to run Invoke on a hosted GPU service instead of their own hardware.", + "download.aibadgr.headline": "Run on AI Badgr", + "download.aibadgr.description": "Run on the AI Badgr hosted GPU service.", + "download.runpod.headline": "Run on RunPod", + "download.runpod.description": "Run on the RunPod hosted GPU service.", + "download.railway.headline": "Run on Railway", + "download.railway.description": "Run on the Railway hosted GPU service.", + "download.separator": "OR", + "systemRequirements.title": "System Requirements", + "systemRequirements.description": "Please check the system requirements page to make sure your hardware is capable of running the desired models.", + "settings.type": "Type", + "settings.default": "Default", + "settings.environment": "Env", + "settings.values": "Values:" } diff --git a/docs/src/lib/base-path.ts b/docs/src/lib/base-path.ts index 23042d899a5..8e80ab7f70c 100644 --- a/docs/src/lib/base-path.ts +++ b/docs/src/lib/base-path.ts @@ -4,3 +4,12 @@ export const withBase = (path: string, baseUrl: string) => { return `${normalizedBase}${normalizedPath}`; }; + +export const localizePath = (path: string, locale?: string) => { + if (!locale) { + return path; + } + + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + return `/${locale}${normalizedPath}`; +}; diff --git a/docs/src/lib/components/DownloadOptions.astro b/docs/src/lib/components/DownloadOptions.astro index 95124322fd9..51a73e4d28b 100644 --- a/docs/src/lib/components/DownloadOptions.astro +++ b/docs/src/lib/components/DownloadOptions.astro @@ -1,7 +1,16 @@ --- import { CardGrid, LinkCard, Icon, LinkButton } from '@astrojs/starlight/components'; import { type StarlightIcon } from '@astrojs/starlight/types'; -import { withBase } from '../base-path'; +import { localizePath, withBase } from '../base-path'; + +const t = Astro.locals.t; +let locale: string | undefined; +try { + locale = Astro.locals.starlightRoute.locale; +} catch { + // Content is also rendered outside a Starlight route when generating llms-full.txt. + locale = undefined; +} type LauncherDownloadOption = { icon: StarlightIcon; @@ -13,59 +22,59 @@ type LauncherDownloadOption = { const launcherDownloadOptions: Record = { windows: { icon: 'seti:windows', - headline: 'Download for Windows', - note: 'Requires Windows 10 or later, and NVIDIA or AMD GPU.', + headline: t('download.windows.headline'), + note: t('download.windows.note'), launcherDownloadLink: 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe', - launcherDownloadLabel: 'Download EXE', + launcherDownloadLabel: t('download.windows.action'), }, macos: { icon: 'apple', - headline: 'Download for MacOS', - note: 'Requires Apple Silicon (M-Series). Not compatible with Intel.', + headline: t('download.macos.headline'), + note: t('download.macos.note'), launcherDownloadLink: 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg', - launcherDownloadLabel: 'Download DMG', + launcherDownloadLabel: t('download.macos.action'), }, linux: { icon: 'linux', - headline: 'Download for Linux', - note: 'Requires NVIDIA or AMD GPU. Compatible with most distributions.', + headline: t('download.linux.headline'), + note: t('download.linux.note'), launcherDownloadLink: 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage', - launcherDownloadLabel: 'Download AppImage', + launcherDownloadLabel: t('download.linux.action'), }, }; const manualDownloadOptions = { github: { - headline: 'Download from GitHub', - description: 'For advanced users who want to set up Invoke manually or contribute to the project.', + headline: t('download.github.headline'), + description: t('download.github.description'), href: 'https://github.com/invoke-ai/InvokeAI/releases', }, docker: { - headline: 'Run with Docker', - description: 'For users who want to run Invoke without installing dependencies directly on their system.', - href: withBase('/configuration/docker/', import.meta.env.BASE_URL), + headline: t('download.docker.headline'), + description: t('download.docker.description'), + href: withBase(localizePath('/configuration/docker/', locale), import.meta.env.BASE_URL), }, }; const hostedOptions = { aibadgr: { - headline: 'Run on AI Badgr', - description: 'Run on the AI Badgr hosted GPU service', - href: 'https://aibadgr.com/gpu/launch?template=invokeai', - }, + headline: t('download.aibadgr.headline'), + description: t('download.aibadgr.description'), + href: 'https://aibadgr.com/gpu/launch?template=invokeai', + }, runpod: { - headline: 'Run on RunPod', - description: 'Run on the RunPod hosted GPU service', - href: 'https://www.runpod.io/blog/invoke-ai-stable-diffusion-runpod-nfz18', - }, + headline: t('download.runpod.headline'), + description: t('download.runpod.description'), + href: 'https://www.runpod.io/blog/invoke-ai-stable-diffusion-runpod-nfz18', + }, railway: { - headline: 'Run on Railway', - description: 'Run on the Railway hosted GPU service', - href: 'https://railway.com/deploy/invokeai', - }, + headline: t('download.railway.headline'), + description: t('download.railway.description'), + href: 'https://railway.com/deploy/invokeai', + }, }; --- @@ -99,7 +108,7 @@ const hostedOptions = {
- OR + {t('download.separator')}
@@ -113,8 +122,8 @@ const hostedOptions = {
-

Hosted Options

-

For users who want to run Invoke on a hosted GPU service instead of their own hardware.

+

{t('download.hosted.heading')}

+

{t('download.hosted.description')}

{ diff --git a/docs/src/lib/components/EditLink.astro b/docs/src/lib/components/EditLink.astro new file mode 100644 index 00000000000..943f70b60e1 --- /dev/null +++ b/docs/src/lib/components/EditLink.astro @@ -0,0 +1,32 @@ +--- +import { Icon } from '@astrojs/starlight/components'; + +const crowdinProjectUrl = 'https://crowdin.com/project/invoke'; +const { editUrl, locale } = Astro.locals.starlightRoute; +const href = locale ? crowdinProjectUrl : editUrl; +const label = locale ? Astro.locals.t('page.translateLink') : Astro.locals.t('page.editLink'); +--- + +{ + href && ( + + + {label} + + ) +} + + diff --git a/docs/src/lib/components/Footer.astro b/docs/src/lib/components/Footer.astro index 65137dec3b9..00c21017db4 100644 --- a/docs/src/lib/components/Footer.astro +++ b/docs/src/lib/components/Footer.astro @@ -5,7 +5,7 @@ import PageFooter from '@astrojs/starlight/components/Footer.astro';
- This site was designed and developed by Aether Fox Studio
+ +{ + locale && ( + + ) +} diff --git a/docs/src/lib/components/SettingsDocs.astro b/docs/src/lib/components/SettingsDocs.astro index c539b27cd46..e1c9e2c9d6e 100644 --- a/docs/src/lib/components/SettingsDocs.astro +++ b/docs/src/lib/components/SettingsDocs.astro @@ -1,6 +1,8 @@ --- import settingsData from '../../generated/settings.json'; +const t = Astro.locals.t; + const groupedSettings = Object.entries( settingsData.settings.reduce((groups, setting) => { const category = setting.category || 'OTHER'; @@ -62,15 +64,15 @@ const formatCategoryName = (category) => {
- Type + {t('settings.type')} {formatType(setting.type)}
- Default + {t('settings.default')} {formatValue(setting.default)}
- Env + {t('settings.environment')} {setting.env_var}
@@ -78,7 +80,7 @@ const formatCategoryName = (category) => { {setting.description} {setting.literal_values.length > 0 && ( - Values: {setting.literal_values.map((v) => {formatValue(v)}).reduce((prev, curr) => [prev, ' ', curr])} + {t('settings.values')} {setting.literal_values.map((v) => {formatValue(v)}).reduce((prev, curr) => [prev, ' ', curr])} )} {Object.keys(setting.validation).length > 0 && ( diff --git a/docs/src/lib/components/SystemRequirmentsLink.astro b/docs/src/lib/components/SystemRequirmentsLink.astro index 16455b8e749..64821d35862 100644 --- a/docs/src/lib/components/SystemRequirmentsLink.astro +++ b/docs/src/lib/components/SystemRequirmentsLink.astro @@ -1,10 +1,18 @@ --- import { LinkCard } from '@astrojs/starlight/components'; -import { withBase } from '../base-path'; +import { localizePath, withBase } from '../base-path'; + +let locale: string | undefined; +try { + locale = Astro.locals.starlightRoute.locale; +} catch { + // Content is also rendered outside a Starlight route when generating llms-full.txt. + locale = undefined; +} ---