- Add get_suggested_languages() to i18n.py returning ≤6 ranked suggestions (current locale → Accept-Language header → popular language fallbacks); refactor _parse_accept_language to share a common _parse_accept_language_entries() helper - Inject suggested_languages into every template context (base.py) - Redesign nav-bar language dropdown (base.html): button shows current-language flag emoji; dropdown lists 5-7 suggestions with flags and native names; Alpine.js search input filters all 77 languages live; footer shows count and Search shortcut - Add language.search_placeholder and language.no_results keys to all 77 translation JSON files (en values; external script propagates translations to other locales) - Remove test_all_languages_have_same_keys (external sync script owns key completeness); add TestGetSuggestedLanguages (7 unit tests); update test_language_selector_in_nav for new HTML - Update InternationalizationGuide.md: single-step en.json-only workflow for adding new translation keys - Update .github/copilot-instructions.md: add i18n/l10n section documenting the en.json-only rule for future agents Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
14 KiB
Internationalization (i18n) & Localization (l10n) Guide
DocuElevate supports 77 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 | 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 |
Western Frisian | Frysk | 🇳🇱 | Tier 2 |
gl |
Galician | Galego | 🇪🇸 | Tier 2 |
li |
Limburgish | Limburgs | 🇳🇱 | Tier 2 |
vls |
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 |
he |
Hebrew | עברית | 🇮🇱 | Tier 4 |
ar |
Arabic | العربية | 🇸🇦 | Tier 4 |
fa |
Persian | فارسی | 🇮🇷 | Tier 4 |
af |
Afrikaans | Afrikaans | 🇿🇦 | Tier 4 |
zh |
Chinese | 中文 | 🇨🇳 | Tier 5 |
zh-TW |
Traditional 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 |
hi |
Hindi | हिन्दी | 🇮🇳 | Tier 5 |
bn |
Bengali | বাংলা | 🇧🇩 | Tier 5 |
gu |
Gujarati | ગુજરાતી | 🇮🇳 | Tier 5 |
ml |
Malayalam | മലയാളം | 🇮🇳 | Tier 5 |
mr |
Marathi | मराठी | 🇮🇳 | Tier 5 |
ta |
Tamil | தமிழ் | 🇮🇳 | Tier 5 |
te |
Telugu | తెలుగు | 🇮🇳 | Tier 5 |
ur |
Urdu | اردو | 🇵🇰 | Tier 5 |
si |
Sinhala | සිංහල | 🇱🇰 | Tier 5 |
ne |
Nepali | नेपाली | 🇳🇵 | Tier 5 |
th |
Thai | ไทย | 🇹🇭 | Tier 5 |
km |
Khmer | ខ្មែរ | 🇰🇭 | Tier 5 |
id |
Indonesian | Bahasa Indonesia | 🇮🇩 | Tier 5 |
ms |
Malay | Bahasa Melayu | 🇲🇾 | Tier 5 |
jv |
Javanese | Basa Jawa | 🇮🇩 | Tier 5 |
tl |
Tagalog | Filipino | 🇵🇭 | Tier 5 |
mn |
Mongolian | Монгол | 🇲🇳 | Tier 5 |
kk |
Kazakh | Қазақ тілі | 🇰🇿 | Tier 5 |
uz |
Uzbek | Oʻzbekcha | 🇺🇿 | Tier 5 |
az |
Azerbaijani | Azərbaycan dili | 🇦🇿 | Tier 5 |
hy |
Armenian | Հայերեն | 🇦🇲 | Tier 5 |
ka |
Georgian | ქართული | 🇬🇪 | Tier 5 |
sw |
Swahili | Kiswahili | 🇰🇪 | Tier 6 |
am |
Amharic | አማርኛ | 🇪🇹 | Tier 6 |
ha |
Hausa | Hausa | 🇳🇬 | Tier 6 |
yo |
Yoruba | Yorùbá | 🇳🇬 | Tier 6 |
ig |
Igbo | Igbo | 🇳🇬 | Tier 6 |
zu |
Zulu | isiZulu | 🇿🇦 | Tier 6 |
eo |
Esperanto | Esperanto | 🌍 | Tier 7 |
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 covers Non-EU European, Middle Eastern, and South African (Afrikaans) languages. Tier 5 covers Asian and Central Asian languages. Tier 6 covers African languages. Tier 7 covers constructed languages.
How Language Is Detected
DocuElevate resolves the display language in the following priority order:
- User profile preference — stored in the database (
UserProfile.preferred_language) and loaded into the session on login - Cookie —
docuelevate_langcookie (30-day expiry, set when user selects a language) - Browser
Accept-Languageheader — the highest-priority match among supported languages - Default — English (
en)
Selecting Your Language
Via the Navigation Bar
Click the 🌐 globe icon in the top navigation bar. A dropdown menu shows all available languages with their native names and flag emoji. The current language is highlighted with a blue checkmark.
Via the API
# Set language to German
curl -X POST http://localhost:8000/api/i18n/language \
-H "Content-Type: application/json" \
-d '{"language": "de"}'
# List all available languages
curl http://localhost:8000/api/i18n/languages
Via Cookie (Programmatic)
Set the docuelevate_lang cookie to any supported language code:
document.cookie = "docuelevate_lang=fr; max-age=2592000; path=/";
location.reload();
For Developers
Translation File Structure
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
├── 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
├── 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
├── 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:
{
"nav.dashboard": "Dashboard",
"nav.upload": "Upload",
"upload.max_size": "Maximum file size: {size}",
"footer.copyright": "DocuElevate {year}"
}
Using Translations in Templates
The _() function is available globally in all Jinja2 templates:
{# Simple translation #}
<h1>{{ _("dashboard.title") }}</h1>
{# Translation with placeholders #}
<p>{{ _("upload.max_size", size="50 MB") }}</p>
{# Translation in attributes #}
<button aria-label="{{ _('common.save') }}">{{ _("common.save") }}</button>
Using Translations in Python
from app.utils.i18n import translate
# Basic translation
text = translate("nav.dashboard", "de") # → "Übersicht"
# With placeholders
text = translate("footer.copyright", "fr", year="2025") # → "DocuElevate 2025"
Localization Helpers
Format dates, times, and numbers according to locale conventions:
{# In templates — locale is automatically detected #}
<span>{{ format_date_l10n(document.created_at) }}</span>
<span>{{ format_number_l10n(file_count) }}</span>
# In Python
from app.utils.i18n import format_date, format_number
format_date(date(2025, 3, 15), "de") # → "15. March 2025"
format_date(date(2025, 3, 15), "de", short=True) # → "15.03.2025"
format_number(1234567, "de") # → "1.234.567"
format_number(1234.56, "en") # → "1,234.56"
Adding a New Translation Key
- Add the key and English text to
frontend/translations/en.json - Use
{{ _("your.new.key") }}in templates ortranslate("your.new.key", locale)in Python
That's it. An external automation script picks up new keys in en.json and propagates
translations to all other language files. You never need to touch the non-English JSON
files manually — the translate-and-sync pipeline takes care of it.
AI Fallback Translation
When a translation key exists in English but not in the target language, DocuElevate can use the configured AI provider (OpenAI, Anthropic, etc.) to translate the string on-the-fly:
from app.utils.i18n import translate_with_ai_fallback
# Falls back to AI if no manual translation exists
translated = translate_with_ai_fallback("Welcome to our platform", "de")
The AI fallback:
- Uses the
AI_MODELorOPENAI_MODELsetting - Caches results in memory for the process lifetime
- Returns the original English text if the AI call fails
- Is designed for graceful degradation — the UI never breaks
Adding a New Language
- Create a new JSON file in
frontend/translations/(e.g.,ja.json) - Copy the structure from
en.jsonand translate all values - Add the language to
SUPPORTED_LANGUAGESinapp/utils/i18n.py:{"code": "ja", "name": "Japanese", "native": "日本語", "flag": "🇯🇵"}, - Add locale formatting rules to
_LOCALE_FORMATSin the same file - Create a database migration if needed (the
preferred_languagecolumn already accepts any string up to 10 characters)
Database Migration
Migration 027_add_user_language_preference adds a preferred_language
column to the user_profiles table. This column stores the user's chosen
UI language as an ISO 639-1 code (e.g., "de", "fr"). A NULL value
means "auto-detect from browser settings."
API Reference
GET /api/i18n/languages
Returns all supported languages and the current active language.
Response:
{
"languages": [
{"code": "en", "name": "English", "native": "English", "flag": "🇬🇧"},
{"code": "de", "name": "German", "native": "Deutsch", "flag": "🇩🇪"}
],
"current": "en",
"default": "en"
}
POST /api/i18n/language
Set the preferred UI language. Persists in session, cookie, and database.
Request:
{"language": "de"}
Response:
{
"language": "de",
"message": "Language changed to Deutsch"
}
Configuration
No additional configuration is required. The i18n system works out of the box with the default English language and automatically detects browser preferences.
| Setting | Default | Description |
|---|---|---|
Browser Accept-Language |
Auto-detected | Used when no explicit preference is set |
docuelevate_lang cookie |
Not set | Set when user selects a language via the UI |
UserProfile.preferred_language |
NULL |
Stored in DB for authenticated users |