Этот плагин предоставляет получить доступ к контент 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 учетные данные (только на стороне сервера)
# Примечание: Phrase интерфейс приложений API ожидает только имя пользователя, а НЕ полный электронная почта адрес
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.След._PUBLIC_Phrase_плагин_интерфейс приложений 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] Шаблон по умолчанию',
},
],
/**
* @optional
* Если вы хотите показать или скрыть панель управления 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({
шаблон: {
types: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
шаблон: (prev) =>
prev.фильтровать((шаблон) => !TRANSLATABLE_SCHEMAS.includes(шаблон.Идентификатор)),
},
plugins: [
// ...
phrasePlugin({
// Ваши параметры конфигурации здесь
}),
],
})
Исключение PTD из список документов
PTD (Phrase Translation Documents) — это временные документ, которые не должны отображаться в обычных список документ внутри Sanity Studio. Константа NOT_PTD предоставляет GROQ фильтровать для этой цели:
// sanity.config.ts
импортировать { NOT_PTD } из '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({
document: {
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 для обработки этих запросов. Один из самых простых способов сделать это — использовать бессерверные функции через фронтенд-фреймворки, такие как NextJS, Remix, SvelteKit или Nuxt.
Получить доступ к настройке обработчика с помощью шаблона Request-Response через импортировать {createRequestHandler} из или использовать внутренний обработчик напрямую через импортировать {createInternalHandler} из . Убедитесь, что CORS-запросы обрабатываются правильно, если у studio и конечной точки разные источники.
Каталог приложений 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 { След.ApiRequest, След.ApiResponse } from 'next'
импортировать { PHRASE_CONFIG } from 'phraseConfig'
импортировать { createInternalHandler } from 'sanity-плагин-Phrase/backend'
импортировать { writeToken } 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({ токен: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
экспортировать default async function handler(
req: NextApiRequest,
res: След.ApiResponse,
) {
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.статус(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 { След.ApiRequest, След.ApiResponse } from 'next'
импортировать { PHRASE_CONFIG } from 'phraseConfig'
импортировать { createInternalHandler } from 'sanity-плагин-Phrase/backend'
импортировать { writeToken } from '~/lib/sanity.интерфейс приложений API'
импортировать { клиент } from '~/lib/sanity.клиент'
const phraseHandler = createInternalHandler({
phraseCredentials: {
userName: process.env.PHRASE_пользователь_NAME || '',
password: process.env.PHRASE_PASSWORD || '',
},
sanityClient: клиент.withConfig({ токен: writeToken }),
pluginOptions: PHRASE_CONFIG,
})
экспортировать default async function handler(
req: NextApiRequest,
res: След.ApiResponse,
) {
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.статус(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). Создайте вопрос, если требуется конкретный адаптер, или обратитесь к этому repository's 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(
(item) =>
typeof item === 'object' &&
!!item &&
'_type' in item &&
typeof item._type === 'строка' &&
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 === 'строка' &&
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, можно ограничить путем реализации опции . Эта функция эквивалентна той, которая передается в свойства hidden поля в Sanity. Она получает контекст с текущий пользователь и документ и должна возвращать логическое значение.
Пример ограничения доступ к панель управления Phrase для пользователь с роль администратор:
const PHRASE_CONFIG = definePhraseOptions({
// ...
isPhraseDashboardHidden: (context) => {
const isAdmin = (context.currentUser.roles || []).some(
(r) => r.name === 'admin',
)
// Скрыть если не администратор
return !isAdmin
},
})