Das Plug-in bietet Zugriff auf Phrase übersetzten Content aus dem Sanity Studio heraus.
Es werden nur Übersetzungen auf Dokumentebene unterstützt. Übersetzungen auf Feldebene werden nicht unterstützt.
Funktionen:
-
Echtzeit-Vorschau
Übersetzungen werden synchron gehalten, damit Linguisten und Übersetzer Vorschau-Änderungen in Echtzeit sehen können.
-
Intelligente Neuübersetzungen
Das Plug-in gleicht ab, welcher Content sich seit der letzten Übersetzung geändert hat, und sendet nur diese Änderungen an Phrase.
-
Automatische Referenzübersetzung
Beim Erstellen von Übersetzungen können Editoren wählen, auch Dokumente zu übersetzen, auf die verwiesen wird, und das Plug-in verknüpft diese automatisch nach Zielsprache.
-
Flexible Schemas
Unabhängig von der Struktur passt sich das Plug-in an und stellt sicher, dass der finale übersetzte Content den Sanity-Schemas entspricht.
-
Phrase Workflows
Die Übersetzungs-Workflows in Phrase bleiben gleich; eine Umschulung oder Neukonfiguration der Vorgänge ist nicht erforderlich.
Die Installation erfolgt über die Befehlszeile.
Es wird vorausgesetzt, dass ein Website-Generator und Sanity Studio bereits konfiguriert sind. Falls nicht, verwenden Sie eine der Starter-Vorlagen, die von Sanity bereitgestellt werden.
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 Sie das Plug-in konfigurieren, müssen die folgenden Umgebungsvariablen festlegen werden. Erstellen Sie eine `.env`-Datei (oder `.env.local` für Next.js) im Projekt-Stammverzeichnis.
Die folgenden Beispiele beziehen sich auf NextJS. Für andere Frameworks lesen Sie bitte deren spezifische Dokumentation und beachten Sie, dass das Präfix `NEXT_PUBLIC_` für öffentliche Variablen möglicherweise entfernt werden muss.
Wichtig
Server-seitige Variablen (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) sollten niemals dem Kunde gegenüber offengelegt werden. In Next.js werden nur Variablen mit dem Präfix NEXT_PUBLIC_ im Browser offengelegt.
# Basis-URL Ihrer Website (wird für Vorschau-Links verwendet)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# URL, unter der sich der Plug-in-Backend-Handler befinden wird
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Phrase-Rechenzentrumsregion ('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 Server-seitig)
SANITY_WRITE_TOKEN=""
# Phrase-Anmeldeinformationen (nur Server-seitig)
# 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 sanity.config.ts mit den erforderlichen Konfigurationsoptionen hinzugefügt:
// sanity.config.ts
importieren { defineConfig } from 'sanity'
importieren {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Erforderlich: i18n-Adapter für Dokument-Internationalisierung
i18nAdapter: documentInternationalizationAdapter(),
// Erforderlich: Dokument-Typen, die übersetzt werden können
translatableTypes: ['page', 'post', 'article'],
// Erforderlich: Ausgangssprache (Primärsprache)
// Dies muss mit der Sprache übereinstimmen, die in Ihrer Phrase-Projekt-Vorlage definiert ist
sourceLang: 'en',
// Erforderlich: Zielsprachen, in die Benutzer übersetzen können
// Verwenden Sie dieselben Codes wie für Ihre Sanity-Dokumente
// Diese Liste muss mit den Sprachen übereinstimmen, die in Ihrer Phrase-Projekt-Vorlage definiert sind
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// Erforderlich: Ihre Backend-API-Endpunkt-URL
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,"
// Erforderlich: Phrase-Rechenzentrumsregion ('eu' oder 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Erforderlich: Phrase-Projekt-Vorlagen, die für Editoren verfügbar sind
phraseTemplates: [
{
templateUid: 'YOUR_TEMPLATE_UID_HERE',
label: 'Default Translation Vorlage',
},
],
// Erforderlich: Generieren Sie Vorschau-URLs für Linguisten
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 das Übersetzen referenzierter Dokumente (Standard: 3)
maxReferencesDepth: 3,
// Übersetzung von Entwurfsdokumenten erlauben (Standard: false)
translateDrafts: false,
// benutzerdefinierte Datentransformatoren für spezielle Content-Typen
dataTransformers: [],
// Protokollierungskonfiguration für das Debugging
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | 'error' | 'fatal'
},
// Phrase Dashboard basierend auf User-Rollen Ausblenden
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... Ihre bestehende Konfiguration
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... andere Plug-ins
],
})// sanity.config.(js|ts)
importieren {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* Der i18n-Adapter, der für dieses Plug-in zu verwenden ist.
* Er ist für das Abrufen und Ändern von Dokumenten für jede Zielsprache verantwortlich.
*
* Weitere Informationen zu Adaptern finden Sie unten.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Sanity-Schematypen, die das Plug-in übersetzen kann
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* Sprachcode aller Sprachen, in die Benutzer übersetzen können.
* Sollte mit dem in Ihren Sanity-Dokumenten gespeicherten und von Ihrem Front-End verwendeten Code übereinstimmen. Das Plug-in übersetzt es automatisch in das Format von Phrase.
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* Sprachcode der Ausgangssprache, die übersetzt werden soll.
* Sollte mit dem in Ihren Sanity-Dokumenten gespeicherten und von Ihrem Front-End verwendeten Code übereinstimmen. Das Plug-in übersetzt es automatisch in das Format von Phrase.
*/
sourceLang: 'en',
/**
* Wie in den Einstellungen Ihres Phrase-User-Kontos definiert
* Entweder `eu` oder `us`
*/
phraseRegion: 'us|eu',
/**
* Die URL zu Ihrer konfigurierten Plug-in-Backend-API.
*
* **Hinweis:** Befolgen Sie die unten aufgeführten Schritte zur Einrichtung des Endpunkts.
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* Wird verwendet, um Linguisten vom Phrase-Dashboard zur Front-End-Vorschau ihrer Übersetzungen weiterzuleiten.
*/
getDocumentPreview: async (doc, sanityClient) => {
const publishedID = doc._ID.Ersetzen('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* Phrase-Projekt-Vorlagen, die Ihre Redakteure beim Anfordern von Übersetzungen verwenden können.
*
* **Hinweis:** Befolgen Sie die unten aufgeführten Schritte zum Festlegen von Vorlagen.
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
label: '[Sanity.io] Default Vorlage',
},
],
/**
* @optional
* Falls Sie das Phrase Dashboard gemäß User-Berechtigungen anzeigen oder ausblenden möchten.
*
* Empfängt einen Kontext mit dem aktuellen User 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 übersetzbar sind, ü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 übersetzbarer Schematypen. Normalerweise aus einer Indexdatei exportiert
// wo auch immer sich Ihr Sanity-Schema befindet
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']
export default defineConfig({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filtern((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
plugins: [
// ...
phrasePlugin({
// Ihre Konfigurationsoptionen hier
}),
],
})
Ausschluss von PTDs aus Dokumentlisten
PTDs (Phrase Translation Documents) sind temporäre Dokumente, die nicht in normalen Dokumentlisten innerhalb von Sanity Studio erscheinen sollten. Die NOT_PTD Konstante stellt zu diesem Zweck einen GROQ-filter bereit:
// sanity.config.ts
import { NOT_PTD } from 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... andere Konfiguration
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == "post" && ${NOT_PTD}`),
),
// ... andere Elemente
]),
}),
],
})
Ausblenden des Übersetzungsmenüs von PTDs
Bei der Verwendung des Dokument-Internationalisierungs-Plug-ins sollte das Übersetzungsmenü vor PTDs verborgen werden. Das isPtdId Dienstprogramm identifiziert PTD-Dokumente:
import { isPtdId } from 'sanity-plugin-phrase/utils'
importieren { DocumentInternationalizationMenu } from '@sanity/document-internationalization'
// Verwenden Sie dasselbe Array wie translatableTypes aus Ihrer PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']
export default defineConfig({
Dokument: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Nur Übersetzungsmenü für echte Dokumente anzeigen, nicht für PTDs
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
Das Plug-in hat keine Konfiguration in der Phrase UI, aber Phrase muss so konfiguriert werden, dass Webhook-Benachrichtigungen an den Backend-API-Endpunkt gesendet werden. Dies ermöglicht Echtzeit-Aktualisierungen, während der Fortschritt der Übersetzungen voranschreitet.
Webhook erstellen
Einen Webhook mit diesen Einstellungen erstellen:
-
URL
Die URL zum API-Endpunkt des Plug-ins, wie in der Option konfiguriert.
-
Veranstaltungen:
-
Jobs
-
Job gelöscht
-
Job assigned
-
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 alle Änderungen an Phrase-Projekten benachrichtigt wird und Sanity-Daten synchron halten kann.
Projektvorlage(n) einrichten
Konfigurieren Sie Phrase-Projektvorlage(n) mit den Eigenschaften, die für Workflows und Team-Anforderungen erforderlich sind. Es können eine oder mehrere Vorlagen zur Auswahl angeboten werden, wenn eine neue Übersetzung in Auftrag gegeben wird. Phrase-Projektvorlagen müssen über spezifische JSON-Importeinstellungen verfügen, damit das Plug-in korrekt funktioniert. Diese Einstellungen steuern, welche Felder an Übersetzer gesendet und welche als Metadaten beibehalten werden.
JSON-Datei importieren
Regex verwenden, um bestimmte Keys auszuschließen:
(^|.*\/) (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|YOUR_IGNORED_KEYS_HERE| (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|YOUR_IGNORED_KEYS_HERE) /.*)
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.
-
Lokalisierungsspezifische Daten ausschließen, wie die Sprache eines bestimmten Dokuments, wenn
@sanity/document-internationalizationverwendet wird. -
Projektspezifische Keys einschließen, die keine Übersetzung erfordern, wie z. B. ein Slug für Content, der in allen Sprachen denselben Pfad verwendet. Ersetzen Sie
YOUR_IGNORED_KEYS_HEREdurch eine durch Pipes getrennte Liste von Keys, die ignoriert werden sollen. -
Context note:
/_sanityContext
Ausgangssprache
Derzeit geht dieses Plug-in von der Annahme aus, dass eine einzelne Ausgangssprache vorhanden ist. Die Projekt-Vorlage(n) müssen dieselbe Ausgangssprache haben wie die im sourceLanguage des Plug-ins konfigurierte.
Zielsprachen
Stellen Sie sicher, dass die in Phrase ausgewählten Sprachen mit denen in der Konfiguration des Plug-ins synchron sind.
Dies ist der Endpunkt, den das Plug-in verwendet, um mit dem Sanity Studio zu kommunizieren. Er wird verwendet, um sich bei der API von Phrase zu authentifizieren sowie Webhooks und User-Anfragen aus dem Sanity Studio zu empfangen.
Erstellen Sie einen Individuellen API-Endpunkt im Sanity-Projekt, um diese Anfragen zu verarbeiten. Eine der einfachsten Möglichkeiten hierfür ist die Verwendung von serverlosen Funktionen über Front-End-Frameworks wie NextJS, Remix, SvelteKit oder Nuxt.
Zugreifen konfigurieren Sie den Handler mit einem Request-Response-Muster über import {createRequestHandler} from backend oder verwenden Sie den internen Handler direkt über import {createInternalHandler} from . 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-Kunde-Komponente parst.
Dieses Beispiel demonstriert das Erstellen eines Route-Handlers unter dem konfigurierten apiEndpoint-Pfad unter /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 } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
import { client } from '~/lib/sanity.client'
export const maxDuration = 60
exportieren const dynamic = 'force-dynamic'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: Kunde.withConfig({ Token: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
exportieren default async function 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: [Zeichenfolge, 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 } from 'next'
import { PHRASE_CONFIG } from 'phraseConfig'
import { createInternalHandler } from 'sanity-plugin-phrase/backend'
import { writeToken } from '~/lib/sanity.api'
import { client } from '~/lib/sanity.client'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_USER_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: Kunde.withConfig({ Token: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
exportieren default async function 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 präskriptiven Ansatz für die Internationalisierung, und es gibt viele Möglichkeiten, diese zu implementieren. Dieses Plug-in verwendet ein Adaptermuster, um eine Konfiguration basierend darauf zu ermöglichen, wie der Content strukturiert ist und wie er übersetzt werden soll.
Derzeit ist nur der Adapter verfügbar, der von Sanitys offiziellem document-internationalization Plug-in (Version ^2.0.0) verwendet wird. Erstellen Sie ein Problem, falls ein spezifischer Adapter erforderlich ist, oder beziehen Sie sich auf dieses Repository-Paket/src/adapters/document-internationalization.ts für ein Beispiel zur Implementierung eines benutzerdefiniert-en Adapters.
Benutzerdefiniert-e Datentransformatoren
Wenn eine Transformation der Daten vor dem Senden an Phrase erforderlich ist, verwenden Sie die Option . Dies ist nützlich, wenn die Struktur der Daten geändert werden soll oder wenn bestimmte Felder von der Übersetzung ausgeschlossen werden sollen.
Jeder Datentransformator muss die Daten kodieren, bevor sie an Phrase gesendet werden, und sie dekodieren, wenn sie zurückerhalten werden, 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, daher kann die Entwicklung komplex sein. Speichern Sie echte Zielsprache-Dokumente aus dem Sanity-Datensatz als .JSON und verwenden Sie diese als Testdaten für jede Kodierungs-/Dekodierungsfunktion.
Beispiel für die Modifizierung von JSON-kodierten VTT-Dateien in HTML, damit Phrase das Content der Untertitel besser segmentieren kann:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Prü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 // Return undefined to skip transformation
},
},
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 überspringen
// Siehe /demo-nextjs/src/utils/vttJsonTransformer.ts für den vollständigen Ausgangssprache-Code
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Einschränken des Editor-zugreifen
Welche Editoren auf das Phrase Dashboard zugreifen können, lässt sich durch die Implementierung der Option einschränken. Diese Funktion entspricht derjenigen, die an die hidden-Eigenschaft eines Feldes in Sanity übergeben wird. Sie empfängt einen Kontext mit dem aktuellen User und Dokument und muss einen booleschen Wert zurückgeben.
Beispiel für die Einschränkung des Zugriffs auf das Phrase Dashboard für User 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
},
})