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):

fr_FR ASIDE menu rendering in French after the fix

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 / Documentation ASIDE entries to the 26 missing gui/templates/i18n/fr.json keys. That attribution is wrong and was disproved by measurement: the ASIDE is rendered by lang_get() from locale/fr_FR/strings.txt, which lacks 89 $TLS_* keys (including title_test_strategy and the whole href_test_strategy_* family), whereas the fr.json gap 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-side strings.txt gap — 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.