i18n Localizare - sebiboga/testlink-upgraded GitHub Wiki
TestLink 2.0.1 introduce un sistem de internationalizare (i18n) pentru toate ecranele modernizate, permitand utilizatorilor sa schimbe limba interfetei direct din pagina, fara relogare.
Componente: gui/templates/i18n/i18n.js + fisiere JSON per limba
Acoperire: 7 pagini modernizate, 10 limbi disponibile
Prerequisite: Niciunul โ i18n merge pentru orice utilizator logat
- Ce Este i18n?
- Limbi Disponibile 2b. Cele Doua Sisteme de Traducere
- Cum Functioneaza
- Detectarea Limbii
- Switcher-ul de Limba
- Pagini Modernizate cu i18n
- Arhitectura Tehnica
- Adaugarea unei Limbi Noi
- Adaugarea unui Text Nou
- Fisiere si Referinte
i18n (de la internationalization โ 18 litere intre i si n) este sistemul care permite TestLink sa afiseze textele interfetei in limba aleasa de utilizator.
Inainte de i18n:
- Toate textele erau scrise direct in HTML/PHP in Engleza
- Utilizatorul nu putea schimba limba
- Ecranele moderne (Event Viewer, User Management etc.) erau doar in Engleza
Dupa i18n:
- Textele sunt in fisiere JSON separate pe limbi
- Utilizatorul selecteaza limba din dropdown โ pagina se reincarca tradusa
- Detectarea automata a limbii din profilul utilizatorului
TestLink are doua sisteme de traducere care ruleaza in paralel (vezi sectiunea 2b). O limba este cu adevarat completa doar cand exista in amandoua.
| Cod TL | Limba | Nativ | PHP (locale/) |
JS (i18n/) |
|---|---|---|---|---|
en_GB / en_US
|
English | English | Complet (limba de dezvoltare) | Complet |
ro_RO |
Romana | Romana | Navigatie | Complet |
de_DE |
German | Deutsch | Complet | Complet |
fr_FR |
French | Francais | Complet | Complet |
es_ES / es_AR
|
Spanish | Espanol | Complet | Complet |
it_IT |
Italian | Italiano | Complet | Complet |
pt_PT / pt_BR
|
Portuguese | Portugues | Complet | Complet |
ru_RU |
Russian | Russkiy | Complet | Complet |
ja_JP |
Japanese | Nihongo | Complet | Complet |
zh_CN |
Chinese Simplified | Zhongwen | Complet | Complet |
Mai exista in cfg/const.inc.php si cs_CZ, fi_FI, id_ID, ko_KR,
nl_NL, pl_PL. Acestea au fisier PHP, dar nu au bundle JS, deci
ecranele moderne le afiseaza in Engleza.
ro_ROare tradusa integral navigatia (meniul lateral si bara de sus). Restul paginilor PHP cad pe Engleza, cheie cu cheie - vezi sectiunea 2b.
Aceasta este partea care surprinde cel mai des: TestLink traduce in doua feluri diferite, in functie de cine randeaza pagina.
| Sistemul PHP (clasic) | Sistemul JS (modern) | |
|---|---|---|
| Cine il foloseste | Meniul lateral (aside), bara de sus, toate paginile Smarty |
Ecranele modernizate: Event Viewer, User Management, Roles, Profile, Test Project Management |
| Unde stau textele | locale/<xx_YY>/strings.txt |
gui/templates/i18n/<xx>.json |
| Cum se citesc |
lang_get('cheie') in PHP, {$labels.cheie} in Smarty |
TLi18n.t('cheie') sau data-i18n="cheie" in HTML |
| Codul limbii | complet: ro_RO, de_DE
|
scurt: ro, de
|
| De unde vine limba |
$_SESSION['locale'], pus la login din users.locale
|
?locale= -> localStorage -> profilul din /api/userinfo/
|
| Fallback | pe cheie, la en_GB
|
pe fisier, la en.json
|
Un exemplu real: Event Viewer aparea in romana desi locale/ro_RO/ nu
exista. Explicatia e ca Event Viewer e un ecran modern - citeste din
gui/templates/i18n/ro.json, care exista. Meniul lateral, randat de Smarty,
cauta locale/ro_RO/strings.txt, nu il gasea, si cadea pe Engleza.
Regula: daca textul e in meniul lateral sau intr-o pagina veche, e PHP. Daca e intr-un ecran modernizat, e JSON. Pentru o limba completa ai nevoie de ambele.
lang_get() cade pe Engleza pentru fiecare cheie lipsa in parte, nu
pentru tot fisierul. Un strings.txt partial este perfect valid: traduci
cat vrei, restul ramane in Engleza. De asta locale/ro_RO/strings.txt are
79 de chei, nu 2700.
Atentie: o cheie prezenta cu o valoare gresita este mai rea decat una lipsa. Nou logul avea 126 de chei setate literal pe
"undefined"in 9 limbi - acelea afisau cuvantulundefinedin meniu, in loc sa cada pe Engleza. Daca nu traduci o cheie, sterge-o, nu o lasa cu text de umplutura.
1. Pagina se incarca (ex: eventviewer.html)
2. TLi18n.load(callback) este apelat
3. Locale detection:
a. ?locale= din URL โ folosit daca exista
b. Profilul utilizatorului din DB โ /api/userinfo/
c. Default โ 'en'
4. Se incarca fisierul JSON (ex: /gui/templates/i18n/ro.json)
5. TLi18n.apply() traduce toate elementele cu data-i18n="key"
6. TLi18n.initLocaleSwitcher()Afiseaza dropdown-ul
Elementele vizibile au atributul data-i18n:
<span data-i18n="header.eventViewer">Event Viewer</span>
<span data-i18n="header.eventViewerSub">real-time log monitoring</span>Dupa TLi18n.apply(), textele devin:
<span data-i18n="header.eventViewer">Vizualizare Evenimente</span>
<span data-i18n="header.eventViewerSub">monitorizare logs in timp real</span>Pentru texte construite in JS:
// In loc de:
$('#footer').text(r.total + ' events | Generated on ' + new Date().toLocaleString());
// Se foloseste:
$('#footer').text(TLi18n.t('ev.eventsCount', {n: r.total}) + ' | ' + TLi18n.t('common.generatedOn') + ' ' + new Date().toLocaleString());"ev.eventsCount": "{n} events"
"role.deleteUsersMsg": "{n} user(s) currently have this role: {users}"TLi18n.t('ev.eventsCount', {n: 90}) // "90 events"
TLi18n.t('role.deleteUsersMsg', {n: 3, users: "admin, sebiboga"}) // "3 user(s) currently have this role: admin, sebiboga"Ordinea de prioritate (de la cea mai mare la cea mai mica):
| Prioritate | Sursa | Exemplu | Cand se foloseste |
|---|---|---|---|
| 1 |
?locale= URL param |
?locale=ro |
Override explicit โ utilizatorul a schimbat limba |
| 2 | localStorage('tl_locale') |
ro |
Comutarea manuala facuta in switcher โ tine pe toate ecranele |
| 3 | Profilul din DB | ro_RO |
Prima accesare โ detecteaza automat din profil |
| 4 | Default | en |
Daca nu e logat sau profilul nu are locale setat |
Pasul 2 a fost refacut in #1586. Comutarea manuala din switcher se
salveaza in localStorage('tl_locale') si este citita la fiecare incarcare,
asa ca revenirea la un launcher prin butonul Cancel / Back (care nu
transporta ?locale=) pastreaza limba. Consultarea profilului din DB nu
scrie in localStorage, deci o valoare salvata e intotdeauna o alegere
explicita a utilizatorului. Pentru simetrie, formularul de profil
(userInfo.html) scrie acum in magazin aceeasi valoare pe care o salveaza in
DB โ altfel schimbarea limbii din profil ar parea fara efect pe ecranele
modernizate.
Daca bundle-ul unei limbi lipseste, valoarea care a picat este eliminata din
localStorage (numai la un 404 real, nu la o eroare de retea) si urmatoarea
incarcare revine la profil โ nu se "lipeste" sesiunea de Engleza.
Switcherul apare in header-ul fiecarei pagini modernizate:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Vizualizare Evenimente monitorizare logs [Romana โผ]โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Comportament:
- Dropdown cu 19 locale (limbile din profil; cele fara bundle JSON raman in Engleza)
- La selectie: se adauga
?locale=xxin URL โ pagina se reincarca - Selectia se salveaza in
localStorage('tl_locale')si se aplica tuturor ecranelor modernizate, inclusiv dupa revenirea la un launcher prin Cancel / Back (refacut in #1586) - Fara o alegere explicita, limba vine din profilul utilizatorului
| Pagina | Fisier | Chevron Tabs | Locale Switcher |
|---|---|---|---|
| Event Viewer | eventviewer/eventviewer.html |
Nu | โ |
| User Management | usermanagement/usersView.html |
โ | โ |
| Role Management | usermanagement/rolesView.html |
โ | โ |
| Assign Project Roles | usermanagement/usersAssignProject.html |
โ | โ |
| Assign Plan Roles | usermanagement/usersAssignPlan.html |
โ | โ |
| Custom Fields | cfields/cfieldsView.html |
Nu | โ |
| User Profile | usermanagement/userInfo.html |
Nu | โ |
Cele 4 pagini User Management impart aceeasi bara de tab-uri tradusa.
Modul IIFE (Immediately Invoked Function Expression) โ expune API public:
var TLi18n = (function() {
// Private
var _locale = 'en';
var _strings = {};
function load(callback) { ... } // Incarca traducerile
function apply(root) { ... } // Aplica pe elementele data-i18n
function t(key, params) { ... } // Returneaza string tradus
function initLocaleSwitcher(sel) { ... } // Creeaza dropdown-ul
// Public
return {
load: load,
apply: apply,
t: t,
initLocaleSwitcher: initLocaleSwitcher,
getLocale: function() { return _locale; }
};
})();{
"_meta": { "name": "English", "code": "en" },
"app.version": "2.0.1 [TEST]",
"header.eventViewer": "Event Viewer",
"header.eventViewerSub": "real-time log monitoring",
"ev.logLevels": "Log Levels",
"ev.user": "User",
"ev.allUsers": "All users",
"ev.eventsCount": "{n} events",
"common.cancel": "Cancel",
"common.save": "Save",
"common.loading": "Loading..."
}TestLink foloseste coduri de 6 caractere (ro_RO, en_GB), i18n foloseste 2 caractere (ro, en):
var LOCALE_MAP = {
'en_GB': 'en', 'en_US': 'en',
'ro_RO': 'ro',
'de_DE': 'de',
'fr_FR': 'fr',
'es_ES': 'es', 'es_AR': 'es',
'it_IT': 'it',
'pt_BR': 'pt', 'pt_PT': 'pt',
'ru_RU': 'ru',
'ja_JP': 'ja',
'zh_CN': 'zh'
};Exemplu: adaugam Maghiara (hu_HU / hu). Inlocuieste peste tot hu_HU
si hu cu codurile limbii tale.
Deschide cfg/const.inc.php si cauta $g_locales:
$g_locales = array('cs_CZ' => 'Czech', 'de_DE' => 'German', ...);Daca limba nu e in lista, adaug-o. Foloseste entitati HTML pentru diacritice:
'hu_HU' => 'Magyar',Adaug-o si in $g_date_format, $g_timestamp_format, $g_input_date_format
din acelasi fisier, altfel datele se vor formata cu valorile implicite.
Capcana reala:
ro_ROera in$g_localesde la inceput, dar nu existalocale/ro_RO/. Aparea in dropdown si dadea o interfata complet englezeasca. Daca adaugi limba in dropdown, adaug-o si in Pasul 1.
Creeaza directorul si fisierul:
mkdir -p locale/hu_HUstrings.txt este cod PHP, nu un fisier de configurare. Reguli:
<?php
$TLS_STRINGFILE_CHARSET = "UTF-8";
// comentariile se scriu doar cu //
$TLS_projects = 'Projektek';
$TLS_search = 'Kereses';- fiecare cheie are prefixul
$TLS_ - fisierul trebuie salvat UTF-8
- daca textul contine
', foloseste ghilimele duble sau escapeaza - nu trebuie sa traduci tot: ce lipseste cade pe Engleza
description.php nu e obligatoriu - lang_load() cade pe en_GB daca
lipseste.
Verifica intotdeauna:
php -l locale/hu_HU/strings.txtCapcana care nu se vede la
php -l: cinci fisiere de limba se termina cu?>. Daca adaugi text dupa acel tag, PHP il trateaza ca HTML si il tipareste in pagina.php -lspune "No syntax errors" pentru ca e valid sintactic. Adauga intotdeauna inainte de?>, si testeaza asa:php -r 'ob_start(); include $argv[1]; echo strlen(trim(ob_get_clean()));' \ locale/hu_HU/strings.txtTrebuie sa afiseze
0. Orice altceva inseamna ca fisierul scrie in pagina.
cp gui/templates/i18n/en.json gui/templates/i18n/hu.jsonActualizeaza _meta si tradu valorile (nu cheile):
{
"_meta": { "name": "Magyar", "code": "hu" },
"common.save": "Mentes",
"header.projects": "Teszt projektek kezelese"
}Aici nu exista fallback pe cheie: o cheie lipsa se afiseaza ca text brut
(proj.title). Porneste de la en.json si tradu tot.
In gui/templates/i18n/i18n.js, in LOCALE_MAP:
var LOCALE_MAP = {
'en_GB': 'en', 'en_US': 'en',
'hu_HU': 'hu', // <-- adauga
...
};Fara asta, hu_HU cade pe primele doua litere - care aici chiar dau hu,
deci ar merge - dar pentru coduri ca cs_CZ -> cs maparea explicita e
singura garantie.
Nu mai trebuie sa adaugi limba in switcher. Lista vine din
GET /api/userinfo/locales, adica din acelasi $g_locales, exact ca
dropdown-ul din Profile. Cele doua liste nu mai pot ajunge diferite.
Daca gasesti text englezesc hardcodat intr-un template, muta-l in sistemul potrivit inainte sa traduci:
-
Smarty/PHP: adauga cheia in
locale/en_GB/strings.txt, treci-o in lista dingui/templates/dashio/labels/labels.<template>.tpl, apoi foloseste{$labels.cheia}in template. -
Ecran modern: adauga cheia in toate fisierele
i18n/*.jsonsi punedata-i18n="cheia"pe element.
Partea JS - fara sa schimbi nimic in profil:
http://localhost:8000/gui/templates/eventviewer/eventviewer.html?locale=hu
Partea PHP - schimba limba in Profile -> Personal Data -> Locale, apoi salveaza si reincarca. Meniul lateral trebuie sa fie tradus.
Sesiunea pastreaza limba veche pana la reincarcare, iar Smarty tine template-uri compilate. Daca nu vezi schimbarea:
rm -rf gui/templates_c/*si reincarca.
Verificarea acoperirii pentru meniu:
python - <<'EOF'
import re, io
tpl = io.open('gui/templates/dashio/labels/labels.aside.tpl', encoding='utf-8').read()
keys = set(k.strip() for k in re.search(r"s='([^']+)'", tpl, re.S)
.group(1).replace('\n', ' ').split(',') if k.strip())
txt = io.open('locale/hu_HU/strings.txt', encoding='utf-8', errors='replace').read()
missing = [k for k in sorted(keys)
if not re.search(r'^\$TLS_%s\s*=' % re.escape(k), txt, re.M)]
print('lipsesc %d din %d:' % (len(missing), len(keys)), missing)
EOF| Fisier | Rol | Obligatoriu |
|---|---|---|
cfg/const.inc.php |
inregistreaza limba in dropdown + formate de data | Da |
locale/hu_HU/strings.txt |
textele paginilor PHP/Smarty | Da |
gui/templates/i18n/hu.json |
textele ecranelor moderne | Da |
gui/templates/i18n/i18n.js |
maparea hu_HU -> hu
|
Da |
locale/hu_HU/description.php |
descrieri roluri | Nu (cade pe en_GB) |
locale/hu_HU/custom_strings.txt |
suprascrieri locale | Nu |
- Adauga atributul
data-i18npe element:
<button data-i18n="myNewButton">Click Me</button>- Adauga cheia in TOATE fisierele JSON:
"myNewButton": "Click Me" // en.json
"myNewButton": "Apasa Aici" // ro.json
"myNewButton": "Klicken Sie hier" // de.json
... (10 fisiere)// In loc de:
alert('Saved successfully!');
// Foloseste:
alert(TLi18n.t('msg.savedSuccess'));Si adauga cheia in toate fisierele JSON.
| Fisier | Dimensiune | Descriere |
|---|---|---|
gui/templates/i18n/i18n.js |
~210 linii | Modul i18n principal |
gui/templates/i18n/en.json |
~160 chei | Engleza (original) |
gui/templates/i18n/ro.json |
~160 chei | Romana |
gui/templates/i18n/de.json |
~160 chei | Germana |
gui/templates/i18n/fr.json |
~160 chei | Franceza |
gui/templates/i18n/es.json |
~160 chei | Spaniola |
gui/templates/i18n/it.json |
~160 chei | Italiana |
gui/templates/i18n/pt.json |
~160 chei | Portugheza |
gui/templates/i18n/ru.json |
~160 chei | Rusa |
gui/templates/i18n/ja.json |
~160 chei | Japoneza |
gui/templates/i18n/zh.json |
~160 chei | Chineza |
| Zona | Prefix | Nr. Chei | Exemple |
|---|---|---|---|
| Header | header.* |
14 |
header.eventViewer, header.userManagement
|
| Common | common.* |
12 |
common.cancel, common.save, common.loading
|
| Event Viewer | ev.* |
17 |
ev.logLevels, ev.apply, ev.eventsCount
|
| User Management | user.* |
13 |
user.login, user.firstName, user.createUser
|
| Role Management | role.* |
16 |
role.name, role.rights, role.system
|
| Assign | assign.* |
12 |
assign.testProject, assign.saveChanges
|
| Profile | profile.* |
23 |
profile.personalData, profile.changePassword
|
| Custom Fields | cf.* |
18 |
cf.label, cf.type, cf.createCustomField
|
| Tabs | tab.* |
4 |
tab.userManagement, tab.roleManagement
|
| Messages | *.msg.* |
12 |
profile.msg.profileUpdated, role.error.save
|
Editฤrile paralele ale bundle-urilor (agenศi CI) pot scrie blocuri de traducere รฎn fiศierul greศit โ incidentul #543 (pt.json conศinea romรขnฤ, ro.json rusa, ru.json chinezฤ) a produs exact acest simptom.
Guard: tools/lint_i18n.py valideazฤ toate cele 10 bundle-uri:
python3 tools/lint_i18n.py # erori = contaminare de script/JSON invalid
python3 tools/lint_i18n.py --strict # + paritatea cheilor vs en.json devine eroareVerificฤri: JSON valid; paritate chei vs en.json; caractere chirilice รฎn
afara lui ru.json; kana รฎn afara lui ja.json; CJK รฎn afara lui
ja/zh.json; diacritice romรขneศti (ฤ ศ ศ) รฎn afara lui ro.json.
Ruleazฤ-l รฎnainte de fiecare commit care atinge gui/templates/i18n/*.json.
Detalii: i18n-Locale-Bundle-Fix.