From 87d85624be78b2c97faec27745e7c896480a4438 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:03:31 +0000 Subject: [PATCH] feat(i18n): expand supported languages from 31 to 49 with flags and locale formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 18 previously missing language entries to SUPPORTED_LANGUAGES in app/utils/i18n.py so every translation JSON file in frontend/translations/ is properly indexed and served by the language selector: New languages: af, ar, cy, eo, fa, fy, gl, he, ja, kn, ko, li, nds, no, pa, sr, vi, vls Each new language entry includes: - Correct ISO 639-1/639-3 code matching its JSON filename - Native name and display name - Appropriate country/language flag emoji - Locale-specific date and number formatting rules in _LOCALE_FORMATS Also: rename nb "Norwegian" → "Norwegian Bokmål" to distinguish it from no "Norwegian". Update tests/test_i18n.py: - Count assertions: 31 → 49 - Expected code set expanded to all 49 codes - Fix test_unsupported_language_fallback (ja/ko now supported, use xx/yy) Update docs/InternationalizationGuide.md: - Language count: 10 → 49 - Full language table with flags, native names, tiers - Complete file structure listing all 49 JSON files Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/utils/i18n.py | 153 +++++++++++++++++++++++++++++- docs/InternationalizationGuide.md | 119 +++++++++++++++++++---- tests/test_i18n.py | 30 ++++-- 3 files changed, 273 insertions(+), 29 deletions(-) diff --git a/app/utils/i18n.py b/app/utils/i18n.py index ab94a908..e517e234 100644 --- a/app/utils/i18n.py +++ b/app/utils/i18n.py @@ -2,7 +2,7 @@ Provides a JSON-based translation system for the DocuElevate UI with: -* **31 supported languages** covering all major European languages plus ZH +* **49 supported languages** covering European, Asian, Middle-Eastern, and other languages * Browser ``Accept-Language`` detection with cookie & user-profile persistence * AI-powered fallback translation via the configured LLM provider * Locale-aware date, number, and file-size formatting helpers @@ -42,7 +42,8 @@ SUPPORTED_LANGUAGES: list[dict[str, str]] = [ {"code": "pt", "name": "Portuguese", "native": "Português", "flag": "🇵🇹"}, # --- Tier 2: Western & Northern European --- {"code": "nl", "name": "Dutch", "native": "Nederlands", "flag": "🇳🇱"}, - {"code": "nb", "name": "Norwegian", "native": "Norsk", "flag": "🇳🇴"}, + {"code": "nb", "name": "Norwegian Bokmål", "native": "Norsk bokmål", "flag": "🇳🇴"}, + {"code": "no", "name": "Norwegian", "native": "Norsk", "flag": "🇳🇴"}, {"code": "da", "name": "Danish", "native": "Dansk", "flag": "🇩🇰"}, {"code": "sv", "name": "Swedish", "native": "Svenska", "flag": "🇸🇪"}, {"code": "fi", "name": "Finnish", "native": "Suomi", "flag": "🇫🇮"}, @@ -50,6 +51,12 @@ SUPPORTED_LANGUAGES: list[dict[str, str]] = [ {"code": "ga", "name": "Irish", "native": "Gaeilge", "flag": "🇮🇪"}, {"code": "lb", "name": "Luxembourgish", "native": "Lëtzebuergesch", "flag": "🇱🇺"}, {"code": "ca", "name": "Catalan", "native": "Català", "flag": "🏴"}, + {"code": "cy", "name": "Welsh", "native": "Cymraeg", "flag": "🏴󠁧󠁢󠁷󠁬󠁳󠁿"}, + {"code": "fy", "name": "Frisian", "native": "Frysk", "flag": "🇳🇱"}, + {"code": "gl", "name": "Galician", "native": "Galego", "flag": "🇪🇸"}, + {"code": "li", "name": "Limburgish", "native": "Limburgs", "flag": "🇳🇱"}, + {"code": "vls", "name": "West Flemish", "native": "West-Vlams", "flag": "🇧🇪"}, + {"code": "nds", "name": "Low German", "native": "Plattdüütsch", "flag": "🇩🇪"}, # --- Tier 3: Central & Eastern European --- {"code": "pl", "name": "Polish", "native": "Polski", "flag": "🇵🇱"}, {"code": "cs", "name": "Czech", "native": "Čeština", "flag": "🇨🇿"}, @@ -63,11 +70,24 @@ SUPPORTED_LANGUAGES: list[dict[str, str]] = [ {"code": "et", "name": "Estonian", "native": "Eesti", "flag": "🇪🇪"}, {"code": "lv", "name": "Latvian", "native": "Latviešu", "flag": "🇱🇻"}, {"code": "lt", "name": "Lithuanian", "native": "Lietuvių", "flag": "🇱🇹"}, - # --- Tier 4: Non-EU European & Other --- + {"code": "sr", "name": "Serbian", "native": "Српски", "flag": "🇷🇸"}, + # --- Tier 4: Non-EU European, Middle Eastern & African --- {"code": "tr", "name": "Turkish", "native": "Türkçe", "flag": "🇹🇷"}, {"code": "uk", "name": "Ukrainian", "native": "Українська", "flag": "🇺🇦"}, {"code": "ru", "name": "Russian", "native": "Русский", "flag": "🇷🇺"}, + {"code": "he", "name": "Hebrew", "native": "עברית", "flag": "🇮🇱"}, + {"code": "ar", "name": "Arabic", "native": "العربية", "flag": "🇸🇦"}, + {"code": "fa", "name": "Persian", "native": "فارسی", "flag": "🇮🇷"}, + {"code": "af", "name": "Afrikaans", "native": "Afrikaans", "flag": "🇿🇦"}, + # --- Tier 5: Asian languages --- {"code": "zh", "name": "Chinese", "native": "中文", "flag": "🇨🇳"}, + {"code": "ja", "name": "Japanese", "native": "日本語", "flag": "🇯🇵"}, + {"code": "ko", "name": "Korean", "native": "한국어", "flag": "🇰🇷"}, + {"code": "vi", "name": "Vietnamese", "native": "Tiếng Việt", "flag": "🇻🇳"}, + {"code": "pa", "name": "Punjabi", "native": "ਪੰਜਾਬੀ", "flag": "🇮🇳"}, + {"code": "kn", "name": "Kannada", "native": "ಕನ್ನಡ", "flag": "🇮🇳"}, + # --- Tier 6: Constructed & other languages --- + {"code": "eo", "name": "Esperanto", "native": "Esperanto", "flag": "🌍"}, ] SUPPORTED_LANGUAGE_CODES: set[str] = {lang["code"] for lang in SUPPORTED_LANGUAGES} @@ -503,6 +523,133 @@ _LOCALE_FORMATS: dict[str, dict[str, Any]] = { "thousands_sep": "\u00a0", "decimal_sep": ",", }, + # --- New languages --- + "no": { + "date": "%d. %B %Y", + "date_short": "%d.%m.%Y", + "datetime": "%d. %B %Y %H:%M", + "thousands_sep": "\u00a0", + "decimal_sep": ",", + }, + "cy": { + "date": "%d %B %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "fy": { + "date": "%d %B %Y", + "date_short": "%d-%m-%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "gl": { + "date": "%d de %B de %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d de %B de %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "li": { + "date": "%d %B %Y", + "date_short": "%d-%m-%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "vls": { + "date": "%d %B %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "nds": { + "date": "%d. %B %Y", + "date_short": "%d.%m.%Y", + "datetime": "%d. %B %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "sr": { + "date": "%d. %B %Y.", + "date_short": "%d.%m.%Y.", + "datetime": "%d. %B %Y. %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "he": { + "date": "%d %B %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "ar": { + "date": "%d %B %Y", + "date_short": "%Y/%m/%d", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "fa": { + "date": "%d %B %Y", + "date_short": "%Y/%m/%d", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "af": { + "date": "%d %B %Y", + "date_short": "%Y/%m/%d", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": "\u00a0", + "decimal_sep": ",", + }, + "ja": { + "date": "%Y年%m月%d日", + "date_short": "%Y/%m/%d", + "datetime": "%Y年%m月%d日 %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "ko": { + "date": "%Y년 %m월 %d일", + "date_short": "%Y.%m.%d", + "datetime": "%Y년 %m월 %d일 %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "vi": { + "date": "ngày %d tháng %m năm %Y", + "date_short": "%d/%m/%Y", + "datetime": "ngày %d tháng %m năm %Y %H:%M", + "thousands_sep": ".", + "decimal_sep": ",", + }, + "pa": { + "date": "%d %B %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "kn": { + "date": "%d %B %Y", + "date_short": "%d/%m/%Y", + "datetime": "%d %B %Y %H:%M", + "thousands_sep": ",", + "decimal_sep": ".", + }, + "eo": { + "date": "%d-a de %B %Y", + "date_short": "%Y-%m-%d", + "datetime": "%d-a de %B %Y %H:%M", + "thousands_sep": "\u00a0", + "decimal_sep": ",", + }, } diff --git a/docs/InternationalizationGuide.md b/docs/InternationalizationGuide.md index 2b192602..9f459028 100644 --- a/docs/InternationalizationGuide.md +++ b/docs/InternationalizationGuide.md @@ -1,27 +1,67 @@ # Internationalization (i18n) & Localization (l10n) Guide -DocuElevate supports **10 languages** for its web UI, with automatic browser +DocuElevate supports **49 languages** for its web UI, with automatic browser language detection, user-preference persistence, and an AI-powered fallback translator for strings that haven't been manually translated yet. ## Supported Languages -| Code | Language | Native Name | Priority | -|------|------------|-------------|----------| -| `en` | English | English | Tier 1 | -| `de` | German | Deutsch | Tier 1 | -| `fr` | French | Français | Tier 1 | -| `es` | Spanish | Español | Tier 1 | -| `it` | Italian | Italiano | Tier 1 | -| `pt` | Portuguese | Português | Tier 1 | -| `nl` | Dutch | Nederlands | Tier 2 | -| `pl` | Polish | Polski | Tier 2 | -| `zh` | Chinese | 中文 | Tier 2 | -| `ru` | Russian | Русский | Tier 2 | +| Code | Language | Native Name | Flag | Priority | +|-------|------------------|--------------------|------|----------| +| `en` | English | English | 🇬🇧 | Tier 1 | +| `de` | German | Deutsch | 🇩🇪 | Tier 1 | +| `fr` | French | Français | 🇫🇷 | Tier 1 | +| `es` | Spanish | Español | 🇪🇸 | Tier 1 | +| `it` | Italian | Italiano | 🇮🇹 | Tier 1 | +| `pt` | Portuguese | Português | 🇵🇹 | Tier 1 | +| `nl` | Dutch | Nederlands | 🇳🇱 | Tier 2 | +| `nb` | Norwegian Bokmål | Norsk bokmål | 🇳🇴 | Tier 2 | +| `no` | Norwegian | Norsk | 🇳🇴 | Tier 2 | +| `da` | Danish | Dansk | 🇩🇰 | Tier 2 | +| `sv` | Swedish | Svenska | 🇸🇪 | Tier 2 | +| `fi` | Finnish | Suomi | 🇫🇮 | Tier 2 | +| `is` | Icelandic | Íslenska | 🇮🇸 | Tier 2 | +| `ga` | Irish | Gaeilge | 🇮🇪 | Tier 2 | +| `lb` | Luxembourgish | Lëtzebuergesch | 🇱🇺 | Tier 2 | +| `ca` | Catalan | Català | 🏴 | Tier 2 | +| `cy` | Welsh | Cymraeg | 🏴󠁧󠁢󠁷󠁬󠁳󠁿 | Tier 2 | +| `fy` | Frisian | Frysk | 🇳🇱 | Tier 2 | +| `gl` | Galician | Galego | 🇪🇸 | Tier 2 | +| `li` | Limburgish | Limburgs | 🇳🇱 | Tier 2 | +| `vls` | West Flemish | West-Vlams | 🇧🇪 | Tier 2 | +| `nds` | Low German | Plattdüütsch | 🇩🇪 | Tier 2 | +| `pl` | Polish | Polski | 🇵🇱 | Tier 3 | +| `cs` | Czech | Čeština | 🇨🇿 | Tier 3 | +| `sk` | Slovak | Slovenčina | 🇸🇰 | Tier 3 | +| `hu` | Hungarian | Magyar | 🇭🇺 | Tier 3 | +| `sl` | Slovenian | Slovenščina | 🇸🇮 | Tier 3 | +| `hr` | Croatian | Hrvatski | 🇭🇷 | Tier 3 | +| `ro` | Romanian | Română | 🇷🇴 | Tier 3 | +| `bg` | Bulgarian | Български | 🇧🇬 | Tier 3 | +| `el` | Greek | Ελληνικά | 🇬🇷 | Tier 3 | +| `et` | Estonian | Eesti | 🇪🇪 | Tier 3 | +| `lv` | Latvian | Latviešu | 🇱🇻 | Tier 3 | +| `lt` | Lithuanian | Lietuvių | 🇱🇹 | Tier 3 | +| `sr` | Serbian | Српски | 🇷🇸 | Tier 3 | +| `tr` | Turkish | Türkçe | 🇹🇷 | Tier 4 | +| `uk` | Ukrainian | Українська | 🇺🇦 | Tier 4 | +| `ru` | Russian | Русский | 🇷🇺 | Tier 4 | +| `he` | Hebrew | עברית | 🇮🇱 | Tier 4 | +| `ar` | Arabic | العربية | 🇸🇦 | Tier 4 | +| `fa` | Persian | فارسی | 🇮🇷 | Tier 4 | +| `af` | Afrikaans | Afrikaans | 🇿🇦 | Tier 4 | +| `zh` | Chinese | 中文 | 🇨🇳 | Tier 5 | +| `ja` | Japanese | 日本語 | 🇯🇵 | Tier 5 | +| `ko` | Korean | 한국어 | 🇰🇷 | Tier 5 | +| `vi` | Vietnamese | Tiếng Việt | 🇻🇳 | Tier 5 | +| `pa` | Punjabi | ਪੰਜਾਬੀ | 🇮🇳 | Tier 5 | +| `kn` | Kannada | ಕನ್ನಡ | 🇮🇳 | Tier 5 | +| `eo` | Esperanto | Esperanto | 🌍 | Tier 6 | -> **Tier 1** languages (European priority) have complete, manually-reviewed -> translations. **Tier 2** languages have complete translations but may -> receive less frequent updates. +> **Tier 1** languages (major European) have complete, manually-reviewed +> translations. **Tier 2–3** languages have complete translations but may +> receive less frequent updates. **Tier 4–5** cover Non-EU European, Middle +> Eastern, and Asian languages. **Tier 6** covers constructed languages. ## How Language Is Detected @@ -71,15 +111,54 @@ Translations are stored as flat JSON files in `frontend/translations/`: ``` frontend/translations/ ├── en.json # English (base / reference) +├── af.json # Afrikaans +├── ar.json # Arabic +├── bg.json # Bulgarian +├── ca.json # Catalan +├── cs.json # Czech +├── cy.json # Welsh +├── da.json # Danish ├── de.json # German -├── fr.json # French +├── el.json # Greek +├── eo.json # Esperanto ├── es.json # Spanish +├── et.json # Estonian +├── fa.json # Persian +├── fi.json # Finnish +├── fr.json # French +├── fy.json # Frisian +├── ga.json # Irish +├── gl.json # Galician +├── he.json # Hebrew +├── hr.json # Croatian +├── hu.json # Hungarian +├── is.json # Icelandic ├── it.json # Italian -├── pt.json # Portuguese +├── ja.json # Japanese +├── kn.json # Kannada +├── ko.json # Korean +├── lb.json # Luxembourgish +├── li.json # Limburgish +├── lt.json # Lithuanian +├── lv.json # Latvian +├── nb.json # Norwegian Bokmål +├── nds.json # Low German (Plattdeutsch) ├── nl.json # Dutch +├── no.json # Norwegian +├── pa.json # Punjabi ├── pl.json # Polish -├── zh.json # Chinese -└── ru.json # Russian +├── pt.json # Portuguese +├── ro.json # Romanian +├── ru.json # Russian +├── sk.json # Slovak +├── sl.json # Slovenian +├── sr.json # Serbian +├── sv.json # Swedish +├── tr.json # Turkish +├── uk.json # Ukrainian +├── vi.json # Vietnamese +├── vls.json # West Flemish +└── zh.json # Chinese ``` Each file is a flat key-value dictionary with dot-notation namespacing: diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 22eecdf7..f3a20fda 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -161,7 +161,7 @@ class TestParseAcceptLanguage: @pytest.mark.unit def test_unsupported_language_fallback(self) -> None: - result = _parse_accept_language("ja, ko") + result = _parse_accept_language("xx, yy") assert result is None @pytest.mark.unit @@ -326,7 +326,7 @@ class TestSupportedLanguages: @pytest.mark.unit def test_ten_languages_supported(self) -> None: - assert len(SUPPORTED_LANGUAGES) == 31 + assert len(SUPPORTED_LANGUAGES) == 49 @pytest.mark.unit def test_supported_codes_set(self) -> None: @@ -338,10 +338,8 @@ class TestSupportedLanguages: "it", "pt", "nl", - "pl", - "zh", - "ru", "nb", + "no", "da", "sv", "fi", @@ -349,6 +347,13 @@ class TestSupportedLanguages: "ga", "lb", "ca", + "cy", + "fy", + "gl", + "li", + "vls", + "nds", + "pl", "cs", "sk", "hu", @@ -360,8 +365,21 @@ class TestSupportedLanguages: "et", "lv", "lt", + "sr", "tr", "uk", + "ru", + "he", + "ar", + "fa", + "af", + "zh", + "ja", + "ko", + "vi", + "pa", + "kn", + "eo", } assert SUPPORTED_LANGUAGE_CODES == expected @@ -385,7 +403,7 @@ class TestI18nAPI: assert response.status_code == 200 data = response.json() assert "languages" in data - assert len(data["languages"]) == 31 + assert len(data["languages"]) == 49 assert data["default"] == "en" # Verify each language has required fields for lang in data["languages"]: