enhancement
major
minor
major
minor
minor
#29555
TL-Script label(<constant>) is folded at compile time: the result is fixed to the compiling thread's locale instead of the reading user's
Affects 8.0.0-SNAPSHOT (2026-09-11), tl-model-search.
A TL-Script expression that computes a localized text from a constant, e.g. in the storage expression of a derived tl.core:String attribute,
label(#("Offen"@de, "Pending"@en))
does not localize per reading user. The value is the same text for every session (or, when the expression was compiled outside a session, the raw ResKey literal #("Offen"@de, "Pending"@en) verbatim), while the identical expression evaluated in the script console returns Offen for a German user.
Cause
label() (com.top_logic.model.search.expr.config.operations.Label) inherits GenericMethod.canEvaluateAtCompileTime(), which returns true by default. ConstantFolding therefore evaluates label(<literal>) once during expression compilation (evalAtCompileTime, without evaluation context) and replaces the call by a string literal. Without an explicit lang argument label() reads the locale from the compiling thread, so the folded literal is the translation for that thread's locale (a derived-attribute expression is compiled once when the model loads) and the ResKey is gone from the compiled expression. This violates the contract documented on canEvaluateAtCompileTime: a method that reads data not given in its arguments must not be folded.
Reproduced by compiling label(#("Offen"@de, "Pending"@en)) in an English context: the compiled expression is the literal "Pending", and executing it in a German session still yields Pending.
The same defect exists in the other TL-Script functions that read the session locale implicitly and do not override the compile-time guard:
- localize() (Localize): resolves a ResKey through the session resources.
- fill() (Fill): formats with a MessageFormat in the session locale.
(translate() is not affected, it is declared as not side-effect free; dateFormat() already returns false.)
Note: an argument that is not a literal, e.g. label(if ($x.get(`m:T#paidAt`) == null, #(...), #(...))) with a condition depending on the object, is not folded, because constant folding does not distribute a call over if; only the constant-argument form is affected.
Lösung
Label, Localize and Fill override canEvaluateAtCompileTime() and return false, so the call stays in the compiled expression and is evaluated in the session of the reading user. This holds also with an explicit lang argument, because the label of an object depends on the object's current state and is not a constant.
Regression tests in TestSearchExpression compile label(#(...)) and localize(#(...)) once and execute the compiled expression under a German and an English locale, expecting the respective translation.
Migration
- Behaviour: label(), localize() and fill() in compiled TL-Script expressions (derived attributes, model builders, configured functions) now localize per reading user. Applications that relied on the previous, accidentally fixed translation (the locale of the thread that compiled the expression, typically the model-loading thread) get the correct per-user text now; no code change is needed.
- Custom TL-Script functions implemented in Java (GenericMethod subclasses) that read data not given in their arguments — the session locale, the current user, the time zone, request state — must override canEvaluateAtCompileTime() and return false. Otherwise ConstantFolding evaluates a call with constant arguments once at compile time and freezes the result for every user.