From 7dd2921aa2823cb9018ceed7c86d894b687376e7 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Tue, 14 Jul 2026 08:14:37 +0200 Subject: [PATCH] Generate the language docs from /v3/languages Replace the hand-maintained language lists with generators that render them from the live GET /v3/languages response: the supported-languages table (snippets/language-table.jsx), the Voice support matrix and input/target lists, and the style-rules callout on the supported-languages page. Generated content sits between BEGIN/END GENERATED markers; everything outside them stays hand-written. node scripts/update-language-docs.mjs runs every generator; each script also runs standalone and supports --dry-run. Shared plumbing lives in scripts/lib.mjs with zero-dependency unit tests (node --test, wired up in CI via test-scripts.yml). A daily workflow (sync-language-docs.yml) regenerates the docs and opens or updates a bot PR whenever the API response changed, surfacing any generator warnings in the PR body. The regenerated content also refreshes the data itself: new Voice languages (Hindi, Malay, Tamil), the style-rules expansion from 8 to 33 target languages, and the full translate_text long tail in the language table. --- .github/workflows/sync-language-docs.yml | 36 +++ .github/workflows/test-scripts.yml | 21 ++ ...oice-api-service-specification-updates.mdx | 29 +- docs/getting-started/supported-languages.mdx | 6 +- ...supported-languages-formats-and-limits.mdx | 88 ++++--- scripts/generate-language-table.mjs | 118 +++++++++ scripts/generate-style-rules-lists.mjs | 27 ++ scripts/generate-voice-languages.mjs | 65 +++++ scripts/lib.mjs | 79 ++++++ scripts/lib.test.mjs | 165 ++++++++++++ scripts/open-sync-pr.sh | 47 ++++ scripts/update-language-docs.mjs | 25 ++ snippets/language-table.jsx | 248 +++++++++--------- 13 files changed, 776 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/sync-language-docs.yml create mode 100644 .github/workflows/test-scripts.yml create mode 100644 scripts/generate-language-table.mjs create mode 100644 scripts/generate-style-rules-lists.mjs create mode 100644 scripts/generate-voice-languages.mjs create mode 100644 scripts/lib.mjs create mode 100644 scripts/lib.test.mjs create mode 100755 scripts/open-sync-pr.sh create mode 100644 scripts/update-language-docs.mjs diff --git a/.github/workflows/sync-language-docs.yml b/.github/workflows/sync-language-docs.yml new file mode 100644 index 00000000..2d2105f7 --- /dev/null +++ b/.github/workflows/sync-language-docs.yml @@ -0,0 +1,36 @@ +name: Sync language docs + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +concurrency: + group: sync-language-docs + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 22 + - name: Regenerate language docs from /v3/languages + env: + DEEPL_API_KEY: ${{ secrets.DEEPL_API_KEY }} + # stderr goes to a file first (a `>(tee ...)` child is not awaited, the + # next step could read a partial file), then back into the step log + run: | + node scripts/update-language-docs.mjs "$DEEPL_API_KEY" 2> "$RUNNER_TEMP/warnings.txt" || { cat "$RUNNER_TEMP/warnings.txt" >&2; exit 1; } + cat "$RUNNER_TEMP/warnings.txt" >&2 + - name: Open or update the sync PR + env: + GH_TOKEN: ${{ github.token }} + WARNINGS_FILE: ${{ runner.temp }}/warnings.txt + run: scripts/open-sync-pr.sh diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml new file mode 100644 index 00000000..7aa0f189 --- /dev/null +++ b/.github/workflows/test-scripts.yml @@ -0,0 +1,21 @@ +name: Test scripts + +on: + pull_request: + paths: + - "scripts/**" + push: + branches: + - main + paths: + - "scripts/**" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 22 + - run: node --test scripts/*.test.mjs diff --git a/api-reference/voice/deepl-voice-api-service-specification-updates.mdx b/api-reference/voice/deepl-voice-api-service-specification-updates.mdx index ba6399d0..117fc622 100644 --- a/api-reference/voice/deepl-voice-api-service-specification-updates.mdx +++ b/api-reference/voice/deepl-voice-api-service-specification-updates.mdx @@ -33,6 +33,7 @@ audio provides the following functionality:

The realtime audio stream you want to have translated can be in one of the following languages:

@@ -79,6 +84,7 @@ audio provides the following functionality:

The language in which your translations are provided as text can be one of the following:

+

`pt-PT` covers all Portuguese varieties excluding Brazilian Portuguese. `pt` is an unspecified variant kept for backward compatibility; prefer `pt-PT` or `pt-BR`.

diff --git a/docs/getting-started/supported-languages.mdx b/docs/getting-started/supported-languages.mdx index 01b51b00..43dfc213 100644 --- a/docs/getting-started/supported-languages.mdx +++ b/docs/getting-started/supported-languages.mdx @@ -19,5 +19,9 @@ import { LanguageTable } from "/snippets/language-table.jsx" - Style rules are supported for the following target languages: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, and `zh`. For more details, see the [Style Rules API documentation](/docs/customize/using-style-rules). Writing style and tone availability for `/write/rephrase` can also be retrieved via [`GET /v3/languages?resource=write`](/docs/languages/using-the-languages-api). +{/* BEGIN GENERATED style-rules-languages (run: node scripts/update-language-docs.mjs) */} + Style rules are supported for the following target languages: `ar`, `bg`, `cs`, `da`, `de`, `el`, `en`, `es`, `et`, `fi`, `fr`, `he`, `hu`, `id`, `it`, `ja`, `ko`, `lt`, `lv`, `nb`, `nl`, `pl`, `pt`, `ro`, `ru`, `sk`, `sl`, `sv`, `th`, `tr`, `uk`, `vi`, and `zh`. +{/* END GENERATED style-rules-languages */} + + For more details, see the [Style Rules API documentation](/docs/customize/using-style-rules). Writing style and tone availability for `/write/rephrase` can also be retrieved via [`GET /v3/languages?resource=write`](/docs/languages/using-the-languages-api). diff --git a/docs/voice/supported-languages-formats-and-limits.mdx b/docs/voice/supported-languages-formats-and-limits.mdx index a3beeb82..256dc0b3 100644 --- a/docs/voice/supported-languages-formats-and-limits.mdx +++ b/docs/voice/supported-languages-formats-and-limits.mdx @@ -8,49 +8,51 @@ description: "Reference for DeepL Voice API language availability, supported aud Translation is always provided by DeepL. For some languages, transcription and translated speech are provided by external service partners. All source languages can be translated into any target language. -| **Language** | **Transcription** | **Translation** | closed beta
Translated Speech | -| :------------------------------------- | :---------------: | :-------------: | :------------------------------------------------------------: | -| Arabic | ⎋ | ✓ | ⎋ | -| Bengali | ⎋ | ✓ | — | -| Bulgarian | ⎋ | ✓ | ⎋ | -| Chinese (Simplified/Traditional) | ✓ | ✓ | ✓ | -| Croatian | ⎋ | ✓ | — | -| Czech | ✓ | ✓ | ⎋ | -| Danish | ⎋ | ✓ | ⎋ | -| Dutch | ✓ | ✓ | ✓ | -| English (American/British) | ✓ | ✓ | ✓ | -| Estonian | ⎋ | ✓ | — | -| Finnish | ⎋ | ✓ | ⎋ | -| French | ✓ | ✓ | ✓ | -| German | ✓ | ✓ | ✓ | -| Greek | ⎋ | ✓ | ⎋ | -| Hebrew | ⎋ | ✓ | — | -| Hindi beta | ⎋ | ✓ | ⎋ | -| Hungarian | ⎋ | ✓ | ⎋ | -| Indonesian | ✓ | ✓ | ⎋ | -| Irish | ⎋ | ✓ | — | -| Italian | ✓ | ✓ | ✓ | -| Japanese | ✓ | ✓ | ✓ | -| Korean | ✓ | ✓ | ✓ | -| Latvian | ⎋ | ✓ | — | -| Lithuanian | ⎋ | ✓ | — | -| Malay beta | ⎋ | ✓ | ⎋ | -| Maltese | ⎋ | ✓ | — | -| Norwegian Bokmål | ⎋ | ✓ | ⎋ | -| Polish | ✓ | ✓ | ✓ | -| Portuguese (Brazil/Portugal) | ✓ | ✓ | ✓ | -| Romanian | ✓ | ✓ | ⎋ | -| Russian | ✓ | ✓ | ✓ | -| Slovak | ⎋ | ✓ | ⎋ | -| Slovenian | ⎋ | ✓ | — | -| Spanish | ✓ | ✓ | ✓ | -| Swedish | ✓ | ✓ | ✓ | -| Thai | ⎋ | ✓ | — | -| Tagalog | ⎋ | ✓ | — | -| Tamil beta | ⎋ | ✓ | ⎋ | -| Turkish | ✓ | ✓ | ✓ | -| Ukrainian | ✓ | ✓ | ⎋ | -| Vietnamese | ⎋ | ✓ | ⎋ | +{/* BEGIN GENERATED voice-language-matrix (run: node scripts/update-language-docs.mjs) */} +| **Language** | **Transcription** | **Translation** | closed beta
Translated Speech | +| :--- | :---: | :---: | :---: | +| Arabic | ⎋ | ✓ | ⎋ | +| Bengali | ⎋ | ✓ | — | +| Bulgarian | ⎋ | ✓ | ⎋ | +| Chinese (Simplified/Traditional) | ✓ | ✓ | ✓ | +| Croatian | ⎋ | ✓ | — | +| Czech | ✓ | ✓ | ⎋ | +| Danish | ⎋ | ✓ | ⎋ | +| Dutch | ✓ | ✓ | ✓ | +| English (American/British) | ✓ | ✓ | ✓ | +| Estonian | ⎋ | ✓ | — | +| Finnish | ⎋ | ✓ | ⎋ | +| French | ✓ | ✓ | ✓ | +| German | ✓ | ✓ | ✓ | +| Greek | ⎋ | ✓ | ⎋ | +| Hebrew | ⎋ | ✓ | — | +| Hindi beta | ⎋ | ✓ | ⎋ | +| Hungarian | ⎋ | ✓ | ⎋ | +| Indonesian | ✓ | ✓ | ⎋ | +| Irish | ⎋ | ✓ | — | +| Italian | ✓ | ✓ | ✓ | +| Japanese | ✓ | ✓ | ✓ | +| Korean | ✓ | ✓ | ✓ | +| Latvian | ⎋ | ✓ | — | +| Lithuanian | ⎋ | ✓ | — | +| Malay beta | ⎋ | ✓ | ⎋ | +| Maltese | ⎋ | ✓ | — | +| Norwegian (bokmål) | ⎋ | ✓ | ⎋ | +| Polish | ✓ | ✓ | ✓ | +| Portuguese (Brazil/Portugal) | ✓ | ✓ | ✓ | +| Romanian | ✓ | ✓ | ⎋ | +| Russian | ✓ | ✓ | ✓ | +| Slovak | ⎋ | ✓ | ⎋ | +| Slovenian | ⎋ | ✓ | — | +| Spanish | ✓ | ✓ | ✓ | +| Swedish | ✓ | ✓ | ✓ | +| Tagalog | ⎋ | ✓ | — | +| Tamil beta | ⎋ | ✓ | ⎋ | +| Thai | ⎋ | ✓ | — | +| Turkish | ✓ | ✓ | ✓ | +| Ukrainian | ✓ | ✓ | ⎋ | +| Vietnamese | ⎋ | ✓ | ⎋ | +{/* END GENERATED voice-language-matrix */} ✓ provided by DeepL / ⎋ provided by an external service partner / — not available
diff --git a/scripts/generate-language-table.mjs b/scripts/generate-language-table.mjs new file mode 100644 index 00000000..b743575a --- /dev/null +++ b/scripts/generate-language-table.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +// Regenerates the languageData array in snippets/language-table.jsx from the +// live GET /v3/languages endpoint, so the "Languages supported" docs page +// reflects the API instead of a hand-maintained list. +// +// Usage: +// node scripts/generate-language-table.mjs [--dry-run] +// +// The API key is passed as the first argument; it is not read from the +// environment or from any file. Set DEEPL_SERVER_URL to target a different +// API host (e.g. a local mock). + +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ROOT, fetchLanguages, parseArgs } from './lib.mjs'; + +const SNIPPET_PATH = path.join(ROOT, 'snippets', 'language-table.jsx'); +const BEGIN_MARKER = '// BEGIN GENERATED languageData'; +const END_MARKER = '// END GENERATED languageData'; + +// resource to table column derived from it +const FEATURE_RESOURCES = { + write: 'textImprovement', + style_rules: 'styleRules', + translation_memory: 'translationMemory', +}; + +// Display names curated for the docs page where the raw API name is ambiguous. +const NAME_OVERRIDES = { + EN: 'English (all variants)', + PT: 'Portuguese (unspecified variant)', + ZH: 'Chinese (unspecified variant)', + 'DE-DE': 'German (Germany)', + 'FR-FR': 'French (France)', +}; + +// Builds the table rows from the translate_text list plus one feature list per +// FEATURE_RESOURCES entry (in key order), sorted alphabetically by display name. +export function buildRows(translateText, featureLists) { + const languages = new Map(); + for (const lang of translateText) { + const code = lang.lang.toUpperCase(); + languages.set(code, { + code, + name: NAME_OVERRIDES[code] ?? lang.name, + translation: true, + isVariant: !lang.usable_as_source && lang.usable_as_target, + // early_access also gets the beta badge: the table has no separate concept + isBeta: lang.status !== 'stable', + glossaries: 'glossary' in lang.features, + tagHandling: 'tag_handling' in lang.features, + textImprovement: false, + translationMemory: false, + styleRules: false, + }); + } + + Object.values(FEATURE_RESOURCES).forEach((column, i) => { + for (const lang of featureLists[i].filter((l) => l.usable_as_target)) { + const entry = languages.get(lang.lang.toUpperCase()); + if (!entry) { + console.warn(`${lang.lang} supports ${column} but is not a translate_text language, skipped`); + continue; + } + entry[column] = true; + } + }); + + return [...languages.values()].sort((a, b) => a.name.localeCompare(b.name, 'en')); +} + +// Single-quoted JS string literal; the API values end up in executable JSX, +// so backslashes and newlines must not be able to break out of the literal. +const quote = (s) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\r?\n/g, '\\n')}'`; + +export function renderRow(l) { + const beta = l.isBeta ? ' isBeta: true,' : ''; + return ( + ` { code: ${quote(l.code)}, name: ${quote(l.name)}, ` + + `translation: ${l.translation}, isVariant: ${l.isVariant},${beta} ` + + `glossaries: ${l.glossaries}, tagHandling: ${l.tagHandling}, ` + + `textImprovement: ${l.textImprovement}, translationMemory: ${l.translationMemory}, ` + + `styleRules: ${l.styleRules} },` + ); +} + +export async function update({ authKey, dryRun = false }) { + const snippet = await readFile(SNIPPET_PATH, 'utf8'); + const pattern = new RegExp(`[ \\t]*${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`); + if (!pattern.test(snippet)) { + throw new Error(`Markers not found in ${SNIPPET_PATH}`); + } + + const [translateText, ...featureLists] = await Promise.all([ + fetchLanguages(authKey, 'translate_text'), + ...Object.keys(FEATURE_RESOURCES).map((r) => fetchLanguages(authKey, r)), + ]); + + const sorted = buildRows(translateText, featureLists); + const generated = ` ${BEGIN_MARKER} (run: node scripts/update-language-docs.mjs) + const languageData = [ +${sorted.map(renderRow).join('\n')} + ] + ${END_MARKER}`; + + if (dryRun) { + console.log(generated); + return; + } + + await writeFile(SNIPPET_PATH, snippet.replace(pattern, () => generated)); + console.log(`Updated snippets/language-table.jsx (${sorted.length} languages)`); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await update(parseArgs()); +} diff --git a/scripts/generate-style-rules-lists.mjs b/scripts/generate-style-rules-lists.mjs new file mode 100644 index 00000000..00cecd20 --- /dev/null +++ b/scripts/generate-style-rules-lists.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// Regenerates the style-rules language callout on the supported-languages page +// from GET /v3/languages?resource=style_rules. +// +// Usage: node scripts/generate-style-rules-lists.mjs [--dry-run] + +import { fileURLToPath } from 'node:url'; +import { byCode, fetchLanguages, joinList, parseArgs, replaceBlock } from './lib.mjs'; + +export async function update({ authKey, dryRun = false }) { + const languages = await fetchLanguages(authKey, 'style_rules'); + const codes = languages + .filter((l) => l.usable_as_target) + .sort(byCode) + .map((l) => `\`${l.lang}\``); + + await replaceBlock( + 'docs/getting-started/supported-languages.mdx', + 'style-rules-languages', + ` Style rules are supported for the following target languages: ${joinList(codes)}.`, + { dryRun }, + ); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await update(parseArgs()); +} diff --git a/scripts/generate-voice-languages.mjs b/scripts/generate-voice-languages.mjs new file mode 100644 index 00000000..5f8e7536 --- /dev/null +++ b/scripts/generate-voice-languages.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node +// Regenerates the Voice API language tables from GET /v3/languages?resource=voice: +// the support matrix in docs/voice/supported-languages-formats-and-limits.mdx and +// the input/target lists in api-reference/voice/deepl-voice-api-service-specification-updates.mdx. +// +// Usage: node scripts/generate-voice-languages.mjs [--dry-run] + +import { fileURLToPath } from 'node:url'; +import { byCode, byName, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; + +// The matrix folds target-only variants into their base language's row. +const MATRIX_NAME_OVERRIDES = { + en: 'English (American/British)', + pt: 'Portuguese (Brazil/Portugal)', + zh: 'Chinese (Simplified/Traditional)', +}; + +// feature values are objects like { status, external? }; presence means the +// capability exists, external: true means an external partner provides it +export const mark = (feature) => (feature ? (feature.external ? '⎋' : '✓') : '—'); + +export async function update({ authKey, dryRun = false }) { + const languages = await fetchLanguages(authKey, 'voice'); + const sources = languages.filter((l) => l.usable_as_source); + const targets = languages.filter((l) => l.usable_as_target); + + // The matrix only shows source languages (variants fold into the base row), + // so a target-only language without a source base would silently vanish. + const sourceBases = new Set(sources.map((l) => l.lang.split('-')[0])); + for (const t of targets.filter((t) => !sourceBases.has(t.lang.split('-')[0]))) { + console.warn(`voice target ${t.lang} has no source base language and is missing from the matrix`); + } + + const rows = sources + .map((l) => ({ ...l, name: MATRIX_NAME_OVERRIDES[l.lang] ?? l.name })) + .sort(byName) + .map((l) => { + const name = l.status !== 'stable' ? `${l.name} beta` : l.name; + return `| ${name} | ${mark(l.features.transcription)} | ✓ | ${mark(l.features.translated_speech)} |`; + }); + const matrix = [ + '| **Language** | **Transcription** | **Translation** | closed beta
Translated Speech |', + '| :--- | :---: | :---: | :---: |', + ...rows, + ].join('\n'); + await replaceBlock('docs/voice/supported-languages-formats-and-limits.mdx', 'voice-language-matrix', matrix, { dryRun }); + + const item = (l) => `
  • \`${l.lang}\` (${l.name})
  • `; + await replaceBlock( + 'api-reference/voice/deepl-voice-api-service-specification-updates.mdx', + 'voice-input-languages', + sources.sort(byCode).map(item).join('\n'), + { dryRun }, + ); + await replaceBlock( + 'api-reference/voice/deepl-voice-api-service-specification-updates.mdx', + 'voice-target-languages', + targets.sort(byCode).map(item).join('\n'), + { dryRun }, + ); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await update(parseArgs()); +} diff --git a/scripts/lib.mjs b/scripts/lib.mjs new file mode 100644 index 00000000..eab6a954 --- /dev/null +++ b/scripts/lib.mjs @@ -0,0 +1,79 @@ +// Shared helpers for the generate-*-languages scripts. +// See update-language-docs.mjs for the entry point that runs all of them. + +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +export const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +export function parseArgs() { + const args = process.argv.slice(2); + const authKey = args.find((a) => !a.startsWith('--')); + const unknownFlags = args.filter((a) => a.startsWith('--') && a !== '--dry-run'); + if (!authKey || unknownFlags.length > 0) { + if (unknownFlags.length > 0) console.error(`Unknown flag: ${unknownFlags.join(', ')}`); + console.error(`Usage: node ${path.relative(process.cwd(), process.argv[1])} [--dry-run]`); + process.exit(1); + } + return { authKey, dryRun: args.includes('--dry-run') }; +} + +export async function fetchLanguages(authKey, resource) { + const baseUrl = + process.env.DEEPL_SERVER_URL ?? + (authKey.endsWith(':fx') ? 'https://api-free.deepl.com' : 'https://api.deepl.com'); + const url = `${baseUrl}/v3/languages?resource=${resource}&include=beta&include=external`; + const res = await fetch(url, { + headers: { Authorization: `DeepL-Auth-Key ${authKey}` }, + signal: AbortSignal.timeout(30_000), + }); + if (!res.ok) { + throw new Error(`GET ${url} failed: ${res.status} ${await res.text()}`); + } + return res.json(); +} + +const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// Pure core of replaceBlock: swaps the lines between {/* BEGIN GENERATED ... */} +// and {/* END GENERATED */}, keeping the markers. Returns null if the +// markers are missing. The space after the id in both markers keeps prefix ids +// (e.g. "write" vs "write-target") from matching each other's blocks. +export function replaceGeneratedBlock(source, id, content) { + const escaped = escapeRegExp(id); + const pattern = new RegExp( + `(\\{/\\* BEGIN GENERATED ${escaped} [^}]*\\*/\\})[\\s\\S]*?(\\{/\\* END GENERATED ${escaped} \\*/\\})`, + ); + if (!pattern.test(source)) return null; + return source.replace(pattern, (_, begin, end) => `${begin}\n${content}\n${end}`); +} + +export async function replaceBlock(relPath, id, content, { dryRun = false } = {}) { + const filePath = path.join(ROOT, relPath); + const source = await readFile(filePath, 'utf8'); + const updated = replaceGeneratedBlock(source, id, content); + if (updated === null) { + throw new Error(`Markers for "${id}" not found in ${relPath}`); + } + if (dryRun) { + console.log(`--- ${relPath} [${id}] ---`); + console.log(content); + return; + } + await writeFile(filePath, updated); + console.log(`Updated ${relPath} [${id}]`); +} + +// Sort helper: alphabetical by English name, stable for identical names. +export const byName = (a, b) => a.name.localeCompare(b.name, 'en') || a.lang.localeCompare(b.lang, 'en'); + +// Sort helper: alphabetical by language code. +export const byCode = (a, b) => a.lang.localeCompare(b.lang, 'en'); + +// Joins items grammatically: "a", "a and b", "a, b, and c". +export function joinList(items, conjunction = 'and') { + if (items.length <= 1) return items[0] ?? ''; + if (items.length === 2) return `${items[0]} ${conjunction} ${items[1]}`; + return `${items.slice(0, -1).join(', ')}, ${conjunction} ${items.at(-1)}`; +} diff --git a/scripts/lib.test.mjs b/scripts/lib.test.mjs new file mode 100644 index 00000000..80f940e7 --- /dev/null +++ b/scripts/lib.test.mjs @@ -0,0 +1,165 @@ +// Unit tests for the language-docs generators. Zero dependencies: +// node --test scripts/*.test.mjs + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { joinList, replaceGeneratedBlock } from './lib.mjs'; +import { buildRows, renderRow } from './generate-language-table.mjs'; +import { mark } from './generate-voice-languages.mjs'; + +const lang = (code, name, features = {}, extra = {}) => ({ + lang: code, + name, + usable_as_source: true, + usable_as_target: true, + status: 'stable', + features, + ...extra, +}); + +test('joinList handles empty, one, two, and many items', () => { + assert.equal(joinList([]), ''); + assert.equal(joinList(['a']), 'a'); + assert.equal(joinList(['a', 'b']), 'a and b'); + assert.equal(joinList(['a', 'b', 'c']), 'a, b, and c'); + assert.equal(joinList(['a', 'b'], 'or'), 'a or b'); + assert.equal(joinList(['a', 'b', 'c'], 'or'), 'a, b, or c'); +}); + +test('replaceGeneratedBlock swaps content and keeps the markers', () => { + const source = [ + 'before', + '{/* BEGIN GENERATED my-block (run: x) */}', + 'old content', + '{/* END GENERATED my-block */}', + 'after', + ].join('\n'); + const updated = replaceGeneratedBlock(source, 'my-block', 'new content'); + assert.equal( + updated, + [ + 'before', + '{/* BEGIN GENERATED my-block (run: x) */}', + 'new content', + '{/* END GENERATED my-block */}', + 'after', + ].join('\n'), + ); +}); + +test('replaceGeneratedBlock does not cross-match prefix ids', () => { + const source = [ + '{/* BEGIN GENERATED write (run: x) */}', + 'write content', + '{/* END GENERATED write */}', + '{/* BEGIN GENERATED write-target (run: x) */}', + 'target content', + '{/* END GENERATED write-target */}', + ].join('\n'); + const updated = replaceGeneratedBlock(source, 'write', 'replaced'); + assert.match(updated, /replaced/); + assert.match(updated, /target content/); + assert.doesNotMatch(updated, /write content/); +}); + +test('replaceGeneratedBlock returns null when markers are missing', () => { + assert.equal(replaceGeneratedBlock('no markers here', 'my-block', 'x'), null); +}); + +test('replaceGeneratedBlock is idempotent', () => { + const source = [ + '{/* BEGIN GENERATED b (run: x) */}', + 'old', + '{/* END GENERATED b */}', + ].join('\n'); + const once = replaceGeneratedBlock(source, 'b', 'new'); + assert.equal(replaceGeneratedBlock(once, 'b', 'new'), once); +}); + +test('replaceGeneratedBlock preserves dollar sequences in content', () => { + const source = ['{/* BEGIN GENERATED b (run: x) */}', 'old', '{/* END GENERATED b */}'].join('\n'); + const updated = replaceGeneratedBlock(source, 'b', "costs $& and $' or $1"); + assert.match(updated, /costs \$& and \$' or \$1/); +}); + +// buildRows takes one feature list per FEATURE_RESOURCES entry, in key order: +// write, style_rules, translation_memory. +const rows = (translateText, { write = [], styleRules = [], tm = [] } = {}) => + buildRows(translateText, [write, styleRules, tm]); + +test('buildRows maps API flags to table columns', () => { + const [de] = rows([lang('de', 'German', { glossary: {}, tag_handling: {} })], { + write: [lang('de', 'German')], + tm: [lang('de', 'German')], + }); + assert.deepEqual(de, { + code: 'DE', + name: 'German', + translation: true, + isVariant: false, + isBeta: false, + glossaries: true, + tagHandling: true, + textImprovement: true, + translationMemory: true, + styleRules: false, + }); +}); + +test('buildRows flags target-only languages as variants and non-stable status as beta', () => { + const result = rows([ + lang('en-GB', 'English (British)', {}, { usable_as_source: false }), + lang('fr-CA', 'French (Canadian)', {}, { usable_as_source: false, status: 'beta' }), + lang('fr', 'French'), + lang('xx', 'Xxish', {}, { status: 'early_access' }), + ]); + assert.deepEqual( + result.map((l) => [l.code, l.isVariant, l.isBeta]), + [['EN-GB', true, false], ['FR', false, false], ['FR-CA', true, true], ['XX', false, true]], + ); +}); + +test('buildRows applies display-name overrides and sorts rows by display name', () => { + const result = rows([lang('pt', 'Portuguese'), lang('en', 'English'), lang('cs', 'Czech')]); + assert.deepEqual( + result.map((l) => l.name), + ['Czech', 'English (all variants)', 'Portuguese (unspecified variant)'], + ); +}); + +test('buildRows ignores feature entries that are not usable as target', () => { + const [de] = rows([lang('de', 'German')], { + write: [lang('de', 'German', {}, { usable_as_target: false })], + }); + assert.equal(de.textImprovement, false); +}); + +test('buildRows skips feature languages missing from translate_text', () => { + const result = rows([lang('de', 'German')], { styleRules: [lang('xx', 'Xxish')] }); + assert.deepEqual(result.map((l) => [l.code, l.styleRules]), [['DE', false]]); +}); + +test('renderRow emits isBeta only for beta rows and escapes quotes in names', () => { + const [beta, plain] = rows([ + lang('xx', "X'ish", {}, { status: 'beta' }), + lang('yy', 'Yish'), + ]); + assert.match(renderRow(beta), /name: 'X\\'ish', translation: true, isVariant: false, isBeta: true,/); + assert.doesNotMatch(renderRow(plain), /isBeta/); +}); + +test('renderRow cannot be broken out of by backslashes or newlines in API values', () => { + const [row] = rows([lang('xx', 'Trailing\\', {}, {})]); + row.name = 'a\\\nb'; + const rendered = renderRow(row); + assert.match(rendered, /name: 'a\\\\\\nb',/); + // the emitted line must be a valid JS expression, not a syntax error + const parsed = new Function(`return [${rendered}]`)(); + assert.equal(parsed[0].name, 'a\\\nb'); +}); + +test('mark maps voice features to matrix symbols', () => { + assert.equal(mark({ status: 'stable' }), '✓'); + assert.equal(mark({ status: 'beta', external: true }), '⎋'); + assert.equal(mark(undefined), '—'); +}); diff --git a/scripts/open-sync-pr.sh b/scripts/open-sync-pr.sh new file mode 100755 index 00000000..c9ea01db --- /dev/null +++ b/scripts/open-sync-pr.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Opens (or updates) a PR with whatever the language-docs generators changed. +# Called by .github/workflows/sync-language-docs.yml after the generators ran. +set -euo pipefail + +if [ "${GITHUB_ACTIONS:-}" != "true" ]; then + echo "This script pushes branches and opens PRs; run it from CI only." >&2 + exit 1 +fi + +BRANCH=bot/language-docs-sync + +if git diff --quiet; then + echo "Docs already match /v3/languages; nothing to do." + exit 0 +fi + +git config user.name "github-actions[bot]" +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" +git checkout -B "$BRANCH" +git commit -am "Sync language docs with /v3/languages" +git push --force origin "$BRANCH" + +BODY="Automated update: the GET /v3/languages response changed. Generated by the sync-language-docs workflow. Review the diff and merge. + +Do not push manual commits to this branch: the next scheduled sync force-pushes over it." +# Generator warnings (languages skipped or missing from a matrix) belong in +# front of the reviewer, not in the workflow log. +if [ -s "${WARNINGS_FILE:-}" ]; then + BODY="$BODY + +⚠️ Generator warnings: +\`\`\` +$(cat "$WARNINGS_FILE") +\`\`\`" +fi + +if [ "$(gh pr list --head "$BRANCH" --state open --json number --jq length)" -gt 0 ]; then + echo "PR already open; the branch push updated it." + gh pr edit "$BRANCH" --body "$BODY" +else + gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "Sync language docs with /v3/languages" \ + --body "$BODY" +fi diff --git a/scripts/update-language-docs.mjs b/scripts/update-language-docs.mjs new file mode 100644 index 00000000..f2f4944b --- /dev/null +++ b/scripts/update-language-docs.mjs @@ -0,0 +1,25 @@ +#!/usr/bin/env node +// Updates every generated language list in the docs from GET /v3/languages. +// Each target also has its own script for running individually: +// generate-language-table.mjs the table on docs/getting-started/supported-languages +// generate-voice-languages.mjs Voice API matrix and input/target lists +// generate-style-rules-lists.mjs style-rules language mentions +// +// Usage: node scripts/update-language-docs.mjs [--dry-run] + +import { parseArgs } from './lib.mjs'; +import { update as languageTable } from './generate-language-table.mjs'; +import { update as voiceLanguages } from './generate-voice-languages.mjs'; +import { update as styleRulesLists } from './generate-style-rules-lists.mjs'; + +const args = parseArgs(); +try { + await languageTable(args); + await voiceLanguages(args); + await styleRulesLists(args); +} catch (err) { + console.error(err.message); + console.error('Aborted mid-run: earlier targets may already be rewritten. Revert with `git checkout -- .` and retry.'); + process.exit(1); +} +console.log('All language docs updated.'); diff --git a/snippets/language-table.jsx b/snippets/language-table.jsx index 1ee1c7b3..52f8730a 100644 --- a/snippets/language-table.jsx +++ b/snippets/language-table.jsx @@ -11,139 +11,135 @@ export const LanguageTable = () => { styleRules: false }) - // Language data with individual feature support + // BEGIN GENERATED languageData (run: node scripts/update-language-docs.mjs) const languageData = [ - // Fully supported languages (source + target + glossaries + tag handling) - { code: 'AR', name: 'Arabic', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BG', name: 'Bulgarian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'CS', name: 'Czech', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'DA', name: 'Danish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'DE', name: 'German', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, - { code: 'EL', name: 'Greek', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'EN', name: 'English (all variants)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, - { code: 'ES', name: 'Spanish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, - { code: 'ET', name: 'Estonian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'FI', name: 'Finnish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ACE', name: 'Acehnese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'AF', name: 'Afrikaans', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SQ', name: 'Albanian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'AR', name: 'Arabic', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'AN', name: 'Aragonese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'HY', name: 'Armenian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'AS', name: 'Assamese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'AY', name: 'Aymara', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'AZ', name: 'Azerbaijani', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BA', name: 'Bashkir', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'EU', name: 'Basque', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BE', name: 'Belarusian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BN', name: 'Bengali', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BHO', name: 'Bhojpuri', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BS', name: 'Bosnian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BR', name: 'Breton', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'BG', name: 'Bulgarian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'MY', name: 'Burmese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'YUE', name: 'Cantonese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'CA', name: 'Catalan', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'CEB', name: 'Cebuano', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ZH-HANS', name: 'Chinese (simplified)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, + { code: 'ZH-HANT', name: 'Chinese (traditional)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, + { code: 'ZH', name: 'Chinese (unspecified variant)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, + { code: 'HR', name: 'Croatian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'CS', name: 'Czech', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'DA', name: 'Danish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'PRS', name: 'Dari', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'NL', name: 'Dutch', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'EN', name: 'English (all variants)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, + { code: 'EN-US', name: 'English (American)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, + { code: 'EN-GB', name: 'English (British)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, + { code: 'EO', name: 'Esperanto', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ET', name: 'Estonian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'FI', name: 'Finnish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, { code: 'FR', name: 'French', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, - { code: 'HU', name: 'Hungarian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ID', name: 'Indonesian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'FR-CA', name: 'French (Canadian)', translation: true, isVariant: true, isBeta: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, + { code: 'FR-FR', name: 'French (France)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, + { code: 'GL', name: 'Galician', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'KA', name: 'Georgian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'DE', name: 'German', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, + { code: 'DE-DE', name: 'German (Germany)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, + { code: 'DE-CH', name: 'German (Swiss)', translation: true, isVariant: true, isBeta: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, + { code: 'EL', name: 'Greek', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'GN', name: 'Guarani', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'GU', name: 'Gujarati', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'HT', name: 'Haitian Creole', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'HA', name: 'Hausa', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'HE', name: 'Hebrew', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'HI', name: 'Hindi', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'HU', name: 'Hungarian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'IS', name: 'Icelandic', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'IG', name: 'Igbo', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ID', name: 'Indonesian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'GA', name: 'Irish', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, { code: 'IT', name: 'Italian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, { code: 'JA', name: 'Japanese', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, + { code: 'JV', name: 'Javanese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'PAM', name: 'Kapampangan', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'KK', name: 'Kazakh', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'GOM', name: 'Konkani', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, { code: 'KO', name: 'Korean', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, - { code: 'LT', name: 'Lithuanian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'LV', name: 'Latvian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'NB', name: 'Norwegian Bokmål', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'NL', name: 'Dutch', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PL', name: 'Polish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PT', name: 'Portuguese (unspecified variant)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: false, styleRules: false }, - { code: 'RO', name: 'Romanian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'RU', name: 'Russian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SK', name: 'Slovak', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SL', name: 'Slovenian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SV', name: 'Swedish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TR', name: 'Turkish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'UK', name: 'Ukrainian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ZH', name: 'Chinese (unspecified variant)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, - - // Target-only language variants (cannot be used as source) - { code: 'DE-CH', name: 'German (Swiss)', translation: true, isVariant: true, isBeta: true, glossaries: true, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: true }, - { code: 'EN-GB', name: 'English (British)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, - { code: 'EN-US', name: 'English (American)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, - { code: 'FR-CA', name: 'French (Canadian)', translation: true, isVariant: true, isBeta: true, glossaries: true, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: true }, - { code: 'PT-BR', name: 'Portuguese (Brazilian)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: false, styleRules: false }, - { code: 'PT-PT', name: 'Portuguese (European)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ZH-HANS', name: 'Chinese (simplified)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, - { code: 'ZH-HANT', name: 'Chinese (traditional)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, - - // Target-only variant + { code: 'KMR', name: 'Kurdish (Kurmanji)', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'CKB', name: 'Kurdish (Sorani)', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'KY', name: 'Kyrgyz', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'LA', name: 'Latin', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'LV', name: 'Latvian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'LN', name: 'Lingala', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'LT', name: 'Lithuanian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'LMO', name: 'Lombard', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'LB', name: 'Luxembourgish', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MK', name: 'Macedonian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MAI', name: 'Maithili', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MG', name: 'Malagasy', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MS', name: 'Malay', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ML', name: 'Malayalam', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MT', name: 'Maltese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MI', name: 'Maori', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MR', name: 'Marathi', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'MN', name: 'Mongolian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'NE', name: 'Nepali', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'NB', name: 'Norwegian (bokmål)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'OC', name: 'Occitan', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'OM', name: 'Oromo', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'PAG', name: 'Pangasinan', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'PS', name: 'Pashto', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'FA', name: 'Persian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'PL', name: 'Polish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'PT-BR', name: 'Portuguese (Brazilian)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, + { code: 'PT-PT', name: 'Portuguese (European)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: false }, + { code: 'PT', name: 'Portuguese (unspecified variant)', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: false, styleRules: true }, + { code: 'PA', name: 'Punjabi', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'QU', name: 'Quechua', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'RO', name: 'Romanian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'RU', name: 'Russian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'SA', name: 'Sanskrit', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SR', name: 'Serbian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ST', name: 'Sesotho', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SCN', name: 'Sicilian', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SK', name: 'Slovak', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'SL', name: 'Slovenian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'ES', name: 'Spanish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, { code: 'ES-419', name: 'Spanish (Latin American)', translation: true, isVariant: true, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: false }, - - // Text-only languages (both source and target, but no glossaries or tag handling) - { code: 'ACE', name: 'Acehnese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'AF', name: 'Afrikaans', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'AN', name: 'Aragonese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'AS', name: 'Assamese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'AY', name: 'Aymara', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'AZ', name: 'Azerbaijani', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BA', name: 'Bashkir', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BE', name: 'Belarusian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BHO', name: 'Bhojpuri', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BN', name: 'Bengali', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BR', name: 'Breton', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'BS', name: 'Bosnian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'CA', name: 'Catalan', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'CEB', name: 'Cebuano', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'CKB', name: 'Kurdish (Sorani)', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'CY', name: 'Welsh', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'EO', name: 'Esperanto', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'EU', name: 'Basque', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'FA', name: 'Persian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'GA', name: 'Irish', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'GL', name: 'Galician', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'GN', name: 'Guarani', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'GOM', name: 'Konkani', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'GU', name: 'Gujarati', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HA', name: 'Hausa', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HE', name: 'Hebrew', translation: true, isVariant: false, glossaries: true, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HI', name: 'Hindi', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HR', name: 'Croatian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HT', name: 'Haitian Creole', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'HY', name: 'Armenian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'IG', name: 'Igbo', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'IS', name: 'Icelandic', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'JV', name: 'Javanese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'KA', name: 'Georgian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'KK', name: 'Kazakh', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'KMR', name: 'Kurdish (Kurmanji)', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'KY', name: 'Kyrgyz', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'LA', name: 'Latin', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'LB', name: 'Luxembourgish', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'LMO', name: 'Lombard', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'LN', name: 'Lingala', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MAI', name: 'Maithili', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MG', name: 'Malagasy', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MI', name: 'Maori', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MK', name: 'Macedonian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ML', name: 'Malayalam', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MN', name: 'Mongolian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MR', name: 'Marathi', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MS', name: 'Malay', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MT', name: 'Maltese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'MY', name: 'Burmese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'NE', name: 'Nepali', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'OC', name: 'Occitan', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'OM', name: 'Oromo', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PA', name: 'Punjabi', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PAG', name: 'Pangasinan', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PAM', name: 'Kapampangan', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PRS', name: 'Dari', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'PS', name: 'Pashto', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'QU', name: 'Quechua', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SA', name: 'Sanskrit', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SCN', name: 'Sicilian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SQ', name: 'Albanian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SR', name: 'Serbian', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ST', name: 'Sesotho', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SU', name: 'Sundanese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'SW', name: 'Swahili', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TA', name: 'Tamil', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TE', name: 'Telugu', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TG', name: 'Tajik', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TH', name: 'Thai', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TK', name: 'Turkmen', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TL', name: 'Tagalog', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TN', name: 'Tswana', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TS', name: 'Tsonga', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'TT', name: 'Tatar', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'UR', name: 'Urdu', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'UZ', name: 'Uzbek', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'VI', name: 'Vietnamese', translation: true, isVariant: false, glossaries: true, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'WO', name: 'Wolof', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'XH', name: 'Xhosa', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'YI', name: 'Yiddish', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'YUE', name: 'Cantonese', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, - { code: 'ZU', name: 'Zulu', translation: true, isVariant: false, glossaries: false, tagHandling: false, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SU', name: 'Sundanese', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SW', name: 'Swahili', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'SV', name: 'Swedish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'TL', name: 'Tagalog', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TG', name: 'Tajik', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TA', name: 'Tamil', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TT', name: 'Tatar', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TE', name: 'Telugu', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TH', name: 'Thai', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'TS', name: 'Tsonga', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TN', name: 'Tswana', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'TR', name: 'Turkish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'TK', name: 'Turkmen', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'UK', name: 'Ukrainian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'UR', name: 'Urdu', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'UZ', name: 'Uzbek', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'VI', name: 'Vietnamese', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: true, styleRules: true }, + { code: 'CY', name: 'Welsh', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'WO', name: 'Wolof', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'XH', name: 'Xhosa', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'YI', name: 'Yiddish', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { code: 'ZU', name: 'Zulu', translation: true, isVariant: false, glossaries: false, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, ] + // END GENERATED languageData // Filter and sort data const filteredData = useMemo(() => {