Dokumentation · Label-Modell
Ausdrücke
Funktionen, Formate, Kulturen.
Ein Ausdruck berechnet den Text eines Etikett-Elements aus dem Datensatz: Format([price], "C", [culture]), If([qty] > 10, "BULK", "SINGLE"), GS1("01", [gtin], "10", [lot]). Diese Seite beschreibt jede Funktion mit Parametern, Ergebnissen und Fehlern, dazu Operatoren, Umwandlungen, Kulturen und Formate – jedes Beispiel darauf wird von einem Test gegen die echte Engine ausgewertet.
Expression-Builder öffnen Funktionsreferenz
01Wo Ausdrücke verwendet werden
Jedes Element des Label-Modells hat die Eigenschaft Expression (JSON "expression"): Textelemente, Barcodes (Code) und RFID-Elemente (Data). Ist ein Ausdruck gesetzt, hat er Vorrang vor Text/Code und vor DataField. Das Ergebnis wird mit der Engine-Kultur in Text umgewandelt und dann gedruckt; Platzhalter {t:key} im Ergebnis werden wie in festen Texten übersetzt.
{ "type": "text", "x": 4, "y": 4, "expression": "Upper([name]) + \", \" + [customer.city]" }
{ "type": "barcode", "x": 4, "y": 20, "symbology": "Gs1_128", "expression": "GS1(\"01\", [gtin], \"10\", [lot])" }
Die Engine ist ZplCore.Labels.ExpressionEngine; jedes Label hat eine (label.Expressions) für eigene Funktionen, Variablen und die Kultur. Ausdrücke werden beim Binden des Etiketts je Datensatz ausgewertet (label.For<T>(), JSON-Datensätze, DataRow …) und bei label.ToZpl() ohne Daten einmal.
Zu den Beispielen. Sofern eine Tabelle nichts anderes angibt, lesen die Beispiele den Beispieldatensatz unten, laufen im ersten Datensatz eines Auftrags mit 3 Datensätzen auf einem Etikett mit 100 × 50 mm, verwenden die Standard-Engine-Kultur (invariant), und
Now() ist der 22. Oktober 2026, 14:30:05. Die Spalte „Ergebnis“ zeigt den gedruckten Text. Ein Klick auf einen Ausdruck öffnet ihn im
Expression-Builder.
{
"price": 1234.5,
"oldPrice": 1499,
"culture": "de-DE",
"name": "Müller",
"first": "Anna",
"last": "Schmidt",
"sku": "ab-123",
"qty": 3,
"weight": 0.4567,
"date": "2026-10-22",
"deDate": "22.10.2026",
"gtin": "09521234543213",
"lot": "LOT42",
"serial": "42",
"textPrice": "19.99",
"textOld": "9.99",
"empty": "",
"nothing": null,
"inStock": true,
"Preis [EUR]": 4.5,
"Preis.netto": 3.1,
"customer": { "name": "ACME GmbH", "city": "Graz", "zip": "8010" },
"items": [ { "name": "Bolt" }, { "name": "Nut" } ]
}
02Schnellstart: ein Preisschild in mehreren Kulturen
Zwei Textelemente: der alte Preis, durchgestrichen, nur wenn er höher ist als der aktuelle Preis, und der aktuelle Preis als Währung. Die Kultur kommt aus dem Datensatz ([culture]), sodass ein einziges Etikett deutsche, US- oder japanische Preise druckt:
{ "type": "text", "expression": "If([oldPrice] > [price], \"instead of \" + Format([oldPrice], \"C\", [culture]), \"\")", "strikeout": true },
{ "type": "text", "expression": "Format([price], \"C\", [culture])", "fontSize": 22, "bold": true }
| Ausdruck | Datensatz | Ergebnis |
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "") | Beispiel (de-DE) | instead of 1.499,00 € |
Format([price], "C", [culture]) | Beispiel (de-DE) | 1.234,50 € |
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "") | en-US | instead of $1,499.00 |
Format([price], "C", [culture]) | en-US | $1,234.50 |
Format([price], "C", [culture]) | ja-JP | ¥1,234 |
If([oldPrice] > [price], "instead of " + Format([oldPrice], "C", [culture]), "") | alter Preis niedriger | (leer) |
Format([price], "C", [culture]) | kein Feld culture | ¤1,234.50 |
- Das Kultur-Argument ist ein Kulturname wie
"de-DE", als Literal geschrieben oder aus einem Feld gelesen. Fehlt es oder ist es leer → die Engine-Kultur (Standard: invariant, Währungszeichen ¤). Siehe Kulturen.
- Der Vergleich
[oldPrice] > [price] ist numerisch – auch wenn Preise als Text kommen (CSV, JSON-Strings): Zwei Texte, die Zahlen sind, werden als Zahlen verglichen. Siehe Typen.
- Ein leeres Ergebnis druckt nichts; das durchgestrichene Element bleibt dann einfach leer.
03Syntax
| Element | Schreibweise | Hinweise |
| Feld | [price], [customer.city], [items[0].name], [Preis [EUR]] | Wert des Datensatzes (oder eines anderen Elements, siehe Felder). Klammern innerhalb eines Namens werden verschachtelt. Ohne exakten Treffer wird Groß-/Kleinschreibung ignoriert. |
| Name ohne Klammern | price, LABEL_NUMBER, SHIFT | Buchstaben, Ziffern, _. Wird zuerst als Variable, dann als Feld gesucht. |
| Zahl | 42, 1.5, .5 | Ziffern mit . als Dezimaltrennzeichen, immer invariant. Kein Exponent (1e3 ist ein Fehler), kein Tausendertrennzeichen; negative Zahlen mit unärem -. |
| Text | "instead of ", "He said ""hi""" | Doppelte Anführungszeichen, am Ende geschlossen (ein offener Text ist ein Fehler); ein Anführungszeichen im Text wird verdoppelt. Keine Backslash-Escapes. Im Label-JSON werden die Anführungszeichen noch einmal maskiert: "expression": "Upper(\"x\")". |
| Boolescher Wert | true, false | Beliebige Schreibweise. Gedruckt als True/False. |
| Funktionsaufruf | Format([price], "C") | Funktionsnamen ignorieren Groß-/Kleinschreibung. Argumente sind Ausdrücke, getrennt durch ,. |
| Klammern | ([a] + [b]) * 2 | Gruppierung. |
| Member-Zugriff | Math.Max(1, [qty]) | Nur für Typen, die mit AddTypeReference (C#) registriert sind, siehe C#-API. |
| Ausdruck | Ergebnis |
"He said ""hi""" | He said "hi" |
.5 + 1 | 1.5 |
007 | 7 |
price * 2 | 2469 |
[PRICE] | 1234.5 |
TRUE | True |
1e3 | Unexpected 'e3' |
04Operatoren & Rangfolge
Von niedrigster zu höchster Rangfolge; Operatoren derselben Stufe werden von links nach rechts ausgewertet.
| Stufe | Operatoren | Bedeutung |
| 1 | or || | Logisches Oder (beide Seiten werden ausgewertet) |
| 2 | and && | Logisches Und (beide Seiten werden ausgewertet) |
| 3 | = == <> != < <= > >= | Vergleich: numerisch, wenn eine Seite eine Zahl ist oder beide Seiten Texte sind, die Zahlen sind, sonst als Text ohne Beachtung der Groß-/Kleinschreibung |
| 4 | + - & | + addiert zwei Zahlen und verbindet alles andere als Text; & verbindet immer als Text; - subtrahiert |
| 5 | * / % | Multiplikation, Division, Rest (immer numerisch) |
| 6 | - not ! | Unäres Minus, logisches Nicht |
| 7 | ( ), Aufrufe, . | Gruppierung, Funktionsaufrufe, Member-Zugriff |
| Ausdruck | Ergebnis | Warum |
1 + 2 * 3 | 7 | * vor + |
(1 + 2) * 3 | 9 | Gruppierung |
10 / 4 | 2.5 | Division ist nie eine Ganzzahldivision |
10 % 3 | 1 | Rest |
-7 % 3 | -1 | Vorzeichen der linken Seite |
10 / 0 | Infinity | kein Fehler; mit If absichern |
2 + "3" | 23 | Text auf einer Seite → verbunden |
"2" * "3" | 6 | * wandelt immer in Zahlen um |
"2" - "3" | -1 | - wandelt immer in Zahlen um |
1 + 2 & 3 | 33 | gleiche Stufe, von links nach rechts: (1 + 2) & 3 |
[qty] & " pcs" | 3 pcs | & verbindet |
"a" = "A" | True | Textvergleich ignoriert Groß-/Kleinschreibung |
"01" = 1 | True | Zahl auf einer Seite → numerisch |
"01" = "1" | True | beide Texte sind Zahlen → numerisch |
"10" < "9" | False | beide Texte sind Zahlen → 10 < 9 |
"a10" < "a9" | True | Textvergleich, Zeichen für Zeichen |
3 > 2 > 1 | False | (3 > 2) = True, dann True > 1 → 1 > 1 |
[qty] > 2 and [inStock] | True | |
not [qty] = 3 | False | not bindet zuerst: (not 3) = 3 |
not ([qty] = 3) | False | mit Klammern |
[qty] <> 3 || [qty] != 4 | True | |
05Typen & Umwandlung
Werte sind Zahlen (double), Text, boolesche Werte, Datumswerte (DateTime) oder leer (null). Ganzzahl- und Dezimalwerte aus Datensätzen (C# int, long, decimal, JSON-Zahlen) werden zu Zahlen; alles andere behält seinen Typ. Umgewandelt wird dort, wo ein Operator oder eine Funktion einen anderen Typ braucht:
| Umwandlung | Regel |
| → Text | leer → ""; boolesche Werte → True/False; Zahlen und Datumswerte mit der Engine-Kultur (invariant: 1234.5, 10/22/2026 14:30:05). |
| → Zahl | leer → 0; true → 1, false → 0; Text wird mit der Engine-Kultur gelesen, dann invariant, sonst 0 (NumberStyles.Float: Vorzeichen, Dezimaltrennzeichen, Exponent – keine Tausendertrennzeichen). |
| → boolescher Wert | leer → false; Text → true, außer "", "0" oder "false" (beliebige Schreibweise); Zahlen → true, außer 0. |
| → Datum | ein Datum bleibt; Text wird mit der Engine-Kultur gelesen, dann invariant, sonst 0001-01-01. Für feste Formate ToDate(text, format) verwenden. |
+ addiert nur, wenn beide Seiten Zahlen sind. Eine Zahl aus einem Textfeld ist Text: [textPrice] + 1 verbindet. Vergleiche sind numerisch, wenn eine Seite eine Zahl ist oder beide Seiten Texte sind, die Zahlen sind (gelesen mit der Engine-Kultur, dann invariant); anderer Text wird als Text verglichen. Zum Rechnen mit Num() (oder ToNumber(text, culture)) umwandeln:
| Ausdruck | Ergebnis | Warum |
[qty] + 1 | 4 | JSON-Zahl |
[textPrice] + 1 | 19.991 | Text + Zahl → verbunden |
Num([textPrice]) + 1 | 20.99 | zuerst umgewandelt |
[textOld] > [textPrice] | False | beides Zahltexte: 9,99 > 19,99 |
Num([textOld]) > Num([textPrice]) | False | 9.99 > 19.99 als Zahlen |
[textPrice] * 2 | 39.98 | * wandelt um |
true + 1 | True1 | ein boolescher Wert ist für + keine Zahl |
1 / 3 | 0.3333333333333333 | volle Genauigkeit; mit Round oder Format runden |
0.1 + 0.2 | 0.30000000000000004 | binäre Gleitkommazahl |
Format(0.1 + 0.2, "0.00") | 0.30 | für den Druck formatieren |
Today() | 10/22/2026 00:00:00 | Datum → Text (invariant) |
Mit der Engine-Kultur de-DE (C#: label.Expressions.Culture) folgen dieselben Umwandlungen deutschen Konventionen:
| Ausdruck (Engine-Kultur de-DE) | Ergebnis |
[price] | 1234,5 |
[price] + " EUR" | 1234,5 EUR |
Num("19,99") * 2 | 39,98 |
Num("19.99") * 2 | 39,98 |
Format(ToDate([deDate]), "yyyy-MM-dd") | 2026-10-22 |
Today() | 22.10.2026 00:00:00 |
06Leere & fehlende Werte
Ein Feld, das fehlt, JSON-null oder C#-null ist, ist leer. Ohne Daten (label.ToZpl()) ist jedes Feld leer. Leere Werte lösen nie einen Fehler aus: Sie drucken als "", zählen als 0 und als false.
| Ausdruck | Ergebnis | Warum |
[missing] | (leer) | unbekanntes Feld |
Upper([nothing]) & "|" | | | leerer Text |
[nothing] * 2 | 0 | leer → 0 |
[missing] + 1 | 1 | leer ist keine Zahl → verbunden: "" + "1" |
[missing] + 1 + 1 | 11 | "1" + 1 → wieder verbunden |
Num([missing]) + 1 + 1 | 2 | zuerst umwandeln |
If([nothing], "yes", "no") | no | leer → false |
IsEmpty([empty]) | True | "" |
IsEmpty([nothing]) | True | null |
IsEmpty(" ") | False | ein Leerzeichen ist Text |
Coalesce([empty], [nothing], "n/a") | n/a | erster nicht leerer Wert |
If(IsEmpty([lot]), "", "LOT " + [lot]) | LOT LOT42 | Text nur, wenn ein Wert vorhanden ist |
Format([nothing], "C", "de-DE") | (leer) | nichts zu formatieren |
07Felder & Bindung
[name] liest einen Wert des aktuellen Datensatzes. Datensätze können C#-Objekte (Properties und Felder), Dictionaries, ExpandoObject, JsonElement/JsonNode, DataRow oder LiteDB-Dokumente sein; LabelBinding.Map("name", r => …) fügt berechnete Felder hinzu. Die Suche:
- Ein Name mit
. oder [n] ist ein Pfad: [customer.city], [items[0].name]. Eine Spalte, deren Name genau dieser Text ist, hat Vorrang vor dem Pfad (importierte Tabellen haben oft Spalten wie Preis.netto).
- Zuerst der exakte Name, dann ohne Beachtung der Groß-/Kleinschreibung.
- Namen anderer Elemente:
[Items.Total] oder [Items.Total.Text] ist der gedruckte Text des Elements namens Total – auch wenn es später auf dem Etikett kommt (Zirkelbezüge ergeben ""). Ein bloßes [Total] findet ein Element erst, nachdem es gedruckt wurde, und nur, wenn kein Feld des Datensatzes so heißt.
| Ausdruck | Ergebnis |
[customer.city] | Graz |
[items[1].name] | Nut |
[items[5].name] | (leer) |
[Preis [EUR]] * 2 | 9 |
[Preis.netto] | 3.1 |
[CUSTOMER.CITY] | Graz |
[customer.zip] + " " + [customer.city] | 8010 Graz |
[inStock] | True |
var label = new Label(LabelUnit.Mm, 100, 50)
.Add(new TextItem { Name = "Net", Y = 4, Expression = "Format([price] / 1.2, \"0.00\")" })
.Add(new TextItem { Y = 12, Expression = "\"net \" + [Items.Net] + \" / gross \" + Format([price], \"0.00\")" });
// mit { price = 12 }: "net 10.00 / gross 12.00"
08Auftragsvariablen
Variablen werden ohne Klammern geschrieben (mit Klammern funktionieren nur die mit ✓ markierten Bindungsvariablen).
| Variable | Wert | […] |
LABEL_INDEX | Nullbasierte Nummer des Etiketts (Datensatzes) im Auftrag | – |
LABEL_NUMBER | Einsbasiert: LABEL_INDEX + 1 | – |
REPEAT_INDEX | Nullbasierter Elementindex innerhalb eines RepeaterItem | – |
RECORD_INDEX | Nullbasierter Datensatzindex (Bindungen) | ✓ |
PAGE_NUMBER | Einsbasierte Datensatznummer (Bindungen) | ✓ |
TOTAL_PAGES | Anzahl der Datensätze, wenn bekannt (Collections, WithRecordCount), sonst leer | ✓ |
LABEL_WIDTH, LABEL_HEIGHT | Etikettengröße in Etiketteneinheiten (Bindungen) | ✓ |
eigene, z. B. SHIFT | label.Expressions.Variables["SHIFT"] = "B" oder SetCustomVariable | – |
Die Namen sind nicht reserviert: Ein Feld des Datensatzes namens PAGE_NUMBER (oder einer der anderen Bindungsvariablen) hat Vorrang vor der Variablen. LABEL_INDEX, LABEL_NUMBER und REPEAT_INDEX haben ohne Klammern geschrieben Vorrang vor gleichnamigen Feldern. Ohne Bindung (label.ToZpl()) sind die Bindungsvariablen leer.
| Ausdruck | Ergebnis |
"Etikett " + LABEL_NUMBER | Etikett 1 |
"Seite " + PAGE_NUMBER + " von " + TOTAL_PAGES | Seite 1 von 3 |
RECORD_INDEX | 0 |
[LABEL_WIDTH] + " x " + LABEL_HEIGHT + " mm" | 100 x 50 mm |
[LABEL_NUMBER] | (leer) |
"Seite " + PAGE_NUMBER | Seite A-7 |
09Kulturen
Eine Kultur bestimmt Dezimal- und Tausendertrennzeichen, Währung, Prozentzeichen, Datumsreihenfolge, Monats- und Tagesnamen – und bei manchen Kulturen sogar den Kalender. zplCore verwendet .NET-Kulturen (ICU-Daten des Betriebssystems). Sie wird an zwei Stellen festgelegt:
| Wo | Gilt für | Standard |
Kultur-Argument von Format/ToString (3.), ToNumber (2.), IsNumeric (2.), ToDate (3.) | nur diesen Aufruf | die Engine-Kultur |
Engine-Kultur label.Expressions.Culture (C#) oder engine.Culture | jede Umwandlung des Etiketts: Zahlen und Datumswerte als Text, Lesen von Zahlen und Datumswerten, Upper/Lower, Format ohne Kultur | CultureInfo.InvariantCulture |
- Der Standard ist invariant, damit sich Etiketten nie mit den Regionaleinstellungen des Servers ändern:
1234.5, 10/22/2026, Währungszeichen ¤.
- Label-JSON hat keine Engine-Kultur (
Label.Expressions wird nicht serialisiert). In JSON-Vorlagen die Kultur als Argument übergeben – als Literal "de-DE" oder aus dem Datensatz [culture]. Label.Culture ist etwas anderes: Es wählt die Sprache der Übersetzungen {t:key}.
- Das Argument ist ein Kulturname:
"de-DE", "en-US", "fr-CH", eine neutrale Sprache wie "de" oder "". Ein Wert, der kein Text ist (Zahl, leeres Feld), oder leerer Text bedeutet die Engine-Kultur.
- Unbekannte Namen lösen
FormatException: Unknown culture '…' aus: Angenommen werden nur Kulturen, die .NET kennt (in den ICU-Daten vordefiniert) – "!!", "xx-YY" oder "foo" schlagen fehl.
- Datengebundene Elemente ohne Ausdruck verwenden stattdessen
DataFieldFormat und TextItem.CultureName (ohne Kultur: invariant mit einem Format); Ausdrücke lesen CultureName nicht. Der Standardkalender und die normalen Leerzeichen (unten) gelten auch dort.
| Ausdruck | Ergebnis | Kultur aus |
Format([price], "C", "de-DE") | 1.234,50 € | Literal |
Format([price], "C", [culture]) | 1.234,50 € | Feld |
Format([price], "C", If([customer.city] = "Graz", "de-AT", "en-US")) | € 1.234,50 | berechnet |
Format([price], "C") | ¤1,234.50 | Engine (invariant) |
Format([price], "C", "") | ¤1,234.50 | Engine |
Format([price], "C", [missing]) | ¤1,234.50 | Engine |
Format([price], "N2", "de") | 1.234,50 | neutrale Kultur |
Format([price], "C", "!!") | Unknown culture '!!' | fehlerhaft |
Format([price], "C", "xx-YY") | Unknown culture 'xx-YY' | unbekannt |
Upper("istanbul") | ISTANBUL | Engine (invariant) |
Die Engine-Kultur ändert die Umwandlungen ohne Argumente (hier tr-TR, wo der Großbuchstabe von i İ ist):
| Ausdruck (Engine-Kultur tr-TR) | Ergebnis |
Upper("istanbul") | İSTANBUL |
Format([price], "C") | ₺1.234,50 |
Format([price], "C", "en-US") | $1,234.50 |
var label = Label.FromJson(json); // JSON: keine Engine-Kultur
label.Expressions.Culture = CultureInfo.GetCultureInfo("de-DE"); // C#: für das ganze Etikett
// Format([price], "C") → "1.234,50 €", [price] + " EUR" → "1234,5 EUR"
// Format([price], "C", "en-US") → "$1,234.50" (das Argument hat Vorrang)
Ergebnisse je Kultur
Dieselben Werte in gängigen Kulturen. Der Engine druckt sie unter Windows, Linux und macOS gleich: Sonder-Leerzeichen werden zu normalen Leerzeichen – die geschützten und schmalen geschützten Leerzeichen der Tausendergruppen von fr-FR, pl-PL oder sv-SE und in Uhrzeiten wie 2:30 PM (U+00A0, U+202F, U+2000–U+200A, U+205F), die ICU-Versionen unterschiedlich setzen und Druckerschriften oft nicht drucken können – und Datumswerte verwenden den Standardkalender der Kultur (unten). Andere Zeichen stammen weiter aus den ICU-Daten des Betriebssystems, z. B. der Apostroph von de-CH (’ oder ') und die Richtungszeichen arabischer Texte; die Tests vergleichen diese normalisiert.
| Ausdruck | Ergebnis |
Format(1234.5, "C", "de-DE") | 1.234,50 € |
Format(1234.5, "C", "de-AT") | € 1.234,50 |
Format(1234.5, "C", "de-CH") | CHF 1’234.50 |
Format(1234.5, "C", "en-US") | $1,234.50 |
Format(1234.5, "C", "en-GB") | £1,234.50 |
Format(1234.5, "C", "fr-FR") | 1 234,50 € |
Format(1234.5, "C", "fr-CH") | 1 234.50 CHF |
Format(1234.5, "C", "it-IT") | 1.234,50 € |
Format(1234.5, "C", "es-ES") | 1.234,50 € |
Format(1234.5, "C", "nl-NL") | € 1.234,50 |
Format(1234.5, "C", "pl-PL") | 1 234,50 zł |
Format(1234.5, "C", "sv-SE") | 1 234,50 kr |
Format(1234.5, "C", "ja-JP") | ¥1,234 |
Format(1234.5, "C", "zh-CN") | ¥1,234.50 |
Format(1234.5, "C", "ar-SA") | 1٬234٫50 ر.س. |
Format(1234.5, "C", "ar-AE") | 1,234.50 د.إ. |
| Ausdruck | de-DE | en-US | en-GB | fr-FR | fr-CH | ja-JP | ar-SA |
Format(1234.5, "N2", c) |
Format(1234.5, "N2", "de-DE")1.234,50 |
Format(1234.5, "N2", "en-US")1,234.50 |
Format(1234.5, "N2", "en-GB")1,234.50 |
Format(1234.5, "N2", "fr-FR")1 234,50 |
Format(1234.5, "N2", "fr-CH")1 234,50 |
Format(1234.5, "N2", "ja-JP")1,234.50 |
Format(1234.5, "N2", "ar-SA")1٬234٫50 |
Format(0.125, "P1", c) |
Format(0.125, "P1", "de-DE")12,5 % |
Format(0.125, "P1", "en-US")12.5% |
Format(0.125, "P1", "en-GB")12.5% |
Format(0.125, "P1", "fr-FR")12,5 % |
Format(0.125, "P1", "fr-CH")12,5% |
Format(0.125, "P1", "ja-JP")12.5% |
Format(0.125, "P1", "ar-SA")12٫5٪ |
Format(1234.5, "#,##0.00", c) |
Format(1234.5, "#,##0.00", "de-DE")1.234,50 |
Format(1234.5, "#,##0.00", "en-US")1,234.50 |
Format(1234.5, "#,##0.00", "en-GB")1,234.50 |
Format(1234.5, "#,##0.00", "fr-FR")1 234,50 |
Format(1234.5, "#,##0.00", "fr-CH")1 234,50 |
Format(1234.5, "#,##0.00", "ja-JP")1,234.50 |
Format(1234.5, "#,##0.00", "ar-SA")1٬234٫50 |
Format(ToDate([date]), "d", c) |
Format(ToDate([date]), "d", "de-DE")22.10.2026 |
Format(ToDate([date]), "d", "en-US")10/22/2026 |
Format(ToDate([date]), "d", "en-GB")22/10/2026 |
Format(ToDate([date]), "d", "fr-FR")22/10/2026 |
Format(ToDate([date]), "d", "fr-CH")22.10.2026 |
Format(ToDate([date]), "d", "ja-JP")2026/10/22 |
Format(ToDate([date]), "dd/MM/yyyy", "ar-SA")11/05/1448 |
Format(ToDate([date]), "D", c) |
Format(ToDate([date]), "D", "de-DE")Donnerstag, 22. Oktober 2026 |
Format(ToDate([date]), "D", "en-US")Thursday, October 22, 2026 |
Format(ToDate([date]), "D", "en-GB")Thursday, 22 October 2026 |
Format(ToDate([date]), "D", "fr-FR")jeudi 22 octobre 2026 |
Format(ToDate([date]), "D", "fr-CH")jeudi, 22 octobre 2026 |
Format(ToDate([date]), "D", "ja-JP")2026年10月22日木曜日 |
Format(ToDate([date]), "MMMM", "ar-SA")جمادى الأولى |
Kalender. Jede Kultur formatiert und liest Datumswerte in ihrem Standardkalender aus einer festen Tabelle (ExpressionEngine.StandardCalendar), auf jedem Betriebssystem gleich: ar-SA islamischer Umm-al-Qura-Kalender (22. Okt. 2026 = 11. Dschumada l-ula 1448; die letzte Spalte oben zeigt das Muster dd/MM/yyyy und den Monatsnamen), th/th-TH thailändisch-buddhistisch (Jahr 2569), persisch für fa, fa-IR, fa-AF, ps, ps-AF, ckb-IR, lrc, mzn und uz-Arab (Jahr 1405); alle anderen Kulturen gregorianisch. Ohne die Tabelle hing das Ergebnis vom Server ab: Linux formatierte ar-SA gregorianisch, Windows im Umm-al-Qura-Kalender. Für gregorianische Datumswerte mit arabischen Texten z. B. ar-AE oder ar-EG verwenden oder ein festes Muster mit invarianter Kultur: Format(ToDate([date]), "dd.MM.yyyy").
| Ausdruck | Ergebnis |
Format(ToDate([date]), "d", "th-TH") | 22/10/2569 |
Format(ToDate([date]), "yyyy/MM/dd", "fa-IR") | 1405/07/30 |
Format(ToDate([date]), "d", "ar-AE") | 22/10/2026 |
Format(1234567.891, "N", "en-IN") | 12,34,567.891 |
Format(1234567.891, "N", "de-CH") | 1’234’567.891 |
12Funktionsreferenz
Alle 47 eingebauten Funktionen. Namen ignorieren Groß-/Kleinschreibung. Fehlende Argumente sind leer (null); zusätzliche Argumente werden ignoriert, sofern nicht anders angegeben. Argumente werden wie unter Typen beschrieben umgewandelt: „Text“-Argumente durchlaufen die Text-Umwandlung, „Zahl“-Argumente die Zahlen-Umwandlung (keine Zahl → 0), Anzahlen werden auf ganze Zahlen abgeschnitten.
Text
Upper(text) · Lower(text)
- Groß- / Kleinschreibung mit der Engine-Kultur. Gibt Text zurück.
Len(text)
- Anzahl der UTF-16-Zeichen. Gibt eine Zahl zurück (leer → 0).
Left(text, n) · Right(text, n)
- Die ersten / letzten
n Zeichen; n wird auf 0…Länge begrenzt (nie ein Fehler).
Mid(text, start, n?)
- 1-basierter Start (1 = erstes Zeichen, Werte unter 1 zählen als 1),
n Zeichen oder der Rest.
Substring(text, start, n?)
- 0-basierter Start (begrenzt),
n Zeichen oder der Rest.
Trim(text, chars?) · TrimStart(text) · TrimEnd(text)
- Entfernt Leerraum an beiden Enden / am Anfang / am Ende.
Trim mit einem 2. Argument entfernt stattdessen diese Zeichen (jedes Zeichen des Textes ist eines, das entfernt wird).
Replace(text, find, with)
- Ersetzt jedes Vorkommen (Groß-/Kleinschreibung wird beachtet). Ein leeres
find gibt den Text unverändert zurück.
PadLeft(text, width, char?) · PadRight(text, width, char?)
- Füllt auf
width Zeichen mit dem ersten Zeichen von char auf (Standard: Leerzeichen). Längerer Text bleibt unverändert.
Contains(text, part) · StartsWith(text, part) · EndsWith(text, part)
- True/false, ohne Beachtung der Groß-/Kleinschreibung.
IndexOf(text, part)
- 0-basierte Position von
part ohne Beachtung der Groß-/Kleinschreibung, −1, wenn nicht vorhanden.
Concat(value, …)
- Verbindet beliebig viele Werte als Text (leere Werte fügen nichts hinzu).
Repeat(text, n)
- Der Text
n-mal; n wird auf 0…10.000 begrenzt.
| Ausdruck | Ergebnis |
Upper([name]) | MÜLLER |
Lower("ÄÖÜ") | äöü |
Len([name]) | 6 |
Left([name], 3) | Mül |
Left([name], 99) | Müller |
Right([name], 2) | er |
Mid([name], 2, 3) | üll |
Mid([name], 2) | üller |
Substring([name], 1, 3) | üll |
Trim(" x ") & "|" | x| |
Trim("--x--", "-") | x |
"|" & TrimStart(" x ") & "|" | |x | |
"|" & TrimEnd(" x ") & "|" | | x| |
Replace([name], "ü", "ue") | Mueller |
Replace([sku], "-", "") | ab123 |
PadLeft([serial], 6, "0") | 000042 |
PadRight("42", 5, "*") | 42*** |
PadLeft("123456", 3, "0") | 123456 |
Contains([name], "MÜL") | True |
StartsWith([sku], "AB") | True |
EndsWith([name], "x") | False |
IndexOf([name], "L") | 2 |
IndexOf([name], "z") | -1 |
Concat([first], " ", [last], [nothing]) | Anna Schmidt |
Repeat("*", 5) | ***** |
Left([first], 1) + ". " + Upper([last]) | A. SCHMIDT |
Bedingungen
If(condition, then, else?)
- Gibt
then zurück, wenn die Bedingung wahr ist (siehe Umwandlung in boolesche Werte), sonst else (fehlt es → leer). Beide Zweige werden vor der Auswahl ausgewertet, ein Zweig darf also keinen Fehler auslösen (z. B. GS1 mit leerem Wert) – stattdessen die Argumente auswählen.
IsEmpty(value)
- True für leere Werte und
""; " " und 0 sind nicht leer.
Coalesce(value, …)
- Der erste Wert, der weder leer noch
"" ist; leer, wenn es keinen gibt.
| Ausdruck | Ergebnis |
If([qty] > 2, "BULK", "SINGLE") | BULK |
If([inStock], "in stock", "sold out") | in stock |
If([qty] > 5, "BULK") | (leer) |
If("0", "yes", "no") | no |
If("false", "yes", "no") | no |
If("abc", "yes", "no") | yes |
If([qty] = 1, "1 piece", [qty] + " pieces") | 3 pieces |
If([weight] < 1, Format([weight] * 1000, "0 'g'"), Format([weight], "0.0 'kg'")) | 457 g |
Coalesce([empty], [customer.name]) | ACME GmbH |
Coalesce(0, "x") | 0 |
IsEmpty(0) | False |
Mathematik
Round(number, decimals?)
- Rundet die Hälfte von null weg (2,5 → 3, −2,5 → −3) auf 0…15 Nachkommastellen (Standard 0; Werte außerhalb werden begrenzt, negative Stellen runden also auf ganze Zahlen). Es gilt binäre Gleitkommaarithmetik:
Round(1.005, 2) ergibt 1.
Floor(number) · Ceiling(number) · Abs(number)
- Abrunden / Aufrunden, Betrag.
Min(number, …) · Max(number, …)
- Kleinstes / größtes der Argumente als Zahl; ohne Argumente 0.
| Ausdruck | Ergebnis |
Round(2.5) | 3 |
Round(-2.5) | -3 |
Round(2.345, 2) | 2.35 |
Round(1.005, 2) | 1 |
Round([price], -1) | 1235 |
Round([price] * 1.2, 2) | 1481.4 |
Floor(-2.7) | -3 |
Ceiling([qty] / 2) | 2 |
Abs(-3) | 3 |
Max(3, "10", 2) | 10 |
Min([qty], 2) | 2 |
Min() | 0 |
Datum
Now() · Today()
- Aktuelles Datum mit Uhrzeit / heute um 00:00 aus
Expressions.Clock (Standard: die lokale Uhr). Die Uhr für Tests oder Nachdrucke mit dem ursprünglichen Datum setzen.
AddDays(date, days) · AddMonths(date, months) · AddYears(date, years)
- Addiert (oder subtrahiert, negativ) Tage – Bruchteile erlaubt – oder ganze Monate / Jahre. Ein Datum als Text wird wie bei
ToDate(text) umgewandelt. Das Monatsende bleibt gültig (31. Jan. + 1 Monat = 28./29. Feb.).
ToDate(text, format?, culture?)
- Liest ein Datum. Mit
format muss der Text exakt passen (DateTime.TryParseExact, Namen in culture); andernfalls – oder wenn er nicht passt – wird der Text mit der Engine-Kultur gelesen, dann invariant. Text, der kein Datum ist, ergibt 0001-01-01 (kein Fehler): bei Bedarf mit Format(…, "yyyy") = "0001" prüfen.
| Ausdruck | Ergebnis |
Format(AddDays(Today(), 30), "dd.MM.yyyy") | 21.11.2026 |
Format(AddDays(Now(), -0.5), "dd.MM. HH:mm") | 22.10. 02:30 |
Format(AddMonths(ToDate([date]), 18), "MM/yyyy") | 04/2028 |
Format(AddMonths(ToDate("2027-01-31"), 1), "yyyy-MM-dd") | 2027-02-28 |
Format(AddYears([date], 2), "yyyy-MM-dd") | 2028-10-22 |
Format(ToDate([deDate], "dd.MM.yyyy"), "yyyy-MM-dd") | 2026-10-22 |
Format(ToDate("22. Oktober 2026", "d. MMMM yyyy", "de-DE"), "d") | 10/22/2026 |
Format(ToDate([deDate]), "yyyy-MM-dd") | 0001-01-01 |
Format(ToDate("10/22/2026"), "yyyy-MM-dd") | 2026-10-22 |
Format(ToDate("garbage"), "yyyy") | 0001 |
Umwandlung & Formatierung
Format(value, format?, culture?) · ToString(value, format?, culture?)
ToString ist dieselbe Funktion unter anderem Namen. Zahlen, Datumswerte und andere formatierbare Werte: value.ToString(format, culture) (siehe Zahlen- und Datumsformate). Text mit einem Format wird mit der Engine-Kultur als Zahl gelesen und formatiert; anderer Text, boolesche Werte und leere Werte werden unverändert in Text umgewandelt. Ohne Format: der Wert als Text in culture. Fehler: unbekannter Kulturname, ein Format, das der Wert ablehnt (D, X bei einer Zahl mit Nachkommastellen).
Str(value)
- Wert als Text mit der Engine-Kultur.
Num(value)
- Wert als Zahl mit der Engine-Kultur (dann invariant); keine Zahl → 0.
ToNumber(text, culture?)
- Wie
Num, der Text wird zuerst in culture gelesen. Akzeptiert Vorzeichen, ein Dezimaltrennzeichen und Exponent; mit Kultur-Argument auch die Tausendertrennzeichen dieser Kultur an ihren Stellen ("1.234,5" in de-DE → 1234.5, aber "12,5" in en-US ist keine Zahl). Ohne Kultur keine Tausendertrennzeichen. Keine Zahl → 0.
IsNumeric(value, culture?)
- True für Zahlen und für Text, der sich in
culture als Zahl lesen lässt (mit Kultur-Argument wie bei ToNumber samt Tausendertrennzeichen; Standard: Engine-Kultur); false für boolesche Werte, leere Werte und anderen Text.
| Ausdruck | Ergebnis |
Format([price]) | 1234.5 |
Format([price], "", "de-DE") | 1234,5 |
ToString([price], "0.0") | 1234.5 |
Format([inStock], "C") | True |
Str([qty]) + Str(1) | 31 |
Num("12.5") * 2 | 25 |
Num("12,5") | 0 |
ToNumber("12,5", "de-DE") * 2 | 25 |
ToNumber("1.234,5", "de-DE") | 1234.5 |
ToNumber("1,234.5", "en-US") | 1234.5 |
ToNumber("12,5", "en-US") | 0 |
ToNumber("1.234,5") | 0 |
ToNumber(Replace("1.234,5", ".", ""), "de-DE") | 1234.5 |
ToNumber("1e3") | 1000 |
IsNumeric([textPrice]) | True |
IsNumeric("12,5") | False |
IsNumeric("12,5", "de-DE") | True |
IsNumeric("1.234,5", "de-DE") | True |
IsNumeric([sku]) | False |
IsNumeric(true) | False |
Prüfziffern
Gs1CheckDigit(digits)
- Gibt die Ziffern mit angehängter GS1-Mod-10-Prüfziffer zurück (GTIN, SSCC, GLN …). Die Eingabe wird nicht geprüft – nur Ziffern übergeben.
Mod10(digits) · Luhn(digits) · Mod11(digits) · Mod43(text)
- Geben nur das Prüfzeichen zurück: GS1/UPC-Gewichtung 3-1, Luhn (Karten), Mod 11 mit Gewichten 2…7 (
X für 10), Code 39 Mod 43. Fehler: für das Verfahren ungültige Zeichen.
VoicePickCode(gtin, lot, date?)
- Der 4-stellige PTI-Voice-Pick-Code (CRC-16 aus GTIN + Charge [+ Datum als YYMMDD]); ein Datumswert wird als
yyMMdd geschrieben.
| Ausdruck | Ergebnis |
Gs1CheckDigit("400638133393") | 4006381333931 |
Gs1CheckDigit("0952123454321") | 09521234543213 |
Mod10("400638133393") | 1 |
Luhn("7992739871") | 3 |
Mod11("12345") | 5 |
Mod43("CODE39") | W |
VoicePickCode("10850510002011", "46587443HG234", "101026") | 1997 |
VoicePickCode("10850510002011", "46587443HG234") | 0862 |
GS1
GS1(ai1, value1, ai2, value2, …)
- Baut aus Paaren den GS1-Element-String in Klammerschreibweise
(01)…(10)…. AIs werden als Text geschrieben ("01", 2–4 Ziffern; Klammern werden entfernt). Jedes Paar wird gegen die GS1-Tabelle geprüft – bekannter AI, Länge, Zeichensatz, Prüfziffer, Datum, Pflicht- und Ausschlusskombinationen –, ein Fehler löst eine FormatException mit dem betroffenen AI aus, sodass nie ein falscher Code gedruckt wird. Als Expression eines GS1-128-Barcodes (Symbology.Gs1_128) verwenden: Die FNC1-Trenner nach Werten variabler Länge werden beim Schreiben des Barcodes eingefügt. Werte dürfen keine Klammern enthalten.
| Ausdruck | Ergebnis |
GS1("01", [gtin], "10", [lot]) | (01)09521234543213(10)LOT42 |
GS1("01", [gtin], "17", Format(AddMonths(Today(), 14), "yyMMdd"), "10", [lot]) | (01)09521234543213(17)271222(10)LOT42 |
GS1("(01)", [gtin]) | (01)09521234543213 |
GS1("01", "09521234543214") | GS1(): AI 01: Bad checksum |
GS1("01", [gtin], "10", [empty]) | GS1(): AI 10 has no value |
GS1(1, [gtin]) | GS1(): '1' is not an application identifier (2 to 4 digits, written as text, e.g. "01") |
GS1("01") | GS1() takes pairs of application identifier and value, e.g. GS1("01", [Gtin], "10", [Lot]) |
GS1("10", "A(B)") | GS1(): the value of AI 10 contains a parenthesis, which GS1-128 printed with ^BC mode D removes from the data |
"GTIN " + Mid(GS1("01", [gtin]), 5) | GTIN 09521234543213 |
label.Add(new BarcodeItem
{
X = 5, Y = 20, Symbology = Symbology.Gs1_128,
Expression = "GS1(\"01\", [gtin], \"17\", Format(ToDate([expiry]), \"yyMMdd\"), \"10\", [lot])",
});
Mehr zu GS1-Barcodes: GS1 in der Dokumentation.
13C#-API
ExpressionEngine lässt sich eigenständig oder über label.Expressions verwenden:
using System.Globalization;
using ZplCore.Labels;
var engine = new ExpressionEngine
{
Culture = CultureInfo.GetCultureInfo("de-DE"), // Standard: InvariantCulture
Clock = () => new DateTime(2026, 10, 22, 14, 30, 5), // Now()/Today()
};
engine.Variables["VAT"] = 0.19; // eigene Variablen
engine.SetCustomFunction("Euro", a => ExpressionEngine.Num(a[0]).ToString("0.00", CultureInfo.InvariantCulture) + " EUR");
var record = new Dictionary<string, object?> { ["price"] = 12.5m };
object? value = engine.Evaluate("[price] * (1 + VAT)", f => record.GetValueOrDefault(f)); // 14.875 (double)
string text = engine.EvaluateToString("Euro([price])", f => record.GetValueOrDefault(f)); // "12.50 EUR"
string date = engine.EvaluateToString("Format(Today(), \"D\")"); // "Donnerstag, 22. Oktober 2026"
| Member | Zweck |
Evaluate(expression, fieldResolver?) | Ergebnis als Wert (Zahl = double, Text, bool, DateTime, null). Der Resolver erhält jedes [name] (und unbekannte Namen ohne Klammern); ohne ihn sind Felder leer. |
EvaluateToString(expression, fieldResolver?) | Ergebnis als Text mit Culture – das, was ein Etikett druckt. |
Culture | Engine-Kultur (siehe Kulturen). |
Clock | Uhr von Now()/Today(). |
Variables, SetCustomVariable | Eigene Variablen, Namen ignorieren Groß-/Kleinschreibung. |
Functions, SetCustomFunction | Alle Funktionen (eingebaute lassen sich ersetzen): Func<object?[], object?> erhält die ausgewerteten Argumente. |
AddTypeReference(type, alias?) | Erlaubt Type.Member und Member auf Werten dieses Typs, z. B. Math.Max(1, [qty]), DateTime.Now.AddDays(3). Sonst ist nichts erreichbar. |
Str, Num, Bool (statisch), ToText, ToNum | Die Umwandlungen der Engine, für eigene Funktionen (statisch = invariant). |
Threadsicherheit: Einmal konfiguriert, darf Evaluate auf vielen Threads laufen (Etikettenaufträge tun das). Das Konfigurieren (Variablen, Funktionen, Typreferenzen) ist nicht threadsicher. Eigene Funktionen, Typreferenzen und eine eigene Uhr lassen Aufträge standardmäßig sequenziell laufen, siehe parallele Erzeugung.
14Fehler
Ein ungültiger Ausdruck löst eine FormatException aus, wenn das Element erzeugt wird (ToZpl, Render, Bindungsaufträge) – das Etikett wird nicht mit falschem Text gedruckt. Die Meldungen sind englisch:
| Ausdruck | Meldung |
Foo(1) | Unknown function Foo |
Upper([name] | Missing ) |
1 + | Unexpected '' |
1 2 | Unexpected '2' |
"a" $ "b" | Unexpected '$' at 4 |
[name | Missing ] |
1.2.3 | The input string '1.2.3' was not in a correct format. |
Format([price], "C", "!!") | Unknown culture '!!' |
Format(2.5, "X") | Format specifier was invalid. |
Upper("abc) | Missing closing quote of the text at 6 |
[customer].name | Type JsonElement is not registered (AddTypeReference) |
Math.Max(1, 2) | Member Max on null |
Math.Max(…) ohne AddTypeReference(typeof(Math)) liest Math als (fehlendes) Feld – daher „Member Max on null“.
Falsche Daten lösen keinen Fehler aus (außer in GS1 und bei ungültigen Kulturnamen): Unbekannte Felder sind leer, Text, der keine Zahl ist, zählt als 0, Text, der kein Datum ist, ergibt 0001-01-01. Etiketten vor dem Druck mit Label.Validate(record) und den Etikettentests prüfen.
15Rezepte
| Aufgabe | Ausdruck | Ergebnis |
| Preis mit der Währung des Datensatzes | Format([price], "C", [culture]) | 1.234,50 € |
| Durchgestrichener alter Preis nur, wenn höher | If([oldPrice] > [price], Format([oldPrice], "C", [culture]), "") | 1.499,00 € |
| Ersparnis in Prozent | "-" + Format(1 - [price] / [oldPrice], "P0", "de-DE") | -18 % |
| Preis mit Steuer, 2 Nachkommastellen | Format([price] * 1.2, "N2", "de-DE") + " € inkl. 20 % MwSt." | 1.481,40 € inkl. 20 % MwSt. |
| Grundpreis (pro kg) | Format([price] / [weight], "#,##0.00", "de-DE") + " €/kg" | 2.703,09 €/kg |
| Mindesthaltbarkeit: heute + 30 Tage | "Mindestens haltbar bis " + Format(AddDays(Today(), 30), "dd.MM.yyyy") | Mindestens haltbar bis 21.11.2026 |
| Mindesthaltbarkeit aus einem Datumstext | Format(AddMonths(ToDate([deDate], "dd.MM.yyyy"), 6), "MM/yyyy") | 04/2027 |
| Herstellungsdatum, ISO | Format(Now(), "yyyy-MM-dd HH:mm") | 2026-10-22 14:30 |
| Gewicht in g oder kg | If([weight] < 1, Format([weight] * 1000, "0 'g'"), Format([weight], "0.00 'kg'", "de-DE")) | 457 g |
| Gewicht mit 3 Nachkommastellen | Format([weight], "0.000 'kg'", "de-DE") | 0,457 kg |
| Barcode-Daten mit Prüfziffer | Gs1CheckDigit("400638" + PadLeft([serial], 6, "0")) | 4006380000421 |
| Seriennummer mit Präfix | "SN-" + PadLeft(LABEL_NUMBER, 5, "0") | SN-00001 |
| Seite x von y | PAGE_NUMBER + "/" + TOTAL_PAGES | 1/3 |
| Name: Initiale + Nachname | Upper(Left([first], 1)) + ". " + [last] | A. Schmidt |
| Adresszeile, leere Teile übersprungen | Trim(Concat([customer.zip], " ", [customer.city], If(IsEmpty([nothing]), "", ", " + [nothing]))) | 8010 Graz |
| Bedingter Text | If([qty] >= 10, "BULK", If([qty] > 1, "MULTI", "SINGLE")) | MULTI |
| Ersatztext | Coalesce([nothing], [customer.name], "unbekannt") | ACME GmbH |
| Artikelnummer in Großbuchstaben ohne Bindestrich | Upper(Replace([sku], "-", "")) | AB123 |
| Preise aus Textfeldern vergleichen | If(Num([textOld]) > Num([textPrice]), "SALE", "") | (leer) |
| GS1-128-Daten mit Mindesthaltbarkeit | GS1("01", [gtin], "15", Format(AddDays(Today(), 30), "yyMMdd"), "10", [lot]) | (01)09521234543213(15)261121(10)LOT42 |
16Stolperfallen
- Text, der keine Zahl ist, wird als Text verglichen.
"a10" < "a9" ist wahr; zwei Zahltexte ("9.99", "19.99") werden als Zahlen verglichen.
+ mit einem leeren Feld verbindet. [missing] + 1 + 1 ergibt "11"; Num([missing]) + 1 + 1 schreiben.
- Datumswerte aus JSON/CSV sind Text.
Format([date], "dd.MM.yyyy") druckt den Text unverändert; Format(ToDate([date]), …) verwenden, bei Texten, die nicht ISO sind, mit Muster: ToDate([deDate], "dd.MM.yyyy").
- Keine Kultur → invariant.
Format([price], "C") druckt ¤1,234.50; eine Kultur übergeben oder in C# label.Expressions.Culture setzen.
ar-SA und th-TH verwenden andere Kalender für Datumswerte.
- Die Formate
D/X brauchen ganze Zahlen: Format(2.5, "D3") löst einen Fehler aus; vorher runden oder "000" verwenden.
- Tausendertrennzeichen lesen nur
ToNumber/IsNumeric mit Kultur-Argument; Num und Umwandlungen ohne Kultur brauchen Zahlen ohne Trennzeichen.
- Text, der kein Datum bzw. keine Zahl ist, löst keinen Fehler aus: Er wird zu 0001-01-01 bzw. 0.
not bindet stärker als =: not ([a] = 1) schreiben.
- Beide Zweige von
If werden ausgewertet. Ein GS1()-Aufruf mit leerem Wert löst auch in dem Zweig einen Fehler aus, der nicht gewählt wird.
- Einheitentexte in Formaten: in Anführungszeichen setzen (
"0.0 'kg'").
- Anführungszeichen im Label-JSON:
"expression": "Format([price], \"C\", \"de-DE\")" – die JSON-Escapes gehören nicht zum Ausdruck.
- Grenzen der Engine: höchstens 256 Ebenen verschachtelter Klammern, Argumente und unärer Operatoren (
ExpressionEngine.MaxNesting); tiefere Ausdrücke lösen FormatException aus, statt den Stack zu erschöpfen. Der Online-Builder begrenzt Länge und Verschachtelung zusätzlich.