Skip to content

Commit b95441b

Browse files
committed
Update translations for SQLAdmin: Added new phrases and updated existing translations in multiple languages (en, az, de, ru, tr). Generated new translations.json file for easier management of language strings.
1 parent 469d14e commit b95441b

18 files changed

Lines changed: 769 additions & 789 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ secure:
4949
# Extract translatable strings into the .pot template and sync every catalog.
5050
i18n-extract:
5151
uv run python -c "import os; os.makedirs('sqladmin/translations', exist_ok=True)"
52-
uv run pybabel extract -F babel.cfg -o sqladmin/translations/admin.pot .
52+
uv run pybabel extract -F pyproject.toml -o sqladmin/translations/admin.pot .
5353
uv run pybabel update -i sqladmin/translations/admin.pot -d sqladmin/translations -D admin
5454

5555
# Create a catalog for a new locale, e.g. `make i18n-init LOCALE=fr`.

docs/internationalization.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
SQLAdmin can translate its interface into multiple languages using
2-
[Babel](https://babel.pocoo.org/). Compiled catalogs for English (`en`), German (`de`), Azerbaijani (`az`),
3-
Russian (`ru`) and Turkish (`tr`) ship with the
2+
[Babel](https://babel.pocoo.org/). Compiled catalogs for Azerbaijani (`az`),
3+
German (`de`), English (`en`), Russian (`ru`) and Turkish (`tr`) ship with the
44
package.
55

66
Translation is optional. Install the extra to enable it:
@@ -25,7 +25,7 @@ language switcher in the navigation:
2525
engine,
2626
i18n_config=I18nConfig(
2727
default_locale="en",
28-
language_switcher=["en", "de", "az", "ru", "tr"],
28+
language_switcher=["en", "az", "de", "ru", "tr"],
2929
),
3030
)
3131
```
@@ -146,4 +146,4 @@ and `.mo` (compiled) files.
146146

147147
Model field labels and WTForms validation messages are not covered by this
148148
layer. Field labels come from your SQLAlchemy columns (use `column_labels`
149-
to customise them), and WTForms ships its own validator translations.
149+
to customise them), and WTForms ships its own validator translations.

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,11 @@ exclude_lines = [
111111
fail_under = 95
112112
show_missing = true
113113
skip_covered = true
114+
115+
[tool.babel]
116+
# Message extraction mapping for `pybabel extract -F pyproject.toml`.
117+
# Only Jinja templates are scanned; SQLAdmin marks UI strings with _("...").
118+
[[tool.babel.mappings]]
119+
method = "jinja2"
120+
pattern = "sqladmin/templates/**.html"
121+
extensions = "jinja2.ext.i18n"

scripts/seed_translations.py

Lines changed: 36 additions & 288 deletions
Original file line numberDiff line numberDiff line change
@@ -1,306 +1,54 @@
1-
import copy
1+
import json
22
import os
3+
import sys
4+
from typing import Dict
35

46
from babel.messages.catalog import Catalog
57
from babel.messages.mofile import write_mo
68
from babel.messages.pofile import write_po
79

8-
ROOT = os.path.join("sqladmin", "translations")
10+
DEFAULT_MAPPINGS = os.path.join("scripts", "translations.json")
11+
OUTPUT_ROOT = os.path.join("sqladmin", "translations")
12+
DOMAIN = "admin"
913

10-
# Every translatable UI string (the msgids marked with _("...") in templates).
11-
MESSAGES = [
12-
"Logout",
13-
"Login",
14-
"Login to",
15-
"Username",
16-
"Enter username",
17-
"Password",
18-
"Search",
19-
"Filters",
20-
"Apply Filter",
21-
"Save",
22-
"Save and continue editing",
23-
"Save and add another",
24-
"Save as new",
25-
"Column",
26-
"Value",
27-
"Please confirm",
28-
"Cancel",
29-
"Delete",
30-
"Delete selected items",
31-
"Showing",
32-
"Language",
33-
"Export",
34-
"Import CSV",
35-
"New",
36-
"Edit",
37-
"Actions",
38-
"View",
39-
"to",
40-
"of",
41-
"items",
42-
"prev",
43-
"next",
44-
"Show",
45-
"Page",
46-
"Go Back",
47-
"Yes",
48-
"Close",
49-
"No file selected",
50-
"Skip invalid rows and continue import",
51-
"Import result",
52-
"Missed rows",
53-
"Import",
54-
"Download CSV",
55-
"Download TXT",
56-
"Refresh list",
57-
"Clear filter",
58-
"Done",
59-
"Select all",
60-
"Select item",
61-
"Import progress",
62-
]
6314

64-
# locale -> {msgid: msgstr}. English is the source language (empty catalog).
65-
TRANSLATIONS: dict[str, dict[str, str]] = {
66-
"en": {},
67-
"az": {
68-
"Logout": "Çıxış",
69-
"Login": "Daxil ol",
70-
"Login to": "Daxil ol:",
71-
"Username": "İstifadəçi adı",
72-
"Enter username": "İstifadəçi adını daxil edin",
73-
"Password": "Şifrə",
74-
"Search": "Axtarış",
75-
"Filters": "Filtrlər",
76-
"Apply Filter": "Filtri tətbiq et",
77-
"Save": "Yadda saxla",
78-
"Save and continue editing": "Yadda saxla və redaktəyə davam et",
79-
"Save and add another": "Yadda saxla və başqasını əlavə et",
80-
"Save as new": "Yeni kimi yadda saxla",
81-
"Column": "Sütun",
82-
"Value": "Dəyər",
83-
"Please confirm": "Təsdiq edin",
84-
"Cancel": "Ləğv et",
85-
"Delete": "Sil",
86-
"Delete selected items": "Seçilmiş elementləri sil",
87-
"Showing": "Göstərilir",
88-
"Language": "Dil",
89-
"Export": "İxrac et",
90-
"Import CSV": "CSV idxal et",
91-
"New": "Yeni",
92-
"Edit": "Redaktə et",
93-
"Actions": "Əməliyyatlar",
94-
"View": "Bax",
95-
"to": "-",
96-
"of": "/",
97-
"items": "element",
98-
"prev": "əvvəlki",
99-
"next": "növbəti",
100-
"Show": "Göstər",
101-
"Page": "Səhifə",
102-
"Go Back": "Geri qayıt",
103-
"Yes": "Bəli",
104-
"Close": "Bağla",
105-
"No file selected": "Fayl seçilməyib",
106-
"Skip invalid rows and continue import": "Yanlış sətirləri ötür və idxalı davam etdir", # noqa: E501
107-
"Import result": "İdxal nəticəsi",
108-
"Missed rows": "Buraxılmış sətirlər",
109-
"Import": "İdxal et",
110-
"Download CSV": "CSV yüklə",
111-
"Download TXT": "TXT yüklə",
112-
"Refresh list": "Siyahını yenilə",
113-
"Clear filter": "Filtri təmizlə",
114-
"Done": "Hazır",
115-
"Select all": "Hamısını seç",
116-
"Select item": "Elementi seç",
117-
"Import progress": "İdxal gedişatı",
118-
},
119-
"de": {
120-
"Logout": "Abmelden",
121-
"Login": "Anmelden",
122-
"Login to": "Anmelden bei",
123-
"Username": "Benutzername",
124-
"Enter username": "Benutzername eingeben",
125-
"Password": "Passwort",
126-
"Search": "Suche",
127-
"Filters": "Filter",
128-
"Apply Filter": "Filter anwenden",
129-
"Save": "Speichern",
130-
"Save and continue editing": "Speichern und weiter bearbeiten",
131-
"Save and add another": "Speichern und weiteres hinzufügen",
132-
"Save as new": "Als neu speichern",
133-
"Column": "Spalte",
134-
"Value": "Wert",
135-
"Please confirm": "Bitte bestätigen",
136-
"Cancel": "Abbrechen",
137-
"Delete": "Löschen", # noqa: E501
138-
"Delete selected items": "Ausgewählte Einträge löschen",
139-
"Showing": "Anzeige",
140-
"Language": "Sprache",
141-
"Export": "Exportieren",
142-
"Import CSV": "CSV importieren",
143-
"New": "Neu",
144-
"Edit": "Bearbeiten",
145-
"Actions": "Aktionen",
146-
"View": "Ansehen",
147-
"to": "bis",
148-
"of": "von",
149-
"items": "Einträge",
150-
"prev": "zurück",
151-
"next": "weiter", # noqa: E501
152-
"Show": "Zeige",
153-
"Page": "Seite",
154-
"Go Back": "Zurück",
155-
"Yes": "Ja",
156-
"Close": "Schließen",
157-
"No file selected": "Keine Datei ausgewählt",
158-
"Skip invalid rows and continue import": "Ungültige Zeilen überspringen und Import fortsetzen", # noqa: E501
159-
"Import result": "Importergebnis",
160-
"Missed rows": "Übersprungene Zeilen",
161-
"Import": "Importieren",
162-
"Download CSV": "CSV herunterladen",
163-
"Download TXT": "TXT herunterladen",
164-
"Refresh list": "Liste aktualisieren",
165-
"Clear filter": "Filter löschen",
166-
"Done": "Fertig",
167-
"Select all": "Alle auswählen",
168-
"Select item": "Eintrag auswählen",
169-
"Import progress": "Importfortschritt",
170-
},
171-
"ru": {
172-
"Logout": "Выход",
173-
"Login": "Вход",
174-
"Login to": "Вход в",
175-
"Username": "Имя пользователя",
176-
"Enter username": "Введите имя пользователя",
177-
"Password": "Пароль",
178-
"Search": "Поиск",
179-
"Filters": "Фильтры",
180-
"Apply Filter": "Применить фильтр",
181-
"Save": "Сохранить",
182-
"Save and continue editing": "Сохранить и продолжить редактирование",
183-
"Save and add another": "Сохранить и добавить ещё",
184-
"Save as new": "Сохранить как новое",
185-
"Column": "Столбец",
186-
"Value": "Значение",
187-
"Please confirm": "Подтвердите",
188-
"Cancel": "Отмена",
189-
"Delete": "Удалить",
190-
"Delete selected items": "Удалить выбранные элементы",
191-
"Showing": "Показано",
192-
"Language": "Язык",
193-
"Export": "Экспорт",
194-
"Import CSV": "Импорт CSV",
195-
"New": "Создать",
196-
"Edit": "Изменить",
197-
"Actions": "Действия",
198-
"View": "Просмотр",
199-
"to": "–",
200-
"of": "из",
201-
"items": "элементов",
202-
"prev": "назад",
203-
"next": "вперёд",
204-
"Show": "Показать",
205-
"Page": "страница",
206-
"Go Back": "Назад",
207-
"Yes": "Да",
208-
"Close": "Закрыть",
209-
"No file selected": "Файл не выбран",
210-
"Skip invalid rows and continue import": "Пропустить неверные строки и продолжить импорт", # noqa: E501
211-
"Import result": "Результат импорта",
212-
"Missed rows": "Пропущенные строки",
213-
"Import": "Импорт",
214-
"Download CSV": "Скачать CSV",
215-
"Download TXT": "Скачать TXT", # noqa: E501
216-
"Refresh list": "Обновить список",
217-
"Clear filter": "Очистить фильтр",
218-
"Done": "Готово",
219-
"Select all": "Выбрать всё",
220-
"Select item": "Выбрать элемент",
221-
"Import progress": "Ход импорта",
222-
},
223-
"tr": {
224-
"Logout": "Çıkış",
225-
"Login": "Giriş",
226-
"Login to": "Giriş:",
227-
"Username": "Kullanıcı adı",
228-
"Enter username": "Kullanıcı adını girin",
229-
"Password": "Parola",
230-
"Search": "Ara",
231-
"Filters": "Filtreler",
232-
"Apply Filter": "Filtreyi uygula",
233-
"Save": "Kaydet",
234-
"Save and continue editing": "Kaydet ve düzenlemeye devam et",
235-
"Save and add another": "Kaydet ve başka ekle",
236-
"Save as new": "Yeni olarak kaydet",
237-
"Column": "Sütun",
238-
"Value": "Değer",
239-
"Please confirm": "Lütfen onaylayın",
240-
"Cancel": "İptal",
241-
"Delete": "Sil",
242-
"Delete selected items": "Seçili öğeleri sil",
243-
"Showing": "Gösteriliyor",
244-
"Language": "Dil",
245-
"Export": "Dışa aktar",
246-
"Import CSV": "CSV içe aktar",
247-
"New": "Yeni",
248-
"Edit": "Düzenle",
249-
"Actions": "İşlemler",
250-
"View": "Görüntüle",
251-
"to": "-",
252-
"of": "/",
253-
"items": "öğe",
254-
"prev": "önceki",
255-
"next": "sonraki",
256-
"Show": "Göster",
257-
"Page": "Sayfa",
258-
"Go Back": "Geri dön",
259-
"Yes": "Evet",
260-
"Close": "Kapat",
261-
"No file selected": "Dosya seçilmedi",
262-
"Skip invalid rows and continue import": "Geçersiz satırları atla ve içe aktarmaya devam et", # noqa: E501
263-
"Import result": "İçe aktarma sonucu",
264-
"Missed rows": "Atlanan satırlar",
265-
"Import": "İçe aktar",
266-
"Download CSV": "CSV indir",
267-
"Download TXT": "TXT indir",
268-
"Refresh list": "Listeyi yenile",
269-
"Clear filter": "Filtreyi temizle",
270-
"Done": "Tamam",
271-
"Select all": "Tümünü seç",
272-
"Select item": "Öğeyi seç",
273-
"Import progress": "İçe aktarma ilerlemesi",
274-
},
275-
}
15+
def build(mappings_path: str, output_root: str = OUTPUT_ROOT) -> None:
16+
"""Write ``.po``/``.mo`` catalogs (and the ``.pot`` template) from a mapping.
27617
18+
Args:
19+
mappings_path: Path to the JSON file of ``{locale: {source: translation}}``.
20+
output_root: Directory the ``gettext`` tree is written to.
21+
"""
22+
with open(mappings_path, encoding="utf-8") as file:
23+
mappings: Dict[str, Dict[str, str]] = json.load(file)
27724

278-
def build() -> None:
279-
os.makedirs(ROOT, exist_ok=True)
25+
# The full source-string set is the union of every locale's keys, so a
26+
# partially translated locale still lists all messages.
27+
messages = sorted({message for entries in mappings.values() for message in entries})
28+
os.makedirs(output_root, exist_ok=True)
28029

281-
template = Catalog(project="SQLAdmin", domain="admin")
282-
for message in MESSAGES:
30+
template = Catalog(project="SQLAdmin", domain=DOMAIN)
31+
for message in messages:
28332
template.add(message, string="")
284-
with open(os.path.join(ROOT, "admin.pot"), "wb") as f:
285-
write_po(f, template, omit_header=False)
33+
with open(os.path.join(output_root, "admin.pot"), "wb") as file:
34+
write_po(file, template, omit_header=False)
28635

287-
for locale, mapping in TRANSLATIONS.items():
288-
catalog = copy.deepcopy(template)
289-
catalog.locale = locale
290-
catalog.fuzzy = False
291-
for message in catalog: # type: ignore[assignment]
292-
if message.id and message.id in mapping: # type: ignore[attr-defined]
293-
message.string = mapping[message.id] # type: ignore[attr-defined]
36+
for locale, entries in mappings.items():
37+
catalog = Catalog(locale=locale, project="SQLAdmin", domain=DOMAIN)
38+
for message in messages:
39+
catalog.add(message, string=entries.get(message, ""))
29440

295-
directory = os.path.join(ROOT, locale, "LC_MESSAGES")
41+
directory = os.path.join(output_root, locale, "LC_MESSAGES")
29642
os.makedirs(directory, exist_ok=True)
297-
with open(os.path.join(directory, "admin.po"), "wb") as f:
298-
write_po(f, catalog, omit_header=False, sort_output=False)
299-
with open(os.path.join(directory, "admin.mo"), "wb") as f:
300-
write_mo(f, catalog)
301-
translated = sum(1 for m in catalog if m.id and m.string)
302-
print(f"{locale}: {translated}/{len(MESSAGES)} translated")
43+
with open(os.path.join(directory, "admin.po"), "wb") as file:
44+
write_po(file, catalog, omit_header=False, sort_output=False)
45+
with open(os.path.join(directory, "admin.mo"), "wb") as file:
46+
write_mo(file, catalog)
47+
48+
translated = sum(1 for message in messages if entries.get(message))
49+
print(f"{locale}: {translated}/{len(messages)} translated")
30350

30451

30552
if __name__ == "__main__":
306-
build()
53+
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MAPPINGS
54+
build(path)

0 commit comments

Comments
 (0)