diff --git a/.github/workflows/sync-language-docs.yml b/.github/workflows/sync-language-docs.yml new file mode 100644 index 00000000..6c95c788 --- /dev/null +++ b/.github/workflows/sync-language-docs.yml @@ -0,0 +1,27 @@ +name: Sync language docs + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +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 }} + run: node scripts/update-language-docs.mjs "$DEEPL_API_KEY" + - name: Open or update the sync PR + env: + GH_TOKEN: ${{ github.token }} + 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/improve-text.mdx b/api-reference/improve-text.mdx index 720388b7..27fa997a 100644 --- a/api-reference/improve-text.mdx +++ b/api-reference/improve-text.mdx @@ -69,17 +69,17 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ The language of the improved text. Currently, the following languages are supported: - * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `ja` (Japanese) - * `ko` (Korean) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) - * `zh`/`zh-Hans` (simplified Chinese) +{/* BEGIN GENERATED write-target-languages (run: node scripts/update-language-docs.mjs) */} + * Chinese: `zh`, `zh-Hans` + * English: `en`, `en-GB`, `en-US` + * French: `fr` + * German: `de` + * Italian: `it` + * Japanese: `ja` + * Korean: `ko` + * Portuguese: `pt`, `pt-BR`, `pt-PT` + * Spanish: `es` +{/* END GENERATED write-target-languages */} You can also retrieve supported languages programmatically via GET /v3/languages?resource=write. @@ -105,15 +105,14 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: - * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) - +{/* BEGIN GENERATED write-writing-style-languages (run: node scripts/update-language-docs.mjs) */} + * English: `en-GB`, `en-US` + * French: `fr` + * German: `de` + * Italian: `it` + * Portuguese: `pt`, `pt-BR`, `pt-PT` + * Spanish: `es` +{/* END GENERATED write-writing-style-languages */} Styles prefixed with `prefer_` will fall back to the `default` style when used with a language that does not support styles (this is recommended for cases where no `target_lang` is set), the non-prefixed writing styles (except `default`) will return a HTTP 400 error in that case. @@ -140,14 +139,14 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: - * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) +{/* BEGIN GENERATED write-tone-languages (run: node scripts/update-language-docs.mjs) */} + * English: `en-GB`, `en-US` + * French: `fr` + * German: `de` + * Italian: `it` + * Portuguese: `pt`, `pt-BR`, `pt-PT` + * Spanish: `es` +{/* END GENERATED write-tone-languages */} Tones prefixed with `prefer_` will fall back to the `default` tone when used with a language that does not support tones (this is recommended for cases where no `target_lang` is set), the non-prefixed tones (except `default`) will return a HTTP 400 error in that case. 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 ce2d303a..fcfa3e69 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](/api-reference/style-rules). Writing style and tone availability for `/write/rephrase` can also be retrieved via [`GET /v3/languages?resource=write`](/api-reference/languages/retrieve-supported-languages-by-resource). +{/* 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](/api-reference/style-rules). Writing style and tone availability for `/write/rephrase` can also be retrieved via [`GET /v3/languages?resource=write`](/api-reference/languages/retrieve-supported-languages-by-resource). diff --git a/docs/voice/supported-languages-formats-and-limits.mdx b/docs/voice/supported-languages-formats-and-limits.mdx index 65a855f0..3d136698 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..bb18bf19 --- /dev/null +++ b/scripts/generate-language-table.mjs @@ -0,0 +1,105 @@ +#!/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)', +}; + +export async function update({ authKey, dryRun = false }) { + const [translateText, ...featureLists] = await Promise.all([ + fetchLanguages(authKey, 'translate_text'), + ...Object.keys(FEATURE_RESOURCES).map((r) => fetchLanguages(authKey, r)), + ]); + + 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, + isBeta: lang.status === 'beta', + 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]) { + 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; + } + }); + + const sorted = [...languages.values()].sort((a, b) => a.name.localeCompare(b.name, 'en')); + const lines = sorted.map((l) => { + const beta = l.isBeta ? ' isBeta: true,' : ''; + return ( + ` { code: '${l.code}', name: '${l.name.replace(/'/g, "\\'")}', ` + + `translation: ${l.translation}, isVariant: ${l.isVariant},${beta} ` + + `glossaries: ${l.glossaries}, tagHandling: ${l.tagHandling}, ` + + `textImprovement: ${l.textImprovement}, translationMemory: ${l.translationMemory}, ` + + `styleRules: ${l.styleRules} },` + ); + }); + const generated = ` ${BEGIN_MARKER} (run: node scripts/update-language-docs.mjs) + const languageData = [ +${lines.join('\n')} + ] + ${END_MARKER}`; + + if (dryRun) { + console.log(generated); + return; + } + + 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}`); + } + 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..0ab33b16 --- /dev/null +++ b/scripts/generate-style-rules-lists.mjs @@ -0,0 +1,24 @@ +#!/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.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..b071c9b9 --- /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 === 'beta' ? `${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/generate-write-lists.mjs b/scripts/generate-write-lists.mjs new file mode 100644 index 00000000..25e69b11 --- /dev/null +++ b/scripts/generate-write-lists.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// Regenerates the three language lists in api-reference/improve-text.mdx +// (target_lang, writing_style, tone) from GET /v3/languages?resource=write. +// +// Usage: node scripts/generate-write-lists.mjs [--dry-run] + +import { fileURLToPath } from 'node:url'; +import { fetchLanguages, groupByBase, parseArgs, replaceBlock } from './lib.mjs'; + +const FILE = 'api-reference/improve-text.mdx'; + +export const bullets = (langs) => + groupByBase(langs) + .map((group) => ` * ${group.name}: ${group.entries.map((l) => `\`${l.lang}\``).join(', ')}`) + .join('\n'); + +export async function update({ authKey, dryRun = false }) { + const languages = await fetchLanguages(authKey, 'write'); + const targets = languages.filter((l) => l.usable_as_target); + + await replaceBlock(FILE, 'write-target-languages', bullets(targets), { dryRun }); + await replaceBlock( + FILE, + 'write-writing-style-languages', + bullets(targets.filter((l) => 'writing_style' in l.features)), + { dryRun }, + ); + await replaceBlock( + FILE, + 'write-tone-languages', + bullets(targets.filter((l) => 'tone' in l.features)), + { 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..5dca68e4 --- /dev/null +++ b/scripts/lib.mjs @@ -0,0 +1,99 @@ +// 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('--')); + if (!authKey) { + 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}` } }); + 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'); + +// Display-name overrides for codes whose raw API name is ambiguous in a flat list. +export const NAME_OVERRIDES = { + 'de-DE': 'German (Germany)', + 'fr-FR': 'French (France)', +}; + +export const displayName = (lang) => NAME_OVERRIDES[lang.lang] ?? lang.name; + +// 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)}`; +} + +// Groups language entries by base subtag (de, de-CH, de-DE form the "German" +// group), entries code-sorted within each group, groups alphabetical by name. +export function groupByBase(langs) { + const groups = new Map(); + for (const lang of [...langs].sort(byCode)) { + const base = lang.lang.split('-')[0]; + if (!groups.has(base)) groups.set(base, []); + groups.get(base).push(lang); + } + return [...groups.values()] + .map((entries) => ({ + name: (entries.find((l) => !l.lang.includes('-')) ?? entries[0]).name.replace(/\s*\(.*\)$/, ''), + entries, + })) + .sort((a, b) => a.name.localeCompare(b.name, 'en')); +} diff --git a/scripts/lib.test.mjs b/scripts/lib.test.mjs new file mode 100644 index 00000000..23a76821 --- /dev/null +++ b/scripts/lib.test.mjs @@ -0,0 +1,129 @@ +// 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 { displayName, groupByBase, joinList, replaceGeneratedBlock } from './lib.mjs'; +import { mark } from './generate-voice-languages.mjs'; +import { bullets } from './generate-write-lists.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('groupByBase groups variants under the base language', () => { + const groups = groupByBase([ + lang('de-DE', 'German'), + lang('de', 'German'), + lang('es', 'Spanish'), + lang('de-CH', 'German (Swiss)'), + ]); + assert.deepEqual( + groups.map((g) => [g.name, g.entries.map((l) => l.lang)]), + [ + ['German', ['de', 'de-CH', 'de-DE']], + ['Spanish', ['es']], + ], + ); +}); + +test('groupByBase orders groups alphabetically by name, not code', () => { + const groups = groupByBase([lang('nl', 'Dutch'), lang('es', 'Spanish'), lang('fr', 'French')]); + assert.deepEqual(groups.map((g) => g.name), ['Dutch', 'French', 'Spanish']); +}); + +test('groupByBase derives the group name from a variant-only group', () => { + const groups = groupByBase([lang('zh-Hans', 'Chinese (simplified)')]); + assert.deepEqual(groups.map((g) => g.name), ['Chinese']); +}); + +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/); +}); + +test('mark maps voice features to matrix symbols', () => { + assert.equal(mark({ status: 'stable' }), '✓'); + assert.equal(mark({ status: 'beta', external: true }), '⎋'); + assert.equal(mark(undefined), '—'); +}); + +test('bullets renders grouped code lists', () => { + const out = bullets([ + lang('en-GB', 'English (British)'), + lang('en', 'English'), + lang('de', 'German'), + ]); + assert.equal(out, [' * English: `en`, `en-GB`', ' * German: `de`'].join('\n')); +}); + +test('displayName applies overrides and falls back to the API name', () => { + assert.equal(displayName(lang('de-DE', 'German')), 'German (Germany)'); + assert.equal(displayName(lang('ja', 'Japanese')), 'Japanese'); +}); diff --git a/scripts/open-sync-pr.sh b/scripts/open-sync-pr.sh new file mode 100755 index 00000000..40fbffab --- /dev/null +++ b/scripts/open-sync-pr.sh @@ -0,0 +1,29 @@ +#!/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" + +# gh pr create fails if a PR for the branch is already open; in that case the +# push above already updated it. +gh pr create \ + --title "Sync language docs with /v3/languages" \ + --body "Automated update: the GET /v3/languages response changed. Generated by the sync-language-docs workflow. Review the diff and merge." \ + || echo "PR already open; the branch push updated it." diff --git a/scripts/update-language-docs.mjs b/scripts/update-language-docs.mjs new file mode 100644 index 00000000..0d910bb0 --- /dev/null +++ b/scripts/update-language-docs.mjs @@ -0,0 +1,28 @@ +#!/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-write-lists.mjs target_lang/writing_style/tone lists on improve-text +// 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 writeLists } from './generate-write-lists.mjs'; +import { update as styleRulesLists } from './generate-style-rules-lists.mjs'; + +const args = parseArgs(); +try { + await languageTable(args); + await voiceLanguages(args); + await writeLists(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(() => {