統合機能

Sanity (TMS)

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

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

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

機能:

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

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

  • スマート再翻訳

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

  • 自動参照翻訳

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

  • 柔軟なスキーマ

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

  • Phraseワークフロー

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

Sanityプラグインのインストール

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

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

インストール

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

npm install sanity-plugin-phrase

# または pnpm、yarn、bun

環境変数

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

以下の例は 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"

# 書き込み権限を持つ Sanity API トークン(サーバー側のみ)
SANITY_WRITE_TOKEN=""

# Phrase 認証情報(サーバー側のみ)
# 備考:Phrase API はユーザー名部分のみを期待し、完全なメールアドレスではありません。
PHRASE_USER_NAME="phraseUsername"
PHRASE_PASSWORD="secretPassword"

プラグインの設定

プラグインは sanity.config.ts に必要な設定オプションと共に追加されます:

// sanity.config.ts
インポート { defineConfig } from 'sanity'
インポート {
  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',

  // 必須:エディター向けのプロジェクトテンプレートが利用可能です
  phraseTemplates: [
    {
      テンプレートUID:'YOUR_TEMPLATE_UID_HERE',
      ラベル:'デフォルト翻訳テンプレート',
    },
  ],

  // 必須:言語学者向けのプレビュー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'
  },

  // ユーザーの役割に基づいてダッシュボードのフレーズを非表示にする
  isPhraseDashboardHidden: (context) =>
    !(context.currentUser.roles || []).some((r) => r.name === 'admin'),
})

export default defineConfig({
  // ... 既存の設定
  plugins: [
    phrasePlugin(PHRASE_CONFIG),
    // ... 他のプラグイン
  ],
})// sanity.config.(js|ts)
インポート {
  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',

  /**
   * フレーズダッシュボードから翻訳のフロントエンドプレビューに言語学者をリダイレクトするために使用されます。
   */
  getDocumentPreview: async (doc, sanityClient) => {
    const publishedId = doc._id.replace('drafts.', '')
    return `${process.env.NEXT_PUBLIC_FRONT_END_URL}/api/draft?publishedId=${publishedId}`
  },

  /**
   * 編集者が翻訳をリクエストする際に使用できるフレーズプロジェクトテンプレート。
   *
   * **注意:** 以下に示すテンプレートの設定手順に従ってください。
   */
  phraseTemplates: [
    {
      テンプレートUID:'1jYg0Pc1d8kAHUyM0tgdmt',
      label: '[Sanity.io] デフォルトテンプレート',
    },
  ],

  /**
   * @optional
   * ユーザーの権限に応じてフレーズダッシュボードを表示または非表示にしたい場合。
   *
   * 現在のユーザーとドキュメントのコンテキストを受け取り、ブール値を返す必要があります。
   */
  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'

// 翻訳可能なスキーマタイプのリスト。通常、インデックスファイルからエクスポートされます。
// サニティスキーマがある場所
const TRANSLATABLE_SCHEMAS = ['ページ', '投稿', 'コース', 'レッスン', '定義']

export default defineConfig({
  スキーマ: {
    タイプ: injectPhraseIntoSchema(TRANSLATABLE_SCHEMAS, PHRASE_CONFIG),
    テンプレート: (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({
  ドキュメント: {
    不安定な_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オプションで設定された通り。

  • イベント:

    • ジョブ

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

      • Job assigned

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

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

    • プロジェクト

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

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

    • その他

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

これにより、プラグインは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で選択した言語がプラグインの設定と同期していることを確認してください。

サニティapiエンドポイント

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

これらのリクエストを処理するために、サニティプロジェクトにカスタム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:'メソッドは許可されていません' )}
    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 ルート: 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:'メソッドは許可されていません' )}
    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には国際化に対する処方的アプローチはなく、実装方法は多岐にわたります。このプラグインは、コンテンツの構造と翻訳方法に基づいて設定を可能にするアダプターパターンを使用しています。

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

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

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

各データトランスフォーマーは、Phraseに送信する前にデータをエンコードし、Sanityに保存する前に変換するために受信したときにデコードする必要があります。複数のトランスフォーマーをスタックして順次実行できます。

プラグインはトランスフォーマーを単独でテストする方法を提供しないため、開発は複雑になる可能性があります。Sanityデータセットから実際のターゲットドキュメントを.JSONに保存し、エンコード/デコード関数の各テストデータとして使用します。

JSONエンコードされたVTTファイルをHTMLに変更する例。Phraseが字幕のコンテンツをより良くセグメントできるようにするための:

{ DataTransformer }をインポートする 'sanity-plugin-phrase'

const vttJsonTransformer:DataTransformer = {
  エンコード: {
    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を返す
    },
  },
  デコード: {
    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 = {
  エンコード: {
    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
    },
  },
  デコード: {
    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(
  エンコードされた:EncodedSubtitles,
):StoredSubtitleNode[]
declare function encodeSubtitles(nodes:StoredSubtitleNode[]):EncodedSubtitles

エディタアクセスの制限

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

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

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.