Bugfix Issue 1840 fr json Missing 26 en Keys - sebiboga/testlink-upgraded GitHub Wiki
| Issue | #1840 |
| Branch | fix/issue-1840 |
| Commits |
cca8a6d01 (the fix), b17e121c5 (suite + docs + screenshots) |
| Files changed |
gui/templates/i18n/fr.json (+26 / −0), CHANGELOG (1 entry) |
| Severity | minor — cosmetic, but the leaked text is garbage, not English |
| Regression suite |
tmp/TLU_Test_Cases.md → ## Regression — Issue #1840: … (M1–M7, all PASS) |
| Follow-ups filed | #1843 (the ASIDE symptom — different i18n layer), #1844 (missing CI key-set gate) |
gui/templates/i18n/fr.json defined 6700 of the 6726 keys in en.json.
The 26 absent ones belong to four shipped features, and the user-visible result
is worse than untranslated English: because TLi18n.t() has no per-key
fallback, the raw dotted key itself was written into the DOM.
Measured, rolesView.html?locale=fr_FR, editing a system role (which un-hides
#roleLockNote):
document.querySelector('#roleLockNote').textContent.trim()
// pre-fix : "role.editLocked" <-- raw key on screen
// post-fix: "Ce rôle est défini par le système et ne peut pas être modifié."
document.querySelector('.fa-copy').getAttribute('title')
// pre-fix : "role.duplicateRole" <-- raw key in a tooltip
// post-fix: "Dupliquer"Static confirmation:
python3 -c "
import json
en=json.load(open('gui/templates/i18n/en.json'))
fr=json.load(open('gui/templates/i18n/fr.json'))
print(len(en), len(fr), len([k for k in en if k not in fr]))"
# 6726 6700 26 <- pre-fix 6726 6726 0 <- post-fixTwo hops, both read from source:
-
gui/templates/i18n/i18n.js:165—t(key)isvar str = _strings[key] || key;._stringsis whatever single bundle was fetched ati18n.js:131(/gui/templates/i18n/<locale>.json). There is no English merge, so an absent key resolves to itself. -
gui/templates/i18n/i18n.js:187—apply()writest(key)intotextContentfor every[data-i18n]node, unconditionally. The English literal the screen author put in the HTML as a fallback is therefore overwritten by the raw key.
Why the keys went missing — four independent feature batches, each confirmed
with git log -S against gui/templates/i18n/en.json:
| commit | feature | keys |
|---|---|---|
588897593 |
Test Strategy Bug Structure / Bug Lifecycle chapters |
ts.chapterBug* (4) |
a381901e3 |
Req. Monitor Overview group-by-spec + toolbar |
rmo.grid.* (7), rmo.* (3) |
9d380b9dc |
Create Test Cases counters |
rctc.* (6) |
4bcf10e07 |
Roles Management dialogs (a restore of gutted i18n) |
role.* (6) |
Nothing in the pipeline compares key sets across bundles — CI only runs
python3 -m json.tool, which a bundle missing 26 keys passes trivially. That
missing guard-rail is now #1844.
9d380b9dc is titled "i18n(reqCreateTestCases): rctc.* keys in all 10 locale
bundles". Measured today:
grep -c rctc.countFilled gui/templates/i18n/*.json
# en 1, ja 1, pt 1, ro 1, ru 1, zh 1, de 0, es 0, fr 0, it 06 of 10, not 10. Reviewing the message instead of the diff is what let the gap through — worth remembering when reading i18n commit titles.
-
fr.json— 26 keys, every one referenced by live code:-
rctc.*→gui/templates/requirements/reqCreateTestCases.html:257(TLi18n.t()inside a toast) -
role.*→gui/templates/usermanagement/rolesView.html:101,508andapi/roles/index.php:642(messageKeyreturned to the client) -
ts.*→api/strategy/index.php:77,78(chapter list,key/descKeypairs) -
rmo.grid.*/rmo.*→gui/templates/requirements/reqMonitorOverview.html:55and siblings
-
-
Same 26 keys, same origin:
de.json,es.json,it.json.set(missing(de)) == set(missing(fr)) == set(missing(es)) == set(missing(it))measuredTrue. RESOLVED — #1844 (branchtask/issue-1844, commitc0f874cad): all 10 bundles backfilled to 6781 keys and the new blocking gateai/verify_i18n_coverage.shnow makes this class impossible to merge silently. See Bugfix — Issue #1844. -
Different gaps elsewhere:
ja/pt/ru20 each,ro6,zh40 — also RESOLVED in #1844.
26 lines added, 0 removed, en.json untouched, each key inserted at its
alphabetical position inside its own group:
"rctc.countFilled": "Nombres de cas de test remplis d'après la couverture requise",
"rctc.countInfo": "Exigences",
"rctc.countReset": "Nombres de cas de test réinitialisés à 1",
"rctc.createdOk": "Cas de test créés",
"rctc.toggleAll": "Sélectionner / désélectionner toutes les exigences",
"rctc.toggleCount": "Remplir automatiquement le nombre de cas de test d'après la couverture requise",
"rmo.grid.expandCollapseGroups": "Déplier / Replier les groupes",
"rmo.grid.groupItem": "({count} élément)",
"rmo.grid.groupItems": "({count} éléments)",
"rmo.grid.groupsCollapsed": "Groupes repliés",
"rmo.grid.groupsExpanded": "Groupes dépliés",
"rmo.grid.hideGroupedColumn": "Masquer la colonne groupée",
"rmo.grid.showAllColumns": "Afficher toutes les colonnes",
"rmo.multiSort": "Tri multi-critères",
"rmo.resetFilters": "Réinitialiser les filtres",
"rmo.resetToDefaultState": "Réinitialiser l'état par défaut",
"role.duplicateRole": "Dupliquer",
"role.editLocked": "Ce rôle est défini par le système et ne peut pas être modifié.",
"role.error.duplicate": "Erreur lors de la duplication du rôle",
"role.noRights": "Vous n'avez pas les droits requis pour gérer les rôles (droit role_management requis).",
"role.systemDeleteDenied": "Les rôles système ne peuvent pas être supprimés.",
"role.warningUsersWillBeReset": "Les utilisateurs concernés recevront le rôle",
"ts.chapterBugLifecycle": "Cycle de Vie du Bug",
"ts.chapterBugLifecycleDesc": "Les états traversés par un défaut, avec un exemple de mapping GitHub : statut, assignés et labels.",
"ts.chapterBugStructure": "Structure du Bug",
"ts.chapterBugStructureDesc": "Les sections obligatoires d'un rapport de défaut : ENVIRONMENT, STEPS TO REPRODUCE, EXPECTED RESULTS, ACTUAL RESULTS."Three decisions worth recording:
-
Reuse existing terminology instead of inventing a second one. The same two
chapter titles were already translated in
fr.jsonunder sibling keys —ts.bugLifecycleHeader="Cycle de Vie du Bug",ts.bugStructureHeader="Structure du Bug". Those exact strings are reused, so one file does not end up with two competing renderings of one chapter name. -
Placeholders asserted, not eyeballed.
rmo.grid.groupItem/groupItemsare interpolated with{count}; the insert script verifies every{token}of theen.jsonsource value still exists in the translation before writing, so a dropped{}can never print literally. -
Rejected alternatives.
-
A CI key-set gate (the report's option (b)) — build infrastructure that
would fail immediately on the
de/es/it/ja/pt/ru/zhbacklog, and not needed to stop the user-visible defect. Filed as #1844 rather than smuggled into a data fix. -
Merging
en.jsonper key insidei18n.js— heals every locale and every future gap, but changes shared runtime behaviour for every screen in the product (extra bundle fetch, new merge semantics) to paper over missing data. That is the "architectural" branch of the rulebook; the data is the defect.
-
A CI key-set gate (the report's option (b)) — build infrastructure that
would fail immediately on the
role.warningUsersWillBeReset = "Affected users will get role" in en.json
— no article, no role name. A code review flagged it as a truncated English
sentence. It is not: it is a deliberate prefix, completed with the role name
at gui/templates/usermanagement/rolesView.html:593
$('#deleteResetNote').html(TLi18n.t('role.warningUsersWillBeReset')
+ ' <b>' + esc(r.replacementRole.name) + '</b>').show();rendering "Affected users will get role Administrator". The French mirrors that shape exactly — "Les utilisateurs concernés recevront le rôle Administrateur" — which is why the missing article is carried over rather than silently "corrected": adding a noun in the translation would put the role name on screen twice.
| # | Case | Measured |
|---|---|---|
| M1 | key-set diff fr vs en
|
en 6726 / fr 6726 / missing 0 (was 6700 / 26) |
| M2 | python3 -m json.tool gui/templates/i18n/fr.json |
exit 0 |
| M3 | nothing overwritten |
git diff --numstat → 26 0 fr.json; en.json absent from the diff |
| M4 | placeholders interpolate |
groupItem.format(count=3) → (3 élément); groupItems.format(count=7) → (7 éléments); in-browser TLi18n.t('rmo.grid.groupItem',{count:4}) → (4 élément)
|
| M5 | runtime fr_FR
|
26-key unresolved probe → [] (was 6/6 raw keys) |
| M6 | runtime en_GB unaffected |
role.duplicateRole→Duplicate, rmo.grid.expandCollapseGroups→Expand/Collapse Groups, rctc.countInfo→Requirements
|
| M7 | Event Viewer | fresh logout+login: events 32→63 rows but count(distinct description) unchanged at 32; the 60 log_level=32 rows still collapse to the same 30 legacy strings; log_level=16 rows are audit_login_success; no ERROR row. Console: no errors/warnings (2 pre-existing a11y hints on rolesView.html) |
M7 is the decisive one for "did I fix the right thing": every remaining warning
comes from lang_get() / locale/fr_FR/strings.txt, while the
gui/templates/i18n/*.json path fires no event at all.
Edit a system role (the admin row, id 8) — that is what un-hides
#roleLockNote, so the affected node is actually on screen.
Before the fix — raw keys leak into the dialog and the tooltip:

After the fix — French labels:

The first pair of screenshots taken for this bug was byte-identical (both 46011 bytes) because the affected nodes are
display:noneuntil the dialog opens. They were re-captured by checking outfr.jsonatHEAD~1, replaying the same click, then restoring the fixed bundle. Sizes now differ (41161 vs 43547) and the DOM readings above are the authoritative evidence — the model that took them cannot read pixels.
The report's second symptom — Test Strategy / Documentation in English
inside an otherwise French ASIDE menu — is not caused by these 26 keys. The
ASIDE is rendered by lang_get() from locale/fr_FR/strings.txt, not from the
gui/templates/i18n/*.json bundles:
grep -n "title_test_strategy" locale/fr_FR/strings.txt # -> no match
grep -n "title_test_strategy" locale/en_GB/strings.txt # -> 2313:$TLS_title_test_strategy = "Test Strategy";locale/fr_FR/strings.txt lacks 89 of the 2783 $TLS_* keys in
locale/en_GB/strings.txt, including title_test_strategy and the whole
href_test_strategy_* family. The Event Viewer says so explicitly:
32 | GUI - Projet ID : 0 | string 'title_test_strategy' is not localized for locale 'fr_FR' - using en_GB
32 | GUI - Projet ID : 0 | string 'href_test_strategy_bug_lifecycle' is not localized for locale 'fr_FR' - using en_GB
Two i18n layers, two different fallbacks: lang_get() falls back to English,
TLi18n.t() falls back to the raw key. Tracked as
#1843.
mysql -h 127.0.0.1 -utestlink -ptestlink testlink \
-e "update users set locale='fr_FR' where id=1;"
# open http://localhost:8082/gui/templates/usermanagement/rolesView.html?locale=fr_FR
# click the pencil on the system role "admin" (id 8), then devtools console:
# ['role.editLocked','role.duplicateRole'].map(k => [k, TLi18n.t(k)])
# expected: French strings, none equal to its own key
python3 -c "
import json
en=json.load(open('gui/templates/i18n/en.json'))
fr=json.load(open('gui/templates/i18n/fr.json'))
print('missing', len([k for k in en if k not in fr]))" # expected: 0