diff --git a/src/components/PairReviewModal.jsx b/src/components/PairReviewModal.jsx
index 778979b..89e90e4 100644
--- a/src/components/PairReviewModal.jsx
+++ b/src/components/PairReviewModal.jsx
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { apiPost, apiPatch } from '../lib/api';
import { buildRows } from '../lib/pairRows';
+import PlaceholderText from './PlaceholderText';
const LANGS = [
{ code: 'de', flag: '🇩🇪' },
@@ -130,7 +131,7 @@ function Row({ row }) {
const val = row.cell(l.code);
return (
- {val || 'fehlt'}
+ {val ?
: 'fehlt'}
);
})}
diff --git a/src/components/PlaceholderText.jsx b/src/components/PlaceholderText.jsx
new file mode 100644
index 0000000..64c0390
--- /dev/null
+++ b/src/components/PlaceholderText.jsx
@@ -0,0 +1,29 @@
+import { parsePlaceholderSegments } from '../lib/pairRows';
+
+// Rendert Satztext mit farbig markierten Placeholdern:
+// {{label.o:id}} → indigo (Objekt-Verknüpfung, passend zu den 🔗-Buttons)
+// {{label.w:id}} → emerald (Wort-Verknüpfung)
+// ⟦PHn:label⟧ → rot (geleaktes Übersetzungs-Token, Datenfehler)
+const KIND_STYLES = {
+ object: { className: 'bg-indigo-100 text-indigo-800 rounded px-0.5', title: 'Objekt-Placeholder' },
+ word: { className: 'bg-emerald-100 text-emerald-800 rounded px-0.5', title: 'Wort-Placeholder' },
+ broken: { className: 'bg-red-100 text-red-700 rounded px-0.5 line-through', title: 'Kaputtes Übersetzungs-Token — bitte reparieren' },
+};
+
+export default function PlaceholderText({ text }) {
+ const segs = parsePlaceholderSegments(text);
+ return (
+ <>
+ {segs.map((s, i) => {
+ const style = s.kind && KIND_STYLES[s.kind];
+ if (!style) return {s.text};
+ return (
+
+ {s.text}
+
+ );
+ })}
+ >
+ );
+}
diff --git a/src/lib/pairRows.js b/src/lib/pairRows.js
index afba87d..0c1eb27 100644
--- a/src/lib/pairRows.js
+++ b/src/lib/pairRows.js
@@ -10,6 +10,26 @@ export function strip(text) {
});
}
+// Zerlegt einen Satz in Segmente für die farbige Placeholder-Anzeige:
+// { text, kind: 'object' | 'word' | 'broken' | null, id? }
+// 'broken' = geleakte ⟦PHn:label⟧-Tokens aus der Übersetzung (sollten nicht vorkommen).
+const SEGMENT_RE = /\{\{([^.{}]+)\.(w|o):([0-9a-f-]{36})\}\}|⟦PH\d+:([^⟧]*)⟧/g;
+
+export function parsePlaceholderSegments(text) {
+ if (!text) return [];
+ const str = String(text);
+ const segs = [];
+ let last = 0;
+ for (const m of str.matchAll(SEGMENT_RE)) {
+ if (m.index > last) segs.push({ text: str.slice(last, m.index), kind: null });
+ if (m[1] !== undefined) segs.push({ text: m[1], kind: m[2] === 'o' ? 'object' : 'word', id: m[3] });
+ else segs.push({ text: m[4], kind: 'broken' });
+ last = m.index + m[0].length;
+ }
+ if (last < str.length) segs.push({ text: str.slice(last), kind: null });
+ return segs;
+}
+
// Baut die Anzeigezeilen je nach answer_type. Jede Zeile ist entweder
// - { kind: 'lang', label, color, cell(l) } → eine Zelle pro Sprache (übersetzbar)
// - { kind: 'single', label, color, value } → ein einzelner Wert (nicht sprachabhängig)
@@ -19,13 +39,14 @@ export function buildRows(content) {
const rows = [];
const wordsCell = (stmt) => (l) =>
(stmt?.words || []).map(w => w[`titel_${l}`] || '—').join(', ');
+ // Roher Text inkl. {{…}}-Placeholder — die Anzeige läuft über .
const sentenceCell = (stmt, prefix) => (l) =>
- strip(stmt?.sentence?.[`${prefix}_${l}`] || '');
+ stmt?.sentence?.[`${prefix}_${l}`] || '';
// Frage (yes_no / question / word)
if (content.question) {
rows.push({ kind: 'lang', label: 'Frage', color: 'text-slate-700',
- cell: l => strip(content.question[`sentence_${l}`] || '') });
+ cell: l => content.question[`sentence_${l}`] || '' });
}
if (type === 'yes_no') {
diff --git a/src/pages/Publish.jsx b/src/pages/Publish.jsx
index 215c29d..4442aed 100644
--- a/src/pages/Publish.jsx
+++ b/src/pages/Publish.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState, useRef, useCallback } from 'react';
import Layout from '../components/Layout';
import { apiFetch, apiPost } from '../lib/api';
import { buildRows } from '../lib/pairRows';
+import PlaceholderText from '../components/PlaceholderText';
const LANGS = [
{ code: 'de', flag: '🇩🇪' },
@@ -124,7 +125,7 @@ function PairRow({ pair, flagged, onToggleFlag, objIndexById, onAssign, assignin
const isQuestion = row.label === 'Frage';
return (
- {val ||
— {row.label} fehlt —}
+ {val ?
:
— {row.label} fehlt —}
);
})}