Bugfix Issue 781 TLi18n TwoArg Throw - sebiboga/testlink-upgraded GitHub Wiki

Bugfix β€” Issue #781: TLi18n.t(key, 'default') throws 'Cannot use in operator' and aborts dashboard widget rendering

Issue: #781 Branch: fix/issue-781 Β· Commits: 1c0688b55 (fix), 41a744591 (regression suite + fixture), 306767447 (docs + screenshots) Status: FIXED & VERIFIED (2026-08-31)

Symptom

A call to the client-side i18n helper passing a plain string as the second argument β€” e.g. TLi18n.t("dash.generatedOn", "Generated on") β€” made the Dashboard (gui/templates/mainpage/mainPage.html) throw before drawing any widget:

Uncaught TypeError: Cannot use 'in' operator to search for 'length' in "Generated on"

Result: execution-status table stays empty (#execTotal = 0), the Monthly Growth / open-issues widgets never appear, even though the BFF api/mainpage/index.php returns correct data.

Dashboard rendering correctly (post-fix)

Root cause chain

  1. gui/templates/i18n/i18n.js:129-137 β€” function t(key, params) treats params as an interpolation bucket and iterates it with jQuery $.each.
  2. jQuery's $.each / isArrayLike evaluates 'length' in obj. Passing a plain string as params makes that in check throw: TypeError: Cannot use 'in' operator to search for 'length' in "Generated on" β€” verified in isolation (node: 'length' in "Generated on" reproduces the exact message).
  3. The throw happens while render() draws the widgets, so the whole widget pass aborts.

Why it no longer manifests on the default branch: the modern Dashboard rewrite (57378a549 β†’ ec804b086 "review fixes" β†’ 1274af79c, Refs #780) uses single-argument t('dash.generatedOn') / t('dash.noRecords') etc. throughout, with the English literals supplied by the locale bundles. grep across all gui/templates/**/*.html shows zero remaining two-argument string t() calls, so the primary cause is already absent from the current source. The issue stayed open because the underlying fragility of t() was never removed.

Blast radius: the failure lived in the shared t(key, params) contract used by every modernized screen (cfieldsView.html, etc.), so a future two-arg call could reintroduce the crash on any screen. Current calling sites = 0.

Approach β€” harden the shared t() contract (defense-in-depth)

The minimal, safe fix is a type guard inside TLi18n.t() (gui/templates/i18n/i18n.js):

function t(key, params) {
    var str = _strings[key] || key;
    if (params && typeof params === 'object') {
      $.each(params, function(k, v) {
        str = str.replace(new RegExp('\\{' + k + '\\}', 'g'), v);
      });
    }
    return str;
}

Interpolation now only runs for a typeof 'object' value (the documented {k: v} bucket, or an [{0: v}] array used by resultsTCFlat.html/absoluteLatest.html). A stray plain-string second argument is ignored instead of being fed to $.each, so the whole class of Β«Cannot use 'in' operatorΒ» crash cannot recur on any modernized screen.

Alternatives rejected:

  • Removing the second parameter from the t() signature entirely: a breaking API change with no benefit β€” object interpolation (t('key', {n:5})) is a legit, documented use (i18n.js:14-18) and must keep working.
  • Only fixing mainPage.html: the reported file already had no two-arg calls, so this alone would have done nothing and left every other screen vulnerable to the same latent crash.
  • An initial && !Array.isArray(params) guard (review finding): it regressed the two array-interpolation callers (rtf.rowCount in resultsTCFlat.html, alx.rowCount in absoluteLatest.html). typeof 'object' alone is sufficient β€” it still blocks the string bug case while keeping arrays working.

Files changed

File Change
gui/templates/i18n/i18n.js t() gains a typeof params === 'object' guard before $.each (defense-in-depth)
tmp/TLU_Test_Cases.md +Suite 781 (dashboard i18n regression, PASS)
tmp/fixtures_781.php fixture β€” project DASH781, plan Plan781, build B781, 5 executions (3 passed/1 failed/1 blocked)

Verification

  • node --check gui/templates/i18n/i18n.js β†’ no syntax errors.
  • Primary: tmp/fixtures_781.php β†’ open the modern Dashboard for the DASH781 fixture: execution table populated (Passed 3/60%, Failed 1/20%, Blocked 1/20%, Not Run 0/0%, Total 5), Monthly Growth widget visible, browser console 0 errors.
  • In-page checks (evaluate_script):
    1. TLi18n.t('dash.generatedOn','Generated on') β†’ "Generated on", no throw (pre-fix class).
    2. TLi18n.t('dash.testPlan') (single-arg) β†’ "Test Plan", no throw.
    3. TLi18n.t('dash.completed',{n:5}) (object interpolation) β†’ "Completed", no throw.
    4. TLi18n.t('rtf.rowCount',[5]) (array interpolation) β†’ "5 rows", no throw.
  • events table: SELECT COUNT(*) FROM events WHERE log_level IN (4,8) β†’ 0 (no new Error/Warning).