Этот плагин предоставляет доступ к контенту, переведенному в Phrase, прямо из Sanity studio.
Поддерживаются только переводы на уровне документа. Переводы на уровне полей не поддерживаются.
Функции:
-
Предварительный просмотр в реальном времени
Переводы синхронизируются, чтобы лингвисты и переводчики могли видеть изменения в предварительном просмотре в реальном времени.
-
Умный повторный перевод
Плагин отслеживает, какой контент изменился с момента последнего перевода, и отправляет в Phrase только эти изменения.
-
Автоматический перевод ссылок
При отправке на перевод редакторы могут выбрать перевод документов, на которые ссылается текущий, и плагин автоматически свяжет их по языку перевода.
-
Гибкие схемы
Независимо от структуры, плагин адаптируется к ней и гарантирует, что итоговый контент соответствует схемам Sanity.
-
Рабочие процессы Phrase
Рабочие процессы перевода в Phrase остаются прежними; переобучение или перенастройка операций не требуются.
Установка выполняется через командную строку.
Предполагается, что генератор веб-сайтов и Sanity Studio уже настроены. Если нет, используйте один из Starter шаблонов, предоставленных Sanity.
Установка
Перейдите в проект, содержащий экземпляр Sanity Studio, и установите плагин:
npm install sanity-plugin-phrase # или pnpm, yarn, bun
Переменные окружения
Перед тем как настроить плагин, необходимо задать следующие переменные окружения. Создать файл `.env` (или `.env.local` для След.js) в корневой папке проект.
Примеры ниже приведены для След.JS. Для других фреймворков обратитесь к их документации и учтите, что префикс `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=\"\"
# Учетные данные Phrase (только на стороне сервера)
# Примечание: интерфейс приложений API Phrase ожидает только имя пользователя, а НЕ полный адрес электронная почта
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"
Конфигурация плагин
Плагин добавлен в sanity.config.ts с необходимыми параметрами конфигурации:
// sanity.config.ts
import { defineConfig } from 'sanity'
import {
phrasePlugin,
definePhraseOptions,
documentInternationalizationAdapter,
} from '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: 'Default Translation Template',
},
],
// Обязательно: Создание URL-адресов предварительного просмотра для лингвистов
getDocumentPreview: (doc, sanityClient) => {
const publishedId = doc._id.Заменить('drafts.', '')
return `${process.env.NEXT_PUBLIC_BASE_URL}/интерфейс приложений API/draft?Идентификатор=${publishedId}`
},
// Дополнительные настройки
// Максимальная глубина для перевода ссылочных документов (по умолчанию: 3)
maxReferencesDepth: 3,
// Разрешить перевод черновиков документов (по умолчанию: false)
translateDrafts: false,
// Пользовательские преобразователи данных для специальных типов контента
dataTransformers: [],
// Конфигурация ведения журнала для отладки
logger: {
minimumLogLevel: 'info', // 'debug' | 'info' | 'предупреждение' | 'error' | 'fatal'
},
// Скрыть панель управления Phrase на основе ролей пользователя
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ... ваша существующая конфигурация
plugins: [
phrasePlugin(PHRASE_CONFIG),
// ... другие плагины
],
})// sanity.config.(js|ts)
import {
phrasePlugin,
documentInternationalizationAdapter,
} from 'sanity-plugin-phrase'
const PHRASE_CONFIG = definePhraseOptions({
/**
* Адаптер i18n, который нужно использовать для этого плагина.
* Он будет отвечать за получение и изменение документов для каждого перевод.
*
* Дополнительную информацию об адаптерах см. ниже.
*/
i18nAdapter: documentInternationalizationAdapter(),
/**
* Типы схем Sanity, которые плагин может переводить
*/
translatableTypes: ['page', 'post', 'course', 'lesson', 'definition'],
/**
* Языковый код всех языков, на которые пользователи могут переводить.
* Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим интерфейсом. Плагин автоматически переведет его в формат Phrase.
*/
supportedTargetLangs: ['cz', 'es', 'pt', 'fr', 'de', 'it', 'nl', 'pl', 'ru'],
/**
* Языковый код оригинала, который будет переведен.
* Должен совпадать с тем, который хранится в ваших документах Sanity и используется вашим интерфейсом. Плагин автоматически переведет его в формат Phrase.
*/
"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.Заменить('drafts.', '')
return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
},
/**
* Шаблоны проектов Phrase, которые ваши редакторы могут использовать при запросе перевода.
*
* **Примечание:** выполните действия по настройке шаблонов, описанные ниже
*/
phraseTemplates: [
{
templateUid: '1jYg0Pc1d8kAHUyM0tgdmt',
label: '[Sanity.io] Шаблон по умолчанию',
},
],
/**
* Дополнительно
* Если вы хотите показать или скрыть панель управления Phrase в зависимости от прав пользователя.
*
* Получает контекст с текущим пользователем и документом и должен возвращать логическое значение.
*/
isPhraseDashboardHidden: (context) =>
!(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})
export default defineConfig({
// ...
plugins: [
// ...
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({
schema: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
templates: (prev) =>
prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
},
plugins: [
// ...
phrasePlugin({
// Ваши параметры конфигурации здесь
}),
],
})
Исключение PTD из списков документов
PTD (Phrase Translation Documents) — это временные документы, которые не должны отображаться в обычных списках документов внутри Sanity Studio. Константа NOT_PTD предоставляет фильтр GROQ для этой цели:
// sanity.config.ts
import { NOT_PTD } from 'sanity-plugin-phrase/utils'
export default defineConfig({
// ... другая конфигурация
plugins: [
structureTool({
structure: (S) =>
S.list()
.title('Контент')
.items([
S.listItem()
.title('Posts')
.schemaType('post')
.child(
S.documentList()
.title('Posts')
.filter(`_type == \"post\" && ${NOT_PTD}`),
),
// ... другие элементы
]),
}),
],
})
Скрытие меню перевода от PTD
При использовании плагина document-internationalization меню перевода должно быть скрыто от PTD. Утилита isPtdId идентифицирует документы PTD:
import { isPtdId } from 'sanity-plugin-phrase/utils'
import { DocumentInternationalizationMenu } from '@sanity/document-internationalization'
// Использовать тот же массив, что и translatableTypes из вашего PHRASE_CONFIG
const TRANSLATABLE_TYPES = ['page', 'post', 'article']
export default defineConfig({
документ: {
unstable_languageFilter: (prev, ctx) => {
const { schemaType, documentId } = ctx
// Показывать меню перевода только для реальных документов, а не для PTD
return TRANSLATABLE_TYPES.includes(schemaType) &&
documentId &&
!isPtdId(documentId)
? [...prev, DocumentInternationalizationMenu]
: prev
},
},
})
Плагин не имеет настроек в пользовательском интерфейсе Phrase, но Phrase должен быть настроен для отправки уведомлений вебхука на конечную точку интерфейса приложений API. Это обеспечивает обновления в режиме реального времени по мере того, как идет ход выполнения переводов.
Создать вебхук
Создать вебхук с этими настройками:
-
URL-адрес
URL-адрес конечной точки интерфейса приложений API плагина, как настроено в параметре .
-
События:
-
Задания
-
Задание удалено
-
Job assigned
-
Срок выполнения задания изменен
-
Перевод задания обновлен
-
-
Проекты
-
Проект удален
-
Срок выполнения проекта изменен
-
-
Другое
-
Предварительный перевод завершен
-
-
Это гарантирует, что плагин получает уведомления о любых изменениях в проектах Phrase и может поддерживать данные Sanity в синхронизированном состоянии.
Настройка шаблона(ов) проекта
Настройте шаблон(ы) проекта Phrase с помощью свойств, необходимых для рабочих процессов и требований Team. При заказе нового перевода можно предложить на выбор один или несколько шаблонов. Шаблоны проектов Phrase должны иметь определенные настройки импорта JSON, чтобы плагин работал правильно. Эти настройки определяют, какие поля отправляются переводчикам, а какие сохраняются в качестве метаданных.
Импорт JSON-файла
Используйте регулярное выражение, чтобы Исключить определенные ключи:
(^|.*\/) (_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) /.*)
Это выражение намеренно включает дублирующиеся ключи, чтобы гарантировать, что они будут проигнорированы парсером регулярных выражений Phrase. Убедитесь, что они правильно дублируются.
-
Исключить данные, специфичные для локализации, например, язык конкретного документа при использовании
@sanity/document-internationalization. -
Включите любые специфичные для проекта ключи, которые не требуют перевода, например, slug для контента, использующего один и тот же путь для всех языков. Замените
YOUR_IGNORED_KEYS_HEREсписком ключей, разделенных вертикальной чертой, которые нужно пропустить. -
Контекстное примечание:
/_sanityContext
Язык оригинала
В настоящее время этот плагин работает исходя из предположения о наличии одного языка оригинала. Шаблон(ы) проекта должен(ны) иметь тот же оригинал, что и настроенный в sourceLanguage плагина.
Языки перевода
Убедитесь, что языки, выбранные в Phrase, синхронизированы с конфигурацией плагина.
Это конечная точка, которую плагин использует для связи с Sanity Studio. Она используется для аутентификации в интерфейсе приложений API Phrase, получения вебхуков и запросов пользователей из Sanity studio.
Создайте пользовательский интерфейс приложений API endpoint в проекте Sanity для обработки этих запросов. Один из самых простых способов сделать это — использовать бессерверные функции через интерфейсы приложений API фронтенд-фреймворков, таких как NextJS, Remix, SvelteKit или Nuxt.
Получите доступ к настройке обработчика с помощью шаблона Request-Response через import {createRequestHandler} from или используйте внутренний обработчик напрямую через import {createInternalHandler} from . Убедитесь, что запросы 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
импортировать type { NextApiRequest, NextApiResponse } from 'next'
импортировать { PHRASE_CONFIG } from 'phraseConfig'
импортировать { createInternalHandler } from 'sanity-плагин-Phrase/backend'
импортировать { writeтокен } from '~/lib/sanity.интерфейс приложений API'
импортировать { клиент } from '~/lib/sanity.клиент'
экспортировать const maxDuration = 60
экспортировать const dynamic = 'force-dynamic'
const phraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.PHRASE_пользователь_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: клиент.withConfig({ token: токен }),
pluginOptions: PHRASE_CONFIG,
})
экспортировать default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('получить доступ-Control-Allow-Origin', '*')
res.setHeader('получить доступ-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('получить доступ-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.запрос,
)
const resBody = await phraseRes.json().catch(() => {})
Array.from(phraseRes.headers.entries()).forEach((value: [строка, 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
импортировать type { NextApiRequest, NextApiResponse } from 'next'
импортировать { PHRASE_CONFIG } from 'phraseConfig'
импортировать { createInternalHandler } from 'sanity-плагин-Phrase/backend'
импортировать { writeтокен } from '~/lib/sanity.интерфейс приложений API'
импортировать { клиент } from '~/lib/sanity.клиент'
const phraseHandler = createInternalHandler({
PhraseCredentials: {
userName: process.env.PHRASE_пользователь_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: клиент.withConfig({ token: токен }),
pluginOptions: PHRASE_CONFIG,
})
экспортировать default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
res.setHeader('получить доступ-Control-Allow-Origin', '*')
res.setHeader('получить доступ-Control-Allow-Methods', 'POST, OPTIONS')
res.setHeader('получить доступ-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.запрос,
)
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 не имеет предписанного подхода к интернационализации, и существует множество способов его реализации. Этот плагин использует шаблон адаптера, чтобы разрешить конфигурацию в зависимости от того, как структурирован контент и как он должен быть переведен.
В настоящее время единственным доступным адаптером является , который используется официальным плагин document-internationalization от Sanity (версия ^2.0.0). Создайте вопрос, если требуется конкретный адаптер, или обратитесь к этому хранилище's package/src/adapters/document-internationalization.ts для примера того, как реализовать пользовательский.
Пользовательские преобразователи данных
Если требуется преобразование данных перед отправкой их в Phrase, используйте опцию . Это полезно, если вы меняете структуру данных или если исключаете определенные поля из перевода.
Каждый преобразователь данных должен encode данные перед отправкой их в Phrase; и decode их при получении обратно, чтобы преобразовать их перед сохранением в Sanity. Несколько преобразователей могут быть объединены в стек и запущены последовательно.
Плагин не предлагает способа тестирования трансформеров в изоляции, поэтому разработка может быть сложной. Сохраните реальные документы перевода из набора данных Sanity в .JSON и используйте их в качестве тестовых данных для каждой функции кодирования/декодирования.
Пример изменения VTT-файлов в кодировке JSON на HTML, чтобы Phrase мог лучше сегментировать контент субтитров:
import { DataTransformer } from 'sanity-plugin-phrase'
const vttJsonTransformer: DataTransformer = {
encode: {
array(arr) {
// Проверить, содержит ли массив узлы субтитров VTT
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 // Вернуть 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(
(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
},
},
}
// Пропуск реализации
// Обратитесь к /demo-nextjs/src/utils/vttJsonTransformer.ts для получения полного оригинала кода
declare function decodeSubtitles(
encoded: EncodedSubtitles,
): StoredSubtitleNode[]
declare function encodeSubtitles(nodes: StoredSubtitleNode[]): EncodedSubtitles
Ограничение доступа редактора
Доступ редакторов к панели управления Phrase можно ограничить, реализовав параметр i. Эта функция эквивалентна функции, передаваемой в свойство hidden поля в Sanity. Она получает контекст с текущим пользователем и документом и должна возвращать логическое значение.
Пример ограничения доступа к панели управления Phrase для пользователей с ролью администратор:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// Скрыть, если не администратор
return !isAdmin
},
})