Плагин предоставляет доступ к переведенному контенту Phrase из студии Sanity.
Поддерживаются только переводы на уровне документа. Переводы на уровне полей не поддерживаются.
Особенности:
-
Предварительные просмотры в реальном времени
Переводы синхронизируются, чтобы лингвисты и переводчики могли видеть изменения в предварительном просмотре в реальном времени.
-
Умные повторные переводы
Плагин сравнивает, какой контент изменился с момента последнего перевода, и отправляет только эти изменения в Phrase.
-
Автоматический перевод ссылок
При выдаче переводов редакторы могут выбрать также перевести документы, на которые ссылается текущий, и плагин автоматически свяжет их по целевому языку.
-
Гибкие схемы
Неважно, какая структура, плагин адаптируется к ней и обеспечивает, чтобы финальный переведенный контент соответствовал схемам Sanity.
-
Рабочие процессы Phrase
Рабочие процессы перевода в Phrase остаются прежними; повторное обучение или перенастройка операций не требуется.
Установка выполняется через командную строку.
Предполагается, что генератор веб-сайтов и Sanity Studio уже настроены. Если нет, используйте один из шаблонов-стартеров, предоставленных Sanity.
Установка
Перейдите к проекту, содержащему экземпляр Sanity Studio, и установите плагин:
npm install sanity-plugin-phrase # или pnpm, yarn, bun
Переменные окружения
Перед настройкой плагина необходимо установить следующие переменные окружения. Создайте файл `.env` (или `.env.local` для Next.js) в корне проекта.
Примеры ниже приведены для NextJS. Для других фреймворков обратитесь к их конкретной документации и обратите внимание, что префикс `NEXT_PUBLIC_` может потребоваться удалить для публичных переменных.
Важно
Переменные на стороне сервера (SANITY_WRITE_TOKEN, PHRASE_USER_NAME, PHRASE_PASSWORD) никогда не должны быть доступны клиенту. В Next.js только переменные с префиксом NEXT_PUBLIC_ доступны браузеру.
# Базовый URL вашего сайта (используется для предварительных ссылок)
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
# URL, где будет находиться обработчик бэкенда плагина
NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT="http://localhost:3000/api/phrase"
# Регион дата-центра Phrase ('eu' или 'us')
NEXT_PUBLIC_PHRASE_REGION="eu"
# Конфигурация проекта Sanity
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
# Токен API Sanity с правами на запись (только на стороне сервера)
SANITY_WRITE_TOKEN=""
# Фраза учетные данные (только на стороне сервера)
# Примечание: API Phrase ожидает только часть имени пользователя, а не полный адрес электронной почты
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"
Конфигурация плагина
Плагин добавляется в sanity.config.ts с необходимыми параметрами конфигурации:
// sanity.config.ts
импорт { defineConfig } из 'sanity'
импорт {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} из 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
// Обязательно: адаптер i18n для интернационализации документа
i18nAdapter: documentInternationalizationAdapter(),
// Обязательно: Типы документов, которые могут быть переведены
translatableTypes: ['page', 'post', 'article'],
// Обязательно: Исходный язык (основной язык)
// Это должно соответствовать языку, определенному в шаблоне проекта Phrase
sourceLang: 'en',
// Обязательно: Целевые языки, на которые пользователи могут переводить
// Используйте те же коды, что и ваши документы Sanity
// Этот список должен соответствовать языкам, определенным в шаблоне проекта Phrase
supportedTargetLangs: ['es', 'fr', 'de', 'pt'],
// Обязательно: URL-адрес вашего API-эндпоинта на сервере
apiEndpoint: process.env.NEXT_PUBLIC_PHRASE_PLUGIN_API_ENDPOINT!,
// Обязательно: Регион дата-центра Phrase ('eu' или 'us')
phraseRegion: process.env.NEXT_PUBLIC_PHRASE_REGION as 'eu' | 'us',
// Обязательно: Шаблоны проектов Phrase, доступные для редакторов
phraseTemplates: [
{
templateUid: 'YOUR_TEMPLATE_UID_HERE',
label: 'Шаблон перевода по умолчанию',
},
],
// Обязательно: Генерировать URL-адреса предварительного просмотра для лингвистов
getDocumentPreview: (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_BASE_URL}/api/draft?id=${publishedId}`
},
// Дополнительные настройки
// Максимальная глубина для перевода ссылочных документов (по умолчанию: 3)
maxReferencesDepth: 3,
// Разрешить перевод черновиков документов (по умолчанию: false)
translateDrafts: false,
// Пользовательские преобразователи данных для специальных типов контента
dataTransformers: [],
// Конфигурация логирования для отладки
логгер: {
минимальныйУровеньЛога: 'информация', // 'отладка' | 'информация' | 'предупреждение' | 'ошибка' | 'фатальная'
},
// Скрыть панель управления Фразами в зависимости от ролей пользователей
isPhraseDashboardHidden: (контекст) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... ваша существующая конфигурация
плагины: [
phrasePlugin(PHRASE_CONFIG),
// ... другие плагины
],
})// sanity.config.(js|ts)
импорт {
phrasePlugin,
documentInternationalizationAdapter,
} из 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* Адаптер i18n, который следует использовать для этого плагина.
* Он будет отвечать за получение и изменение документов для каждого целевого языка.
*
* См. ниже для получения дополнительной информации об адаптерах.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Типы схем Sanity, которые плагин может переводить
*/
переводимыеТипы: ['страница', 'пост', 'курс', 'урок', 'определение'],
/**
* Языковой код всех языков, на которые пользователи могут переводить.
* Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим фронтендом. Плагин автоматически переведет его в формат Фразы.
*/
поддерживаемыеЦелевыеЯзыки: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* Языковой код исходного языка, который будет переведен.
* Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим фронтендом. Плагин автоматически переведет его в формат Фразы.
*/
sourceLang: 'en',
/**
* Как определено в настройках вашей учетной записи Phrase
* Либо `eu`, либо `us`
*/
phraseRegion: 'us|eu',
/**
* URL вашего настроенного плагина для API.
*
* **Примечание:** следуйте шагам по настройке конечной точки, описанным ниже
*/
apiEndpoint: 'https://my-site.com/api/phrase',
/**
* Используется для перенаправления лингвистов с панели управления Phrase на предварительный просмотр их переводов на фронтенде.
*/
getDocumentPreview: async (doc, sanityClient) => {
const publishedId = doc._id.replace('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* Шаблоны проектов Phrase, которые ваши редакторы могут использовать при запросе переводов.
*
* **Примечание:** следуйте шагам по настройке шаблонов, описанным ниже
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
label: '[Sanity.io] Шаблон по умолчанию',
},
],
/**
* @необязательно
* В случае, если вы хотите показать или скрыть панель управления Phrase в зависимости от привилегий пользователя.
*
* Получает контекст с текущим пользователем и документом и должен вернуть булево значение.
*/
isPhraseDashboardHidden: (контекст) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
плагины: [
// ...
phrasePlugin(PHRASE_CONFIG),
],
})
Инъекция схемы
Чтобы указать плагину, какие типы документов могут быть переведены, передайте массив типов документов в функцию injectPhraseIntoSchema в файле sanity.config.ts:
// sanity.config.ts
import { injectPhraseIntoSchema } from 'sanity-plugin-phrase'
// Список переводимых типов схем. Обычно экспортируется из индексного файла
// где бы вы ни находили свою схему Sanity
const TRANSLATABLE_SCHEMAS = ['page', 'post', 'course', 'lesson', 'definition']
export default defineConfig({
схема: {
типы: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
шаблоны: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
плагины: [
// ...
phrasePlugin({
// Ваши параметры конфигурации здесь
}),
],
})
Исключая PTD из списков документов
PTD (Документы перевода фраз) — это временные документы, которые не должны появляться в обычных списках документов внутри Sanity Studio. Константа NOT_PTD предоставляет фильтр GROQ для этой цели:
// sanity.config.ts
импорт { NOT_PTD } из 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... другие настройки
плагины: [
structureTool({
структура: (S) =>
S.list()
.title('Контент')
.items([
S.listItem()
.title('Посты')
.schemaType('пост')
.child(
S.documentList()
.title('Посты')
.filter(`_type == "пост" && ${NOT_PTD}`),
),
// ... другие элементы
]),
}),
],
})
Скрытие меню перевода от PTD
При использовании плагина международной документации меню перевода должно быть скрыто от PTD. Утилита isPtdId идентифицирует документы PTD:
импорт { isPtdId } из 'sanity-plugin-phrase/utils'
импорт { DocumentInternationalizationMenu } из '@sanity/document-internationalization'
// Используйте тот же массив, что и translatableTypes из вашего PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['страница', 'пост', 'статья']
export default defineConfig({
документ: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Показывать меню перевода только для реальных документов, а не для PTD
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: предыдущее
},
},
})
Плагин не имеет конфигурации в интерфейсе Фразы, но Фразу необходимо настроить для отправки уведомлений вебхука на конечную точку API бэкенда. Это позволяет получать обновления в реальном времени по мере выполнения переводов.
Создать вебхук
Создайте вебхук с этими настройками:
-
URL
URL конечной точки API плагина, как настроено в опции .
-
События:
-
Задания
-
Задание удалено
-
Задание назначено
-
Срок выполнения задания изменен
-
Цель задания обновлена
-
-
Проекты
-
Проект удален
-
Срок выполнения проекта изменен
-
-
Другое
-
Предварительный перевод завершен
-
-
Это гарантирует, что плагин уведомляется о любых изменениях в проектах Фразы и может поддерживать данные Sanity в актуальном состоянии.
Настройка проектов шаблон(ов)
Настройте Фразу шаблон проекта(ов) с необходимыми свойствами для рабочих процессов и требований команды. Один или несколько шаблонов могут быть предложены для выбора при заказе нового перевода. Шаблоны проектов Фразы должны иметь определенные настройки импорта JSON, чтобы плагин работал правильно. Эти настройки контролируют, какие поля отправляются переводчикам, а какие сохраняются как метаданные.
Импорт файла JSON
Используйте регулярные выражения, чтобы исключить определенные ключи:
(^|.*\/) (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬ| (_createdAt|_id|_rev|_type|_updatedAt|_ref|_key|_sanityRev|_sanityContext|_strengthenOnPublish|phraseMetadata|_spanMeta|_blockMeta|_diff|marks|ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬ) /.*)
Это выражение включает дублированные ключи намеренно, чтобы гарантировать, что они игнорируются парсером RegEx Фразы. Убедитесь, что они правильно дублированы.
-
Исключите данные, специфичные для локализации, такие как язык данного документа, если используется
@sanity/document-internationalization. -
Включите любые ключи, специфичные для проекта, которые не требуют перевода, такие как slug для контента, использующего один и тот же путь на всех языках. Замените
ВАШИ_ИСКЛЮЧЕННЫЕ_КЛЮЧИ_ЗДЕСЬна список ключей, разделенных символом |, которые нужно игнорировать. -
Контекстное примечание:
/_sanityContext
Язык оригинала
В настоящее время этот плагин работает на основе предположения о наличии одного языка оригинала. Шаблоны проекта(ов) должны иметь тот же язык оригинала, что и тот, который настроен в sourceLanguage плагина.
Языки перевода
Убедитесь, что выбранные языки в Phrase синхронизированы с конфигурацией плагина.
Это конечная точка, которую плагин использует для связи с Sanity Studio. Она используется для аутентификации в API Phrase, получения вебхуков и пользовательских запросов из студии Sanity.
Создайте пользовательскую конечную точку API в проекте Sanity для обработки этих запросов. Один из самых простых способов сделать это - использовать безсерверные функции через фронтенд-фреймворки, такие как NextJS, Remix, SvelteKit или Nuxt.
Получите доступ к конфигурации обработчика с помощью шаблона Запрос-Ответ через import {createRequestHandler} из backend или используйте внутренний обработчик напрямую через import {createInternalHandler} из . Убедитесь, что запросы CORS обрабатываются правильно, если студия и конечная точка имеют разные источники.
Директория приложения NextJS в настоящее время не поддерживается, так как она неправильно разбирает обработчик бэкенда как компонент клиента React.
Этот пример демонстрирует создание обработчика маршрута по настроенному apiEndpoint пути по адресу /api/phrase с использованием маршрутизатора страниц Next.js:
// 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
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,
})
export 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: [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 } 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: client.withConfig({ token: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
export 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
Sanity не имеет предписанного подхода к интернационализации, и существует множество способов его реализации. Этот плагин использует паттерн адаптера для настройки в зависимости от того, как структурирован контент и как он должен быть переведен.
В настоящее время единственным доступным адаптером является , который используется официальным плагином документальной интернационализации Sanity (версия ^2.0.0). Подайте вопрос, если требуется конкретный адаптер, или обратитесь к этому репозиторию package/src/adapters/document-internationalization.ts для примера, как реализовать пользовательский адаптер.
Пользовательские трансформаторы данных
Если требуется преобразование данных перед отправкой их в Phrase, используйте опцию . Это полезно, если необходимо изменить структуру данных или исключить определенные поля из перевода.
Каждый трансформатор данных должен кодировать данные перед отправкой их в Phrase; и декодировать их при получении обратно для преобразования перед сохранением в Sanity. Несколько трансформаторов могут быть объединены и выполнены последовательно.
Плагин не предлагает способа тестирования трансформаторов в изоляции, поэтому разработка может быть сложной. Сохраните реальные целевые документы из набора данных Sanity в .JSON и используйте их в качестве тестовых данных для каждой функции кодирования/декодирования.
Пример изменения JSON-кодированных VTT файлов в HTML, чтобы Phrase мог лучше сегментировать контент субтитров:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Проверьте, содержит ли массив узлы субтитров VTT
if (
arr.every(
(элемент) =>
typeof элемент === 'object' &&
!!элемент &&
'_type' in элемент &&
typeof элемент._type === 'string' &&
элемент._type.startsWith('vtt.'),
)
) {
return encodeSubtitles(arr as StoredSubtitleNode[])
}
return undefined // Вернуть undefined, чтобы пропустить преобразование
},
},
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(
(элемент) =>
typeof элемент === 'object' &&
!!элемент &&
'_type' in элемент &&
typeof элемент._type === 'string' &&
элемент._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
},
},
}
// Пропуск реализации
// Обратитесь к /demo-nextjs/src/utils/vttJsonTransformer.ts для полного исходного кода
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
объявить функцию encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Ограничение доступа редактора
Кто может получить доступ к панели управления Phrase, можно ограничить, реализовав опцию i. Эта функция эквивалентна той, что передана в свойство hidden поля в Sanity. Она получает контекст с текущим пользователем и документом и должна вернуть булево значение.
Пример ограничения доступа к панели управления Phrase для пользователей с ролью администратор:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'администратор',
)
// Скрыть, если не администратор
return !isAdmin
},
})