😺

Chrome で JavaScript から言語検出・翻訳がブラウザ単独でできるようになった話

に公開

はじめに

こんにちは、hamaguchi です

普段仕事ではバックエンドエンジニアとして Ruby on Rails を使ったウェブサービスの開発を行なっていますが、プライベートで色々作ったりする中ではバックエンドだけではなくフロントエンドやインフラの領域も触らざるを得ないため色々と勉強中です
特に TypeScript は慣れておくと便利そうだけどあんまりよくわかっていないので早めに仲良くなっておきたいなと思っています

最近のブラウザは外国語のページを開くと「翻訳しますか?」と聞いてくれたりしますよね?
ここでの翻訳は誰がやっているのかというと、ブラウザ自身が行なっているわけではなく外部リクエストを行い翻訳している場合が多いようです(Firefox はローカルらしい)
試しに iPhone の Chrome で適当な英語のサイトを開いた上で機内モードにしてみると、ブラウザメニューの翻訳がグレーアウトして押せなくなっており、機内モードを解除すると翻訳できるようになっていることが確認できます

今日は、色々条件はありつつも外部サービスに依存せずにブラウザ単独で言語の検出や翻訳ができるようになったと聞いたので実際に試してみました
自身のリソースのみで手軽に翻訳できるのが当たり前の時代になったときに世の中がどのように変わっていくのか、その一旦が垣間見えるような気がして興味深かったです
実は結構前から開発者用のバージョンには入っていたようですが、一般向けに降ってきたのは結構最近のようです
ブラウザ単体での言語検出や翻訳する機能を試してみる中で、以下の2点の知見が得られたので、今回はそれについて書いていきます

  • Chrome の言語検出、翻訳機能の使い方
  • 自身のウェブサービスに組み込むメリット・デメリット

作ったものは以下のリンクにあるので遊びながら読んでみてください

https://takuyayukat.github.io/translator-demo

前提、注意事項

どんなデバイスのどんなブラウザでも使えるわけではなく現在は PC 版の Chrome のみで利用可能な実験的な機能であることに注意が必要です
インスタンスに関するメソッドや引数の使用は将来的に変更される可能性があるため、エラーハンドリングには気を使う必要があります
仕様が変更された箇所のエラーハンドリングが漏れていた場合、ユーザーのブラウザがクラッシュしたりエラー画面が表示され、ユーザーの離脱や大量の問い合わせ、悪い評価につながる可能性があります
可能ならエラーを検知して開発者に通知する仕組みを入れておくと良いでしょう

対応ブラウザ

chrome for developers の Translator API - はじめに によると以下のように書かれています
こちらの環境で試せる範囲で確認したところ 138 では動作せず、現在の一般向けにリリースされている最新版の 139 では動作しました
今 Chrome を開いて更新しろと出ていなければもう 139 以上になっていると思います

Translator API は Chrome 138 安定版から利用できます。

また MDN Web Docs の Translator and Language Detector APIs - Browser compatibility によるとブラウザごとの対応状況は以下のようになっています

Browser compatibility

見事に Chrome のみ対応していることがわかりますね

また、この機能は実験的な機能であり、本番環境での利用には注意が必要という記述があります
本格的に何か作っている場合に本番環境に入れるにはなかなか勇気が必要ですね

Note

ハードウェア要件

Translator API - ハードウェア要件を確認する によると以下のようなハードウェア要件があるようです

  • 言語検出 API と翻訳 API は、パソコン版 Chrome で動作します。これらの API はモバイル デバイスでは動作しません。Prompt API、Summarizer API、Writer API、Rewriter API は、次の条件を満たす場合に Chrome で動作します。
  • オペレーティング システム: Windows 10 または 11、macOS 13 以降(Ventura 以降)、Linux、または Chromebook Plus デバイスの ChromeOS(プラットフォーム 16389.0.0 以降)。Chromebook Plus 以外のデバイスの Android 版 Chrome、iOS 版 Chrome、ChromeOS 版 Chrome は、Gemini Nano を使用する API でまだサポートされていません。
  • ストレージ: Chrome プロファイルを含むボリュームに 22 GB 以上の空き容量がある。
  • GPU: 4 GB を超える VRAM。
  • ネットワーク: 無制限のデータ通信または従量制でない接続。

新しめの PC であれば問題なく動作すると思いますが、古い PC やストレージが逼迫している場合は注意が必要ですね
ストレージに関しては新しい言語のペアで翻訳しようとするとモデルのダウンロードが入るためその辺も含めた要件なのかなと思います
こちらで試してみたところ、 N200 というかなり低価格のミニ PC でも動作することが確認できました

実際に言語検出と翻訳をするページを作ってみる

適当なアプリを Vue で作成して Github Pages でホスティングしました

チャットのようなサービスをイメージして何か新しい文字列が来た時に言語の検出と指定した言語への翻訳を行ないます
入力フォームは翻訳先言語の指定と対象とする文字列です

自分の持っているウェブサービスに組み込む場合は変数で文字列を持つことができますが、自分の持ち物ではないウェブサービスで動作する Chrome の拡張機能を作りたいなどといった場合には observer を仕込んで DOM の変化を監視して翻訳したい文字列を取得するなどの工夫が必要になるかもしれません

とりあえず見た目はこんな感じ

UIイメージ

全体

ソースコードはこんな感じ

style は省略

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'

const version: string = VITE_APP_VERSION || 'unknown'

// 型定義
type supportedLanguage = string // RFC 5646 言語タグ (例: 'en', 'ja', 'fr')

interface LanguageDetector {
  detect: (text: string) => Promise<{ confidence: number; detectedLanguage: string }[]>
  destroy: () => Promise<void>
}

interface Translator {
  translate: (text: string) => Promise<string>
  destroy: () => Promise<void>
}

interface TranslatorOption {
  sourceLanguage: string
  targetLanguage: string
  monitor?: (monitor: CreateMonitor) => void
}

interface DetectorOption {
  expectedInputLanguages: string[]
}

interface CreateMonitor {
  addEventListener: (event: string, callback: (e: { loaded: number }) => void) => void
}

interface Message {
  text: string
  sourceLanguage: supportedLanguage
  targetLanguage: supportedLanguage
  translatedText: string
}

declare global {
  interface Window {
    LanguageDetector: {
      availability: (option?: DetectorOption) => Promise<string>
      create: (option?: DetectorOption) => Promise<LanguageDetector>
    }
    Translator: {
      availability: (params: TranslatorOption) => Promise<string>
      create: (params: TranslatorOption) => Promise<Translator>
    }
  }
}

const windowAI = window as Window

// リアクティブ変数
const inputText = ref('')
const messageLimit = 20
const messages = ref<Message[]>([
  {
    text: 'This is a sample.',
    sourceLanguage: 'en',
    targetLanguage: 'ja',
    translatedText: 'これはサンプルです。',
  },
])
const targetLanguage = ref<supportedLanguage>('ja')
const detectorAvailability = ref<string>('not checked')
const detectorReady = ref<boolean>(false)
const detector = ref<LanguageDetector | null>(null)
const downloadProgress = ref<number>(0)
const isDownloading = ref<boolean>(false)
const translating = ref<boolean>(false)
const isTranslationSupported = ref<boolean>(false)

// 初期化関数
const initialize = async () => {
  if ('LanguageDetector' in windowAI) {
    const option = { expectedInputLanguages: ['en'] }
    detectorAvailability.value = await windowAI.LanguageDetector.availability(option).catch(
      (error) => {
        console.warn(error)
        return 'unavailable'
      },
    )
    if (detectorAvailability.value === 'available') {
      detector.value = await windowAI.LanguageDetector.create(option).catch((error) => {
        console.warn(error)
        return null
      })
      detectorReady.value = detector.value !== null
    }
  }
  let translatorAvailability: boolean = false
  if ('Translator' in windowAI) {
    const tmpOption = { sourceLanguage: 'en', targetLanguage: 'ja' }
    translatorAvailability =
      (await windowAI.Translator.availability(tmpOption).catch((error) => {
        console.warn(error)
        return 'unavailable'
      })) !== 'unavailable'
    if (!translatorAvailability) {
      console.warn('Translator is unavailable with option:', tmpOption)
    }
  }

  isTranslationSupported.value = detectorReady.value && translatorAvailability
}

// UI イベントハンドラ
const onSubmit = async () => {
  if (isDownloading.value || translating.value) return

  translating.value = true

  const params: Omit<Message, 'sourceLanguage' | 'translatedText'> = {
    text: inputText.value,
    targetLanguage: targetLanguage.value,
  }
  const newMessage = await detectAndTranslate(params)

  messages.value = [newMessage, ...messages.value].slice(0, messageLimit)

  translating.value = false
  inputText.value = ''
}

// メイン翻訳処理
const detectAndTranslate = async (
  params: Omit<Message, 'sourceLanguage' | 'translatedText'>,
): Promise<Message> => {
  let sourceLanguage = 'unknown'
  if (!isTranslationSupported.value) {
    return { ...params, sourceLanguage, translatedText: '(ブラウザが未対応です)' }
  }

  if (!detectorReady.value) {
    return { ...params, sourceLanguage, translatedText: '(言語検出が初期化されていません)' }
  }

  if (!inputText.value.trim()) {
    return { ...params, sourceLanguage, translatedText: '(入力が空です)' }
  }

  sourceLanguage = await detectLanguage(params.text).catch((error) => {
    console.warn(error)
    return 'unknown'
  })

  if (sourceLanguage === params.targetLanguage) {
    return { ...params, sourceLanguage, translatedText: '(翻訳は不要です)' }
  }

  const translatedText = await translateText({
    ...params,
    sourceLanguage,
  }).catch((error) => {
    console.warn(error)
    return '(不明なエラーが発生しました)'
  })

  return { ...params, sourceLanguage, translatedText: translatedText }
}

// Web API 関数群
const detectLanguage = async (text: string): Promise<string> => {
  if (!detector.value) throw new Error('LanguageDetector not initialized.')
  if (!text) throw new Error('No text to detect language.')

  const results = await detector.value.detect(text)
  return results[0].detectedLanguage
}

const checkTranslatorAvailability = async ({
  sourceLanguage,
  targetLanguage,
}: Omit<Message, 'text' | 'translatedText'>): Promise<string> => {
  return await windowAI.Translator.availability({ sourceLanguage, targetLanguage })
}

const translateText = async ({
  text,
  sourceLanguage,
  targetLanguage,
}: Omit<Message, 'translatedText'>): Promise<string> => {
  const option: TranslatorOption = { sourceLanguage, targetLanguage }
  const availability = await checkTranslatorAvailability(option).catch((error) => {
    console.warn(error)
    return 'unknown'
  })

  if (availability === 'unavailable') return '(この言語ペアは翻訳できません)'
  if (availability === 'unknown') return '(翻訳APIで使用するオプションが不正です)'

  const updateDownloadProgress = (monitor: CreateMonitor) => {
    isDownloading.value = true
    downloadProgress.value = 0

    monitor.addEventListener('downloadprogress', (e: { loaded: number }) => {
      downloadProgress.value = Math.round(e.loaded * 100)
      if (e.loaded >= 1) {
        isDownloading.value = false
      }
    })
  }
  if (availability === 'downloadable') option.monitor = updateDownloadProgress

  const translator = await windowAI.Translator.create(option).catch((error) => {
    console.warn(error)
    return null
  })
  if (!translator) return '(翻訳インスタンスの初期化に失敗しました)'

  const result = await translator.translate(text).catch((error) => {
    console.warn(error)
    return '(翻訳中にエラーが発生しました)'
  })
  translator.destroy()
  return result
}

onMounted(() => {
  initialize()
})

onBeforeUnmount(async () => {
  if (detector.value) await detector.value.destroy()
})
</script>

<template>
  <div class="app">
    <div class="header">
      <span class="version"> ver.{{ version }} </span>
    </div>

    <section class="input-section">
      <h2>Input</h2>
      <form @submit.prevent="onSubmit" class="translate-form">
        <div class="input-group">
          <label>Target Language:</label>
          <input
            v-model="targetLanguage"
            placeholder="e.g., 'ja' for Japanese"
            class="text-input"
          />
        </div>
        <div class="input-group">
          <label>Input Text:</label>
          <input v-model="inputText" placeholder="Enter text to translate" class="text-input" />
        </div>
        <button type="submit" :disabled="!inputText.trim() || isDownloading || translating">
          {{ isDownloading ? 'Downloading...' : translating ? 'Translating...' : 'Translate' }}
        </button>
      </form>

      <!-- Progress Bar -->
      <div v-if="isDownloading" class="progress-container">
        <div class="progress-label">Downloading translation model...</div>
        <div class="progress-bar">
          <div class="progress-fill" :style="{ width: `${downloadProgress}%` }"></div>
        </div>
        <div class="progress-text">{{ downloadProgress }}%</div>
      </div>
    </section>

    <section class="messages-section">
      <h2>Messages</h2>
      <div class="messages">
        <template v-for="(message, index) in messages" :key="index">
          <hr v-if="index !== 0" />
          <div class="message">
            <div class="original">
              <span class="lang">{{ message.sourceLanguage }}</span>
              <strong>{{ message.text }}</strong>
            </div>
            <div class="translated">
              <span class="lang">{{ message.targetLanguage }}</span>
              <em>{{ message.translatedText }}</em>
            </div>
          </div>
        </template>
      </div>
    </section>
  </div>
</template>

では、具体的にどのコードで何をしているかみていきます

言語の検出

ブラウザが LanguageDetector に対応しているかの確認

if ('LanguageDetector' in self) {
  // self(window) に LanguageDetector が存在するか確認
  // 存在する場合は対応ブラウザを利用中
}

LanguageDetector が利用可能かの確認

self に LanguageDetector がある場合は availability メソッドで指定したい option を含めた条件で利用可能かの確認ができます
optionexpectedInputLanguages に検出したい言語を複数指定することができ、入力される言語がある程度予測できる場合には optionexpectedInputLanguages で指定することで精度の向上が期待できそうです
返り値は available, unavailable, downloadable, downloading のいずれかの文字列になります
available の場合は create メソッドで detector の初期化するとすぐ使うことができます
downloadable, downloading の場合はダウンロードが完了するまで使えないのでなにかしらのハンドリングしましょう
また、今回は問答無用で初期化フローの中で languageDetector を作成しているため即座にダウンロードが発生する可能性があります
実際にユーザーが利用するサービスではユーザーの操作を持ってダウンロードを開始するなどした方が良さそうです

declare global {
  interface Window {
    LanguageDetector: {
      availability: (option?: DetectorOption) => Promise<string>
      create: (option?: DetectorOption) => Promise<LanguageDetector>
    }
    Translator: {
      availability: (params: TranslatorOption) => Promise<string>
      create: (params: TranslatorOption) => Promise<Translator>
    }
  }
}

const windowAI = window as Window

const option = { expectedInputLanguages: ["en"] } // option = undefined と等価
const detectorAvailability = await windowAI.LanguageDetector.availability(option).catch(
  (error) => {
    console.warn(error)
    return 'unavailable'
  },
)

ここで無効な言語コードを指定した場合以下のようなエラーになりました
有効なBCP 47言語タグ( RFC 5646で規定) から必要な言語コードを選ぶと良いようです

Uncaught (in promise) RangeError: Failed to execute 'availability' on 'LanguageDetector': Invalid language tag: hoge

もしユーザーに expectedInputLanguages を選ばせるような UI を作る場合は、無効な言語コードを指定されないように注意が必要ですね
LanguageDetector.availabilitycatch でエラーを拾ってなにかしらのハンドリングして unavailable を返すなど必要に応じた処理を行いましょう

detector の初期化

available なことが確認できたら、次は同様の option を指定して create メソッドで detector の初期化を行います
基本的には同じ option を使えば後続の create メソッドも成功するはずですが、実験的な機能ということもあり、仕様変更などによるエラーを catch で拾っておくと良いでしょう

detector = await windowAI.LanguageDetector.create(option).catch(
  (error) => {
    console.warn(error)
    return null
  },
)

実際に言語を検出してみる

これでやっと言語の検出が可能になりました
使ってみましょう

const inputText = 'hogehoge'
const results = await detector.detect(inputText).catch(
  (error) => {
    console.warn(error)
    return []
  },
)
console.log('Language detection results:', results)

結果は confidencedetectedLanguage のオブジェクトが confidence の降順で返ってくるようです
特にこだわりがなければ1個目の detectedLanguage を採用すれば良さそうです
ここまで来たのにエラーになった場合にどうするかは実装によるかなと思います
今回は catch でエラーを拾って空配列を返すようにしました

[
  {confidence: 0.3907545208930969, detectedLanguage: 'en'},
  {confidence: 0.09575021266937256, detectedLanguage: 'af'},
  {confidence: 0.07058317959308624, detectedLanguage: 'de'},
  {confidence: 0.06282620877027512, detectedLanguage: 'sk'},
  {confidence: 0.06182785704731941, detectedLanguage: 'fy'},
  {confidence: 0.048887163400650024, detectedLanguage: 'ja-Latn'},
  {confidence: 0.026718324050307274, detectedLanguage: 'ku'},
  {confidence: 0.02259100414812565, detectedLanguage: 'pt'},
  {confidence: 0.01949344016611576, detectedLanguage: 'lb'},
  {confidence: 0.018723944202065468, detectedLanguage: 'nl'},
  ...
]

LanguageDetector インスタンスの破棄

不要になったら destroy メソッドでインスタンスを破棄します
今回は expectedInputLanguages にはデフォルトの ["en"] を指定しており、このオプションを変更する必要はないため使いまわしています
ページ遷移などで不要になったタイミングで忘れずに破棄しておきたいですね
今回は onBeforeUnmount で破棄しました
Promise を返すので同期的に処理する必要があれば await したり、破棄後の処理があれば then で続けたりできそうです

detector.destroy()

言語検出により、その文字列が一体何語で書かれているのか推測することができました
これでやっと翻訳する準備が整いました
次に、検出した言語から指定した言語に翻訳してみましょう

言語検出結果を使って文字列を翻訳してみる

ブラウザが Translator に対応しているかの確認

翻訳も同様に Translator が存在するかの確認は以下のように行うことができます

if ('Translator' in self) {
  // self(window) に Translator が存在するか確認
  // 存在する場合は対応ブラウザを利用中
}

Translator が利用可能かの確認

ここも同様に、availability メソッドで指定したい option を含めた条件で利用可能かの確認ができます
optionsourceLanguage, targetLanguage に翻訳したい言語ペアを指定します
指定する言語ペアが利用可能な場合は available、未対応やダウンロード可能な場合は LanguageDetector と同様に unavailable, downloadable, downloading のいずれかの文字列になります
無効な言語コードを指定した場合には LanguageDetector と同様にエラーになるので必要に応じてハンドリングしましょう

const windowAI = window as Window

const option = { sourceLanguage, targetLanguage }
const result = await windowAI.Translator.availability(option).catch(
  (error) => {
    console.warn(error)
    return 'unknown'
  },
)

translator の初期化

unavailable でないことが確認できたら、次は同様の option を指定して create メソッドで translator の初期化を行います
downloadingdownloadable な場合なにかしらのハンドリングが必要ですが、LanguageDetector と同様に今回は気にしません

ここで、言語コードには zh-Hant-TW のような細かい指定も可能なようですが、create で作成されたインスタンスを見ると zh-Hant のように内部で利用可能なコードに変換されていました
「複数の Translator インスタンスを保存しておき、状況に応じて後で使う」などの処理を行ない場合はインスタンスを作成済みなのに完全一致で検索すると見つからないということが起こり得るので注意が必要です

また、optionmonitor にコールバック関数を渡すことでダウンロードの進捗を取得できたため、一応申し訳程度にプログレスバーを出してみました

const updateDownloadProgress = (monitor: CreateMonitor) => { ... }

const option: TranslatorOption = {
  sourceLanguage,
  targetLanguage,
  ...(availability === 'downloadable' && { monitor: updateDownloadProgress }),
}
const translator = await windowAI.Translator.create(option).catch(
  (error) => {
    console.warn(error)
    return null
  },
)

if (!translator) return '(翻訳器の初期化に失敗しました)'
const result = await translator.translate(text).catch(
  (error) => {
    console.warn(error)
    return '(翻訳中にエラーが発生しました)'
  },
)

実際に翻訳してみる

言語ペアを指定して Translator インスタンスを作成し、作成したインスタンスの translate メソッドに翻訳したい文字列を渡すと翻訳した文字列が返ってきます

const text = 'this is a sample'
const translatedText = await translator.translate(text)
console.log('Translation result:', translatedText)
Translation result: これはサンプルです

翻訳した文字列が返ってきました

Translator インスタンスの破棄

不要になったら destroy メソッドでインスタンスを破棄します
同じ言語ペアで何度も使うようであれば破棄せずに使い回しても良さそうです
一度でも使用した言語ペアの translator をずっと保持しておくとメモリを圧迫するなどの影響が出そうな気もしますがどうなんでしょうか?
今回は使うたびに破棄しました

translator.destroy()

自分のウェブサービスに組み込む場合のメリット・デメリット

メリット

主に外部サービスに依存しないことによるメリットが大きいと思います

  • コンテンツを外部へ送信しないためプライバシー・セキュリティ面で安心感がある
  • 比較的容易に実装ができる
  • API キーの管理が不要
  • API 利用料がかからない
  • 外部サービスの障害の影響を受けない
  • 外部サービスの利用制限を気にしなくて良い

デメリット

ブラウザ対応状況やハードウェア要件、ユーザーがちゃんとアップデートしてくれているかなど、開発者のコントロール外の箇所に依存する点が挙げられます

  • 対応ブラウザが PC 版 Chrome のみである
  • ユーザーがちゃんとアップデートしてくれていない場合は使えない
  • ユーザーが Chrome 以外のブラウザを使っている場合は使えない
  • ユーザーの PC がハードウェア要件を満たしている必要がある
  • ユーザー側の状況によりモデルのダウンロードにある程度の通信が発生する

また、使用できるメモリや CPU/GPU のリソースが限られるため、大規模なモデルを使用できる外部サービスに比べて翻訳の品質が劣る可能性があります
ここに関してはどれほどの違いがあるかは定量的な比較は困難なこともあり、検証できていません

その他

いくつかウェブサービスを作って公開していると時折問い合わせが届いたりしますが、世の中には頑なにアップデートを拒否していたり「アプデあるからリロードしてね」とページ上に通知を出しても更新をしてくれない人が一定数います
また、自分が使っているブラウザは Chrome だと思っていたけど実は Edge などというケースもありました

不特定多数の利用が想定されるウェブサービスに導入する場合は、以下のような対応をする必要がありそうです

  • 未対応の環境用のフローを用意する
    • 対応ブラウザ、ハードウェアを使うように促す
      • 未対応な環境であることを表示
      • Chrome でない場合、古い Chrome の場合に「このブラウザでは動作しません。最新版の Chrome にして」など表示
    • 別途外部サービスを使って翻訳する機能もサポートする
      • 開発者が負担?
        • コストがネック
      • ユーザーが負担?
        • APIキーなどの設定項目を保存するための仕組みの整備がネック
        • 秘匿情報の管理や漏洩時のリスクがネック
  • 未対応な環境のユーザーを切り捨て、対応する環境を強制する
    • 強い意志が必要
    • ユーザーからの心象が悪い可能性がある
    • デバイスやブラウザに強く依存し、Web の強みが薄れる

実装時の注意点

  • 実験的な機能であるため、仕様変更や廃止の可能性があることに注意し、エラーハンドリングに気を遣う必要がある
  • Translator インスタンス作成時に zh-Hant-TW など細かく指定しても作成されたインスタンスの言語コードでは zh-Hant になるなど、指定したコードとの完全一致は期待してはいけないことには注意が必要
  • ダウンロードが発生する可能性があることに注意し、ユーザーの操作でダウンロードを開始したりプログレスバーを表示するなどの配慮がほしい

まとめ

Chrome がブラウザ単独で言語検出や翻訳ができるようになったという話を聞いたので試してみました
比較的容易に言語検出や翻訳ができるようになり、ウェブサービスに組み込む難易度自体はそれほど高くなさそうです
利用できる言語ペアも多く、その場で必要に応じて言語ペアを柔軟に指定できるのも良いですね
特に外部サービスに依存しないため、プライバシー・セキュリティ面での安心感や API キーの管理や利用料、外部サービスの障害の影響を受けないなどのメリットがあります
しかしながら、対応ブラウザが PC 版 Chrome のみであることや、一般ユーザー向けとしては最近やっと降ってきた機能であるためユーザーがちゃんとアプデしてくれていない場合があること、ハードウェア要件を満たしている必要があること、モデルのダウンロードにある程度の通信が発生することなど、開発者のコントロール外の要素が多く、その辺りを考慮する必要がありそうです
現状では BtoC 向けのサービスに組み込むにはまだ早く、ユーザーへブラウザを強制できるようなサービス(知り合いしか使わないもの、社内向けサービスなど)に使う程度に止めるか、未対応の環境用のフローを用意した上で組み込むなどが考えられそうです

やってみてね

参考リンク:

GitHubで編集を提案
Social PLUS Tech Blog

Discussion