みなさんこんにちは! Ryoです!
普段は学生NoCodeエンジニアとして、Bubbleをメインとした受託開発を行ったり、NoCode学生会という学生限定のNoCodeオンラインサロンの運営をしております。
今回はBubbleのサイトを多言語対応させる方法についてお伝えしていこうと思います。
今後世界に飛び出すサービスを制作する上で多言語対応は必須スキルになりますので、是非ともマスターしていただきたいです。
それでは早速やっていきましょう!!
現在のサイト言語を検出する
まずは現在のサイトの言語を検出していきましょう!
Bubbleエディタのプラグインタブから「Browser Timezone and Locale」プラグインをインストールしましょう。

こちらのプラグインは主にワークフローで扱うプラグインですが、エディタに設置しないと機能しないので
インストールが完了したら、エディタ上にエレメントを設置しましょう

これでBrowser Timezone and Localeプラグインが使えるようになりました。
次にワークフローでプラグインを使う設定をしていきましょう。

[Page is loaded]をトリガーとしてワークフローを作成し、[Go to Page]より現在のページ名を選択します。
この時、ワークフローの条件として[Get data from page URL]より
・Type:Parameter
・Parameter name:lang(他の単語でもOK)
・Type:text
と設定しましょう。
その後、ページ遷移時に[Send more parameter to the page]にてプラグインにて取得したユーザーの言語をlangに代入します。
これでユーザーの言語が検出できました。
続いていよいよ多言語対応です!
言語設定
それでは早速多言語対応をさせていきましょう!
まずはエディタのSettingsタブのLanguagesに移動します。

現在のみなさんの言語設定はこんな感じになっているかと思います。
現在言語設定が日本語になっていないという場合も今回はあまり支障ありませんのでご安心ください。
ここに特定のワードの翻訳コードを入れていきたいので設定していきます。

Designタブにテキストを一つ配置して[Insert dynamic data]より[App Text(?)]を選択します。
この時出てくるText IDに好きなIDを割り振ります。
今回は[title]というIDを割り振りました。

すると下にSettingsタブで翻訳設定をしろという指示が出てきます。
早速Settingsタブに戻ってみましょう!

下の翻訳設定の部分に先ほど設定したtitleの翻訳設定の項目が追加されていると思います。
ここに文字を入力し、各言語ごとに翻訳された文字を入力していけば、サイトの多言語化の完成です!
あとは根気で翻訳するだけですね!
Airtableを使ってもっと便利に翻訳設定!
先ほどの翻訳方法をみて「結局人力なんじゃ工数変わらないよ!!!!」と叫んだ方たくさんいると思います。
そんな方向けにお手軽に翻訳処理を自動化する方法を伝授しましょう!
まずは先ほど説明したApp Textの設定を一通り終わらせましょう。
このApp Textに入力している文字が翻訳の元になりますので、実際にサイトに表示させたい文字をApp Textに入力してください
通常の言語翻訳について不安が残るという方は、Bubbleエキスパートのけいさんが便利な翻訳CSVを作ってくれているのでそちらをご参考ください。
https://note.com/nocoder_k/n/n51cb0d475415

追加で3つほどApp Textを作っておきました。
App Textは今回は英語で書いてあります。
ここに関しては日本語で書いて全く問題ありません。
では、まずこちらの言語設定ファイルをエクスポートしてCSVファイルをダウンロードしていきます。
言語設定の上の[Export]ボタンをクリックして

どの言語ファイルをエクスポートするかの選択ができるので、特定の言語のみ翻訳したい場合はこちらから選択しましょう。
モンゴル語(mn-mn)とボスニア語(bs-ba)のみ今回の翻訳に対応できなかったのであらかじめチェックを外しておくと便利です。
(後から削除することも可能です)
選択が完了したらExportボタンをクリックすれば言語情報が入ったCSVファイルがダウンロードされます。
ダウンロードが終わったらAirtableへ移動しましょう。
Airtableとは?

AirtableとはExcelのような形のいわゆるスプレッドシートのような見た目をしているデータベースです。
データ入力がとっても簡単でそこからフォームを作ったりカレンダー形式で出力したり、アプリを作ったりがサクッとできちゃうので僕も愛用しているノーコードツールの一つですね。
今回はこちらのAirtableを使って簡単に翻訳処理を行っていきます。

新規登録が完了したい状態だと、このような画面になっていると思います。
まずは先ほどダウンロードしたCSVファイルを展開していきたいので一番右の[Add a base]をクリックします。


[Add a base]をクリックできたら、[Import data]をクリックして、データの種類を選択する画面でCSVファイルを選択します。
ファイル選択を促されるので、先ほどダウンロードしたCSVファイルを選択し、baseを作成していきましょう。

作成されたAirtable baseを開くとこのようになっていると思います。
たくさんの言語が並んでいてなんだが圧倒されますねw
ここから各言語への翻訳をかけていきます
Airtable x Google Translate APIで翻訳しよう
今回の翻訳作業ではAirtableとGoogle Translate APIを利用して翻訳作業をしていきます。
簡単に言ってしまえばAirtable上で各言語にGoogle翻訳するということですね。
まずはGoogleのAPI Keyを取得する必要があるので、Google developer consoleに行ってGoogle Translate APIを取得します。
Bubbleでサイトを公開するときは、Google API Keyを取得する必要があるので公式が出しているこの動画を参考にしながら、API Keyを生成してみてください。
Google Translate APIについてはAPIライブラリで[Cloud Translation API]を有効にすれば、上の動画で生成したAPI Keyをそのまま使うことができます。
APIが生成できたらAirtableに戻っていよいよ翻訳設定です。

先ほど作成したAirtableのbaseから右上にある[APPS]というアイコンをクリックして右側のサイドバーを出します。
[Apps give your base superpowers.]という文字とアイコンが出てくると思いますので、下の[Add an app]をクリックして写真左のようなポップアップを表示させましょう。
次にポップアップ右上の薄い紫色の[Scripting]を選択します。

Scriptingの説明などが出てきますが、一旦無視して右上のAdd appをクリックします。

するとおそらくこんな感じの画面が出てくると思います。
ここの左上のコード入力の部分に翻訳用のコードを入力し、翻訳処理を実行していきます。
「コ…コード!? コードなんて書けないよ!!」
という方、ご安心ください。もちろん使用するコードはこちらでご用意させていただきました。
//select source langauge using ISO 2 letter code
let source = "en";
// Translate API key
let key="YourAPIcodeHere"; // <= change your API here
// Select table
let table = await input.tableAsync('Select table');
// Get language list from columns
let fields = [];
for (let field of table.fields) {
if (field.name !== "plugin_name" && field.name !== "text_name" && field.name !== "text_code" ){
fields.push(field.name);
}
}
//console.log(fields);
//set counter for total
let counter = 0;
//iterate over all columns/fields with all languages
for (let field of fields){
//get records data for Name and selected Language only
let records = await table.selectRecordsAsync({fields:["text_code", field]});
//output.inspect(records);
//select records for selected language where language field is empty and
//text _code(translation text) exists
let nonEmptyRecords = records.records.filter(record => {
let target = record.getCellValue(field);
let source = record.getCellValue('text_code');
return !target && source;
});
//console.log(nonEmptyRecords.length);
//convert to simple array with records to translate
let recordsToTranslate = [];
for (let record of nonEmptyRecords)
{
recordsToTranslate.push({
"id" : record.id,
"name" : record.getCellValue("text_code")
});
}
let totalNumberToTranslate = recordsToTranslate.length;
if (totalNumberToTranslate === 0) {
output.markdown(`Skipping ${field} no missing translations.`);
continue;
}
output.markdown(`Translating ${totalNumberToTranslate} records to ${field}`);
//output.inspect(recordsToTranslate)
//prepare variables for http fetch request to API
let q = [];
let target = field.split("_")[0]; // changing locale ( BCP 47 ) to ISO 639-1
let url=`https://translation.googleapis.com/language/translate/v2?format=text&${source}=en&key=${key}&target=${target}`;
// batch translate and update records in increments of 50
while (recordsToTranslate.length > 0) {
//prepare batch of 50
let batch = recordsToTranslate.slice(0, 50);
q = batch.map(recordsToTranslate => recordsToTranslate.name);
//output.inspect(batch);
// json for fetch request
let data = {
"q": q
};
//fetch POST request with all params
let apiResponse = await fetch(url,{
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
//cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
//credentials: 'same-origin', // include, *same-origin, omit
//headers: {
// 'Content-Type': 'application/json'
// 'Content-Type': 'application/x-www-form-urlencoded',
//},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data) // body data type must match "Content-Type" header
});
//extract translation array from the response
let response = await apiResponse.json();
response = response.data.translations ;
//output.inspect(response);
//create batch of records to update
let updateRecords =[];
for (let i = 0; i < batch.length; i++) {
updateRecords.push({
id: batch[i].id,
fields: {
[field]: response[i].translatedText
}
});
}
//update airtable for empty records
await table.updateRecordsAsync(updateRecords);
recordsToTranslate = recordsToTranslate.slice(50);
output.text(`Updated ${totalNumberToTranslate-recordsToTranslate.length}/${totalNumberToTranslate} records`);
}
counter++;
}
output.markdown(`**Updated total ${counter} languages.**`);
こちらのコードをコピーして、先ほどのScriptingのコードの部分にペーストしてください。
デフォルトで書いてあるコードは消してしまってOKです。
4,5行目のtranslation APIの部分の[YourAPIcodeHere]の部分に先ほど生成したAPI Keyを代入したらコードの設定は完了です。
右側のRunボタンを押してコードを実行しましょう。
- 先ほどモンゴル語(mn-mn)とボスニア語(bs-ba)をそのままダウンロードしてしまった場合は実行前に対象の行を削除しましょう
- App Textを英語以外の言語で書いた場合は、2行目の[let source = “en”]のenの部分を設定した言語コードに置き換えましょう
(日本語の場合はjaです)

こんな感じで全体が翻訳されたら完了です。
翻訳が完了したら、翻訳後のCSVファイルを生成しBubbleにデータを渡しましょう。

CSVファイル生成前に上のようにApp Text以外の項目を全て削除してあげてください。
左上の3点リーダーをクリックして[Download CSV]をクリックすると自動的にCSVファイルがダウンロードされます。
無事にCSVファイルがダウンロードできたらBubbleのLanguages設定に戻りましょう。

先ほどの画面に戻ってきたら右上の[Import]をクリックします。

インポートするファイルを選択する画面になるので[Pick a file to upload]をクリックして先ほどダウンロードしたAirtableのCSVファイルを選択します。

ファイルが選択できたら[2 Validate data]というボタンが出てくるのでここをクリックします。

次に出てくる[3 Upload data]も躊躇わずにクリックです!

このポップアップが出てきたらインポート完了です!
これで先ほど翻訳処理したデータをBubbleに渡すことができました
こんな容量でApp Textの量をどんどん増やしていけば、多言語対応サイトの完成です!
ぜひ試してみてください!!
あとがき
いかがだったでしょうか
今回はBrowser Timezone and LocaleプラグインとAirtableとGoogle Translationなど結構多くのサービスを組み合わせたので少々難易度は高くなってしまったかもしれませんが、サイトの多言語対応は今後必ず必須テクニックになるのでぜひとも今回の記事で身につけて貰えばと思います。
今後もこんなプラグインの使い方や応用方法、Bubbleの知っておくと便利な豆知識などを発信していますのでぜひ他の記事も読んでみてください。
ためになった方はTwitterやFacebookなどで拡散お願いいたします。
それではまた次回の記事でお会いしましょう!!