「この翻訳、また手作業ですか？」同僚のため息を解決したGAS自動翻訳ツール開発奮闘記（非開発職の試行錯誤と成長の記録）

## はじめに：同僚の不便を解決するための小さな挑戦、その始まり

こんにちは。リスクマネジメント業務を担当していますが、データ分析や業務自動化・効率化にも積極的に関わっている社会人3年目のHNGSNGGNと申します。GitHubやQiitaに記事を投稿するのは初めてなので、至らない点も多いかと思いますが、同じような悩みを持つ方々の少しでもお役に立てればと思い、勇気を出して共有します。

私が今日共有するお話は、Google Apps Script（以下GAS）を活用して作成した「API/決済不要！Googleスプレッドシート自動翻訳・更新ツール」の開発記です。

私の会社は韓国に本社があり、本社から共有される多くのGoogleスプレッドシート資料は韓国語で作成されています。日本の同僚たちはこれらの資料を業務で活用するために、毎回翻訳作業が必要で、その過程は非常に非効率的でした。

- `IMPORTRANGE`を使っても、元シートの行/列の非表示やセル内の画像が正しく反映されない問題がありました。
- 韓国人メンバーが手動で翻訳するにも、全てのシートを翻訳するには業務負荷が大きすぎ、元シートに変更があるたびに再翻訳が必要という困難がありました。

結局、多くの日本人同僚は重要な内容だけを特定のセルからコピーし、Papagoなどの翻訳サイトに貼り付けて業務を行っていました。その姿を見て「これはちょっと違うな…同僚たちがもっと楽に働ける方法はないだろうか？」と思ったのです。

「同僚の不便を解消しよう！」そして「このような単純な反復作業は自動化できるのではないか？」という好奇心が、このプロジェクトの始まりでした。ここでの私の最大の目標は「使いやすさ」でした。もちろん、Pythonや外部サーバーを使えばより強力なツールを作れたかもしれませんが、コーディングに詳しくない同僚たちが複雑な設定のために使わなくなっては意味がないと考えました。そのため、「Googleスプレッドシート」だけで、「無料」で、そして「簡単にコピーしてすぐに使える」ことを目指しました。

実はGAS自体は、以前にも簡単なデータ経由作業や、趣味で楽しんでいるゲーム「勝利の女神：NIKKE」のユニオンレイド管理シートを作る際に少し触った経験はありました。しかし、これほど多くの機能を備えた「ツール」と呼べるものを本格的に作るのは初めてだったので、ワクワクと同時に不安も大きかったです。

---

## Version 0.1：「とりあえず押してみてください！」 - 段階的実行方式の誕生

本格的なツール制作経験がない私が手探りで作った最初のバージョンは、今見るとかなり未熟なものでした。
アイデアはシンプルでした。「元シートの書式はそのままに、翻訳された値だけを入れ替える！」

1.  元シートのタブを`copyTo()`でコピーしてくる。（書式維持！）
2.  コピーしたシートの内容を`clearContent()`で全て消す。
3.  A1セルに待望の数式を入力！
    `=MAP(IMPORTRANGE("元ID", "元シート!全範囲"), LAMBDA(cell, IF(cell="", "", GOOGLETRANSLATE(cell, "ko", "ja"))))`

しかし、現実は甘くありませんでした。
- 日付が数字に！ `IMPORTRANGE`だけならある程度維持された日付書式も、`GOOGLETRANSLATE`や`MAP`関数と組み合わせた瞬間、日付は決まって`45755`のようなシリアル値に変わってしまいました。この問題を解決するため、リアルタイム翻訳ではなく、ボタンを押すたびに値と書式を一緒に復元する手動更新方式に方向転換することになりました。
-GAS 6分実行制限の壁！ 続けてタブを数個処理すると、すぐにタイムアウトエラーが発生しました。



結局、ユーザーがメニューを複数の段階に分けて順にクリックしなければならない方式で妥協するしかありませんでした。

![image (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4057520/b2c8f4c8-10e6-444d-9b88-f4fa44c153d0.png)


このバージョンを恐る恐る日本人の同僚に見せたところ、驚くべきことに「最高です！」「実用的です」というポジティブなフィードバックをいただきました。もちろん、改善点も明確でした。

- 「メニューが複雑すぎる」: はい、ユーザーが1,2,3と順番にクリックしなければなりませんでしたから。（しかし、この時の段階的実行のアイデアは、後に「高速実行」モードの基盤となりました。）このフィードバックが、「ワンクリック実行」の夢を再び追いかける決定的なきっかけとなりました。
- 「処理時間を事前に教えてほしい」: 現在の実行前確認ポップアップ、R16進行状況ログのアイデアはここから生まれました。

このフィードバックを受け、「これは本気でちゃんと作ってみなければ」という強いモチベーションを得ました。

---

## Version 1.0への道：6分の壁、APIとの駆け引き、そして終わらないデバッグ

「ワンクリック」という目標は思った以上に険しい道のりでした。最大の障壁はやはり「GASの6分実行時間制限」であり、無料版の限界の中で`GOOGLETRANSLATE`や`IMPORTRANGE`といった繊細なAPIとの綱引きが始まりました。（実はこの頃からChatGPT、Gemini、Perplexityなど様々なAIに「GASでこんなものを作りたいんだけど、どうすればいい？」と聞きながら本格的にコードを書き始めました。私の乏しいコーディングスキルをAIが大いに補ってくれました。）

### 最初の難関：「トリガーチェーンなら何でもできると思った」- 暴走するトリガーと「ロード中...」地獄

「ワンクリック」を実現するために最初に試したのは「トリガーチェーン」でした。シート処理の作業を個別の関数に分け、一つが終わると次の関数を時間ベーストリガーで呼び出す方式です。「すごい、これなら6分制限なんて関係ないじゃないか！」と、簡単に解決できると思っていました。

- **第1次試行（全トリガー登録）:** `for`ループで全タブのトリガーを一括登録 → 失敗。（トリガー数の上限20個に抵触）
- **第2次試行（ブロックシステム）:** 10タブずつまとめて処理しよう！ → 失敗。（10タブでも同時API呼び出しに耐えられず） → じゃあ4～5タブずつ？ → とりあえず実行はされるものの、今度はGoogle APIが悲鳴を上げ始めました。
    - **「ロード中...」無限ループ！** 同時に複数のトリガーがAPIを呼び出すと、一部のタブは翻訳途中で止まり、一部は永遠に「ロード中...」のままフリーズしました。（これが最もデバッグが難しく、長く悩んだ部分かもしれません。最初の数タブはうまくいくのに、ある時点から突然止まってしまうため、原因究明に数日かかりました。トリガーは無敵だと思っていたのに、思った以上に不安定なやつでした。）
    - **無限トリガーバグ！** 状態管理がこじれ、特定のタブが延々とトリガーを呼び出し続ける無限ループに陥ることもありました。

当初は`block2`（全タブの翻訳数式を一時シートに入力）を全て終えてから`block3`（翻訳された値を実際のシートにコピー）を進める並行作業が効率的だと考えていましたが、同時API呼び出し制限という壁にぶつかりました。

結局、「速度」より「安定性」を選ぶしかありませんでした。数えきれないほどのテストとAIとの協業の末、1タブにつき1つのトリガーで順次実行し、各タブの`block2`と`block3`をまとめて処理する方式（現在の「低速実行」モード）に方向転換しました。

### 第2の難関：`GOOGLETRANSLATE`呼び出し最小化 - バッチ翻訳、その甘い誘惑と壮絶な失敗

順次実行で安定性は確保しましたが、依然として`GOOGLETRANSLATE`関数の「ロード中...」問題は悩みの種でした。この問題はブロックあたり4～5タブを同時に処理する際に`GOOGLETRANSLATE`の呼び出しが過剰になることで発生していました。そこでAPI呼び出しを最小限に抑える方法を模索し、「バッチ翻訳」というアイデアに挑戦しました。複数のセルのテキストを`JOIN`で一つにまとめ、一度に翻訳してから`SPLIT`で分割する方式です。

- **区切り文字選定の困難:**
    - `CHAR(10)`（改行）: 元のセルに改行が含まれているとデータが大惨事に！
    - HTMLタグ（`<SEP>`など）: `GOOGLETRANSLATE`が勝手に削除・変換してしまう！
    - 特殊文字の組み合わせ（`◆◆◆`など）: 同じ内容が連続すると、区切り文字を無視して一つの文章として翻訳してしまう！（データ損失）
- **結論:** バッチ翻訳はきっぱり諦めました。無料で提供される`GOOGLETRANSLATE`関数だけでは安定した実装は困難だと判断。結局、1タブ処理方式に戻り、`MAP(IMPORTRANGE(...), LAMBDA(cell, ...))`構造を維持しつつ、空白セルは翻訳しないように条件を追加してAPIの負荷を最小限に抑えました。

「ロード中...」問題は`SpreadsheetApp.flush()`と`Utilities.sleep()`を適材適所に配置し、`hasLoadingOrErrorStrings`関数によるポーリングで数式計算の完了を粘り強く確認することで対応しました。

### 第3の難関：消える画像と書式との戦い (Version 3の始まり)

API呼び出し問題を解決すると、今度はディテールが気になり始めました。画像と書式の保持問題です。

![image (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4057520/c842f25e-6768-4d8a-9728-d268802c8e44.png)

- **書式問題の解決:** `copyTo(targetRange, {formatOnly:true})`は神の一手。しかし！ 異なるファイル間では機能しない。 結局、元シートを現在のドキュメントに一時的に丸ごとコピーする`_format_tmp`シートを作成し、そこから書式をコピーして適用する方式で解決しました。
- **画像保持問題:**
    - AIは「A1セルが空白で、セル内画像があるB1セルに`setValue`すればB1の画像は残る」と言っていましたが、テスト結果は画像の削除でした。
    - 数多くのテストの末、GASの`setValue`はどんな値を入力してもセル内画像を削除するという痛い事実を学びました。
    - 最終的な解決策は、`IMPORTRANGE`で画像を取得できず`#REF!`エラーが出るセルは、翻訳数式自体で空白("")として処理し、値のコピー時に`isErrorCell`で該当セルは`setValue`をスキップすることで、元シート（コピーされた`copied`シート）の画像をそのまま残すというものでした。パフォーマンスのため、列ごとに`#REF!`の有無を確認し、`#REF!`がない列は`setValues`で高速に、ある列だけセルごとに`#REF!`でない値のみ`setValue`する方式に最適化しました。

### その他の細かいバグとの戦い

*   **シート並び替えの不具合:** `moveActiveSheet`を繰り返すと`引数が無効です`エラーが発生 → 並び替えロジックを改善。
*   **D列のタブ名重複:** 「공지사항」と「알림사항」がどちらも「お知らせ」に翻訳されてしまう問題 → `_ (2)`を自動で付加するロジックを追加。
*   **逆翻訳問題:** KOR→JPN翻訳時に、元々日本語だったテキストが韓国語に翻訳されてしまう → 「AUTO」（言語自動認識）オプションを追加。`AUTO`を使用すればこの問題は発生しないため、問題が発生する特定のタブはユーザーがメニューから手動で「AUTO」で更新するように誘導。
*   **非表示シートの処理:** 元シートで非表示になっているシートは最初から読み込まないように`isSheetHidden()`を追加。

---

## Version 2.0：「もっと速くできませんか？」に応えて - 二つの実行モードとユーザー利便性の最大化

順次トリガーチェーン方式（「低速実行」）は安定的でしたが、やはり「遅い」というフィードバックがありました。「数タブだけ早く修正したいのに、全部回さないといけないの？」という質問に応えるため、「高速実行（5分制限）」モードを追加することにしました。

![image (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4057520/e6d1e9e8-638a-464c-a7e7-0f4f2007c4a9.png)

- **開発背景とアイデア:**
    - 全てのタブを毎回完璧に翻訳する必要がない時や、特定のタブだけ急いで確認・修正したい時のための機能が必要だと感じました。
    - GASの6分実行時間制限を活用し、5分（300秒）以内にできるだけ多くのタブを順次処理し、時間切れになったら安全に中断して次回の実行に備える方式です。（初期バージョンの段階的実行のアイデアを発展させました！）
- **核心ロジック:**
    *   `startTime = new Date().getTime()`で関数の開始時間を記録。
    *   各タブを処理する前に`elapsedTime`で経過時間をチェック。
    *   `if (elapsedTime > (maxTime - 60000))`条件で、残りの実行時間が1分未満の場合（安全マージン60秒）、現在処理中のタブの一時ファイルを`cleanupCurrentProcessing()`関数で整理。
    *   そして`showPartialCompletionMessage()`関数で、ユーザーに「時間制限のため部分的に完了しました。最後の完了タブはOOOで、次回はXXXタブから開始されます（H23セルに自動設定）」と親切に案内。
- **ユーザー選択肢の強化:**
    - **「実行」ボタン（管理シート）:** 「高速実行（5分制限）」モード。H23セルで開始タブを指定するか、空欄の場合は最初から実行。
    - **「低速実行」ボタン（管理シート）:** 従来のトリガーチェーン方式。時間はかかるが全タブを確実に処理。
    - **「自動化の登録」（管理シート5番機能）:** 毎日指定した時間に「低速実行」モードでバックグラウンドで全タブを自動更新。リアルタイム反映ではありませんが、この機能のおかげでユーザーは毎朝最新の翻訳版を見ることができます。
    - **「現在タブ更新」メニュー（上部メニュー6番機能）:** 現在開いているタブだけを、管理シートで事前に設定した複数の言語ペア（最大4つ）から選んで即時翻訳・更新。よく見る重要なタブだけを素早く更新したい時に非常に便利です。
        - **多言語対応:** このメニューは単にKOR↔JPNだけでなく、韓国語、日本語、英語、中国語（簡体）、台湾語（繁体）間の多様な組み合わせをサポートします。
- **「使い方」シートの進化:** これらの多様な機能をユーザーが簡単に理解し活用できるよう、「使い方」シートも説明を補強し続けました。

---

## 開発を通じて学び感じたこと：非開発者でもできる！ (with AI & 根気)

このプロジェクトは私にとって単なるコーディング課題以上のものでした。通勤時間も、食事中も、シャワーを浴びている時でさえ「あ、こうすれば最適化できるかも？」と考えるほど没頭しました。 まるで解けない数学の問題をポケットの付箋に書いておき、何日も考え続けた学生時代のように、久しぶりに深い没入の楽しさを感じました。特に最後の10%のディテールとバグを修正する過程に最も多くの時間を費やしましたが、完成が目の前にちらついてやめられませんでした。

- **「本当の問題」を解決する楽しさ:** リスクマネジメント業務をしていますが、データ分析と業務自動化を通じて同僚の業務効率を上げることに貢献したいと考えていました。「ありがとう」「おかげで仕事が楽になった」という一言が大きな力になりました。
- **フィードバックは成長の糧、そしてコミュニケーションの重要性:** 同僚たちの肯定的なフィードバックと具体的な改善点のおかげで、プロジェクトを継続的に発展させることができました。特に日本人同僚の視点からのフィードバックは、UI/UX改善に決定的な役割を果たしました。
- **AIは強力な助っ人、しかし鵜呑みは禁物:** 正直、AIツールがなければこのツールは完成しなかったでしょう。しかし、AIが全てを解決してくれるわけではありませんでした。特定の複雑さを超える問題（画像保持問題のように！）に対しては、私が直接関数を勉強し、ロジックを理解して初めてAIに適切な修正を依頼できました。結局、AIを効果的に活用するためには、その回答を批判的に検討し、実際のテストで検証し、自分の問題に合わせて応用する能力が重要だと学びました。
- **「完璧さ」より「役立つこと」、そして「漸進的な改善」:** 最初から全ての機能を完璧に備えたツールを作ろうとしていたら、おそらく途中で挫折していたでしょう。コア機能から素早く実装し、実際のユーザーフィードバックを通じて優先順位を決め、一つずつ機能を追加・改善していく方がはるかに現実的だと気づきました。（ハイパーリンク保持機能はまだ「今後の改善点」として残っていますが、これもいつかは！）
- **ユーザーを考える開発:** 単に機能を実装するだけでなく、「これを誰が、どう使うのか？」「どうすればもっと簡単に使えるか？」を常に考えました。ユーザーに一度に多くの情報を与えるのではなく、必要な機能を必要な瞬間に自然に発見し活用できるように案内することが重要だと学びました。取扱説明書（「使い方」）を作成し、注意事項を追加し、エラーメッセージ一つ一つを日本人同僚にも分かりやすく修正する過程は、コードと同じくらいユーザーエクスペリエンス(UX)が重要であることを教えてくれました。
- **記録と共有の力、そして初めての挑戦:** 実はGitHubやQiitaに自分の制作物をアップロードし、このような長い文章を書くのは今回が初めてです。これまでは一人で作って一人で使うか、ごく親しい同僚にだけ共有するレベルでした。しかし、このプロジェクトを通じて経験した数多くの試行錯誤と問題解決の過程、そしてその中で得た小さな気づきが、もしかしたら私と同じような悩みを持つ非開発者の方々には小さな勇気を、開発者の方々には「ああ、現場ではこんな問題もあるのか」という小さな参考になるのではないかと思い、勇気を出してみました。（もしこの記事を読んで「あ、私も同じ問題で悩んだ！」という方がいらっしゃれば、コメントやGitHubイシューで教えてください！一緒に悩んで、より良い解決策を見つけていけたら嬉しいです。）

## アップデートログ

###  2025-06-17 – ハイパーリンク保持機能の追加
- 元ファイルに含まれる ハイパーリンク付きのセル情報が翻訳後に消えてしまう 問題に対応。
- `getRichTextValue()` と `setRichTextValue()` を用いて、翻訳テキストにリンク情報を再適用。
- 対応ロジック概要（抜粋）:

```javascript
if (srcRich && srcRich.getLinkUrl()) {
  var link = srcRich.getLinkUrl();
  var richVal = SpreadsheetApp.newRichTextValue()
    .setText(translatedVal)
    .setLinkUrl(link)
    .build();
  targetCell.setRichTextValue(richVal);
} else {
  targetCell.setValue(translatedVal);
}
```
---

## 終わりに

まだ至らない点も多く、今後改善すべき部分も残っていますが、この自動翻訳ツールが韓国本社と日本支社間の円滑なコミュニケーションに少しでも貢献し、私のようにGASとAIを活用して業務自動化に挑戦する方々の小さなインスピレーションになることを願っています。

- **GitHub Repository (ソースコード＆README):** [https://github.com/HNGSNGGN/gsheet-auto-translate-tool](https://github.com/HNGSNGGN/gsheet-auto-translate-tool)
- **Google Sheetsツール コピー用リンク:** [https://docs.google.com/spreadsheets/d/1iiGXb3FoQvdisdWWc9KplwzdHD07Ne1aYBnhvTI56ao/copy](https://docs.google.com/spreadsheets/d/1iiGXb3FoQvdisdWWc9KplwzdHD07Ne1aYBnhvTI56ao/copy)

長い文章を読んでいただき、ありがとうございました！どんなフィードバックでも大歓迎です。