i18n Localizare - sebiboga/testlink-upgraded GitHub Wiki

i18n โ€” Internationalizare (Localizare) โ€” 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


Table of Contents

  1. Ce Este i18n?
  2. Limbi Disponibile 2b. Cele Doua Sisteme de Traducere
  3. Cum Functioneaza
  4. Detectarea Limbii
  5. Switcher-ul de Limba
  6. Pagini Modernizate cu i18n
  7. Arhitectura Tehnica
  8. Adaugarea unei Limbi Noi
  9. Adaugarea unui Text Nou
  10. Fisiere si Referinte

1. Ce Este i18n?

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

2. Limbi Disponibile

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_RO are tradusa integral navigatia (meniul lateral si bara de sus). Restul paginilor PHP cad pe Engleza, cheie cu cheie - vezi sectiunea 2b.


2b. Cele Doua Sisteme de Traducere

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

De ce conteaza

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.

Fallback pe cheie (partea PHP)

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 cuvantul undefined in meniu, in loc sa cada pe Engleza. Daca nu traduci o cheie, sterge-o, nu o lasa cu text de umplutura.


3. Cum Functioneaza

Fluxul unei Pagini

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

In HTML โ€” Elemente Statice

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>

In JavaScript โ€” Texte Dinamice

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());

Interpolare

"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"

4. Detectarea Limbii

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.


5. Switcher-ul de Limba

Switcherul apare in header-ul fiecarei pagini modernizate:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Vizualizare Evenimente  monitorizare logs  [Romana โ–ผ]โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Comportament:

  1. Dropdown cu 19 locale (limbile din profil; cele fara bundle JSON raman in Engleza)
  2. La selectie: se adauga ?locale=xx in URL โ†’ pagina se reincarca
  3. 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)
  4. Fara o alegere explicita, limba vine din profilul utilizatorului

6. Pagini Modernizate cu i18n

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.


7. Arhitectura Tehnica

Fisierul Principal: i18n.js

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; }
  };
})();

Fisiere JSON โ€” Structura

{
  "_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..."
}

Codul de Mapare Locale

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'
};

8. Adaugarea unei Limbi Noi

Exemplu: adaugam Maghiara (hu_HU / hu). Inlocuieste peste tot hu_HU si hu cu codurile limbii tale.

Pasul 0 - Verifica daca limba e deja inregistrata

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_RO era in $g_locales de la inceput, dar nu exista locale/ro_RO/. Aparea in dropdown si dadea o interfata complet englezeasca. Daca adaugi limba in dropdown, adaug-o si in Pasul 1.

Pasul 1 - Partea PHP: locale/hu_HU/strings.txt

Creeaza directorul si fisierul:

mkdir -p locale/hu_HU

strings.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.txt

Capcana 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 -l spune "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.txt

Trebuie sa afiseze 0. Orice altceva inseamna ca fisierul scrie in pagina.

Pasul 2 - Partea JS: gui/templates/i18n/hu.json

cp gui/templates/i18n/en.json gui/templates/i18n/hu.json

Actualizeaza _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.

Pasul 3 - Maparea locale -> bundle

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.

Pasul 4 - Textele care nu-s inca in nicio limba

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 din gui/templates/dashio/labels/labels.<template>.tpl, apoi foloseste {$labels.cheia} in template.
  • Ecran modern: adauga cheia in toate fisierele i18n/*.json si pune data-i18n="cheia" pe element.

Pasul 5 - Testeaza

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

Rezumat - fisiere atinse

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

9. Adaugarea unui Text Nou

In HTML

  1. Adauga atributul data-i18n pe element:
<button data-i18n="myNewButton">Click Me</button>
  1. 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 JavaScript

// In loc de:
alert('Saved successfully!');

// Foloseste:
alert(TLi18n.t('msg.savedSuccess'));

Si adauga cheia in toate fisierele JSON.


10. Fisiere si Referinte

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

Chei de Traducere per Zona

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

11. Validarea Bundle-urilor (Lint Guard)

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 eroare

Verificฤƒ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.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ