diff --git a/package.json b/package.json
index 2df824f51804..00ed731b47ac 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"check-github-github-links": "node src/links/scripts/check-github-github-links.js",
"close-dangling-prs": "tsx src/workflows/close-dangling-prs.ts",
"content-changes-table-comment": "tsx src/workflows/content-changes-table-comment.ts",
+ "convert-liquid-markdown-tables": "tsx src/tools/scripts/convert-liquid-markdown-tables.ts",
"copy-fixture-data": "node src/tests/scripts/copy-fixture-data.js",
"count-translation-corruptions": "tsx src/languages/scripts/count-translation-corruptions.ts",
"debug": "cross-env NODE_ENV=development ENABLED_LANGUAGES=en nodemon --inspect src/frame/server.ts",
@@ -42,6 +43,7 @@
"lint": "eslint '**/*.{js,mjs,ts,tsx}'",
"lint-content": "node src/content-linter/scripts/lint-content.js",
"lint-translation": "vitest src/content-linter/tests/lint-files.js",
+ "liquid-markdown-tables": "tsx src/tools/scripts/liquid-markdown-tables/index.ts",
"generate-code-scanning-query-list": "tsx src/code-scanning/scripts/generate-code-scanning-query-list.ts",
"generate-content-linter-docs": "tsx src/content-linter/scripts/generate-docs.ts",
"move-content": "node src/content-render/scripts/move-content.js",
diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts
index 268779d0deba..20e963a4f4e7 100644
--- a/src/fixtures/tests/playwright-rendering.spec.ts
+++ b/src/fixtures/tests/playwright-rendering.spec.ts
@@ -615,6 +615,7 @@ test.describe('translations', () => {
test('switch to Japanese from English using widget on article', async ({ page }) => {
await page.goto('/get-started/start-your-journey/hello-world')
+ await expect(page).toHaveURL('/en/get-started/start-your-journey/hello-world')
await page.getByRole('button', { name: 'Select language: current language is English' }).click()
await page.getByRole('menuitemradio', { name: '日本語' }).click()
await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world')
@@ -631,7 +632,6 @@ test.describe('translations', () => {
// If you go, with the Japanese cookie, to the English page directly,
// it will offer a link to the Japanese URL in a banner.
await page.goto('/en/get-started/start-your-journey/hello-world')
- await page.getByRole('link', { name: 'Japanese' }).click()
await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world')
})
})
diff --git a/src/frame/components/ClientSideLanguageRedirect.ts b/src/frame/components/ClientSideLanguageRedirect.ts
new file mode 100644
index 000000000000..6d75c2fe7705
--- /dev/null
+++ b/src/frame/components/ClientSideLanguageRedirect.ts
@@ -0,0 +1,21 @@
+import { useEffect } from 'react'
+import { useRouter } from 'next/router'
+
+import { useLanguages } from 'src/languages/components/LanguagesContext'
+import Cookies from 'src/frame/components/lib/cookies'
+import { USER_LANGUAGE_COOKIE_NAME } from 'src/frame/lib/constants.js'
+
+export function ClientSideLanguageRedirect() {
+ const { locale, asPath, replace } = useRouter()
+ const { languages } = useLanguages()
+ const availableLanguageKeys = new Set(Object.keys(languages))
+
+ useEffect(() => {
+ const cookieValue = Cookies.get(USER_LANGUAGE_COOKIE_NAME)
+ if (cookieValue && cookieValue !== locale && availableLanguageKeys.has(cookieValue)) {
+ const newPath = `/${cookieValue}${asPath}`
+ replace(newPath, undefined, { locale: cookieValue })
+ }
+ }, [locale, availableLanguageKeys, asPath])
+ return null
+}
diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx
index d6fecb1985e5..57ca23c59120 100644
--- a/src/frame/components/DefaultLayout.tsx
+++ b/src/frame/components/DefaultLayout.tsx
@@ -12,6 +12,7 @@ import { useMainContext } from 'src/frame/components/context/MainContext'
import { useTranslation } from 'src/languages/components/useTranslation'
import { Breadcrumbs } from 'src/frame/components/page-header/Breadcrumbs'
import { useLanguages } from 'src/languages/components/LanguagesContext'
+import { ClientSideLanguageRedirect } from './ClientSideLanguageRedirect'
import { DomainNameEditProvider } from 'src/links/components/useEditableDomainContext'
const MINIMAL_RENDER = Boolean(JSON.parse(process.env.MINIMAL_RENDER || 'false'))
@@ -124,6 +125,7 @@ export const DefaultLayout = (props: Props) => {
Skip to main content
+
{isHomepageVersion ? null : }
{/* Need to set an explicit height for sticky elements since we also
diff --git a/src/tools/scripts/liquid-markdown-tables/convert.ts b/src/tools/scripts/liquid-markdown-tables/convert.ts
new file mode 100644
index 000000000000..587c5a40d5fe
--- /dev/null
+++ b/src/tools/scripts/liquid-markdown-tables/convert.ts
@@ -0,0 +1,43 @@
+/**
+ * See docstring in index.ts for more information about how to use this script.
+ */
+import fs from 'fs'
+
+import chalk from 'chalk'
+
+import { processFile } from './lib'
+
+type Options = {
+ dryRun: boolean
+}
+
+export async function convert(files: string[], options: Options) {
+ if (!files.length) {
+ console.error(chalk.red('No files specified'))
+ process.exit(1)
+ }
+
+ for (const file of files) {
+ const info = fs.statSync(file)
+ if (info.isDirectory()) {
+ console.error(chalk.red('Directories are currently not supported. Only files.'))
+ process.exit(1)
+ }
+ }
+
+ for (const file of files) {
+ console.log(chalk.grey(`Processing file ${chalk.bold(file)}`))
+ const content = fs.readFileSync(file, 'utf8')
+ const newContent = await processFile(content)
+ if (content !== newContent) {
+ if (options.dryRun) {
+ console.log(chalk.green('Would have written changes to disk'))
+ } else {
+ console.log(chalk.green(`Updating ${chalk.bold(file)}`))
+ fs.writeFileSync(file, newContent, 'utf-8')
+ }
+ } else {
+ console.log(chalk.yellow('No changes needed'))
+ }
+ }
+}
diff --git a/src/tools/scripts/liquid-markdown-tables/find.ts b/src/tools/scripts/liquid-markdown-tables/find.ts
new file mode 100644
index 000000000000..f696bfa30150
--- /dev/null
+++ b/src/tools/scripts/liquid-markdown-tables/find.ts
@@ -0,0 +1,45 @@
+import fs from 'fs'
+
+import chalk from 'chalk'
+import walk from 'walk-sync'
+
+import { processFile } from './lib'
+
+type Options = {
+ filter?: string[]
+}
+
+export async function find(options: Options) {
+ const files = [
+ ...walk('data', {
+ includeBasePath: true,
+ globs: ['**/*.md'],
+ ignore: ['**/README.md'],
+ }),
+ ...walk('content', {
+ includeBasePath: true,
+ globs: ['**/*.md'],
+ ignore: ['**/README.md'],
+ }),
+ ].filter((filePath) => {
+ if (options.filter && options.filter.length) {
+ return options.filter.some((filter) => filePath.includes(filter))
+ }
+ return true
+ })
+ console.log(chalk.grey(`${chalk.bold(files.length.toLocaleString())} files to search.`))
+
+ const found: string[] = []
+ for (const filePath of files) {
+ const content = fs.readFileSync(filePath, 'utf8')
+ const newContent = await processFile(content)
+ if (content !== newContent) {
+ console.log(chalk.green(filePath))
+ found.push(filePath)
+ }
+ }
+ console.log('\n')
+ console.log(
+ chalk.grey(`Found ${chalk.bold(found.length.toLocaleString())} files that can be converted.`),
+ )
+}
diff --git a/src/tools/scripts/liquid-markdown-tables/index.ts b/src/tools/scripts/liquid-markdown-tables/index.ts
new file mode 100644
index 000000000000..e904256eb8b8
--- /dev/null
+++ b/src/tools/scripts/liquid-markdown-tables/index.ts
@@ -0,0 +1,69 @@
+/**
+ * This script helps you rewrite Markdown files that might contain
+ * tables with Liquid `ifversion` tags the old/wrong way.
+ * For example:
+ *
+ * | Header | Header 2 |
+ * |--------|----------|
+ * | bla | bla |{% ifversion dependency-review-action-licenses %}
+ * | foo | foo |{% endif %}{% ifversion dependency-review-action-fail-on-scopes %}
+ * | bar | bar |{% endif %}
+ * | baz | baz |
+ * {%- ifversion dependency-review-action-licenses %}
+ * | qux | qux |{% endif %}
+ *
+ * Will become:
+ *
+ * | Header | Header 2 |
+ * |--------|----------|
+ * | bla | bla |
+ * | {% ifversion dependency-review-action-licenses %} |
+ * | foo | foo |
+ * | {% endif %} |
+ * | {% ifversion dependency-review-action-fail-on-scopes %} |
+ * | bar | bar |
+ * | {% endif %} |
+ * | baz | baz |
+ * | {% ifversion dependency-review-action-licenses %} |
+ * | qux | qux |
+ * | {% endif %} |
+ *
+ * Run the script like this:
+ *
+ * npm run liquid-markdown-tables -- convert content/path/to/article.md
+ * git diff
+ *
+ * To *find* files that you *can* convert, use:
+ *
+ * npm run liquid-markdown-tables -- find
+ * # or
+ * npm run liquid-markdown-tables -- find --filter content/mydocset
+ *
+ * This will print out paths to files that most likely contain the old/wrong Liquid `ifversion` tags.
+ *
+ */
+
+import { program } from 'commander'
+
+import { convert } from './convert'
+import { find } from './find'
+
+program
+ .name('liquid-markdown-tables')
+ .description('CLI for finding and converting Liquid in Markdown tables')
+
+program
+ .command('convert')
+ .description('Clean up Markdown tables that use Liquid `ifversion` tags the old/wrong way')
+ .option('--dry-run', "Don't actually write changes to disk", false)
+ // .arguments('[files-or-directories...]', '')
+ .arguments('[files...]')
+ .action(convert)
+
+program
+ .command('find')
+ .description('Find Markdown tables that use Liquid `ifversion` tags the old/wrong way')
+ .option('--filter ', 'Filter by file path')
+ .action(find)
+
+program.parse(process.argv)
diff --git a/src/tools/scripts/liquid-markdown-tables/lib.ts b/src/tools/scripts/liquid-markdown-tables/lib.ts
new file mode 100644
index 000000000000..6c071d055a42
--- /dev/null
+++ b/src/tools/scripts/liquid-markdown-tables/lib.ts
@@ -0,0 +1,68 @@
+// E.g. `{%- ifversion dependency-review-action-licenses %}\n`
+const ifVersionRegex = /^{%-?\s*ifversion\s+([\w- ]+)\s*-?%}\n/
+const ifVersionEndRegex = /\|({%-?\s*ifversion\s+([\w- ]+)\s*-?%})\n/
+// E.g. `... |{% endif %}{% ifversion dependency-review-action-fail-on-scopes %}\n`
+const endifIfVersionRegex = /\|({%-?\s*endif\s*%})({%-?\sifversion\s+([\w- ]+)\s*-?%})\n/
+const endifRegex = /\|({%-?\s*endif\s*%})\n/
+const endifAloneRegex = /^({%-?\s*endif\s*%})\n/
+
+// Split a string by newlines while keeping the newlines
+function splitAndKeepNewlines(str: string) {
+ const lines = str.split(/(\r\n|\r|\n)/)
+ const result: string[] = []
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].match(/(\r\n|\r|\n)/)) {
+ result[result.length - 1] += lines[i]
+ } else {
+ result.push(lines[i])
+ }
+ }
+ return result
+}
+
+export async function processFile(content: string) {
+ let inTable = false
+ let inFrontmatter = false
+ let inMarkdown = false
+ const newLines: string[] = []
+ for (let line of splitAndKeepNewlines(content)) {
+ if (line === '---\n') {
+ if (!inFrontmatter) {
+ inFrontmatter = true
+ } else {
+ inFrontmatter = false
+ inMarkdown = true
+ }
+ }
+ if (inMarkdown) {
+ if (line.startsWith('|') && line.endsWith('|\n')) {
+ inTable = true
+ } else if (inTable && line === '\n') {
+ inTable = false
+ }
+ if (inTable) {
+ // E.g. `{%- ifversion dependency-review-action-licenses %}\n`
+ if (ifVersionRegex.test(line)) {
+ const better = line.replace('{%-', '{%').replace('-%}', '%}').trim()
+ line = `| ${better} |\n`
+ } else if (ifVersionEndRegex.test(line)) {
+ line = line
+ .replace(ifVersionEndRegex, '|\n| $1 |\n')
+ .replace('{%-', '{%')
+ .replace('-%}', '%}')
+ } else if (endifIfVersionRegex.test(line)) {
+ line = line
+ .replace(endifIfVersionRegex, '|\n| $1 |\n| $2 |\n')
+ .replace('{%-', '{%')
+ .replace('-%}', '%}')
+ } else if (endifRegex.test(line)) {
+ line = line.replace(endifRegex, '|\n| $1 |\n')
+ } else if (endifAloneRegex.test(line)) {
+ line = line.replace(endifAloneRegex, '| $1 |\n').replace('{%-', '{%')
+ }
+ }
+ }
+ newLines.push(line)
+ }
+ return newLines.join('')
+}