enhancement
major
minor
major
minor
minor
#29591
ResKey.decode akzeptiert in #(…)-Literalen nur doppelt gequotete Übersetzungen — #('Travel'@en) schlägt fehl, obwohl TL-Script beide Quote-Arten erlaubt
Betrifft 8.0.0-SNAPSHOT (2026-09-15), tl-basic (ResKeyEncoding).
Die textuelle Form eines literalen ResKey mit Übersetzungen ist #("Travel"@en, "Reise"@de). Sie wird überall dort gelesen, wo ein ResKey aus einem String entsteht: ResKey.decode(String), das Konfigurationsformat ResKey.ValueFormat (jede ResKey-Property in Layout- und Anwendungskonfiguration), ResKeyJsonBinding und seit #29558 auch der Instanz-Import (value="…" eines I18N-Attributs).
Mit einfachen Anführungszeichen schlägt das Parsen fehl:
ResKey.decode("#('Travel'@en, 'Reise'@de)")
→ IllegalArgumentException: Cannot parse resource key from '#('Travel'@en, 'Reise'@de)': no translation.
TL-Script akzeptiert dagegen für dasselbe Literal beide Quote-Arten (SearchExpressionParser.jj, Token STRING: '…' oder "…"), sodass #('Travel'@en, 'Reise'@de) in TL-Script gültig ist und in der TL-Script-Dokumentation auch so geschrieben wird. Wer die Schreibweise aus TL-Script in eine Konfiguration oder Importdatei übernimmt, bekommt einen Parsefehler bzw. (vor #29558) stillschweigend keinen Wert.
Analyse
ResKeyEncoding.LITERAL ist eine ungeklammerte Alternation
{{{#!java static final String LITERAL =
"'" + "(" + "(?:" + "^\\'*" + "|" + QUOTED_SPECIAL + ")*" + ")" + "'" + "|" +
"\"" + "(" + "(?:" + "^\\\"*" + "|" + QUOTED_SPECIAL + ")*" + ")" + "\"";
}}}
und wird in TAGGED_STRING_PATTERN als "(?:" + LITERAL + LANGTAG + ")?" eingesetzt. Dadurch bindet LANGTAG nur an den Double-Quote-Zweig; der Single-Quote-Zweig endet ohne Sprach-Tag und passt deshalb nie auf 'Travel'@en. Der Rest des Parsers (parseTranslations, unquote(valSquote, valDquote)) behandelt beide Gruppen bereits gleichwertig, der Fehler steckt allein in der Klammerung.
Lösung
LITERAL ist als nicht-fangende Gruppe (?:'…'|"…") geklammert, sodass das Sprach-Tag an beide Quote-Arten bindet. Die Nummerierung der Capture-Gruppen (1 = einfach gequoteter Wert, 2 = doppelt gequoteter Wert, 3 = Sprach-Tag, …) bleibt unverändert, parseTranslations ist unberührt. Die Kodierung (ResKey.encode) erzeugt weiterhin die doppelt gequotete kanonische Form.
Damit gilt #('Travel'@en, 'Reise'@de) überall, wo ein ResKey aus einem String gelesen wird: ResKey.decode, ResKey.ValueFormat (Konfiguration), ResKeyJsonBinding und der Instanz-Import.
Testfälle in TestResKeyEncoding: einfach gequotete Übersetzungen, gemischte Quote-Arten innerhalb eines Literals, \' und ungeschützte " in einfach gequoteten Übersetzungen, die Suffix-Form (tooltip: {'…'@en}) mit einfachen Quotes, die Rückkodierung aller Schreibweisen in dieselbe kanonische Form, das Lesen über ResKey.ValueFormat sowie die weiterhin abgewiesenen fehlerhaften Formen (fehlendes Sprach-Tag, unbeendetes Literal).