Skip to content

Lokalisierte Templates ​

Sprachversionen halten alle Übersetzungen einer E-Mail in einem Template: ein gemeinsamer Blockbaum plus l10n-Ebenen mit dem Text, den Eigenschaftsüberschreibungen und den ausgeblendeten Blöcken jeder Sprache aus settings.locales. Autoren fügen Versionen im Editor hinzu, bearbeiten sie und füllen sie mit Übersetzen über Ihren TranslationProvider; resolveLocale und --locale projizieren, prüfen und rendern eine Version.

Sprachversionen teilen eine Struktur. Für unabhängige Blockbäume pro Sprache speichern Sie separate Templates und wechseln mit editor.load(id).

Einrichtung ​

ts
import { init } from "@templatical/editor";
import "@templatical/editor/style.css";

const editor = await init({
  container: "#editor",
  localization: { locales: ["en", "de", "fr"] },
});

localization.locales listet die BCP-47-Tags, die Autoren hinzufügen können. settings.locale des Templates ist seine Standardsprache; ein leeres Template beginnt auf Englisch ("en"). Die Liste allein schreibt nichts ins Template: Das Menü Sprachen erscheint im Header, und ein Autor fügt dort eine Version hinzu. Für eine statische Seite ohne Bundler binden Sie den Editor wie im Schnellstart über das CDN ein und übergeben denselben localization-Schlüssel.

Ergänzen Sie translation für Übersetzen und localization.doNotTranslate für Markennamen; siehe Übersetzen. Die UI-Sprache des Editors (init({ locale })) ist unabhängig von Template-Sprachen; siehe Internationalisierung. Bestehende Templates brauchen keine Migration; siehe Kompatibilität.

Eine Sprache hinzufügen ​

Öffnen Sie Sprachen, wählen Sie Sprache hinzufügen und dann eine Sprache; das Untermenü bietet die konfigurierten Sprachen an, die dem Template fehlen. Beschriftungen nennen jede Sprache in ihrer eigenen Sprache, etwa de als Deutsch und fr als Français, überall dort, wo sie {language} oder {default} zeigen. Wählen Sie die neue Version im Menü aus, um sie zu bearbeiten: Die Sprachleiste unter dem Header zeigt Bearbeitung: {language}. Zurück zu {default} in der Leiste oder die Standardzeile im Menü führt zur Standardsprache zurück. Die Schaltfläche Weitere Aktionen für {language} jeder Version bietet Als Standardsprache festlegen (Standardsprache) und {language} entfernen…, das eine Bestätigung verlangt und sich rückgängig machen lässt.

Text- und Eigenschaftsänderungen in einer Sprachansicht betreffen nur diese Sprache. Hat ein übersetzbares Feld einen Standardwert, speichert eine Bearbeitung text mit seinem sourceHash; fehlt der Standardwert, etwa ein nicht vorhandener Preheader, speichert sie eine set-Überschreibung ohne Veraltungsprüfung. Ein Wert gleich dem Standard hinterlässt keine Überschreibung. Der Einstellungsbereich zeigt eine Sprachmarkierung an jedem überschriebenen oder übersetzten Feld und das Symbol Auf {default} zurücksetzen: Es stellt nur dieses Feld aus dem Standard wieder her und erhält benachbarte Überschreibungen. Änderungen zurücksetzen im Statusabzeichen entfernt alle set-Überschreibungen dieses Blocks; seine text-Übersetzungen bleiben erhalten. In {language} ausblenden in den Blockeinstellungen lässt einen Geisterblock auf der Arbeitsfläche stehen; Anzeigen stellt ihn wieder her, und der Renderer lässt ausgeblendete Blöcke aus.

Hinzufügen, Verschieben, Löschen, Duplizieren, Einfügen und Ziehen ändern den gemeinsamen Baum für alle Sprachen. Die Sprachleiste zeigt Strukturänderungen gelten für jede Sprache einmal pro Sitzung und Template. Eine Version in settings.locales, die in localization.locales fehlt, bleibt auswählbar, schreibgeschützt und als Nicht aktiviert markiert: Wert- und Strukturänderungen werden abgelehnt. Wer dort einen Block auswählt, nimmt trotzdem seine Sperre für diese Sprache, sodass bei Zusammenarbeit jemand in der schreibgeschützten Ansicht und jemand, der eine andere Sprache bearbeitet, denselben Block gleichzeitig halten.

Die aktive Sprache ist Ansichtsstatus und wird nicht gespeichert; jede Sitzung startet im Standard. editor.getLocale(), editor.setLocale(tag) und init({ onLocaleChange }) lesen, wählen und melden sie; eine unbekannte Version wirft SdkError mit Code unknown_locale. Siehe Editor-API.

Aus Beefree, Unlayer und Topol importierte Sprachen nutzen dieselben Ebenen. Jeder Importer akzeptiert defaultLocale?: string für eine Quelle ohne deklarierten Standard, und template-tools import --default-locale <tag> übergibt ihn über die CLI; ein ungültiger Tag dort endet mit Exit-Code 2. Ein Importer ignoriert eine deklarierte Standardsprache oder ein defaultLocale, das kein Sprach-Tag ist, mit einer Warnung und nimmt den nächsten gültigen Wert oder "en". Prüfen Sie report.warnings auf verworfene Sprachen und tragen Sie die importierten Sprachen in localization.locales ein, um sie zu bearbeiten. Ohne Editor schreiben Sie Versionen mit den Locale-Operationen oder mit strings import (Übersetzen).

Übersetzen ​

Übergeben Sie einen TranslationProvider als init({ translation }) neben localization; ohne ihn gibt es keine Übersetzungsoberfläche. In einer aktivierten Sprachansicht sendet {count} Felder übersetzen in der Sprachleiste die fehlenden und veralteten Felder (die Lücken), sodass aktuelle Übersetzungen und manuelle Änderungen erhalten bleiben; ohne Lücken ist die Aktion ausgeblendet. Alles erneut übersetzen… sendet nach einer Bestätigung, die die Feldzahl nennt und das Ersetzen manueller Änderungen erklärt, jedes geeignete Feld; ohne geeignete Felder ist die Aktion deaktiviert. Das Statusabzeichen eines Blocks bietet Übersetzen bei fehlendem Text und Erneut übersetzen bei veraltetem Text oder abweichenden Merge-Tags.

Übersetzen maskiert Merge-Tags, Logik-Tags und die Begriffe aus localization.doNotTranslate (zum Beispiel ["Sable"]) als nummerierte Platzhalter und übernimmt ein zurückgegebenes Feld nur, wenn es genau die gesendeten Platzhalter enthält; jedes andere Feld behält seinen bisherigen Wert und wird gemeldet. HTML-Blöcke, leere Standardwerte, set-Überschreibungen und ausgeblendete Blöcke werden nicht gesendet: Übersetzen Sie HTML-Blöcke von Hand. Der Übersetzungsvertrag definiert Platzhalter, Batch-Grenzen, Fehler und die DeepL-Einstellungen.

Während eines Durchlaufs zeigt die Sprachleiste {done} von {total} werden übersetzt… mit Abbrechen, und der Editor bleibt bearbeitbar. Vor Abbruch oder Fehler zurückgegebene Felder werden gemeinsam als ein Rückgängig-Schritt angewendet; ein Durchlauf ohne Änderung zeichnet nichts auf. Mit Zusammenarbeit ist ein Durchlauf eine gewöhnliche Bearbeitung, und Blöcke, die eine andere Sitzung in dieser Sprache oder strukturell hält, werden übersprungen. Die schließbare Ergebnismeldung nennt übersetzte Felder, wegen geänderter Tags beibehaltene Felder, nicht zurückgegebene Felder, wegen laufender Bearbeitung übersprungene Blöcke und die Fehlermeldung des Übersetzungsdienstes. Schließen Sie sie, um wieder die Zahlen zu sehen; ein Ansichtswechsel löscht sie.

KI-Chat, Mit KI beheben, Umformulieren und Alt-Text schreiben bearbeiten die aktive Sprache; siehe KI in Sprachansichten.

Für den Austausch als Dateien mit einem Übersetzungsdienst oder Übersetzer schreibt strings export die Lücken einer Sprache als JSON, und strings import wendet die übersetzte Datei an:

bash
npx -y @templatical/template-tools strings export template.json --locale de --out strings.de.json
npx -y @templatical/template-tools strings import template.json strings.de.json --json

Setzen Sie den text jedes Eintrags und lassen Sie id, sourceHash und Platzhalter unverändert. Ein veralteter Eintrag hat kein text: Seine alte Übersetzung steht in previous, das der Import nie liest; übersetzen Sie ihn daher neu aus source. Import wendet jeden String an, dessen sourceHash noch zum aktuellen Standard passt und dessen Platzhalter exakt zurückkommen, schreibt die angenommenen Strings und endet mit Exit 1, wenn ein String abgelehnt wurde. Beide Befehle lehnen die Standardsprache ab, und der Import lehnt eine Datei ab, die exportiert wurde, bevor eine andere Sprache zum Standard wurde. Übergeben Sie beiden Befehlen dieselben --do-not-translate- und --definitions-Dateien; siehe Strings.

Im Code ist derselbe Rundlauf localeStrings → JSON → applyLocaleStrings aus @templatical/types. Dieses vollständige TypeScript-Beispiel extrahiert die Lücken mit geschütztem Sable, simuliert eine JSON-Bearbeitung durch einen Übersetzer und wendet die zurückgegebenen Locale-Operationen an; übergeben Sie beiden Helfern dieselbe doNotTranslate-Liste. localeStrings liefert die gespeicherte Übersetzung eines veralteten Eintrags in text, und applyLocaleStrings stempelt jeden erhaltenen text mit dem Hash zum Extraktionszeitpunkt; ein unverändert angewendeter veralteter Eintrag macht das Feld daher aktuell, wie Beibehalten. Entfernen Sie text aus veralteten Einträgen, bevor Sie sie an einen Übersetzer geben, wie das Beispiel es durch Verschieben nach previous tut, oder verwenden Sie strings export. Mit only: "gaps" tragen nur veraltete Einträge text; mit only: "all" listet localeCoverage(content, locale).stale sie auf. Ein Standardwert, der bearbeitet wird, während die Datei unterwegs ist, macht diesen String veraltet. Die reinen Helfer bereinigen zurückgegebenes HTML nicht: Bereinigen Sie es vor dem Anwenden und erhalten Sie dabei die Platzhalter.

ts
import {
  applyLocaleStrings,
  applyOperations,
  createDefaultTemplateContent,
  createTitleBlock,
  localeStrings,
  resolveLocale,
  SYNTAX_PRESETS,
} from "@templatical/types";

const original = createDefaultTemplateContent("Arial", { locale: "en" });
original.blocks.push(
  createTitleBlock({
    id: "greeting",
    content: "<p>Hello {{first_name}}, welcome to Sable</p>",
  }),
);
const base = applyOperations(original, [{ op: "addLocale", locale: "de" }]);
const syntax = SYNTAX_PRESETS.liquid;
const doNotTranslate = ["Sable"];
const extracted = localeStrings(base, "de", {
  syntax,
  only: "gaps",
  doNotTranslate,
});
const json = JSON.stringify({
  sourceLocale: "en",
  targetLocale: "de",
  // With only: "gaps", only an out-of-date entry has `text`: its stored
  // translation. Move it to `previous`, as `strings export` does, so an entry
  // returned untouched can't be applied as current.
  strings: extracted.strings.map(({ text, ...field }) =>
    text === undefined ? field : { ...field, previous: text },
  ),
  tokens: extracted.tokens,
});

// Send this JSON to a translator, who sets each entry's `text`.
// Keep ids, source hashes and tokens unchanged.
const file: {
  sourceLocale: string;
  targetLocale: string;
  strings: { id: string; text?: string; sourceHash: string }[];
  tokens: Record<string, string[]>;
} = JSON.parse(json);
file.strings[0].text = '<p>Hallo <x id="1"/>, willkommen bei <x id="2"/></p>';
// Skip entries without `text`, as `strings import` does.
const translated = file.strings.flatMap(({ id, text, sourceHash }) =>
  text === undefined ? [] : [{ id, text, sourceHash }],
);
const { operations, rejected } = applyLocaleStrings(
  base,
  file.targetLocale,
  translated,
  { syntax, tokens: file.tokens, doNotTranslate },
);
if (rejected.length) throw new Error(JSON.stringify(rejected));
const content = applyOperations(base, operations);
const german = resolveLocale(content, "de");
// Save `content`: it keeps every language. `german` is the German version.
console.log(JSON.stringify(content), JSON.stringify(german));

Eine Sprache prüfen ​

Die Sprachschaltfläche im Header nennt die aktive Sprache; in einer Sprachansicht mit Lücken ergänzt sie ein Warnsymbol und die Zahl der Lücken, mit „{missing} fehlen, {stale} veraltet“ im Tooltip. Ihr zugänglicher Name nennt den Sprachnamen, dann die sichtbare Lückenzahl, das Sprach-Tag und dieselbe Aufschlüsselung; ohne Lücken bleibt er zum Beispiel „Deutsch (de)“. Die Sprachleiste zeigt dieselben Zahlen oder Vollständig. Ein Block mit einem Lokalisierungszustand trägt ein Symbol als Statusabzeichen in seiner oberen rechten Ecke (oben links in einer Rechts-nach-links-Ansicht); sein Tooltip und sein zugänglicher Name nennen die Beschriftung unten. Wählen Sie das Abzeichen oder Zum Block springen bei einem Lokalisierungsbefund im Bereich Probleme, um sein Popover zu öffnen. Bei einem kurzen ausgewählten Block in einer schmalen Arbeitsfläche steht die Aktionsleiste neben den Abzeichen, sodass diese und ihre Fokusringe sichtbar bleiben.

Abzeichen-BeschriftungBedeutungAktionen
In {language} ausgeblendetDer Block ist nicht in der E-Mail dieser SpracheAnzeigen
Nicht übersetztEin Feld hat keinen Text in dieser Sprache; die E-Mail zeigt den StandardtextÜbersetzen
VeraltetDer Standardtext wurde nach der Übersetzung geändertErneut übersetzen, Beibehalten, {default} verwenden
Merge-Tags unterscheiden sichDie Merge-Tags einer aktuellen Übersetzung weichen vom Standardtext abErneut übersetzen, {default} verwenden
Geändert inEigenschaften sind in dieser Sprache überschriebenÄnderungen zurücksetzen
In {count} Sprachen veraltetIn der Standardansicht: Übersetzungen dieses Blocks sind veraltetÖffnen, pro Sprache

Ein Block mit mehreren Zuständen zeigt den ersten in dieser Reihenfolge; bei mehreren Feldern in einem Zustand zählt seine Beschriftung sie („{count} nicht übersetzt“). Übersetzen und Erneut übersetzen benötigen einen Übersetzungs-Provider. In der Vorschau erscheinen keine Abzeichen, und die Blöcke eines Layouts haben keine (Layouts). template-tools validate --locale <tag> und lintTemplate(content, { contentLocale }) melden fehlende, veraltete und Merge-Tag-Befunde als localization.untranslated, localization.stale und localization.merge-tags-differ, unten beschrieben; siehe Ergebnis prüfen.

Fehlende Werte ​

Eine fehlende Übersetzung fällt auf den Standardsprachwert zurück. Der Block zeigt Nicht übersetzt, die Abdeckung meldet das Feld als fehlend und localization.untranslated gibt eine Warnung aus. Leere Standardwerte und ausgeblendete Blöcke zählen nicht als fehlend. Inhalt von HTML-Blöcken zählt mit, obwohl Übersetzen und strings export ihn nicht senden: Die Sprachleiste zählt ihn als fehlend, ohne {count} Felder übersetzen dafür anzubieten, und die Warnung bleibt, bis Sie ihn in der Sprachansicht von Hand übersetzen. Eine absichtlich leere Übersetzung ist ein Wert und löst keinen Fallback aus.

Veraltete Werte ​

Jede Übersetzung speichert den sourceHash des Standardwerts, gegen den sie geschrieben wurde. Ändert sich der Standard, ist die Übersetzung Veraltet, und localization.stale meldet sie. Erneut übersetzen übersetzt sie neu. Beibehalten behält den Text und speichert den aktuellen Hash, auch wenn der geänderte Standard inzwischen der Übersetzung entspricht; die Abdeckung zählt sie dann als übersetzt. {default} verwenden entfernt die Übersetzung, sodass der Standard erscheint. Keine dieser Aktionen ändert benachbarte Überschreibungen, und Aus- und Einblenden eines Blocks erhält seinen Text. In der Standardansicht markiert In {count} Sprachen veraltet die betroffenen Blöcke. Überschreibungen in set haben keinen Quell-Hash. Typen beschreibt das Hash-Format.

Merge-Tags ​

Übernehmen Sie die Merge-Tags des Standards einschließlich wiederholter Vorkommen in jedes übersetzte Feld: localization.merge-tags-differ vergleicht sie als Multimenge, und der Editor bietet in jeder Sprache dieselbe Merge-Tag-Liste an. Eine aktuelle Übersetzung mit abweichenden Tags zeigt Merge-Tags unterscheiden sich. Ihr Erneut übersetzen sendet mit einem Provider alle geeigneten Textfelder dieses Blocks. Würde dies eine weitere aktuelle Übersetzung im Block ersetzen, fragt das Popover zuerst nach einer Bestätigung mit der Feldzahl und erklärt, dass manuelle Änderungen ersetzt werden. Nach der Bestätigung wird die Übersetzung als ein Rückgängig-Schritt angewendet; ohne weitere aktuelle Übersetzung zum Ersetzen beginnt Erneut übersetzen sofort. Während der Bestätigung führt Escape zu Erneut übersetzen zurück; ein zweites Escape schließt das Popover. {default} verwenden entfernt nur die abweichenden Übersetzungen. Fehlender und veralteter Text haben Vorrang vor diesem Zustand. Die Regel ist eine Warnung, aber die CLI-Validierung endet bei ihr mit Exit 1, auch bei valid: true; fehlende und veraltete Übersetzungen allein ändern den Exit-Code nicht.

Eine Sprache rendern ​

renderToMjml(content, { locale: "de" }), editor.toMjml({ locale: "de" }), editor.toHtml({ locale: "de" }) und template-tools render template.json --locale de rendern eine Version. Ohne locale rendern sie den Standard, unabhängig von der Ansicht im Editor; eine unbekannte Version scheitert mit unknown_locale. Projizierte Sprache und Richtung bestimmen MJML-lang und dir. Lokaler MJML-Export benötigt den optionalen Peer @templatical/renderer, HTML einen Provider mit render.toHtml oder render.compileMjml; siehe Rendering & Export.

Mit installiertem @templatical/types und @templatical/renderer fügt dieses vollständige TypeScript-Beispiel Deutsch hinzu, schreibt Text und Richtung, prüft die Abdeckung und rendert deutsches MJML:

ts
import {
  applyOperations,
  createDefaultTemplateContent,
  createTitleBlock,
  localeCoverage,
  resolveLocale,
  sourceHash,
  type TemplateContent,
} from "@templatical/types";
import { renderToMjml } from "@templatical/renderer";

const title = createTitleBlock({ content: "Welcome" });
const original: TemplateContent = createDefaultTemplateContent("Arial", {
  locale: "en",
});
original.blocks.push(title);
const content = applyOperations(original, [
  { op: "addLocale", locale: "de" },
  {
    op: "updateBlockLocale",
    blockId: title.id,
    locale: "de",
    text: {
      content: { value: "Willkommen", sourceHash: sourceHash(title.content) },
    },
  },
  { op: "updateSettingsLocale", locale: "de", set: { direction: "ltr" } },
]);
const german = resolveLocale(content, "de");
const coverage = localeCoverage(content, "de");
if (coverage.missing.length > 0 || coverage.stale.length > 0) {
  throw new Error(
    `German needs translating: ${[...coverage.missing, ...coverage.stale].join(", ")}`,
  );
}
const mjml = await renderToMjml(content, { locale: "de" });
console.log(german.settings.locale, mjml);

resolveLocale(content, locale) ist die Projektion hinter jedem Rendern: Sie wendet Text, Überschreibungen und Einstellungen an, entfernt ausgeblendete Blöcke, entfernt die Lokalisierungsfelder und setzt settings.locale auf die projizierte Sprache. Fehlende Werte bleiben beim Standard. Speichern Sie den Originalinhalt mit allen Versionen, nicht die Projektion. Die Vorschau zeigt die aktive Sprache mit dem zusammengesetzten Layout, bevor resolvePreview läuft, und ein Sprachwechsel verwirft das Ergebnis der vorherigen Sprache.

Ein benutzerdefinierter Block mit einem text- oder set-Eintrag in der angefragten Sprache verliert in der Projektion sein zwischengespeichertes renderedHtml der Standardsprache und rendert daher über renderCustomBlock; ohne Callback wird er ausgelassen, statt in der Standardsprache zu erscheinen. Der Export des Editors rendert ihn in der angefragten Sprache. Siehe Rendering benutzerdefinierter Blöcke.

Datenstruktur ​

settings.locale ist die Standardsprache. settings.locales?: string[] enthält die anderen Versionen, ohne Duplikate oder Standardsprache. Jeder Schlüssel in block.l10n und settings.l10n muss in dieser Liste stehen.

MitgliedVertrag
block.l10n[locale].textRecord<string, LocalizedText>; jeder Wert ist { value: string; sourceHash: string }
block.l10n[locale].setRecord<string, unknown> für URLs, Bilder, Stile und andere Überschreibungen; null bedeutet, dass die Eigenschaft in dieser Sprache fehlt
block.l10n[locale].hiddentrue lässt den Block und seine Nachfahren in dieser Sprache aus
settings.l10n[locale]SettingsLocaleLayer: optionale text (preheaderText) und set (etwa direction, Schriften und Farben), ohne hidden

Sprach-Tags müssen ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$ entsprechen: 2–3 ASCII-Buchstaben, gefolgt von beliebig vielen mit Bindestrich getrennten Subtags aus 2–8 ASCII-Buchstaben oder Ziffern. Formen mit x-… oder i-… am Anfang werden abgelehnt, weil ihr Sprachsegment nur einen Buchstaben hat; historische Formen wie no-bok und zh-min-nan entsprechen dem Muster und werden akzeptiert.

Ein Feldpfad verbindet Eigenschaftsnamen mit .; bei Arrays ist das nächste Segment die id des Elements, niemals sein Index: items.nav-shop.text, rows.row-one.cells.cell-one.content, fieldValues.heading, styles.padding. Eine Feld-ID ist <blockId>/<path> oder settings/<path>. IDs von Menüeinträgen, Social-Icons, Tabellenzeilen und Tabellenzellen dürfen weder . noch / enthalten: validateTemplate meldet sie, und addBlock, updateBlock und setContent lehnen eine Operation, die eine solche ID hinzufügt (einen neuen Block, Eintrag, eine neue Zeile oder Zelle), mit invalid_operation ab. Eine ID, die schon im Template steht, bleibt bearbeitbar, sodass ein damit gespeichertes Template weiter bearbeitet werden kann; benennen Sie sie um, um das Element zu lokalisieren und die Validierung zu bestehen.

text enthält nur lesbare Felder: die TRANSLATABLE_FIELDS jedes Blocktyps (Inhalt, Alt-Text, Button-Text, Countdown-Beschriftungen, Menüeintragstext, Tabellenzelleninhalt), den Preheader sowie benutzerdefinierte text- und textarea-Felder. Alles andere, etwa URLs, Bildquellen, Größen und Farben, ist eine set-Überschreibung. Ein Pfad steht höchstens in text oder set. set kann columns einer Sektion und vollständige integrierte Listen (items, rows, rows.<id>.cells, icons) nicht ersetzen; überschreiben Sie stattdessen die Eigenschaften einzelner Elemente. Leere Ebenen und Felder werden weggelassen, nie als {} oder [] gespeichert. Typen enthält die Deklarationen und Pfadregeln.

Layouts ​

Eine init({ layout })-Hülle wird um das Template herum zusammengesetzt, außerhalb davon, ihr Text gehört also nicht zum Template. Ein Layout kann eigene settings.locales, Block-l10n und settings.l10n tragen, auch für die Wrapper-Kinder neben dem Slot, unabhängig von den Sprachen des Templates. Vorschau, toMjml(), toHtml(), die Test-E-Mail-Vorschau und renderToMjml(content, { layout, locale }) projizieren es vor dem Zusammensetzen mit resolveLocale(layout, locale, { missing: "default" }); fehlt dem Layout die Sprache, erscheint seine Standardsprache.

Eine Sprache kann den Slot des Layouts und einen Wrapper, der ihn enthält, nicht ausblenden, weil das Layout diese Sprache sonst nicht zusammensetzen könnte: updateBlockLocale lehnt hidden: true auf ihnen mit invalid_operation ab, moveBlock und addBlock lehnen es ab, den Slot unter einen Block zu setzen, den eine Sprache ausblendet, und validateTemplate meldet eine solche Ebene. Jeder andere Layout-Block lässt sich wie in einem Template ausblenden.

Der Editor bearbeitet das Layout nie. Seine Blöcke haben weder Auswahl noch Lint- oder Statusabzeichen, Übersetzen sendet sie nicht, und die Sprachzahlen enthalten sie nicht. Übersetzen Sie stattdessen die Layout-Datei, zum Beispiel mit der CLI:

bash
npx -y @templatical/template-tools edit layout.json --op '{"op":"addLocale","locale":"de"}' --json
npx -y @templatical/template-tools strings export layout.json --locale de --out layout.de.json
npx -y @templatical/template-tools strings import layout.json layout.de.json --json
npx -y @templatical/template-tools validate layout.json --locale de --json

Setzen Sie zwischen Export und Import den text jedes Eintrags in layout.de.json, wie unter Übersetzen.

Standardsprache ​

settings.locale ist die Standardsprache: der Text, auf den jede Version zurückfällt und aus dem sie übersetzt wird. Um eine Version zu übernehmen, öffnen Sie Sprachen, wählen bei der Version Weitere Aktionen für {language} und dann Als Standardsprache festlegen. Die Bestätigung {language} als Standardsprache festlegen? nennt, falls vorhanden, die Felder ohne Text in dieser Sprache: „{count} Felder haben keinen Text in {language} und behalten ihren Text in {default}. Übersetzen Sie sie zuerst. {default} behält eine Kopie; spätere Änderungen in {language} markieren sie als veraltet.“ Als Standard festlegen wendet die Änderung als ein Rückgängig-Schritt an, wechselt die Ansicht zum neuen Standard und meldet „{language} ist jetzt die Standardsprache.“; Rückgängig entfernt diese Meldung. Der Eintrag ist deaktiviert, mit dem Grund darunter, solange der aktuelle Standard nicht in localization.locales steht („Füge {default} zuerst zu den aktivierten Sprachen hinzu“), weil der alte Standard sonst eine Version würde, die niemand bearbeiten oder zurück zum Standard machen kann; und solange Blöcke in dieser Sprache ausgeblendet sind („Zeige zuerst die {count} in {language} ausgeblendeten Blöcke“). In einem schreibgeschützten Editor oder für eine nicht aktivierte Version wird er nicht angeboten.

Die Änderung ist eine Operation, { "op": "setDefaultLocale", "locale": "de" }:

  • Der deutsche Text und die deutschen Überschreibungen werden zur Basis. Englisch übernimmt den Platz von Deutsch in settings.locales, mit seinen abweichenden Werten in einer englischen Ebene; Text, den Deutsch gleich übersetzt hatte, bleibt eine aktuelle englische Übersetzung.
  • Vor der Änderung fällt ein Feld ohne deutschen Text auf Englisch zurück, sodass die deutsche E-Mail dort Englisch zeigt. defaultLocaleGaps(content, "de", definitions?) listet diese Feld-IDs vor der Änderung. Danach behält die deutsche Basis diesen englischen Text, und Englisch behält für diese Felder eine eigene aktuelle Kopie: eine spätere deutsche Bearbeitung markiert Englisch als veraltet und lässt die englische E-Mail unverändert. Nach der Änderung erscheint kein Hinweis mehr, dass die deutsche Basis noch englischen Text enthält. Übersetzen Sie die Lücken zuerst ins Deutsche, mit Übersetzen oder strings export und strings import, bis defaultLocaleGaps(content, "de") [] liefert. Ein leerer Standardwert, etwa das alt: "" eines dekorativen Bildes, hat keinen Text zu bewahren und ist keine Lücke. Übersetzen und strings export senden keine HTML-Blöcke: Übersetzen Sie HTML-Blöcke von Hand in der deutschen Ansicht.
  • Andere Sprachen behalten ihren Zustand gegenüber der neuen Basis: Aktueller Text wird neu gestempelt und bleibt aktuell; veralteter Text behält seinen gespeicherten Hash und bleibt veraltet, außer dieser Hash entspricht dem übernommenen deutschen Wert, dann wird er aktuell.
  • Eine Version mit ausgeblendeten Blöcken wird mit invalid_operation abgelehnt; localeHiddenBlocks(content, "de") listet die zuerst einzublendenden Blöcke. Eine unbekannte Version wird ebenfalls mit invalid_operation abgelehnt, und mit Zusammenarbeit lehnt jede Sperre einer anderen Sitzung die Änderung mit locked_by_others ab.

updateSettings kann settings.locale nicht ändern, solange Versionen bestehen, und KI in einer Sprachansicht kann setDefaultLocale nicht ausführen. Operationen beschreibt die neu gestempelten Hashes, entfernte optionale Felder, die Umkehrung und die Ablehnungsmeldungen. Prüfen Sie das Ergebnis: Die gespeicherte Datei hat settings.locale "de" und "en" in settings.locales, template-tools validate template.json --json meldet valid: true, und validate template.json --locale en --json listet diese Felder nicht mehr als localization.untranslated: Sowohl Lücken als auch Text, den die deutsche Version gleich übersetzt hat, behalten aktuelle englische Einträge.

Für Lücken benutzerdefinierter Blöcke übergeben Sie Definitionen an createSetDefaultLocaleOperation(content, "de", definitions) und wenden die zurückgegebene Operation an. Ihr customTextFields erfasst die deklarierten text-/textarea-Pfade für deterministische Zusammenarbeit. Der Editor erfasst seine registrierten Definitionen automatisch; CLI-Aufrufer übergeben edit --definitions definitions.json. Eine Operation ohne diese Metadaten erkennt nur benutzerdefinierte Pfade, die bereits einen Texteintrag haben.

Kommentare ​

Kommentar-Threads werden zwischen den Sprachen geteilt. Jeder neue Kommentar und jede Antwort sendet CommentInput.locale, die Sprache der Ansicht, in der er geschrieben wurde, einschließlich des Tags der Standardsprache; Ihr Kommentar-Provider speichert ihn und liefert Comment.locale zurück. Ein Kommentar ohne Tag, gespeichert vor den Sprachversionen oder von einem Kommentar-Backend, das locale nicht speichert, zählt zur aktuellen Standardsprache des Templates und folgt einem Wechsel der Standardsprache; ein Kommentar mit Tag behält sein Tag. Mit konfigurierter Lokalisierung zeigt jeder Kommentar und jede Antwort ein Sprachabzeichen, und Diese Sprache im Kommentarbereich zeigt die Threads, deren Hauptkommentar oder eine Antwort in der aktiven Sprache ist, mit allen Nachrichten und Abzeichen. Der Filter ist Ansichtsstatus und wird nicht gespeichert.

Betreffzeilen ​

Ein Template hat kein Betreff-Feld: Die Betreffzeile gehört dem Host, der die E-Mail versendet, daher enthalten Übersetzen und strings export sie nicht. Übersetzen Sie sie mit demselben TranslationProvider als ein text-Feld: Maskieren Sie ihre Merge-Tags und geschützten Begriffe mit maskTags, senden Sie sie und stellen Sie die Antwort mit unmaskTags wieder her, das null liefert, wenn ein Platzhalter fehlt oder geändert wurde. Dieses vollständige TypeScript-Beispiel benötigt @templatical/types und @templatical/ai-kit; ersetzen Sie den Mock durch den Provider, den Sie init({ translation }) übergeben:

ts
import { maskTags, SYNTAX_PRESETS, unmaskTags } from "@templatical/types";
import type { TranslationProvider } from "@templatical/types";
import { createMockTranslationProvider } from "@templatical/ai-kit/testing";

// Replace the mock with the provider you pass to init({ translation }).
const translation: TranslationProvider = createMockTranslationProvider({
  translations: {
    de: { subject: '<x id="1"/>, Ihre <x id="2"/>-Bestellung ist unterwegs' },
  },
});

const subject = "{{first_name}}, your Sable order is on its way";
const masked = maskTags(subject, SYNTAX_PRESETS.liquid, "text", {
  doNotTranslate: ["Sable"],
});
const result = await translation.translate(
  {
    templateId: null,
    sourceLocale: "en",
    targetLocale: "de",
    fields: [{ id: "subject", text: masked.text, format: "text" }],
  },
  new AbortController().signal,
);
const returned = result.fields.find((field) => field.id === "subject");
const germanSubject = returned
  ? unmaskTags(returned.text, masked.tokens)
  : null;
if (germanSubject === null)
  throw new Error("The translated subject lost or changed a placeholder.");
console.log(germanSubject); // {{first_name}}, Ihre Sable-Bestellung ist unterwegs

Übergeben Sie die Merge-Tag-Syntax und die doNotTranslate-Liste Ihres Editors; das Beispiel verwendet SYNTAX_PRESETS.liquid. Es gibt {{first_name}}, Ihre Sable-Bestellung ist unterwegs aus. Die Feld-id wählen Sie selbst; der Provider gibt sie unverändert zurück. Speichern Sie den Betreff jeder Sprache in Ihren eigenen Daten beim Template.

Operationen und Zusammenarbeit ​

OperationWirkung
addLocaleFügt eine Version zu settings.locales hinzu
removeLocaleEntfernt die Version und alle zugehörigen Block- und Einstellungsebenen
setDefaultLocaleMacht eine Version zum Standard; siehe Standardsprache
updateBlockLocaleSchreibt text, set, unset-Pfade und hidden eines Blocks für eine Sprache
updateSettingsLocaleSchreibt text, set und unset der Einstellungen für eine Sprache

Wenden Sie sie mit applyOperations an und kehren Sie sie mit invertOperations aus @templatical/types um, führen Sie sie mit template-tools edit aus oder senden Sie sie als Kollaborations-Batch. Nur diese Operationen ändern Lokalisierungsfelder: updateBlock und updateSettings lehnen sie ab, addBlock darf das l10n eines neuen Blocks enthalten, und setContent ersetzt alles. Eine fehlgeschlagene expect-Prüfung ergibt precondition_failed; ein ungültiger Pfad, eine ungültige Sprache oder ein nicht übersetzbares Textfeld ergibt invalid_operation. Operationen beschreibt die exakten Formen, die Reihenfolge der Updates und die Rückgängig-Prüfungen.

BlockLock.locale?: string | null und OperationLock.locale?: string | null verwenden null oder einen fehlenden Wert für Standardsprache und Struktur. Eine lokalisierte Bearbeitung an (Block X, de) kollidiert mit einer Sperre einer anderen Sitzung auf (X, de) oder (X, null); eine Sperre auf (X, fr) kann parallel bestehen. updateBlock, moveBlock und deleteBlock auf X kollidieren mit jeder Sperre einer anderen Sitzung auf X (Verschieben und Löschen auch mit Sperren in X), addBlock prüft keine Sperren, und setDefaultLocale kollidiert mit jeder Sperre. onRemoteChange.locale ist die Sprache der ersten Locale-Operation im Batch, ausgenommen setDefaultLocale, sonst null; ein setDefaultLocale-Batch meldet null, da er die Standardsprache des Templates ändert. Exakte Verträge stehen unter Zusammenarbeit.

Kompatibilität ​

Bestehende einsprachige Templates brauchen keine Migration: Sie laden, bearbeiten, speichern und rendern wie bisher. Ohne localization gibt es keine Sprachen-UI, und vorhandene Ebenen bleiben bei Bearbeitungen und Speichern in der Standardsprache erhalten. Der Editor schreibt Lokalisierungsfelder erst, wenn ein Autor eine Sprache hinzufügt.

Alle @templatical/*-Pakete erscheinen gemeinsam unter einer Versionsnummer. Halten Sie sie auf derselben Version: Ein Renderer oder Validator aus der Zeit vor den Sprachversionen kennt l10n nicht; ein älterer Renderer rendert die Standardsprache, ein älterer Validator lehnt das Template ab.

Ein selbst gebautes Kollaborationsbackend muss die Locale-Operationen aus der Operations-Referenz und die Sperr-locale umsetzen; siehe Zusammenarbeit. Bis dahin lehnt es sie als invalid_operation ab, und ein Backend, das die Sperr-locale ignoriert, serialisiert Bearbeitungen pro Block. Ein selbst gebautes Kommentar-Backend speichert locale; siehe Kommentare.

Grenzen ​

  • Die Spaltenstruktur ist für alle Sprachen gemeinsam und lässt sich nicht pro Sprache überschreiben. Die Spaltenreihenfolge folgt der Schreibrichtung jeder Sprache, sodass eine Rechts-nach-links-Version die erste Spalte rechts zeigt, aber explizite linke und rechte Textausrichtung sowie linke/rechte Innenabstände werden nicht gespiegelt: Überschreiben Sie sie pro Sprache.
  • Text in wiederholbaren Elementen eines benutzerdefinierten Blocks lässt sich nicht feldweise übersetzen, weil die Elemente keine IDs haben. Überschreiben Sie stattdessen das gesamte fieldValues.<key> pro Sprache.
  • Übersetzen lässt HTML-Blöcke aus. Übersetzen Sie sie von Hand in der Sprachansicht.
  • Von der KI in einer Sprachansicht hinzugefügte Blöcke gelangen mit ihrem Text als Standardinhalt in den gemeinsamen Baum, wie beim Einfügen. Die KI kann in einer Sprachansicht nicht das gesamte Template ersetzen; wechseln Sie dafür zur Standardsprache.
  • Kommentar-Threads werden zwischen den Sprachen geteilt. Jeder neue Kommentar und jede Antwort speichert über Ihren Kommentar-Provider den Sprach-Tag der Ansicht. Der Versionsverlauf gilt weiterhin pro Template, nicht pro Sprache.
  • Strings werden als JSON ausgetauscht; XLIFF und CSV werden nicht unterstützt.

Ergebnis prüfen ​

Für eine gespeicherte template.json prüfen und rendern Sie eine Sprache:

bash
npx -y @templatical/template-tools validate template.json --locale de --json
npx -y @templatical/template-tools render template.json --locale de --format mjml --out template.de.mjml

validate --locale de prüft die Projektion jeder deklarierten Sprache gegen die normalen Schemas und nennt bei Projektionsfehlern die Sprache; danach lintet es die deutsche Projektion einschließlich localization.untranslated, localization.stale und localization.merge-tags-differ. Lesen Sie die Befunde auch bei Exit 0: Fehlende und veraltete Übersetzungen sind Warnungen. Lesen Sie nach strings import zuerst dessen rejected-Liste.

Exitvalidate und renderstrings import
0Erfolg; Warnungen wie localization.untranslated und localization.stale können bleibenKein String wurde abgelehnt (Einträge ohne text werden übersprungen)
1Ungültige Struktur, eine unbekannte Locale oder bei validate ein Lint-Befund mit Schweregrad Fehler oder localization.merge-tags-differ (auch bei valid: true)Ein String wurde abgelehnt (die angenommenen Strings werden geschrieben), das Template ist ungültig oder targetLocale ist keine seiner Versionen (nichts wird geschrieben)
2Fehlbedienung oder Toolfehler: ein fehlender oder leerer --locale-Wert, eine ungültige --definitions-Datei oder bei validate eine Ausnahme im LinterFehlbedienung: fehlende Argumente oder eine unlesbare oder ungültige Strings-, Definitions- oder Begriffsdatei
3Eine fehlende optionale Abhängigkeit: mjml für render --format html und validate --htmlNicht verwendet

Eine unbekannte Locale ergibt Exit 1 und schreibt unknown_locale auf stderr ohne Ergebnisdokument, auch mit --json. Ein fehlender oder leerer --locale-Wert ergibt Exit 2. valid: true allein bedeutet nicht Exit 0. Siehe Template-Tools.

Übergeben Sie für Übersetzungen benutzerdefinierter Blöcke --definitions definitions.json, ein JSON-Array von CustomBlockDefinition, an validate, render und beide strings-Befehle; Rendering verwendet das Liquid-Template und CSS jedes Typs. Für den Bibliotheksweg verwenden Sie validateTemplate(content, definitions), danach lintTemplate(content, { customBlocks: definitions, contentLocale: "de" }) und renderCustomBlock zum Rendern. Dieses vollständige TypeScript-Beispiel benötigt @templatical/types, @templatical/template-tools, @templatical/quality, @templatical/renderer und liquidjs:

ts
import {
  createCustomBlock,
  createDefaultTemplateContent,
  sourceHash,
  type CustomBlockDefinition,
} from "@templatical/types";
import { validateTemplate } from "@templatical/template-tools";
import { lintTemplate } from "@templatical/quality";
import { renderToMjml } from "@templatical/renderer";
import { Liquid } from "liquidjs";

const definitions: CustomBlockDefinition[] = [
  {
    type: "promo",
    name: "Promo",
    template: "<p>{{ headline }}</p>",
    fields: [{ key: "headline", type: "text", label: "Headline" }],
  },
];
const custom = createCustomBlock(definitions[0]);
custom.fieldValues.headline = "Welcome";
custom.renderedHtml = "<p>Welcome</p>";
custom.l10n = {
  de: {
    text: {
      "fieldValues.headline": {
        value: "Willkommen",
        sourceHash: sourceHash("Welcome"),
      },
    },
  },
};
const content = createDefaultTemplateContent("Arial", { locale: "en" });
content.settings.locales = ["de"];
content.blocks = [custom];

const validation = validateTemplate(content, definitions);
if (!validation.valid) throw new Error(validation.errors.join("\n"));
const issues = lintTemplate(content, {
  customBlocks: definitions,
  contentLocale: "de",
});
const localizationIssues = issues.filter((issue) =>
  issue.ruleId.startsWith("localization."),
);
if (localizationIssues.length > 0) {
  throw new Error(localizationIssues.map((issue) => issue.message).join("\n"));
}
const engine = new Liquid();
const mjml = await renderToMjml(content, {
  locale: "de",
  async renderCustomBlock(block) {
    const definition = definitions.find(
      (entry) => entry.type === block.customType,
    );
    if (!definition)
      throw new Error(`Unknown custom block: ${block.customType}`);
    return engine.parseAndRender(definition.template, block.fieldValues);
  },
});
console.log(mjml);

Prüfen Sie validation.valid, lesen Sie issues und bestätigen Sie, dass das MJML Willkommen statt des zwischengespeicherten Welcome enthält. Siehe Rendering benutzerdefinierter Blöcke und die CLI-Bibliotheks-API. Prüfen Sie ein Übersetzungs-Backend mit runTranslationConformance wie im Übersetzungsvertrag beschrieben.

Im Playground ​

Das Sprachen-Setup öffnet ein Template mit deutschem Text, einem anderen Hero-Bild, einem auf Deutsch ausgeblendeten Block, einem veralteten deutschen Wert, einem Layout-Footer pro Sprache und einem aufgezeichneten Übersetzungs-Provider, mit Sable auf der Liste der nicht zu übersetzenden Begriffe. Wechseln Sie zu Deutsch und nutzen Sie {count} Felder übersetzen oder Erneut übersetzen am veralteten Block; bearbeiten Sie deutschen Text und kehren Sie zu Englisch zurück, um zu prüfen, dass er unverändert bleibt. Wählen Sie Beibehalten, um ein Veraltet-Abzeichen zu entfernen, fügen Sie Französisch hinzu oder wählen Sie Arabisch für Inhalte von rechts nach links. Die Varianten zeigen ein geändertes Tag (?translation=tags-changed), Teilergebnisse beim Abbrechen (?translation=cancel), Kollaborationssperren (?collaboration=1&user=Ada) und 300 Blöcke in 20 Sprachen (?scale=1).