統合機能

サニティ (TMS)

本コンテンツはPhrase Language AIの機械翻訳により、英語から翻訳されています。

プラグインは、サニティスタジオ内からPhraseに翻訳されたコンテンツへのアクセスを提供します。

ドキュメントレベルの翻訳のみがサポートされています。フィールドレベルの翻訳はサポートされていません。

機能:

  • リアルタイムプレビュー

    翻訳は同期されており、言語学者や翻訳者がリアルタイムでプレビューの変更を確認できるようになっています。

  • スマート再翻訳

    プラグインは、前回の翻訳以降に変更されたコンテンツを比較し、その変更のみをPhraseに送信します。

  • 自動参照翻訳

    翻訳を発行する際、編集者は現在のドキュメントに参照されているドキュメントも翻訳することを選択でき、プラグインはターゲット言語によって自動的にリンクします。

  • 柔軟なスキーマ

    構造に関係なく、プラグインはそれに適応し、最終的な翻訳コンテンツがサニティのスキーマに従うことを保証します。

  • Phraseワークフロー

    Phraseの翻訳ワークフローは同じままであり、再トレーニングや再構成の操作は必要ありません。

サニティプラグインのインストール

インストールはコマンドラインで行われます。

ウェブサイトジェネレーターとサニティスタジオがすでに設定されていることが前提とされています。そうでない場合は、サニティが提供するスターターテンプレートのいずれかを使用してください。

インストール

Sanity Studioインスタンスを含むプロジェクトに移動し、プラグインをインストールします:

npm install sanity-plugin-phrase

# または pnpm、yarn、bun

環境変数

プラグインを設定する前に、以下の環境変数を設定する必要があります。.envファイル(またはNext.js用の.env.local)をプロジェクトのルートに作成します。

以下の例はNextJS用です。他のフレームワークについては、それぞれのドキュメントを参照し、公開変数には`NEXT_PUBLIC_`プレフィックスを削除する必要があることに注意してください。

重要

サーバー側の変数(SANITY_WRITE_TOKENPHRASE_USER_NAMEPHRASE_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"

# 書き込み権限を持つSanity APIトークン(サーバー側のみ)
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'],

  // 必須: あなたのバックエンドAPIエンドポイントURL
  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: [],

  // デバッグのためのログ設定
  logger: {
    minimumLogLevel: 'info', // 'debug' | 'info' | 'warning' | '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',

  /**
   * 設定されたプラグインバックエンドAPIのURL。
   *
   * **注意:** 以下に示すエンドポイントの設定手順に従ってください。
   */
  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] デフォルトテンプレート',
    },
  ],

  /**
   * @optional
   * ユーザーの権限に応じてPhraseダッシュボードを表示または非表示にしたい場合。
   *
   * 現在のユーザーとドキュメントのコンテキストを受け取り、真偽値を返す必要があります。
   */
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ...
  plugins: [
    // ...
    phrasePlugin(PHRASE_CONFIG),
  ],
})

Sanityプラグイン設定

スキーマ注入

プラグインに翻訳可能なドキュメントタイプを伝えるために、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: {
    タイプ: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    templates: (prev) =>
      prev.filter((template) => !TRANSLATABLE_SCHEMAS.includes(template.id)),
  },
  plugins: [
    // ...
    phrasePlugin({
      // ここに設定オプションを記入
    }),
  ],
})

ドキュメント一覧からPTDを除外

PTD(フレーズ翻訳ドキュメント)は、Sanity Studio内の通常のドキュメント一覧に表示されるべきではない一時的なドキュメントです。NOT_PTD定数は、この目的のためのGROQフィルタを提供します:

// sanity.config.ts
import { NOT_PTD } from 'sanity-plugin-phrase/utils'

export default defineConfig({
  // ... 他の設定
  plugins: [
    structureTool({
      構造: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem()
              .title('Posts')
              .schemaType('post')
              .child(
                S.documentList()
                  .title('Posts')
                  .filter(`_type == "post" && ${NOT_PTD}`),
              ),
            // ... 他の項目
          ]),
    }),
  ],
})

PTDから翻訳メニューを隠す

ドキュメント国際化プラグインを使用する際、翻訳メニューはPTDから隠す必要があります。isPtdIdユーティリティはPTDドキュメントを識別します:

import { isPtdId } from 'sanity-plugin-phrase/utils'
import { DocumentInternationalizationMenu } from '@sanity/document-internationalization'

// PHRASE_CONFIGのtranslatableTypesと同じ配列を使用してください
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
    },
  },
})

フレーズ設定

プラグインにはフレーズUIでの設定はありませんが、フレーズはバックエンドAPIエンドポイントにウェブフック通知を送信するように設定する必要があります。これにより、翻訳の進捗に応じてリアルタイムで更新されます。

ウェブフックを作成する

これらの設定でwebhookを作成します:

  • URL

    プラグインのAPIエンドポイントのURL、apiEndpointオプションで設定された通り。

  • イベント:

    • ジョブ

      • ジョブが削除されました

      • ジョブが割り当てられました

      • ジョブの納期が変更されました

      • ジョブの訳文が更新されました

    • プロジェクト

      • プロジェクトが削除されました

      • プロジェクトの納期が変更されました

    • その他

      • 一括翻訳が完成しました

これにより、プラグインはPhraseプロジェクトの変更を通知され、Sanityデータを同期させることができます。

セットアッププロジェクトテンプレート

ワークフローとチームの要件に必要なプロパティでPhrase プロジェクトテンプレートを構成します。新しい翻訳を注文する際に選択できるテンプレートを1つ以上提供できます。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のRegExパーサーによって無視されることを保証するために、意図的に重複したキーを含んでいます。それらが正しく重複していることを確認してください。

  • @sanity/document-internationalizationを使用している場合、特定のドキュメントの言語のようなローカライズ特有のデータを除外してください。

  • すべての言語で同じパスを使用するコンテンツのスラッグなど、翻訳を必要としないプロジェクト特有のキーを含めてください。YOUR_IGNORED_KEYS_HEREを無視するキーのパイプ区切りリストに置き換えてください。

  • コンテキストメモ: /_sanityContext

原文言語

現在、このプラグインは単一のソース言語があるという前提で動作しています。プロジェクトテンプレートは、プラグインのsourceLanguageに設定されているものと同じソースを持っている必要があります。

訳文言語

Phraseで選択した言語が、プラグインの設定にあるものと同期していることを確認してください。

Sanity apiEndpoint

これは、プラグインがSanity Studioと通信するために使用するエンドポイントです。これは、PhraseのAPIに認証し、SanityスタジオからのWebhookやユーザーリクエストを受信するために使用されます。

これらのリクエストを処理するために、SanityプロジェクトにカスタムAPIエンドポイントを作成してください。これを行う最も簡単な方法の1つは、NextJS、Remix、SvelteKit、またはNuxtのようなフロントエンドフレームワークを介してサーバーレス関数を使用することです。

インポート{createRequestHandler} from sanity-plugin-phrase/backendを介してリクエスト-レスポンスパターンでハンドラーを構成するか、インポート{createInternalHandler} from sanity-plugin-phrase/backendを介して内部ハンドラーを直接使用してください。スタジオとエンドポイントが異なるオリジンを持つ場合、CORSリクエストが正しく処理されることを確認してください。

NextJSのアプリディレクトリは、バックエンドハンドラーをReactクライアントコンポーネントとして誤って解析するため、現在サポートされていません。

この例は、Next.js Pages Routerを使用して、設定されたapiEndpointパスの/api/phraseでルートハンドラーを作成する方法を示しています:

// app/api/phrase/route.ts
// Next.js APIルートサポート: 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 adapters

Sanity には国際化に関する特定のアプローチはなく、実装方法は多岐にわたります。このプラグインは、コンテンツの構造と翻訳方法に基づいて設定を可能にするアダプターパターンを使用しています。

現在利用可能な唯一のアダプターは documentInternationalizationAdapter で、Sanity の公式 document-internationalization プラグイン(バージョン ^2.0.0)で使用されています。特定のアダプターが必要な場合は問題を報告するか、この repository's package/src/adapters/document-internationalization.ts を参照してカスタマイズされたアダプターの実装例を確認してください。

カスタムデータトランスフォーマー

データを Phrase に送信する前に変換する必要がある場合は、dataTransformers オプションを使用してください。これは、データの構造を変更する場合や、特定のフィールドを翻訳から除外する場合に便利です。

各データトランスフォーマーは、データを 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 === '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
    },
  },
}

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ダッシュボードにアクセスできるかは、isPhraseDashboardHiddenオプションを実装することで制限できます。この関数は、Sanityのフィールドのhiddenプロパティに渡されるものと同等です。現在のユーザーとドキュメントを含むコンテキストを受け取り、booleanを返す必要があります。

adminロールを持つユーザーにPhraseダッシュボードへのアクセスを制限する例:

const PHRASE_CONFIG = definePhraseOptions({
  // ...
  isPhraseDashboardHidden: (context) => {
    const isAdmin = (context.currentUser.roles || []).some(
      (r) => r.name === 'admin',
    )
    // 管理者でない場合は非表示
    return !isAdmin
  },
})
この記事は役に立ちましたか?

Sorry about that! In what way was it not helpful?

The article didn’t address my problem.
I couldn’t understand the article.
The feature doesn’t do what I need.
Other reason.

Note that feedback is provided anonymously so we aren't able to reply to questions.
If you'd like to ask a question, submit a request to our Support team.
Thank you for your feedback.