Das Plug-in bietet Zugriff auf die von Phrase übersetzten Inhalte innerhalb des Sanity Studios.
Es werden nur Übersetzungen auf Dokumentenebene unterstützt. Übersetzungen auf Feldebene werden nicht unterstützt.
Funktionen:
-
Echtzeit-Vorschauen
Übersetzungen werden synchron gehalten, damit Linguisten und Übersetzer Vorschauänderungen in Echtzeit sehen können.
-
Intelligente Neuübersetzungen
Das Plug-in vergleicht, welche Inhalte sich seit der letzten Übersetzung geändert haben, und sendet nur diese Änderungen an Phrase.
-
Automatische Referenzübersetzung
Bei der Ausgabe von Übersetzungen können Redakteure auch wählen, Dokumente zu übersetzen, die von der aktuellen Übersetzung referenziert werden, und das Plug-in verknüpft sie automatisch nach Zielsprache.
-
Flexible Schemata
Unabhängig von der Struktur passt sich das Plug-in daran an und stellt sicher, dass der endgültige übersetzte Inhalt den Sanity-Schemata entspricht.
-
Phrase-Workflows
Die Übersetzungs-Workflows in Phrase bleiben gleich; eine Schulung oder Neukonfiguration der Abläufe ist nicht erforderlich.
Die Installation erfolgt über die Kommandozeile.
Es wird davon ausgegangen, dass ein Website-Generator und Sanity Studio bereits konfiguriert sind. Falls nicht, verwenden Sie eine der von Sanity bereitgestellten Starter-Vorlagen.
Installation
Navigieren Sie zu dem Projekt, das die Sanity Studio-Instanz enthält, und installieren Sie das Plug-in:
npm install sanity-plugin-phrase # oder pnpm, yarn, bun
Umgebungsvariablen
Bevor das Plug-in konfiguriert wird, müssen die folgenden Umgebungsvariablen festgelegt werden. Erstellen Sie eine `.env`-Datei (oder `.env.local` für Next.js) im Projektstamm.
Die folgenden Beispiele gelten für NextJS. Für andere Frameworks konsultieren Sie deren spezifische Dokumentation und beachten Sie, dass das Präfix `NEXT_PUBLIC_` möglicherweise für öffentliche Variablen entfernt werden muss.
Wichtig
Serverseitige Variablen (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) sollten niemals dem Kunden ausgesetzt werden. In Next.js sind nur Variablen, die mit NEXT_PUBLIC_ beginnen, im Browser sichtbar.
# Basis-URL Ihrer Website (verwendet für Vorschau-Links)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# URL, an der der Backend-Handler des Plug-ins zu finden sein wird
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Phrase-Datacenter-Region ('eu' oder 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"
# Sanity-Projektkonfiguration
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
# Sanity API-Token mit Schreibberechtigungen (nur serverseitig)
SANITY_WRITE_TOKEN=""
# Phrase-Anmeldeinformationen (nur serverseitig)
# Hinweis: Die Phrase API erwartet nur den Benutzernamen, NICHT die vollständige E-Mail-Adresse
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"
Plug-in-Konfiguration
Das Plug-in wird zu sanity.config.ts mit den erforderlichen Konfigurationsoptionen hinzugefügt:
// sanity.config.ts
import { defineConfig } from 'sanity'
import {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Erforderlich: i18n-Adapter für die Internationalisierung von Dokumenten
i18nAdapter: documentInternationalizationAdapter(),
// Erforderlich: Dokumenttypen, die übersetzt werden können
translatableTypes: ['page', 'post', 'article'],
// Erforderlich: Ausgangssprache (Primärsprache)
// Dies muss mit der Sprache übereinstimmen, die in Ihrer Phrase-Projektvorlage definiert ist
sourceLang: 'en',
// Erforderlich: Zielsprache, in die Benutzer übersetzen können
// Verwenden Sie dieselben Codes wie Ihre Sanity-Dokumente
// Diese Liste muss mit den in Ihrer Phrase-Projektvorlage definierten Sprachen übereinstimmen
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// Erforderlich: Ihre Backend-API-Endpunkt-URL
apiEndpunkt: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!
// Erforderlich: Phrase-Datacenter-Region ('eu' oder 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Erforderlich: Phrase-Projektvorlagen, die den Redakteuren zur Verfügung stehen
phraseTemplates: [
{
vorlagenUid: 'YOUR_TEMPLATE_UID_HERE',
label: 'Standardübersetzungs-Vorlage',
},
],
// Erforderlich: Vorschau-URLs für Linguisten generieren
getDocumentPreview: (doc, sanityClient) => {
const publishedId = doc._id.Ersetzen('drafts.', '')
return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
},
// Optionale Einstellungen
// Maximale Tiefe für die Übersetzung referenzierter Dokumente (Standard: 3)
maxReferencesDepth: 3,
// Übersetzung von Entwurfsdokumenten erlauben (Standard: false)
translateDrafts: false,
// Benutzerdefinierte Daten-Transformatoren für spezielle Inhaltstypen
dataTransformers: [],
// Protokollierungs-Konfiguration für Debugging
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
},
// Phrase-Dashboard basierend auf Benutzerrollen ausblenden
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... Ihre bestehende Konfiguration
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... andere Plugins
],
})// sanity.config.(js|ts)
import {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* Der i18n-Adapter, der für dieses Plug-in verwendet werden soll.
* Er wird dafür verantwortlich sein, Dokumente für jede Zielsprache abzurufen und zu ändern.
*
* Siehe unten für weitere Informationen zu Adaptern.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Sanity-Schema-Typen, die das Plug-in übersetzen kann
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* Sprachcode aller Sprachen, in die Benutzer übersetzen können.
* Sollte derselbe sein wie der, der in Ihren Sanity-Dokumenten gespeichert ist und von Ihrem Front-End verwendet wird. Das Plug-in wird es automatisch in das Format von Phrase übersetzen.
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* Sprachcode der Ausgangssprache, die übersetzt werden soll.
* Sollte derselbe sein wie der, der in Ihren Sanity-Dokumenten gespeichert ist und von Ihrem Front-End verwendet wird. Das Plug-in wird es automatisch in das Format von Phrase übersetzen.
*/
sourceLang: 'en',
/**
* Wie in den Einstellungen Ihres Phrase-Kontos definiert
* Entweder `eu` oder `us`
*/
phraseRegion: 'us|eu',
/**
* Die URL zu Ihrer konfigurierten Plug-in-Backend-API.
*
* **Hinweis:** Befolgen Sie die Schritte zur Einrichtung des Endpunkts, die unten aufgeführt sind
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* Wird verwendet, um Linguisten vom Phrase-Dashboard zur Front-End-Vorschau ihrer Übersetzungen umzuleiten.
*/
getDocumentPreview: async (doc, sanityClient) => {
const publishedId = doc._id.Ersetzen('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* Phrase-Projektvorlagen, die Ihre Redakteure bei der Anforderung von Übersetzungen verwenden können.
*
* **Hinweis:** Befolgen Sie die Schritte zur Einrichtung von Vorlagen, die unten aufgeführt sind
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
Label: '[Sanity.io] Standardvorlage',
},
],
/**
* @optional
* Falls Sie das Phrase-Dashboard je nach Benutzerrechten anzeigen oder ausblenden möchten.
*
* Erhält einen Kontext mit dem aktuellen Benutzer und Dokument und muss einen booleschen Wert zurückgeben.
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
plugins: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
Schema-Injektion
Um dem Plug-in mitzuteilen, welche Dokumenttypen übersetzt werden können, übergeben Sie ein Array von Dokumenttypen an die injectPhraseIntoSchema-Funktion in der sanity.config.ts-Datei:
// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'
// Liste der übersetzbaren Schema-Typen. Normalerweise aus einer Indexdatei exportiert
// wo auch immer sich Ihr Sanity-Schema befindet
const TRANSLATABLE_SCHEMAS = ['Seite', 'Beitrag', 'Kurs', 'Lektion', 'Definition']
export default defineConfig({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id))
},
plugins: [
// ...
phrasePlugin({
// Ihre Konfigurationsoptionen hier
}),
],
})
Ausschluss von PTDs aus Dokumentlisten
PTDs (Phrase-Übersetzungsdokumente) sind temporäre Dokumente, die nicht in normalen Dokumentlisten innerhalb von Sanity Studio erscheinen sollten. Die NOT_PTD-Konstante bietet einen GROQ-Filter für diesen Zweck:
// sanity.config.ts
importieren { NOT_PTD } von 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... andere Konfiguration
plugins: [
structureTool({
struktur: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == "post" && ${NOT_PTD}`),
),
// ... andere Elemente
]),
}),
],
})
Das Übersetzungsmenü von PTDs ausblenden
Beim Verwenden des Dokument-Internationalisierungs-Plug-ins sollte das Übersetzungsmenü von PTDs ausgeblendet werden. Die isPtdId Utility identifiziert PTD-Dokumente:
import { isPtdId } from 'sanity-plugin-phrase/utils'
importieren { DocumentInternationalizationMenu } von '@sanity/document-internationalization'
// Verwenden Sie dasselbe Array wie translatableTypes aus Ihrer PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['Seite', 'Beitrag', 'Artikel']
export default defineConfig({
Dokument: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Zeigen Sie das Übersetzungsmenü nur für echte Dokumente an, nicht für PTDs
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
Das Plug-in hat keine Konfiguration in der Phrase-Benutzeroberfläche, aber Phrase muss konfiguriert werden, um Webhook-Benachrichtigungen an den Backend-API-Endpunkt zu senden. Dies ermöglicht Echtzeit-Updates, während die Übersetzungen fortschreiten.
Erstellen Sie einen Webhook
Erstellen Sie einen Webhook mit diesen Einstellungen:
-
URL
Die URL zum API-Endpunkt des Plug-ins, wie im Option konfiguriert.
-
Veranstaltungen:
-
Jobs
-
Job gelöscht
-
Job zugewiesen
-
Fälligkeitsdatum des Jobs geändert
-
Job-Ziel aktualisiert
-
-
Projekte
-
Projekt gelöscht
-
Fälligkeitsdatum des Projekts geändert
-
-
Andere
-
Vorübersetzung abgeschlossen
-
-
Dies stellt sicher, dass das Plug-in über Änderungen an Phrase-Projekten informiert wird und die Sanity-Daten synchron halten kann.
Projekte-Vorlage(n) einrichten
Konfigurieren Sie die Phrase Projektvorlage(n) mit den für Workflows und Teamanforderungen erforderlichen Eigenschaften. Eines oder mehrere Vorlagen können angeboten werden, aus denen bei der Bestellung einer neuen Übersetzung gewählt werden kann. Die Projektvorlagen für Phrasen müssen spezifische JSON-Importeinstellungen haben, damit das Plug-in korrekt funktioniert. Diese Einstellungen steuern, welche Felder an Übersetzer gesendet werden und welche als Metadaten erhalten bleiben.
JSON-Datei-Import
Verwenden Sie Regex, um bestimmte Keys auszuschließen:
(^|.*\/) (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|IHRE_IGNORIERTEN_KEYS_HIER| (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|IHRE_IGNORIERTEN_KEYS_HIER) /.*)
Dieser Ausdruck enthält absichtlich duplizierte Keys, um sicherzustellen, dass sie vom RegEx-Parser von Phrase ignoriert werden. Stellen Sie sicher, dass sie korrekt dupliziert sind.
-
Schließen Sie lokalisierungsspezifische Daten aus, wie die Sprache eines bestimmten Dokuments, wenn Sie
@sanity/document-internationalizationverwenden. -
Fügen Sie alle projektspezifischen Keys hinzu, die keine Übersetzung erfordern, wie einen Slug für Inhalte, die denselben Pfad in allen Sprachen verwenden. Ersetzen Sie
IHRE_IGNORIERTEN_KEYS_HIERdurch eine durch Pipes getrennte Liste von Keys, die ignoriert werden sollen. -
Context note:
/_sanityContext
Ausgangssprache
Derzeit geht dieses Plug-in davon aus, dass es eine einzige Ausgangssprache gibt. Die Projektvorlage(n) müssen die gleiche Quelle wie die im Plug-in konfigurierte sourceLanguage haben.
Zielsprache(n)
Stellen Sie sicher, dass die in Phrase gewählten Sprachen mit denen in der Konfiguration des Plug-ins synchronisiert sind.
Dies ist der Endpunkt, den das Plug-in verwendet, um mit dem Sanity Studio zu kommunizieren. Es wird verwendet, um sich bei der API von Phrase zu authentifizieren, Webhooks zu empfangen und Benutzeranfragen aus dem Sanity Studio zu erhalten.
Erstellen Sie einen individuellen API-Endpunkt im Sanity-Projekt, um diese Anfragen zu bearbeiten. Eine der einfachsten Möglichkeiten, dies zu tun, besteht darin, serverlose Funktionen über Front-End-Frameworks wie NextJS, Remix, SvelteKit oder Nuxt zu verwenden.
Zugreifen Sie auf die Konfiguration des Handlers mit einem Request-Response-Muster über import {createRequestHandler} von backend oder verwenden Sie den internen Handler direkt über import {createInternalHandler} von . Stellen Sie sicher, dass CORS-Anfragen korrekt behandelt werden, da das Studio und der Endpunkt unterschiedliche Ursprünge haben.
Das App-Verzeichnis von NextJS wird derzeit nicht unterstützt, da es den Backend-Handler fälschlicherweise als React-Client-Komponente analysiert.
Dieses Beispiel demonstriert die Erstellung eines Routenhandlers am konfigurierten apiEndpoint Pfad bei /api/phrase unter Verwendung des Next.js Pages Routers:
// app/api/phrase/route.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } von 'next'
import { PHRASE_CONFIG } von 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } von '~/lib/sanity.api'
import { client } von '~/lib/sanity.client'
export const maxDuration = 60
export const dynamic = 'force-dynamic'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
exportiere standardmäßig asynchron die Funktion handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method?.toUpperCase() === 'OPTIONS') {
res.status(200).json({})
return
}
if (
!req.method ||
(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
) {
res.status(405).json({ error: 'Method not allowed' })
return
}
const phraseRes = await phraseHandler(
req.method.toUpperCase() === 'POST' ? req.body : req.query,
)
const resBody = await phraseRes.json().catch(() => {})
Array.from(phraseRes.headers.entries()).forEach((value: [string, any]) => {
res.setHeader(value[0], value[1])
})
res.status(phraseRes.status).json(resBody)
}// src/pages/api/phrase.ts
// Next.js API route: https://nextjs.org/docs/pages/building-your-application/routing/api-routes
import type { NextApiRequest, NextApiResponse } von 'next'
import { PHRASE_CONFIG } von 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } von '~/lib/sanity.api'
import { client } von '~/lib/sanity.client'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: client.withConfig({ token: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
exportiere standardmäßig asynchron die Funktion handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method?.toUpperCase() === 'OPTIONS') {
res.status(200).json({})
return
}
if (
!req.method ||
(req.method.toUpperCase() !== 'POST' && req.method.toUpperCase() !== 'GET')
) {
res.status(405).json({ error: 'Method not allowed' })
return
}
const phraseRes = await phraseHandler(
req.method.toUpperCase() === 'POST' ? req.body : req.query,
)
const resBody = await phraseRes.json().catch(() => {})
Array.from(phraseRes.headers.entries()).forEach((value) => {
res.setHeader(value[0], value[1])
})
res.status(phraseRes.status).json(resBody)
}
i18n-Adapter
Sanity hat keinen vorschreibenden Ansatz zur Internationalisierung, und es gibt viele Möglichkeiten, dies umzusetzen. Dieses Plug-in verwendet ein Adaptermuster, um eine Konfiguration basierend darauf zu ermöglichen, wie der Inhalt strukturiert ist und wie er übersetzt werden soll.
Derzeit ist der einzige verfügbare Adapter , der von Sanitys offiziellem document-internationalization plugin (Version ^2.0.0) verwendet wird. Reichen Sie ein Problem ein, wenn ein spezifischer Adapter erforderlich ist, oder verweisen Sie auf dieses repository's package/src/adapters/document-internationalization.ts für ein Beispiel, wie man einen angepassten implementiert.
Benutzerdefinierte Daten-Transformatoren
Wenn es erforderlich ist, Daten vor dem Senden an Phrase zu transformieren, verwenden Sie die Option. Dies ist nützlich, wenn die Struktur der Daten geändert werden muss oder wenn bestimmte Felder von der Übersetzung ausgeschlossen werden sollen.
Jeder Daten-Transformator muss die Daten vor dem Senden an Phrase kodieren und sie beim Empfang zurück dekodieren, um sie vor dem Speichern in Sanity zu transformieren. Mehrere Transformatoren können gestapelt und nacheinander ausgeführt werden.
Das Plug-in bietet keine Möglichkeit, Transformatoren isoliert zu testen, sodass die Entwicklung komplex sein kann. Speichern Sie echte Ziel-Dokumente aus dem Sanity-Datensatz in .JSON und verwenden Sie sie als Testdaten für jede der Funktionen kodieren/dekodieren.
Beispiel für die Modifizierung von JSON-kodierten VTT-Dateien zu HTML, damit Phrase den Inhalt der Untertitel besser segmentieren kann:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Überprüfen, ob das Array VTT-Untertitelknoten enthält
if (
arr.every(
(item) =>
typeof item === 'object' &&
!!item &&
'_type' in item &&
typeof item._type === 'string' &&
item._type.startsWith('vtt.'),
)
) {
return encodeSubtitles(arr as StoredSubtitleNode[])
}
return undefined // Gibt undefined zurück, um die Transformation zu überspringen
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
export const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})export const PHRASE_CONFIG = definePhraseOptions({
// ...
dataTransformers: [vttJsonTransformer],
})
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
if (
arr.every(
(item) =>
typeof item === 'object' &&
!!item &&
'_type' in item &&
typeof item._type === 'string' &&
item._type.startsWith('vtt.'),
)
) {
return encodeSubtitles(arr as StoredSubtitleNode[])
}
return undefined
},
},
decode: {
object(obj) {
if (!!obj && '_type' in obj && obj._type === 'encodedSubtitles') {
return decodeSubtitles(obj as EncodedSubtitles)
}
return undefined
},
},
}
// Implementierung wird übersprungen
// Siehe /demo-nextjs/src/utils/vttJsonTransformer.ts für den vollständigen Quellcode
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Einschränkung des Zugriffs für den Editor
Welche Editoren auf das Phrase-Dashboard zugreifen können, kann durch Implementierung der i Option eingeschränkt werden. Diese Funktion entspricht der, die an die hidden Eigenschaft eines Feldes in Sanity übergeben wird. Sie erhält einen Kontext mit dem aktuellen Benutzer und Dokument und muss einen booleschen Wert zurückgeben.
Beispiel für die Einschränkung des Zugriffs auf das Phrase-Dashboard für Benutzer mit der admin Rolle:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// Ausblenden, wenn kein Admin
return !isAdmin
},
})