mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-08-24 05:04:33 +00:00
feat(i18n): offer every ISO 639-1 language
The table was thirty languages typed by hand. It is now generated: utils/generate_languages.py takes the 184 alpha-2 codes from pycountry, the display names from CLDR through babel, and the writing direction from CLDR character order. 159 languages carry their endonym; the remaining 25 have no CLDR entry and carry their English ISO name. That corrects the right-to-left set, which had four entries and needs ten — dv, ks, ps, sd, ug and yi were simply missed. Only 29 languages ship an interface catalogue, so the other 155 render in English until one is filled. make i18n-ui fills app/i18n/ui/ for them, and make i18n now covers the interface strings as well; neither asks for a string a shipped catalogue already answers, so hand-written entries stay. 184 entries do not fit on a screen, so the language menu scrolls inside itself. overscroll-behavior keeps the page behind it from moving once the list reaches its end. babel and pycountry are dev dependencies: the generator needs them, the application does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,7 @@ describe('Language switcher', () => {
|
||||
cy.visit('/en/');
|
||||
});
|
||||
|
||||
it('names the active language and offers all thirty', () => {
|
||||
it('names the active language and offers every ISO 639-1 code', () => {
|
||||
cy.get('#navbarDropdownLanguage')
|
||||
.should('have.attr', 'data-bs-toggle', 'dropdown')
|
||||
.and('contain.text', 'English');
|
||||
@@ -61,7 +61,7 @@ describe('Language switcher', () => {
|
||||
cy.get('#navbarDropdownLanguage')
|
||||
.parent('.nav-item')
|
||||
.find('> .dropdown-menu a.dropdown-item')
|
||||
.should('have.length', 30);
|
||||
.should('have.length', 184);
|
||||
});
|
||||
|
||||
it('marks the active language', () => {
|
||||
@@ -200,6 +200,21 @@ describe('Translated interface details', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('scrolls inside the language menu instead of past the page', () => {
|
||||
cy.viewport(1280, 720);
|
||||
cy.visit('/en/');
|
||||
cy.get('#navbarDropdownLanguage').click();
|
||||
|
||||
cy.get('.dropdown-menu.language-menu').should($menu => {
|
||||
const menu = $menu[0];
|
||||
expect(menu.scrollHeight, 'taller than it shows').to.be.greaterThan(
|
||||
menu.clientHeight,
|
||||
);
|
||||
expect(menu.getBoundingClientRect().height).to.be.lessThan(720);
|
||||
expect(getComputedStyle(menu).overflowY).to.eq('auto');
|
||||
});
|
||||
});
|
||||
|
||||
it('offers the switcher in the header only', () => {
|
||||
cy.viewport(1280, 720);
|
||||
cy.visit('/en/');
|
||||
@@ -215,7 +230,7 @@ describe('Search engine metadata', () => {
|
||||
});
|
||||
|
||||
it('declares an alternate for every language plus a default', () => {
|
||||
cy.get('link[rel="alternate"][hreflang]').should('have.length', 31);
|
||||
cy.get('link[rel="alternate"][hreflang]').should('have.length', 185);
|
||||
cy.get('link[rel="alternate"][hreflang="x-default"]').should('exist');
|
||||
cy.get('link[rel="alternate"][hreflang="ja"]')
|
||||
.should('have.attr', 'href')
|
||||
|
||||
@@ -120,6 +120,12 @@ div#navbarNavfooter .navbar-nav {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-menu.language-menu {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
main, footer, header, nav {
|
||||
position: relative;
|
||||
box-shadow:
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<a class="nav-link dropdown-toggle btn btn-light" id="navbarDropdownLanguage" role="button" data-bs-toggle="dropdown" data-bs-display="dynamic" aria-expanded="false" title="{{ t('Language') }}">
|
||||
<i class="fa-solid fa-language"></i> {{ languages[lang] }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<ul class="dropdown-menu language-menu">
|
||||
{% for code, endonym in languages.items() %}
|
||||
<li>
|
||||
<a class="dropdown-item {% if code == lang %}active{% endif %}"
|
||||
|
||||
@@ -11,46 +11,17 @@ from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
from app.utils.languages import LANGUAGES, RTL_LANGUAGES
|
||||
except ImportError: # pragma: no cover - supports running from the app/ directory.
|
||||
from utils.languages import LANGUAGES, RTL_LANGUAGES
|
||||
|
||||
I18N_DIR = Path(__file__).resolve().parent.parent / "i18n"
|
||||
UI_DIR = I18N_DIR / "ui"
|
||||
CONTENT_DIR = I18N_DIR / "content"
|
||||
|
||||
SOURCE_LANGUAGE = "en"
|
||||
|
||||
LANGUAGES = {
|
||||
"en": "English",
|
||||
"zh": "中文",
|
||||
"hi": "हिन्दी",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
"ar": "العربية",
|
||||
"bn": "বাংলা",
|
||||
"pt": "Português",
|
||||
"ru": "Русский",
|
||||
"ur": "اردو",
|
||||
"id": "Bahasa Indonesia",
|
||||
"de": "Deutsch",
|
||||
"ja": "日本語",
|
||||
"tr": "Türkçe",
|
||||
"ko": "한국어",
|
||||
"vi": "Tiếng Việt",
|
||||
"it": "Italiano",
|
||||
"th": "ไทย",
|
||||
"pl": "Polski",
|
||||
"nl": "Nederlands",
|
||||
"uk": "Українська",
|
||||
"fa": "فارسی",
|
||||
"ro": "Română",
|
||||
"el": "Ελληνικά",
|
||||
"cs": "Čeština",
|
||||
"sv": "Svenska",
|
||||
"hu": "Magyar",
|
||||
"he": "עברית",
|
||||
"da": "Dansk",
|
||||
"fi": "Suomi",
|
||||
}
|
||||
|
||||
RTL_LANGUAGES = frozenset({"ar", "fa", "he", "ur"})
|
||||
|
||||
AUTOFILL_KEYS = frozenset({"description", "text", "warning", "info", "subtitel"})
|
||||
|
||||
|
||||
196
app/utils/languages.py
Normal file
196
app/utils/languages.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""ISO 639-1 languages, their display names and writing direction.
|
||||
|
||||
Generated by utils/generate_languages.py — edit that script, not this file.
|
||||
|
||||
Display names are CLDR endonyms where CLDR covers the language and the English
|
||||
ISO 639-1 name for the 25 codes it does not.
|
||||
"""
|
||||
|
||||
LANGUAGES = {
|
||||
"en": "English",
|
||||
"aa": "Qafar",
|
||||
"ab": "Аԥсшәа",
|
||||
"ae": "Avestan",
|
||||
"af": "Afrikaans",
|
||||
"ak": "Akan",
|
||||
"am": "አማርኛ",
|
||||
"an": "aragonés",
|
||||
"ar": "العربية",
|
||||
"as": "অসমীয়া",
|
||||
"av": "Avaric",
|
||||
"ay": "Aymara",
|
||||
"az": "azərbaycan",
|
||||
"ba": "башҡорт теле",
|
||||
"be": "беларуская",
|
||||
"bg": "български",
|
||||
"bi": "Bislama",
|
||||
"bm": "bamanakan",
|
||||
"bn": "বাংলা",
|
||||
"bo": "བོད་སྐད་",
|
||||
"br": "brezhoneg",
|
||||
"bs": "bosanski",
|
||||
"ca": "català",
|
||||
"ce": "нохчийн",
|
||||
"ch": "Chamorro",
|
||||
"co": "corsu",
|
||||
"cr": "Cree",
|
||||
"cs": "čeština",
|
||||
"cu": "церковнослове́нскїй",
|
||||
"cv": "чӑваш",
|
||||
"cy": "Cymraeg",
|
||||
"da": "dansk",
|
||||
"de": "Deutsch",
|
||||
"dv": "ދިވެހިބަސް",
|
||||
"dz": "རྫོང་ཁ",
|
||||
"ee": "eʋegbe",
|
||||
"el": "Ελληνικά",
|
||||
"eo": "Esperanto",
|
||||
"es": "español",
|
||||
"et": "eesti",
|
||||
"eu": "euskara",
|
||||
"fa": "فارسی",
|
||||
"ff": "Pulaar",
|
||||
"fi": "suomi",
|
||||
"fj": "Fijian",
|
||||
"fo": "føroyskt",
|
||||
"fr": "français",
|
||||
"fy": "Frysk",
|
||||
"ga": "Gaeilge",
|
||||
"gd": "Gàidhlig",
|
||||
"gl": "galego",
|
||||
"gn": "avañe’ẽ",
|
||||
"gu": "ગુજરાતી",
|
||||
"gv": "Gaelg",
|
||||
"ha": "Hausa",
|
||||
"he": "עברית",
|
||||
"hi": "हिन्दी",
|
||||
"ho": "Hiri Motu",
|
||||
"hr": "hrvatski",
|
||||
"ht": "Kreyòl Ayisyen",
|
||||
"hu": "magyar",
|
||||
"hy": "հայերեն",
|
||||
"hz": "Herero",
|
||||
"ia": "interlingua",
|
||||
"id": "Indonesia",
|
||||
"ie": "Interlingue",
|
||||
"ig": "Igbo",
|
||||
"ii": "ꆈꌠꉙ",
|
||||
"ik": "Inupiaq",
|
||||
"io": "Ido",
|
||||
"is": "íslenska",
|
||||
"it": "italiano",
|
||||
"iu": "ᐃᓄᒃᑎᑐᑦ",
|
||||
"ja": "日本語",
|
||||
"jv": "Jawa",
|
||||
"ka": "ქართული",
|
||||
"kg": "Kongo",
|
||||
"ki": "Gikuyu",
|
||||
"kj": "Kuanyama",
|
||||
"kk": "қазақ тілі",
|
||||
"kl": "kalaallisut",
|
||||
"km": "ខ្មែរ",
|
||||
"kn": "ಕನ್ನಡ",
|
||||
"ko": "한국어",
|
||||
"kr": "Kanuri",
|
||||
"ks": "کٲشُر",
|
||||
"ku": "kurdî (kurmancî)",
|
||||
"kv": "Komi",
|
||||
"kw": "kernewek",
|
||||
"ky": "кыргызча",
|
||||
"la": "Latina",
|
||||
"lb": "Lëtzebuergesch",
|
||||
"lg": "Luganda",
|
||||
"li": "Limburgan",
|
||||
"ln": "lingála",
|
||||
"lo": "ລາວ",
|
||||
"lt": "lietuvių",
|
||||
"lu": "Tshiluba",
|
||||
"lv": "latviešu",
|
||||
"mg": "Malagasy",
|
||||
"mh": "Marshallese",
|
||||
"mi": "Māori",
|
||||
"mk": "македонски",
|
||||
"ml": "മലയാളം",
|
||||
"mn": "монгол",
|
||||
"mr": "मराठी",
|
||||
"ms": "Melayu",
|
||||
"mt": "Malti",
|
||||
"my": "မြန်မာ",
|
||||
"na": "Nauru",
|
||||
"nb": "norsk bokmål",
|
||||
"nd": "isiNdebele",
|
||||
"ne": "नेपाली",
|
||||
"ng": "Ndonga",
|
||||
"nl": "Nederlands",
|
||||
"nn": "norsk nynorsk",
|
||||
"no": "norsk",
|
||||
"nr": "isiNdebele",
|
||||
"nv": "Diné Bizaad",
|
||||
"ny": "Nyanja",
|
||||
"oc": "occitan",
|
||||
"oj": "Ojibwa",
|
||||
"om": "Oromoo",
|
||||
"or": "ଓଡ଼ିଆ",
|
||||
"os": "ирон",
|
||||
"pa": "ਪੰਜਾਬੀ",
|
||||
"pi": "Pali",
|
||||
"pl": "polski",
|
||||
"ps": "پښتو",
|
||||
"pt": "português",
|
||||
"qu": "Runasimi",
|
||||
"rm": "rumantsch",
|
||||
"rn": "Ikirundi",
|
||||
"ro": "română",
|
||||
"ru": "русский",
|
||||
"rw": "Ikinyarwanda",
|
||||
"sa": "संस्कृत भाषा",
|
||||
"sc": "sardu",
|
||||
"sd": "سنڌي",
|
||||
"se": "davvisámegiella",
|
||||
"sg": "Sängö",
|
||||
"sh": "Serbo-Croatian",
|
||||
"si": "සිංහල",
|
||||
"sk": "slovenčina",
|
||||
"sl": "slovenščina",
|
||||
"sm": "Samoan",
|
||||
"sn": "chiShona",
|
||||
"so": "Soomaali",
|
||||
"sq": "shqip",
|
||||
"sr": "српски",
|
||||
"ss": "siSwati",
|
||||
"st": "Sesotho",
|
||||
"su": "Basa Sunda",
|
||||
"sv": "svenska",
|
||||
"sw": "Kiswahili",
|
||||
"ta": "தமிழ்",
|
||||
"te": "తెలుగు",
|
||||
"tg": "тоҷикӣ",
|
||||
"th": "ไทย",
|
||||
"ti": "ትግርኛ",
|
||||
"tk": "türkmen dili",
|
||||
"tl": "Tagalog",
|
||||
"tn": "Setswana",
|
||||
"to": "lea fakatonga",
|
||||
"tr": "Türkçe",
|
||||
"ts": "Xitsonga",
|
||||
"tt": "татар",
|
||||
"tw": "Twi",
|
||||
"ty": "Tahitian",
|
||||
"ug": "ئۇيغۇرچە",
|
||||
"uk": "українська",
|
||||
"ur": "اردو",
|
||||
"uz": "o‘zbek",
|
||||
"ve": "Tshivenḓa",
|
||||
"vi": "Tiếng Việt",
|
||||
"vo": "Volapük",
|
||||
"wa": "walon",
|
||||
"wo": "Wolof",
|
||||
"xh": "IsiXhosa",
|
||||
"yi": "ייִדיש",
|
||||
"yo": "Èdè Yorùbá",
|
||||
"za": "Vahcuengh",
|
||||
"zh": "中文",
|
||||
"zu": "isiZulu",
|
||||
}
|
||||
|
||||
RTL_LANGUAGES = frozenset({"ar", "dv", "fa", "he", "ks", "ps", "sd", "ug", "ur", "yi"})
|
||||
Reference in New Issue
Block a user