From 5d03e44e6b1dae136774ad0b479b9f6236a6dfbe Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 09:28:35 +0200 Subject: [PATCH 01/10] Generate supported-languages table from /v3/languages Replace the hand-maintained languageData array in the language table snippet with output generated from GET /v3/languages, and add the generator script. The array now sits between BEGIN/END GENERATED markers; run node scripts/generate-language-table.mjs to refresh it. --- scripts/generate-language-table.mjs | 158 ++++++++++++++++++++ snippets/language-table.jsx | 214 ++++++++++++++-------------- 2 files changed, 263 insertions(+), 109 deletions(-) create mode 100644 scripts/generate-language-table.mjs diff --git a/scripts/generate-language-table.mjs b/scripts/generate-language-table.mjs new file mode 100644 index 00000000..96ac3ab9 --- /dev/null +++ b/scripts/generate-language-table.mjs @@ -0,0 +1,158 @@ +#!/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 [--force] [--dry-run] +// +// The API key is passed as the first argument; it is not read from the +// environment or from any file. +// +// Env: +// DEEPL_SERVER_URL optional — override the API base URL (e.g. a local mock). +// Defaults to api-free.deepl.com for ":fx" keys, else api.deepl.com. +// +// Flags: +// --dry-run print the generated array instead of writing the snippet +// --force write even if some resources could not be fetched (their +// feature columns will be false for all languages) + +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const SNIPPET_PATH = path.join( + path.dirname(fileURLToPath(import.meta.url)), + '..', + 'snippets', + 'language-table.jsx', +); +const BEGIN_MARKER = '// BEGIN GENERATED languageData'; +const END_MARKER = '// END GENERATED languageData'; + +// resource → 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)', +}; + +const args = process.argv.slice(2); +const force = args.includes('--force'); +const dryRun = args.includes('--dry-run'); +const authKey = args.find((a) => !a.startsWith('--')); +if (!authKey) { + console.error('Usage: node scripts/generate-language-table.mjs [--force] [--dry-run]'); + process.exit(1); +} +const baseUrl = + process.env.DEEPL_SERVER_URL ?? + (authKey.endsWith(':fx') ? 'https://api-free.deepl.com' : 'https://api.deepl.com'); + +async function fetchLanguages(resource) { + 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} → ${res.status} ${await res.text()}`); + } + return res.json(); +} + +const failures = []; +async function tryFetchLanguages(resource) { + try { + return await fetchLanguages(resource); + } catch (err) { + failures.push(`${resource}: ${err.message}`); + return []; + } +} + +const [translateText, ...featureLists] = await Promise.all([ + tryFetchLanguages('translate_text'), + ...Object.keys(FEATURE_RESOURCES).map(tryFetchLanguages), +]); + +if (translateText.length === 0) { + console.error('Could not fetch resource=translate_text — refusing to generate an empty table.'); + failures.forEach((f) => console.error(` ${f}`)); + process.exit(1); +} + +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/generate-language-table.mjs\` to update + const languageData = [ +${lines.join('\n')} + ] + ${END_MARKER}`; + +if (failures.length > 0) { + console.error('Some resources could not be fetched; their columns would be all-false:'); + failures.forEach((f) => console.error(` ${f}`)); + if (!force) { + console.error('Not writing. Re-run with --force to write anyway.'); + process.exit(1); + } +} + +if (dryRun) { + console.log(generated); + process.exit(0); +} + +const snippet = await readFile(SNIPPET_PATH, 'utf8'); +const pattern = new RegExp(`[ \\t]*${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`); +if (!pattern.test(snippet)) { + console.error(`Markers not found in ${SNIPPET_PATH} — expected "${BEGIN_MARKER}" … "${END_MARKER}".`); + process.exit(1); +} +await writeFile(SNIPPET_PATH, snippet.replace(pattern, generated.replace(/\$/g, '$$$$'))); +console.log(`Wrote ${sorted.length} languages to ${SNIPPET_PATH}`); diff --git a/snippets/language-table.jsx b/snippets/language-table.jsx index 1ee1c7b3..a8dc9673 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/generate-language-table.mjs` to update const languageData = [ - // Fully supported languages (source + target + glossaries + tag handling) + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: false, 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: 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: '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: false, styleRules: false }, + { 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: false, styleRules: false }, { code: 'FI', name: 'Finnish', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, { code: 'FR', name: 'French', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: true, translationMemory: true, styleRules: true }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: '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: 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: '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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: true, 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: '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: false, styleRules: false }, { code: 'RU', name: 'Russian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { 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: false, styleRules: false }, { code: 'SL', name: 'Slovenian', translation: true, isVariant: false, glossaries: true, tagHandling: true, textImprovement: false, translationMemory: false, styleRules: false }, + { 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 }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: false, styleRules: false }, + { 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: 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: '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: '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: false, styleRules: false }, + { 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(() => { From 29afd79d2d5562ecb853f42691e282eb9e2b7d62 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 10:18:16 +0200 Subject: [PATCH 02/10] Generate more language lists from /v3/languages Split the generator into one script per target with a parent that runs all of them (scripts/update-language-docs.mjs). New generated blocks: the Voice API language matrix and its service-specification lists, the formality sentences on translate and document, the target_lang, writing_style and tone lists on improve-text, and the style-rules language mentions. Generated blocks sit between BEGIN/END GENERATED markers; everything outside them stays hand-written. --- api-reference/improve-text.mdx | 38 ++-- ...oice-api-service-specification-updates.mdx | 29 ++- docs/getting-started/supported-languages.mdx | 6 +- scripts/generate-formality-lists.mjs | 38 ++++ scripts/generate-language-table.mjs | 173 ++++++------------ scripts/generate-style-rules-lists.mjs | 32 ++++ scripts/generate-voice-languages.mjs | 57 ++++++ scripts/generate-write-lists.mjs | 35 ++++ scripts/lib.mjs | 65 +++++++ scripts/update-language-docs.mjs | 25 +++ snippets/language-table.jsx | 2 +- 11 files changed, 363 insertions(+), 137 deletions(-) create mode 100644 scripts/generate-formality-lists.mjs create mode 100644 scripts/generate-style-rules-lists.mjs create mode 100644 scripts/generate-voice-languages.mjs create mode 100644 scripts/generate-write-lists.mjs create mode 100644 scripts/lib.mjs create mode 100644 scripts/update-language-docs.mjs diff --git a/api-reference/improve-text.mdx b/api-reference/improve-text.mdx index 720388b7..462d46d5 100644 --- a/api-reference/improve-text.mdx +++ b/api-reference/improve-text.mdx @@ -69,17 +69,22 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ The language of the improved text. Currently, the following languages are supported: +{/* BEGIN GENERATED write-target-languages (run: node scripts/update-language-docs.mjs) */} * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) + * `en` (English) + * `en-GB` (English (British)) + * `en-US` (English (American)) * `es` (Spanish) * `fr` (French) * `it` (Italian) * `ja` (Japanese) * `ko` (Korean) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) - * `zh`/`zh-Hans` (simplified Chinese) + * `pt` (Portuguese) + * `pt-BR` (Portuguese (Brazilian)) + * `pt-PT` (Portuguese (European)) + * `zh` (Chinese) + * `zh-Hans` (Chinese (simplified)) +{/* END GENERATED write-target-languages */} You can also retrieve supported languages programmatically via GET /v3/languages?resource=write. @@ -105,15 +110,17 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: +{/* BEGIN GENERATED write-writing-style-languages (run: node scripts/update-language-docs.mjs) */} * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) + * `en-GB` (English (British)) + * `en-US` (English (American)) * `es` (Spanish) * `fr` (French) * `it` (Italian) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) - + * `pt` (Portuguese) + * `pt-BR` (Portuguese (Brazilian)) + * `pt-PT` (Portuguese (European)) +{/* 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 +147,17 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: +{/* BEGIN GENERATED write-tone-languages (run: node scripts/update-language-docs.mjs) */} * `de` (German) - * `en-GB` (British English) - * `en-US` (American English) + * `en-GB` (English (British)) + * `en-US` (English (American)) * `es` (Spanish) * `fr` (French) * `it` (Italian) - * `pt-BR` (Brazilian Portuguese) - * `pt-PT` (Portuguese) + * `pt` (Portuguese) + * `pt-BR` (Portuguese (Brazilian)) + * `pt-PT` (Portuguese (European)) +{/* 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..850754fa 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:

    +{/* BEGIN GENERATED voice-input-languages (run: node scripts/update-language-docs.mjs) */}
  • `ar` (Arabic)
  • `bg` (Bulgarian)
  • `bn` (Bengali)
  • @@ -40,13 +41,14 @@ audio provides the following functionality:
  • `da` (Danish)
  • `de` (German)
  • `el` (Greek)
  • -
  • `en` (British)
  • +
  • `en` (English)
  • `es` (Spanish)
  • `et` (Estonian)
  • `fi` (Finnish)
  • `fr` (French)
  • `ga` (Irish)
  • `he` (Hebrew)
  • +
  • `hi` (Hindi)
  • `hr` (Croatian)
  • `hu` (Hungarian)
  • `id` (Indonesian)
  • @@ -55,8 +57,9 @@ audio provides the following functionality:
  • `ko` (Korean)
  • `lt` (Lithuanian)
  • `lv` (Latvian)
  • +
  • `ms` (Malay)
  • `mt` (Maltese)
  • -
  • `nb` (Norwegian Bokmål)
  • +
  • `nb` (Norwegian (bokmål))
  • `nl` (Dutch)
  • `pl` (Polish)
  • `pt` (Portuguese)
  • @@ -65,12 +68,14 @@ audio provides the following functionality:
  • `sk` (Slovak)
  • `sl` (Slovenian)
  • `sv` (Swedish)
  • +
  • `ta` (Tamil)
  • `th` (Thai)
  • `tl` (Tagalog)
  • `tr` (Turkish)
  • `uk` (Ukrainian)
  • `vi` (Vietnamese)
  • `zh` (Chinese)
  • +{/* END GENERATED voice-input-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:

    +{/* BEGIN GENERATED voice-target-languages (run: node scripts/update-language-docs.mjs) */}
  • `ar` (Arabic)
  • `bg` (Bulgarian)
  • `bn` (Bengali)
  • @@ -86,14 +92,16 @@ audio provides the following functionality:
  • `da` (Danish)
  • `de` (German)
  • `el` (Greek)
  • -
  • `en-GB` (British English)
  • -
  • `en-US` (American-English)
  • +
  • `en` (English)
  • +
  • `en-GB` (English (British))
  • +
  • `en-US` (English (American))
  • `es` (Spanish)
  • `et` (Estonian)
  • `fi` (Finnish)
  • `fr` (French)
  • `ga` (Irish)
  • `he` (Hebrew)
  • +
  • `hi` (Hindi)
  • `hr` (Croatian)
  • `hu` (Hungarian)
  • `id` (Indonesian)
  • @@ -102,26 +110,31 @@ audio provides the following functionality:
  • `ko` (Korean)
  • `lt` (Lithuanian)
  • `lv` (Latvian)
  • +
  • `ms` (Malay)
  • `mt` (Maltese)
  • -
  • `nb` (Norwegian Bokmål)
  • +
  • `nb` (Norwegian (bokmål))
  • `nl` (Dutch)
  • `pl` (Polish)
  • -
  • `pt-BR` (Brazilian Portuguese)
  • -
  • `pt-PT` (Portuguese) (all Portuguese varieties excluding Brazilian Portuguese)
  • -
  • `pt` (Portuguese) (unspecified variant for backward compatibility; please select `pt-PT` or `pt-BR`
  • +
  • `pt` (Portuguese)
  • +
  • `pt-BR` (Portuguese (Brazilian))
  • +
  • `pt-PT` (Portuguese (European))
  • `ro` (Romanian)
  • `ru` (Russian)
  • `sk` (Slovak)
  • `sl` (Slovenian)
  • `sv` (Swedish)
  • +
  • `ta` (Tamil)
  • `th` (Thai)
  • `tl` (Tagalog)
  • `tr` (Turkish)
  • `uk` (Ukrainian)
  • `vi` (Vietnamese)
  • +
  • `zh` (Chinese)
  • `zh-Hans` (Chinese (simplified))
  • `zh-Hant` (Chinese (traditional))
  • +{/* END GENERATED voice-target-languages */}
+

`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..4389647d 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: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `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/scripts/generate-formality-lists.mjs b/scripts/generate-formality-lists.mjs new file mode 100644 index 00000000..fd24149e --- /dev/null +++ b/scripts/generate-formality-lists.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +// Regenerates the formality target-language sentences in +// api-reference/translate.mdx and api-reference/document.mdx from the +// formality feature key on GET /v3/languages. +// +// Usage: node scripts/generate-formality-lists.mjs [--dry-run] + +import { fileURLToPath } from 'node:url'; +import { byCode, displayName, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; + +const TARGETS = [ + { resource: 'translate_text', file: 'api-reference/translate.mdx', id: 'formality-languages-translate-text' }, + { resource: 'translate_document', file: 'api-reference/document.mdx', id: 'formality-languages-translate-document' }, +]; + +export async function update({ authKey, dryRun = false }) { + for (const { resource, file, id } of TARGETS) { + const languages = await fetchLanguages(authKey, resource); + const items = languages + .filter((l) => l.usable_as_target && 'formality' in l.features) + .sort(byCode) + .map((l) => { + const beta = l.features.formality.status === 'beta' ? ', beta' : ''; + return `${l.lang.toUpperCase()} (${displayName(l)}${beta})`; + }); + const list = `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`; + await replaceBlock( + file, + id, + ` This feature currently only works for the following target languages: ${list}.`, + { dryRun }, + ); + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await update(parseArgs()); +} diff --git a/scripts/generate-language-table.mjs b/scripts/generate-language-table.mjs index 96ac3ab9..bb18bf19 100644 --- a/scripts/generate-language-table.mjs +++ b/scripts/generate-language-table.mjs @@ -4,34 +4,22 @@ // reflects the API instead of a hand-maintained list. // // Usage: -// node scripts/generate-language-table.mjs [--force] [--dry-run] +// 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. -// -// Env: -// DEEPL_SERVER_URL optional — override the API base URL (e.g. a local mock). -// Defaults to api-free.deepl.com for ":fx" keys, else api.deepl.com. -// -// Flags: -// --dry-run print the generated array instead of writing the snippet -// --force write even if some resources could not be fetched (their -// feature columns will be false for all languages) +// 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 { fileURLToPath } from 'node:url'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ROOT, fetchLanguages, parseArgs } from './lib.mjs'; -const SNIPPET_PATH = path.join( - path.dirname(fileURLToPath(import.meta.url)), - '..', - 'snippets', - 'language-table.jsx', -); +const SNIPPET_PATH = path.join(ROOT, 'snippets', 'language-table.jsx'); const BEGIN_MARKER = '// BEGIN GENERATED languageData'; const END_MARKER = '// END GENERATED languageData'; -// resource → table column derived from it +// resource to table column derived from it const FEATURE_RESOURCES = { write: 'textImprovement', style_rules: 'styleRules', @@ -47,112 +35,71 @@ const NAME_OVERRIDES = { 'FR-FR': 'French (France)', }; -const args = process.argv.slice(2); -const force = args.includes('--force'); -const dryRun = args.includes('--dry-run'); -const authKey = args.find((a) => !a.startsWith('--')); -if (!authKey) { - console.error('Usage: node scripts/generate-language-table.mjs [--force] [--dry-run]'); - process.exit(1); -} -const baseUrl = - process.env.DEEPL_SERVER_URL ?? - (authKey.endsWith(':fx') ? 'https://api-free.deepl.com' : 'https://api.deepl.com'); - -async function fetchLanguages(resource) { - 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} → ${res.status} ${await res.text()}`); - } - return res.json(); -} +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 failures = []; -async function tryFetchLanguages(resource) { - try { - return await fetchLanguages(resource); - } catch (err) { - failures.push(`${resource}: ${err.message}`); - return []; + 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, + }); } -} - -const [translateText, ...featureLists] = await Promise.all([ - tryFetchLanguages('translate_text'), - ...Object.keys(FEATURE_RESOURCES).map(tryFetchLanguages), -]); -if (translateText.length === 0) { - console.error('Could not fetch resource=translate_text — refusing to generate an empty table.'); - failures.forEach((f) => console.error(` ${f}`)); - process.exit(1); -} - -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; + 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; } - 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/generate-language-table.mjs\` to update + 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 (failures.length > 0) { - console.error('Some resources could not be fetched; their columns would be all-false:'); - failures.forEach((f) => console.error(` ${f}`)); - if (!force) { - console.error('Not writing. Re-run with --force to write anyway.'); - process.exit(1); + if (dryRun) { + console.log(generated); + return; } -} -if (dryRun) { - console.log(generated); - process.exit(0); + 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)`); } -const snippet = await readFile(SNIPPET_PATH, 'utf8'); -const pattern = new RegExp(`[ \\t]*${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`); -if (!pattern.test(snippet)) { - console.error(`Markers not found in ${SNIPPET_PATH} — expected "${BEGIN_MARKER}" … "${END_MARKER}".`); - process.exit(1); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await update(parseArgs()); } -await writeFile(SNIPPET_PATH, snippet.replace(pattern, generated.replace(/\$/g, '$$$$'))); -console.log(`Wrote ${sorted.length} languages to ${SNIPPET_PATH}`); diff --git a/scripts/generate-style-rules-lists.mjs b/scripts/generate-style-rules-lists.mjs new file mode 100644 index 00000000..f3c4b1bf --- /dev/null +++ b/scripts/generate-style-rules-lists.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +// Regenerates the style-rules language mentions from +// GET /v3/languages?resource=style_rules: the custom_instructions note in +// api-reference/translate.mdx and the callout on the supported-languages page. +// +// Usage: node scripts/generate-style-rules-lists.mjs [--dry-run] + +import { fileURLToPath } from 'node:url'; +import { byCode, fetchLanguages, 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}\``); + const list = `${codes.slice(0, -1).join(', ')} or ${codes.at(-1)}`; + + await replaceBlock( + 'api-reference/translate.mdx', + 'style-rules-languages', + ` The target language must be ${list}, or any variants of these languages.`, + { dryRun }, + ); + await replaceBlock( + 'docs/getting-started/supported-languages.mdx', + 'style-rules-languages', + ` Style rules are supported for the following target languages: ${codes.join(', ')}.`, + { 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..1dceb7f1 --- /dev/null +++ b/scripts/generate-voice-languages.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +// Regenerates the Voice API language tables from GET /v3/languages?resource=voice: +// the support matrix in api-reference/voice.mdx and the input/target language +// 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)', +}; + +// v provided by DeepL, external partner symbol otherwise, dash if unavailable +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); + + 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('api-reference/voice.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..2d720283 --- /dev/null +++ b/scripts/generate-write-lists.mjs @@ -0,0 +1,35 @@ +#!/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 { byCode, displayName, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; + +const FILE = 'api-reference/improve-text.mdx'; + +export async function update({ authKey, dryRun = false }) { + const languages = await fetchLanguages(authKey, 'write'); + const targets = languages.filter((l) => l.usable_as_target).sort(byCode); + + const bullets = (langs) => langs.map((l) => ` * \`${l.lang}\` (${displayName(l)})`).join('\n'); + + 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..48ace9df --- /dev/null +++ b/scripts/lib.mjs @@ -0,0 +1,65 @@ +// 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(); +} + +// Replaces the lines between {/* BEGIN GENERATED ... */} and +// {/* END GENERATED */} in an MDX file. The markers themselves stay. +export async function replaceBlock(relPath, id, content, { dryRun = false } = {}) { + const filePath = path.join(ROOT, relPath); + const source = await readFile(filePath, 'utf8'); + const pattern = new RegExp( + `(\\{/\\* BEGIN GENERATED ${id}[^}]*\\*/\\})[\\s\\S]*?(\\{/\\* END GENERATED ${id} \\*/\\})`, + ); + if (!pattern.test(source)) { + throw new Error(`Markers for "${id}" not found in ${relPath}`); + } + const updated = source.replace(pattern, (_, begin, end) => `${begin}\n${content}\n${end}`); + 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; diff --git a/scripts/update-language-docs.mjs b/scripts/update-language-docs.mjs new file mode 100644 index 00000000..3d476876 --- /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-formality-lists.mjs formality sentences on translate/document +// 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 formalityLists } from './generate-formality-lists.mjs'; +import { update as writeLists } from './generate-write-lists.mjs'; +import { update as styleRulesLists } from './generate-style-rules-lists.mjs'; + +const args = parseArgs(); +await languageTable(args); +await voiceLanguages(args); +await formalityLists(args); +await writeLists(args); +await styleRulesLists(args); +console.log('All language docs updated.'); diff --git a/snippets/language-table.jsx b/snippets/language-table.jsx index a8dc9673..528fdb9e 100644 --- a/snippets/language-table.jsx +++ b/snippets/language-table.jsx @@ -11,7 +11,7 @@ export const LanguageTable = () => { styleRules: false }) - // BEGIN GENERATED languageData — run `node scripts/generate-language-table.mjs` to update + // BEGIN GENERATED languageData (run: node scripts/update-language-docs.mjs) const languageData = [ { 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 }, From ea4406c8fda50bbbd1fcdc85b31ef29bd634adad Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 10:37:55 +0200 Subject: [PATCH 03/10] Address review: grouped language lists, list-building fixes, robustness Formality and write language lists are now grouped by base language and sorted alphabetically. Shared joinList handles one- and two-item lists, the marker regex escapes ids and anchors on the trailing space, the parent script reports partial-update state on failure, and the voice generator warns if a target-only language would be missing from the matrix. Restores the pt-PT scope caveat on the service specification page and the final conjunction in the style-rules callout. --- api-reference/improve-text.mdx | 53 ++++++++----------- ...oice-api-service-specification-updates.mdx | 2 +- docs/getting-started/supported-languages.mdx | 2 +- scripts/generate-formality-lists.mjs | 29 +++++----- scripts/generate-style-rules-lists.mjs | 7 ++- scripts/generate-voice-languages.mjs | 10 +++- scripts/generate-write-lists.mjs | 11 ++-- scripts/lib.mjs | 31 ++++++++++- scripts/update-language-docs.mjs | 16 ++++-- 9 files changed, 99 insertions(+), 62 deletions(-) diff --git a/api-reference/improve-text.mdx b/api-reference/improve-text.mdx index 462d46d5..27fa997a 100644 --- a/api-reference/improve-text.mdx +++ b/api-reference/improve-text.mdx @@ -70,20 +70,15 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ The language of the improved text. Currently, the following languages are supported: {/* BEGIN GENERATED write-target-languages (run: node scripts/update-language-docs.mjs) */} - * `de` (German) - * `en` (English) - * `en-GB` (English (British)) - * `en-US` (English (American)) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `ja` (Japanese) - * `ko` (Korean) - * `pt` (Portuguese) - * `pt-BR` (Portuguese (Brazilian)) - * `pt-PT` (Portuguese (European)) - * `zh` (Chinese) - * `zh-Hans` (Chinese (simplified)) + * 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. @@ -111,15 +106,12 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: {/* BEGIN GENERATED write-writing-style-languages (run: node scripts/update-language-docs.mjs) */} - * `de` (German) - * `en-GB` (English (British)) - * `en-US` (English (American)) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `pt` (Portuguese) - * `pt-BR` (Portuguese (Brazilian)) - * `pt-PT` (Portuguese (European)) + * 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. @@ -148,15 +140,12 @@ curl -X POST 'https://api.deepl.com/v2/write/rephrase' \ Currently supported for the following target languages: {/* BEGIN GENERATED write-tone-languages (run: node scripts/update-language-docs.mjs) */} - * `de` (German) - * `en-GB` (English (British)) - * `en-US` (English (American)) - * `es` (Spanish) - * `fr` (French) - * `it` (Italian) - * `pt` (Portuguese) - * `pt-BR` (Portuguese (Brazilian)) - * `pt-PT` (Portuguese (European)) + * 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 850754fa..117fc622 100644 --- a/api-reference/voice/deepl-voice-api-service-specification-updates.mdx +++ b/api-reference/voice/deepl-voice-api-service-specification-updates.mdx @@ -134,7 +134,7 @@ audio provides the following functionality:
  • `zh-Hant` (Chinese (traditional))
  • {/* END GENERATED voice-target-languages */} -

    `pt` is an unspecified variant kept for backward compatibility; prefer `pt-PT` or `pt-BR`.

    +

    `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 4389647d..91a2b35b 100644 --- a/docs/getting-started/supported-languages.mdx +++ b/docs/getting-started/supported-languages.mdx @@ -20,7 +20,7 @@ import { LanguageTable } from "/snippets/language-table.jsx" {/* BEGIN GENERATED style-rules-languages (run: node scripts/update-language-docs.mjs) */} - Style rules are supported for the following target languages: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `zh`. + Style rules are supported for the following target languages: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, 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/scripts/generate-formality-lists.mjs b/scripts/generate-formality-lists.mjs index fd24149e..1abd2848 100644 --- a/scripts/generate-formality-lists.mjs +++ b/scripts/generate-formality-lists.mjs @@ -1,12 +1,12 @@ #!/usr/bin/env node -// Regenerates the formality target-language sentences in +// Regenerates the formality target-language lists in // api-reference/translate.mdx and api-reference/document.mdx from the // formality feature key on GET /v3/languages. // // Usage: node scripts/generate-formality-lists.mjs [--dry-run] import { fileURLToPath } from 'node:url'; -import { byCode, displayName, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; +import { fetchLanguages, groupByBase, parseArgs, replaceBlock } from './lib.mjs'; const TARGETS = [ { resource: 'translate_text', file: 'api-reference/translate.mdx', id: 'formality-languages-translate-text' }, @@ -14,20 +14,23 @@ const TARGETS = [ ]; export async function update({ authKey, dryRun = false }) { - for (const { resource, file, id } of TARGETS) { - const languages = await fetchLanguages(authKey, resource); - const items = languages - .filter((l) => l.usable_as_target && 'formality' in l.features) - .sort(byCode) - .map((l) => { - const beta = l.features.formality.status === 'beta' ? ', beta' : ''; - return `${l.lang.toUpperCase()} (${displayName(l)}${beta})`; - }); - const list = `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`; + const responses = await Promise.all(TARGETS.map(({ resource }) => fetchLanguages(authKey, resource))); + + for (const [i, { file, id }] of TARGETS.entries()) { + const supported = responses[i].filter((l) => l.usable_as_target && 'formality' in l.features); + const bullets = groupByBase(supported) + .map((group) => { + const codes = group.entries.map((l) => { + const beta = l.features.formality.status === 'beta' ? ' (beta)' : ''; + return `\`${l.lang.toUpperCase()}\`${beta}`; + }); + return ` * ${group.name}: ${codes.join(', ')}`; + }) + .join('\n'); await replaceBlock( file, id, - ` This feature currently only works for the following target languages: ${list}.`, + ` This feature currently only works with the following target languages:\n\n${bullets}`, { dryRun }, ); } diff --git a/scripts/generate-style-rules-lists.mjs b/scripts/generate-style-rules-lists.mjs index f3c4b1bf..7fd206bc 100644 --- a/scripts/generate-style-rules-lists.mjs +++ b/scripts/generate-style-rules-lists.mjs @@ -6,23 +6,22 @@ // Usage: node scripts/generate-style-rules-lists.mjs [--dry-run] import { fileURLToPath } from 'node:url'; -import { byCode, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; +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}\``); - const list = `${codes.slice(0, -1).join(', ')} or ${codes.at(-1)}`; await replaceBlock( 'api-reference/translate.mdx', 'style-rules-languages', - ` The target language must be ${list}, or any variants of these languages.`, + ` The target language must be one of the following, or any variant of these languages: ${codes.join(', ')}.`, { dryRun }, ); await replaceBlock( 'docs/getting-started/supported-languages.mdx', 'style-rules-languages', - ` Style rules are supported for the following target languages: ${codes.join(', ')}.`, + ` Style rules are supported for the following target languages: ${joinList(codes)}.`, { dryRun }, ); } diff --git a/scripts/generate-voice-languages.mjs b/scripts/generate-voice-languages.mjs index 1dceb7f1..5e11f775 100644 --- a/scripts/generate-voice-languages.mjs +++ b/scripts/generate-voice-languages.mjs @@ -15,7 +15,8 @@ const MATRIX_NAME_OVERRIDES = { zh: 'Chinese (Simplified/Traditional)', }; -// v provided by DeepL, external partner symbol otherwise, dash if unavailable +// feature values are objects like { status, external? }; presence means the +// capability exists, external: true means an external partner provides it const mark = (feature) => (feature ? (feature.external ? '⎋' : '✓') : '—'); export async function update({ authKey, dryRun = false }) { @@ -23,6 +24,13 @@ export async function update({ authKey, dryRun = false }) { 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) diff --git a/scripts/generate-write-lists.mjs b/scripts/generate-write-lists.mjs index 2d720283..b25a4c56 100644 --- a/scripts/generate-write-lists.mjs +++ b/scripts/generate-write-lists.mjs @@ -5,15 +5,18 @@ // Usage: node scripts/generate-write-lists.mjs [--dry-run] import { fileURLToPath } from 'node:url'; -import { byCode, displayName, fetchLanguages, parseArgs, replaceBlock } from './lib.mjs'; +import { fetchLanguages, groupByBase, parseArgs, replaceBlock } from './lib.mjs'; const FILE = 'api-reference/improve-text.mdx'; +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).sort(byCode); - - const bullets = (langs) => langs.map((l) => ` * \`${l.lang}\` (${displayName(l)})`).join('\n'); + const targets = languages.filter((l) => l.usable_as_target); await replaceBlock(FILE, 'write-target-languages', bullets(targets), { dryRun }); await replaceBlock( diff --git a/scripts/lib.mjs b/scripts/lib.mjs index 48ace9df..99cea687 100644 --- a/scripts/lib.mjs +++ b/scripts/lib.mjs @@ -29,13 +29,18 @@ export async function fetchLanguages(authKey, resource) { return res.json(); } +const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Replaces the lines between {/* BEGIN GENERATED ... */} and // {/* END GENERATED */} in an MDX file. The markers themselves stay. +// The space after the id in both markers keeps prefix ids (e.g. "write" vs +// "write-target") from matching each other's blocks. export async function replaceBlock(relPath, id, content, { dryRun = false } = {}) { const filePath = path.join(ROOT, relPath); const source = await readFile(filePath, 'utf8'); + const escaped = escapeRegExp(id); const pattern = new RegExp( - `(\\{/\\* BEGIN GENERATED ${id}[^}]*\\*/\\})[\\s\\S]*?(\\{/\\* END GENERATED ${id} \\*/\\})`, + `(\\{/\\* BEGIN GENERATED ${escaped} [^}]*\\*/\\})[\\s\\S]*?(\\{/\\* END GENERATED ${escaped} \\*/\\})`, ); if (!pattern.test(source)) { throw new Error(`Markers for "${id}" not found in ${relPath}`); @@ -63,3 +68,27 @@ export const NAME_OVERRIDES = { }; 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/update-language-docs.mjs b/scripts/update-language-docs.mjs index 3d476876..30150a35 100644 --- a/scripts/update-language-docs.mjs +++ b/scripts/update-language-docs.mjs @@ -17,9 +17,15 @@ import { update as writeLists } from './generate-write-lists.mjs'; import { update as styleRulesLists } from './generate-style-rules-lists.mjs'; const args = parseArgs(); -await languageTable(args); -await voiceLanguages(args); -await formalityLists(args); -await writeLists(args); -await styleRulesLists(args); +try { + await languageTable(args); + await voiceLanguages(args); + await formalityLists(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.'); From 4c3176a989109df12c23ad0e208c9d93335bc5f0 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 10:43:11 +0200 Subject: [PATCH 04/10] Add zero-dependency unit tests for the generator scripts Tests run with Node's built-in runner (node --test scripts/*.test.mjs), covering joinList edge cases, groupByBase grouping and ordering, the marker-replacement core (extracted as pure replaceGeneratedBlock), voice matrix symbols, and the grouped bullet renderer. A GitHub workflow runs them on PRs touching scripts/. --- .github/workflows/test-scripts.yml | 21 +++++ scripts/generate-voice-languages.mjs | 2 +- scripts/generate-write-lists.mjs | 2 +- scripts/lib.mjs | 23 +++-- scripts/lib.test.mjs | 129 +++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/test-scripts.yml create mode 100644 scripts/lib.test.mjs diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml new file mode 100644 index 00000000..e3326f8e --- /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@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node --test scripts/*.test.mjs diff --git a/scripts/generate-voice-languages.mjs b/scripts/generate-voice-languages.mjs index 5e11f775..f6327a09 100644 --- a/scripts/generate-voice-languages.mjs +++ b/scripts/generate-voice-languages.mjs @@ -17,7 +17,7 @@ const MATRIX_NAME_OVERRIDES = { // feature values are objects like { status, external? }; presence means the // capability exists, external: true means an external partner provides it -const mark = (feature) => (feature ? (feature.external ? '⎋' : '✓') : '—'); +export const mark = (feature) => (feature ? (feature.external ? '⎋' : '✓') : '—'); export async function update({ authKey, dryRun = false }) { const languages = await fetchLanguages(authKey, 'voice'); diff --git a/scripts/generate-write-lists.mjs b/scripts/generate-write-lists.mjs index b25a4c56..25e69b11 100644 --- a/scripts/generate-write-lists.mjs +++ b/scripts/generate-write-lists.mjs @@ -9,7 +9,7 @@ import { fetchLanguages, groupByBase, parseArgs, replaceBlock } from './lib.mjs' const FILE = 'api-reference/improve-text.mdx'; -const bullets = (langs) => +export const bullets = (langs) => groupByBase(langs) .map((group) => ` * ${group.name}: ${group.entries.map((l) => `\`${l.lang}\``).join(', ')}`) .join('\n'); diff --git a/scripts/lib.mjs b/scripts/lib.mjs index 99cea687..5dca68e4 100644 --- a/scripts/lib.mjs +++ b/scripts/lib.mjs @@ -31,21 +31,26 @@ export async function fetchLanguages(authKey, resource) { const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -// Replaces the lines between {/* BEGIN GENERATED ... */} and -// {/* END GENERATED */} in an MDX file. The markers themselves stay. -// The space after the id in both markers keeps prefix ids (e.g. "write" vs -// "write-target") from matching each other's blocks. -export async function replaceBlock(relPath, id, content, { dryRun = false } = {}) { - const filePath = path.join(ROOT, relPath); - const source = await readFile(filePath, 'utf8'); +// 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)) { + 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}`); } - const updated = source.replace(pattern, (_, begin, end) => `${begin}\n${content}\n${end}`); if (dryRun) { console.log(`--- ${relPath} [${id}] ---`); console.log(content); 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'); +}); From 443959ef826a5035bfb603557ec6dc2c67840bae Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 10:45:20 +0200 Subject: [PATCH 05/10] Bump checkout and setup-node actions to v5 --- .github/workflows/test-scripts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index e3326f8e..7aa0f189 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -14,8 +14,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 with: node-version: 22 - run: node --test scripts/*.test.mjs From bd97b40efe4c3f6ef738ee8b7462b749b8675853 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 15:14:21 +0200 Subject: [PATCH 06/10] Add daily sync workflow for the language docs Scheduled (plus manual dispatch) workflow that reruns the generators against /v3/languages and opens a PR when anything changed. The git and PR logic lives in scripts/open-sync-pr.sh, which refuses to run outside CI. Requires a DEEPL_API_KEY repo secret; use a dedicated cost-capped key. Repeated drift updates the same bot branch and PR. --- .github/workflows/sync-language-docs.yml | 27 ++++++++++++++++++++++ scripts/open-sync-pr.sh | 29 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/sync-language-docs.yml create mode 100755 scripts/open-sync-pr.sh 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/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." From f7b6add28fe98b61b7e701198854e6efd4fa1200 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 15:20:53 +0200 Subject: [PATCH 07/10] Temporarily run sync workflow on PR for smoke test --- .github/workflows/sync-language-docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/sync-language-docs.yml b/.github/workflows/sync-language-docs.yml index 6c95c788..04d5aaf2 100644 --- a/.github/workflows/sync-language-docs.yml +++ b/.github/workflows/sync-language-docs.yml @@ -4,6 +4,10 @@ on: schedule: - cron: "17 6 * * *" workflow_dispatch: + # TEMPORARY smoke-test trigger, remove before merge + pull_request: + paths: + - ".github/workflows/sync-language-docs.yml" permissions: contents: write From 4b3168b7da1329b1eaa3bc3c3e352a59843b4404 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Thu, 9 Jul 2026 15:24:47 +0200 Subject: [PATCH 08/10] Remove temporary smoke-test trigger --- .github/workflows/sync-language-docs.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/sync-language-docs.yml b/.github/workflows/sync-language-docs.yml index 04d5aaf2..6c95c788 100644 --- a/.github/workflows/sync-language-docs.yml +++ b/.github/workflows/sync-language-docs.yml @@ -4,10 +4,6 @@ on: schedule: - cron: "17 6 * * *" workflow_dispatch: - # TEMPORARY smoke-test trigger, remove before merge - pull_request: - paths: - - ".github/workflows/sync-language-docs.yml" permissions: contents: write From 297474c20b23d0f3c794761f09a43deea154b1e4 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Fri, 10 Jul 2026 12:29:37 +0200 Subject: [PATCH 09/10] Adapt generators to the product-family restructure The translate and document overview pages are now OpenAPI stubs, so the formality lists and the translate-page style-rules mention have no docs home anymore; drop that generator and target. The voice matrix moved to docs/voice/supported-languages-formats-and-limits.mdx. --- ...supported-languages-formats-and-limits.mdx | 88 ++++++++++--------- scripts/generate-formality-lists.mjs | 41 --------- scripts/generate-style-rules-lists.mjs | 11 +-- scripts/generate-voice-languages.mjs | 6 +- scripts/update-language-docs.mjs | 3 - 5 files changed, 50 insertions(+), 99 deletions(-) delete mode 100644 scripts/generate-formality-lists.mjs 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-formality-lists.mjs b/scripts/generate-formality-lists.mjs deleted file mode 100644 index 1abd2848..00000000 --- a/scripts/generate-formality-lists.mjs +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env node -// Regenerates the formality target-language lists in -// api-reference/translate.mdx and api-reference/document.mdx from the -// formality feature key on GET /v3/languages. -// -// Usage: node scripts/generate-formality-lists.mjs [--dry-run] - -import { fileURLToPath } from 'node:url'; -import { fetchLanguages, groupByBase, parseArgs, replaceBlock } from './lib.mjs'; - -const TARGETS = [ - { resource: 'translate_text', file: 'api-reference/translate.mdx', id: 'formality-languages-translate-text' }, - { resource: 'translate_document', file: 'api-reference/document.mdx', id: 'formality-languages-translate-document' }, -]; - -export async function update({ authKey, dryRun = false }) { - const responses = await Promise.all(TARGETS.map(({ resource }) => fetchLanguages(authKey, resource))); - - for (const [i, { file, id }] of TARGETS.entries()) { - const supported = responses[i].filter((l) => l.usable_as_target && 'formality' in l.features); - const bullets = groupByBase(supported) - .map((group) => { - const codes = group.entries.map((l) => { - const beta = l.features.formality.status === 'beta' ? ' (beta)' : ''; - return `\`${l.lang.toUpperCase()}\`${beta}`; - }); - return ` * ${group.name}: ${codes.join(', ')}`; - }) - .join('\n'); - await replaceBlock( - file, - id, - ` This feature currently only works with the following target languages:\n\n${bullets}`, - { dryRun }, - ); - } -} - -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 index 7fd206bc..0ab33b16 100644 --- a/scripts/generate-style-rules-lists.mjs +++ b/scripts/generate-style-rules-lists.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node -// Regenerates the style-rules language mentions from -// GET /v3/languages?resource=style_rules: the custom_instructions note in -// api-reference/translate.mdx and the callout on the supported-languages page. +// 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] @@ -12,12 +11,6 @@ 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( - 'api-reference/translate.mdx', - 'style-rules-languages', - ` The target language must be one of the following, or any variant of these languages: ${codes.join(', ')}.`, - { dryRun }, - ); await replaceBlock( 'docs/getting-started/supported-languages.mdx', 'style-rules-languages', diff --git a/scripts/generate-voice-languages.mjs b/scripts/generate-voice-languages.mjs index f6327a09..b071c9b9 100644 --- a/scripts/generate-voice-languages.mjs +++ b/scripts/generate-voice-languages.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // Regenerates the Voice API language tables from GET /v3/languages?resource=voice: -// the support matrix in api-reference/voice.mdx and the input/target language -// lists in api-reference/voice/deepl-voice-api-service-specification-updates.mdx. +// 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] @@ -43,7 +43,7 @@ export async function update({ authKey, dryRun = false }) { '| :--- | :---: | :---: | :---: |', ...rows, ].join('\n'); - await replaceBlock('api-reference/voice.mdx', 'voice-language-matrix', matrix, { dryRun }); + await replaceBlock('docs/voice/supported-languages-formats-and-limits.mdx', 'voice-language-matrix', matrix, { dryRun }); const item = (l) => `
  • \`${l.lang}\` (${l.name})
  • `; await replaceBlock( diff --git a/scripts/update-language-docs.mjs b/scripts/update-language-docs.mjs index 30150a35..0d910bb0 100644 --- a/scripts/update-language-docs.mjs +++ b/scripts/update-language-docs.mjs @@ -3,7 +3,6 @@ // 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-formality-lists.mjs formality sentences on translate/document // generate-write-lists.mjs target_lang/writing_style/tone lists on improve-text // generate-style-rules-lists.mjs style-rules language mentions // @@ -12,7 +11,6 @@ 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 formalityLists } from './generate-formality-lists.mjs'; import { update as writeLists } from './generate-write-lists.mjs'; import { update as styleRulesLists } from './generate-style-rules-lists.mjs'; @@ -20,7 +18,6 @@ const args = parseArgs(); try { await languageTable(args); await voiceLanguages(args); - await formalityLists(args); await writeLists(args); await styleRulesLists(args); } catch (err) { From f63866ca8bb09a14070a56b37458e8c1c3b80328 Mon Sep 17 00:00:00 2001 From: Lucas Mathis Date: Fri, 10 Jul 2026 12:29:37 +0200 Subject: [PATCH 10/10] Regenerate language docs: style rules and translation memory expansion now live Prod /v3/languages now reports the expanded style-rules and translation-memory language support, so the generated table and callout pick it up. --- docs/getting-started/supported-languages.mdx | 2 +- snippets/language-table.jsx | 56 ++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/getting-started/supported-languages.mdx b/docs/getting-started/supported-languages.mdx index 91a2b35b..fcfa3e69 100644 --- a/docs/getting-started/supported-languages.mdx +++ b/docs/getting-started/supported-languages.mdx @@ -20,7 +20,7 @@ import { LanguageTable } from "/snippets/language-table.jsx" {/* BEGIN GENERATED style-rules-languages (run: node scripts/update-language-docs.mjs) */} - Style rules are supported for the following target languages: `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, and `zh`. + 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/snippets/language-table.jsx b/snippets/language-table.jsx index 528fdb9e..52f8730a 100644 --- a/snippets/language-table.jsx +++ b/snippets/language-table.jsx @@ -16,7 +16,7 @@ export const LanguageTable = () => { { 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: 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 }, @@ -29,25 +29,25 @@ export const LanguageTable = () => { { 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: 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: false, 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: false, styleRules: false }, - { code: 'DA', name: 'Danish', 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: 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: 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: false, styleRules: false }, - { code: 'FI', name: 'Finnish', translation: true, isVariant: false, glossaries: true, 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: '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 }, @@ -56,17 +56,17 @@ export const LanguageTable = () => { { 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: false, 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: 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: 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: 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 }, @@ -79,9 +79,9 @@ export const LanguageTable = () => { { 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: 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: 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 }, @@ -94,45 +94,45 @@ export const LanguageTable = () => { { 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: 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: false, styleRules: false }, - { 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: true, 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: '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: false, styleRules: false }, - { code: 'RU', name: 'Russian', translation: true, isVariant: false, glossaries: true, 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: false, styleRules: false }, - { code: 'SL', name: 'Slovenian', 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: 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 }, { 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: 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: 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: 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: 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: 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 },