Bugfix Issue 1839 fr FR strings txt Parse Error - sebiboga/testlink-upgraded GitHub Wiki
Issue #1839: locale/fr_FR/strings.txt PHP parse error — the whole fr_FR locale returned HTTP 500
Files changed: locale/fr_FR/strings.txt (1 line), CHANGELOG
Commits: 2222419a8 (fix), 278007f59 (regression suite), 0eebeafe1 (docs + CHANGELOG) — branch fix/issue-1839
Symptom (measured pre-fix)
locale/fr_FR/strings.txt failed to compile:
$ php -l locale/fr_FR/strings.txt
PHP Parse error: syntax error, unexpected identifier "automatisation" in locale/fr_FR/strings.txt on line 4089
$ for d in locale/*/; do php -l "$d/strings.txt" >/dev/null 2>&1 || echo "PARSE ERROR: $d"; done
PARSE ERROR: locale/fr_FR/ # 1 of 19 bundles
TestLink loads language strings by require-ing that file, and a parse error in
an included file is a compile-time fatal, not a recoverable condition. The
measured in-app symptom was therefore worse than "falls back to English":
$ mysql -h 127.0.0.1 -utestlink -ptestlink testlink -e "update users set locale='fr_FR' where id=1;"
$ curl -s -c c.jar -H 'Content-Type: application/json' -H 'Origin: http://localhost:8082' \
-d '{"login":"admin","password":"admin"}' http://localhost:8082/api/auth/index.php/login
{"status":"ok","success":true,"destination":"\/index.php?caller=login&viewer=web"}
$ curl -s -b c.jar -o aside.json -w "%{http_code}\n" 'http://localhost:8082/api/aside/index.php?action=init'
500 # aside.json = 0 bytes
Server stderr (tmp/php_server.log, display_errors=Off):
[Mon Oct 5 03:17:12 2026] PHP Parse error: syntax error, unexpected identifier "automatisation" in
.../locale/fr_FR/strings.txt on line 4089
[Mon Oct 5 03:17:12 2026] 127.0.0.1:42168 [500]: GET /api/aside/index.php?action=init - syntax error,
unexpected identifier "automatisation" in .../locale/fr_FR/strings.txt on line 4089
So: the whole app shell was down for fr_FR users — nav bar, ASIDE menu,
every screen. The client-side bundle (gui/templates/i18n/fr.json, 6690 keys)
was untouched, which is why this looked like an i18n problem scoped to one screen
instead of a locale-wide outage.
Root cause chain
One line, locale/fr_FR/strings.txt:4089:
$TLS_href_tc_auto_exec = 'Execution de l'automatisation des tests';
Two bare apostrophes inside a single-quoted PHP literal. PHP closes the string
at the first l', so automatisation is lexed as a bare identifier → compile error.
| Hop | Location | Role |
|---|---|---|
| 1 | locale/fr_FR/strings.txt:4089 |
the unparseable literal |
| 2 | lib/functions/lang_api.php:240 |
require($lang_resource_path); — turns the file's syntax error into a request-wide fatal |
| 3 | lib/functions/lang_api.php:283 |
lang_get() → lang_ensure_loaded() → lang_load() is lazy: the first localized string of a request is enough to detonate it |
| 4 | lib/functions/lang_api.php:49-58 |
with no explicit locale, lang_get() takes the locale from $_SESSION['locale'], i.e. the user record |
| 5 | api/aside/index.php:521 |
lang_get('href_tc_auto_exec') with no explicit locale — hit first on login, so the ASIDE menu was dead (legacy twin: gui/templates/dashio/aside.tpl:256) |
Regression source
$ git log -1 --format='%H %s' -L 4089,4089:locale/fr_FR/strings.txt
5cd5962190907541418d21073103458db8f05507 Wire Test Automation Execution into ASIDE + legacy shim (Refs #1587)
+ $TLS_href_tc_auto_exec = 'Execution de l'automatisation des tests';
Commit 5cd596219 (#1587) added the key to all 19 bundles. 18 translations
happened to contain no apostrophe because they elide it ('Ejecucion de la automatizacion de pruebas', 'Execucao da automatizacao de testes') — only
fr_FR broke. The trap is systemic for this file: any future French string
with an apostrophe must be escaped.
Unrelated to the Documentation Hub work in #1275/#1281 — those appended their keys at lines 4102-4103, after the broken line, and the file was already unparseable before them.
The fix
Escape the apostrophes in place, using the same \' escaping the file already
uses elsewhere (locale/fr_FR/strings.txt:4129,
$TLS_error_self_signup_disabled = 'L\'auto-inscription … désactivée.'):
-$TLS_href_tc_auto_exec = 'Execution de l'automatisation des tests';
+$TLS_href_tc_auto_exec = 'Execution de l\'automatisation des tests';
Why this method — an in-place escape is the smallest diff that cannot alter the translated text. Rejected alternatives:
- Double-quoted literal — works, but inconsistent with the rest of the file and
opens up interpolation of a
$that does not exist here. - Rewording the translation (e.g. dropping
de l') — silently changes user-visible copy to dodge a syntax problem. - Hardening
lang_load()to swallow parse errors — would convert a loud, correct failure into a silent, partial-translation failure. The loud failure is the right behaviour; the bug was the one bad byte.
One line, one file. No other locale was touched.
Verification (R1–R10, all PASS)
| ID | Check | Result |
|---|---|---|
| R1 | php -l locale/fr_FR/strings.txt |
No syntax errors detected |
| R2 | php -l over all 19 locale/*/strings.txt |
zero failures (was: PARSE ERROR: locale/fr_FR/) |
| R3 | lang_load('fr_FR') + lang_get(...) |
doc_user_manual = Manuel utilisateur; href_tc_auto_exec = Execution de l'automatisation des tests |
| R3b | locale resolved via $_SESSION['locale'] (the real request path) |
identical French output |
| R4 | GET /api/aside/index.php?action=init, fr_FR admin |
200, 6686 bytes, status: ok, 52 server-side labels in French |
| R5 | same request, en_GB admin — no regression |
200, 6481 bytes, same 52 labels in English |
| R6 | translated text preserved by the escape | identical |
| R7 | browser, fr_FR admin |
navBar.html?locale=fr_FR + aside.html?locale=fr_FR both load; ASIDE renders Tableau de bord, Système, Projets, En révision; nav bar renders Se déconnecter |
| R8 | browser console | <no console messages found> |
| R9 | Event Viewer / events table |
select count(*) from events where log_level in (1,2) (logger.class.php:50-51 ERROR=1, WARNING=2) = 0 |
| R10 | tmp/php_server.log |
exactly 1 PHP Parse error, the pre-fix one at 03:17:12; 0 after the fix |
Screenshot — French ASIDE menu + nav bar after the fix. Note that this is the French ASIDE menu the pre-fix code could not render at all (it was a 500 / zero-byte body):

Regression suite: tmp/TLU_Test_Cases.md → ## Regression — Issue #1839: …,
appended +89/-0. Gate
TLU_REQUIRE_SUITE="Issue #1839" bash ai/verify_test_suites.sh → 7 PASS / 0 FAIL / 0 SKIP.
Regression risk that was checked and cleared
A scan of every bundle for single-quoted literals containing an unescaped '
returns 86 hits, but 85 of them are apostrophes inside trailing //
comments and are harmless — those bundles lint clean:
$TLS_active_click_to_change = '有効 (クリックして無効化)'; // 'Active (click to set inactive)';
Only fr_FR:4089 was a real break, and R2 proves it: after the fix no bundle
fails php -l.
Related, not fixed here
[> Correction (2026-10-05, during the #1840 fix). The paragraph below
attributes the English
Test Strategy/DocumentationASIDE entries to the 26 missinggui/templates/i18n/fr.jsonkeys. That attribution is wrong and was disproved by measurement: the ASIDE is rendered bylang_get()fromlocale/fr_FR/strings.txt, which lacks 89$TLS_*keys (includingtitle_test_strategyand the wholehref_test_strategy_*family), whereas thefr.jsongap only ever leaked the raw dotted key (role.noRights) on the Roles / Req. Monitor Overview screens. The screenshot's English ASIDE entries come from the server-sidestrings.txtgap — tracked as #1843, and unrelated to this page's server-side parse error. The original text is kept below unedited for the audit trail; see Bugfix-Issue-1840-fr-json-Missing-26-en-Keys.
#1840 —
gui/templates/i18n/fr.json is missing 26 of the 6716 en.json keys, so a few
menu entries (Test Strategy, Documentation, visible in the screenshot above)
and the Req. Monitor Overview grid toolbar still render in English for fr_FR
users. That is a client-side missing-translation gap, a different class from
this server-side parse error, and it was filed separately rather than folded into
this one-line fix.