中級者向け No.065

多言語翻訳ツール

テキストを入力して複数言語に翻訳するツール。無料APIを活用し翻訳履歴とクリップボードコピー付き。

🎯 難易度: ★★☆ 📦 ライブラリ: tkinter(標準ライブラリ)のみ ⏱️ 制作時間: 30〜90分

1. アプリ概要

テキストを入力して複数言語に翻訳するツール。無料APIを活用し翻訳履歴とクリップボードコピー付き。

このアプリはnetworkカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★★☆ です。

このアプリは「ネットワーク」カテゴリです。ネットワーク連携は Python の代表的な活用領域で、I/O 待ちと UI 更新の関係や非同期処理の考え方が他の Web API 連携にも直接活きます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。

コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。

応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。

多言語翻訳ツール 実行画面(Windows)
実行画面(Windows)
多言語翻訳ツール 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「⇄ 入れ替え」ボタン(_swap_langs())・「▶ 翻訳」ボタン(_translate())・「📋 コピー」ボタン(_copy_result())で操作
  • キー <Control-Return> から _translate() を実行
  • キー <KeyRelease> から _update_char_count() を実行
  • マウス操作 <Double-1> から _load_history() を実行
  • ttk.Treeview による表形式の一覧表示
  • messagebox.showerror()messagebox.showwarning() によるダイアログ通知
  • ウィンドウはタイトル「多言語翻訳ツール」・サイズ 900x841 で起動

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

このアプリはインターネット接続が必要で、入力した文章は外部の翻訳サービス(api.mymemory.translated.net)へ送信されます。

Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。

  • Python 3.10 以上
  • OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app065.py
import tkinter as tk
from tkinter import ttk, messagebox
import urllib.request
import urllib.parse
import json
import threading


class App065:
    """多言語翻訳ツール"""

    # MyMemory API (無料・登録不要)
    API_URL = "https://api.mymemory.translated.net/get"

    LANGUAGES = {
        "日本語": "ja",
        "英語": "en",
        "中国語(簡体)": "zh-CN",
        "韓国語": "ko",
        "フランス語": "fr",
        "ドイツ語": "de",
        "スペイン語": "es",
        "イタリア語": "it",
        "ポルトガル語": "pt",
        "ロシア語": "ru",
        "アラビア語": "ar",
        "ヒンディー語": "hi",
    }

    def __init__(self, root):
        self.root = root
        self.root.title("多言語翻訳ツール")
        self.root.geometry("900x841")
        self.root.minsize(900, 841)
        self.root.configure(bg="#1e1e1e")
        self._history = []
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="🌍 多言語翻訳ツール",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
        tk.Label(header, text="※ MyMemory翻訳API使用",
                 bg="#252526", fg="#555", font=("Arial", 8)).pack(
            side=tk.LEFT, padx=4)

        # 言語選択
        lang_f = tk.Frame(self.root, bg="#2d2d2d", pady=6)
        lang_f.pack(fill=tk.X, padx=8, pady=4)
        lang_names = list(self.LANGUAGES.keys())

        tk.Label(lang_f, text="翻訳元:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.src_lang_var = tk.StringVar(value="日本語")
        ttk.Combobox(lang_f, textvariable=self.src_lang_var,
                     values=lang_names, state="readonly",
                     width=14).pack(side=tk.LEFT)
        ttk.Button(lang_f, text="⇄ 入れ替え",
                   command=self._swap_langs).pack(side=tk.LEFT, padx=8)
        tk.Label(lang_f, text="翻訳先:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT)
        self.dst_lang_var = tk.StringVar(value="英語")
        ttk.Combobox(lang_f, textvariable=self.dst_lang_var,
                     values=lang_names, state="readonly",
                     width=14).pack(side=tk.LEFT)
        ttk.Button(lang_f, text="▶ 翻訳",
                   command=self._translate).pack(side=tk.LEFT, padx=12)

        # テキストエリア
        text_f = tk.Frame(self.root, bg="#1e1e1e")
        text_f.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)

        # 入力
        left = tk.Frame(text_f, bg="#1e1e1e")
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        tk.Label(left, text="入力テキスト", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.input_text = tk.Text(left, bg="#0d1117", fg="#d4d4d4",
                                   font=("Noto Sans JP", 11), relief=tk.FLAT,
                                   wrap=tk.WORD, insertbackground="#fff",
                                   undo=True)
        self.input_text.pack(fill=tk.BOTH, expand=True)
        self.input_text.bind("<Control-Return>",
                              lambda e: self._translate())
        self.char_lbl = tk.Label(left, text="0 文字", bg="#1e1e1e",
                                  fg="#555", font=("Arial", 8))
        self.char_lbl.pack(anchor="e")
        self.input_text.bind("<KeyRelease>", self._update_char_count)

        ttk.Separator(text_f, orient=tk.VERTICAL).pack(
            side=tk.LEFT, fill=tk.Y, padx=4)

        # 出力
        right = tk.Frame(text_f, bg="#1e1e1e")
        right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        tk.Label(right, text="翻訳結果", bg="#1e1e1e", fg="#888",
                 font=("Arial", 9)).pack(anchor="w")
        self.output_text = tk.Text(right, bg="#0d1117", fg="#4fc3f7",
                                    font=("Noto Sans JP", 11), relief=tk.FLAT,
                                    wrap=tk.WORD, state=tk.DISABLED)
        self.output_text.pack(fill=tk.BOTH, expand=True)
        copy_f = tk.Frame(right, bg="#1e1e1e")
        copy_f.pack(fill=tk.X)
        ttk.Button(copy_f, text="📋 コピー",
                   command=self._copy_result).pack(side=tk.RIGHT)

        # 履歴
        hist_f = tk.LabelFrame(self.root, text="翻訳履歴",
                                bg="#1e1e1e", fg="#888", font=("Arial", 9),
                                padx=4, pady=4)
        hist_f.pack(fill=tk.X, padx=8, pady=(0, 4))
        self.hist_tree = ttk.Treeview(hist_f,
                                       columns=("src_lang", "dst_lang", "src", "dst"),
                                       show="headings", height=4,
                                       selectmode="browse")
        self.hist_tree.heading("src_lang", text="元言語")
        self.hist_tree.heading("dst_lang", text="先言語")
        self.hist_tree.heading("src",      text="元テキスト")
        self.hist_tree.heading("dst",      text="翻訳テキスト")
        self.hist_tree.column("src_lang", width=70,  anchor="center")
        self.hist_tree.column("dst_lang", width=70,  anchor="center")
        self.hist_tree.column("src",      width=300, anchor="w")
        self.hist_tree.column("dst",      width=300, anchor="w")
        hsb = ttk.Scrollbar(hist_f, command=self.hist_tree.yview)
        self.hist_tree.configure(yscrollcommand=hsb.set)
        hsb.pack(side=tk.RIGHT, fill=tk.Y)
        self.hist_tree.pack(fill=tk.X)
        self.hist_tree.bind("<Double-1>", self._load_history)

        self.status_var = tk.StringVar(value="テキストを入力して翻訳してください (Ctrl+Enter)")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

        self.progress = ttk.Progressbar(self.root, mode="indeterminate")
        self.progress.pack(fill=tk.X, padx=8, side=tk.BOTTOM)

    def _swap_langs(self):
        src = self.src_lang_var.get()
        dst = self.dst_lang_var.get()
        self.src_lang_var.set(dst)
        self.dst_lang_var.set(src)
        # テキストも入れ替え
        src_txt = self.input_text.get("1.0", tk.END).strip()
        dst_txt = self.output_text.get("1.0", tk.END).strip()
        self.input_text.delete("1.0", tk.END)
        self.input_text.insert("1.0", dst_txt)
        self.output_text.configure(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", src_txt)
        self.output_text.configure(state=tk.DISABLED)

    def _translate(self):
        text = self.input_text.get("1.0", tk.END).strip()
        if not text:
            messagebox.showwarning("警告", "翻訳するテキストを入力してください")
            return
        src_code = self.LANGUAGES.get(self.src_lang_var.get(), "ja")
        dst_code = self.LANGUAGES.get(self.dst_lang_var.get(), "en")
        self.progress.start()
        self.status_var.set("翻訳中...")
        threading.Thread(target=self._do_translate,
                          args=(text, src_code, dst_code),
                          daemon=True).start()

    def _do_translate(self, text, src_code, dst_code):
        try:
            # 2000文字制限のため分割
            chunks = [text[i:i+400] for i in range(0, len(text), 400)]
            results = []
            for chunk in chunks:
                params = urllib.parse.urlencode({
                    "q": chunk,
                    "langpair": f"{src_code}|{dst_code}",
                })
                url = f"{self.API_URL}?{params}"
                req = urllib.request.Request(
                    url, headers={"User-Agent": "Mozilla/5.0"})
                with urllib.request.urlopen(req, timeout=10) as resp:
                    data = json.loads(resp.read().decode("utf-8"))
                translated = data["responseData"]["translatedText"]
                results.append(translated)
            result = " ".join(results)
            self.root.after(0, self._show_result, result, text)
        except Exception as e:
            self.root.after(0, self._show_error, str(e))

    def _show_result(self, result, src_text):
        self.progress.stop()
        self.output_text.configure(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", result)
        self.output_text.configure(state=tk.DISABLED)
        self.status_var.set("翻訳完了")
        # 履歴
        src_lang = self.src_lang_var.get()
        dst_lang = self.dst_lang_var.get()
        self._history.insert(0, (src_lang, dst_lang, src_text, result))
        self.hist_tree.insert("", 0,
                               values=(src_lang, dst_lang,
                                       src_text[:50], result[:50]))

    def _show_error(self, msg):
        self.progress.stop()
        messagebox.showerror("エラー", f"翻訳に失敗しました:\n{msg}")
        self.status_var.set("エラーが発生しました")

    def _copy_result(self):
        text = self.output_text.get("1.0", tk.END).strip()
        if text:
            self.root.clipboard_clear()
            self.root.clipboard_append(text)
            self.status_var.set("クリップボードにコピーしました")

    def _update_char_count(self, event=None):
        n = len(self.input_text.get("1.0", tk.END).strip())
        self.char_lbl.config(text=f"{n} 文字")

    def _load_history(self, event=None):
        sel = self.hist_tree.selection()
        if not sel:
            return
        idx = self.hist_tree.index(sel[0])
        if idx >= len(self._history):
            return
        src_lang, dst_lang, src, dst = self._history[idx]
        self.src_lang_var.set(src_lang)
        self.dst_lang_var.set(dst_lang)
        self.input_text.delete("1.0", tk.END)
        self.input_text.insert("1.0", src)
        self.output_text.configure(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", dst)
        self.output_text.configure(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App065(root)
    root.mainloop()

5. コード解説

多言語翻訳ツールのコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App065 クラスにアプリの全機能をまとめています(メソッド10個・全242行)。__init__ ではタイトル「多言語翻訳ツール」とウィンドウサイズ 900x841 を設定します。最後に _build_ui() で画面を組み立てます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×6・tk.Label×8・ttk.Combobox×2・ttk.Button×3・tk.Text×2・ttk.Separatortk.LabelFramettk.Treeviewttk.Scrollbarttk.Progressbar で構成しています。見出し付きの枠(LabelFrame)は「翻訳履歴」のラベルでエリアを区切っています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「⇄ 入れ替え」ボタン(_swap_langs())・「▶ 翻訳」ボタン(_translate())・「📋 コピー」ボタン(_copy_result())を command= で接続しています。また bind("<Control-Return>", ...) から _translate()bind("<KeyRelease>", ...) から _update_char_count()bind("<Double-1>", ...) から _load_history() を呼べるようにイベントも登録しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

クラス定数によるデータ定義

クラスの先頭でデータを定数として定義しています:API_URL = 'https://api.mymemory.translated.net/get'。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理

try-except で Exception を捕捉しています。あわせて messagebox.showerror()messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app065.py と保存します。使うのは jsonthreadingtkinterurllib だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App065 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("多言語翻訳ツール")geometry("900x841")configure(bg="#1e1e1e")minsize(900, 841) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×6・tk.Label×8・ttk.Combobox×2・ttk.Button×3・tk.Text×2・ttk.Separatortk.LabelFramettk.Treeviewttk.Scrollbarttk.Progressbar を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(_swap_langs()_translate()_copy_result())につなぎます。bind("<Control-Return>", ...)bind("<KeyRelease>", ...)bind("<Double-1>", ...) の登録も忘れずに。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である _show_result()(14行)・_show_error()(4行)・_load_history()(16行)など を実装します。

  7. 7
    動作確認する

    python app065.py で起動し、各ボタンが反応すること・キー操作(<Control-Return><KeyRelease>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 クラス定数を書き換えて動きを変える

データはクラス定数(API_URLLANGUAGES)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

8. よくある問題と解決法

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

<Control-Return> キーを押しても反応しない

原因:bind("<Control-Return>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。

解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("900x841") の固定サイズで起動するためです。

解決法:geometry()minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(900, 841) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。「▶ 翻訳」を押すと、入力欄の文字が外部の翻訳サービス(api.mymemory.translated.net・13行)へ送られます(179-181行)。仕事の資料・個人情報・パスワードを入れないでください。無料で公開されているサービスなので、提供元の規約と利用量の上限に従い、練習は短い文だけにして回数を抑えてください。下の課題1・課題2は、どちらも翻訳を送らずに確かめられます(課題2はネットにつないでいない状態で試します)。通信に使うのは標準ライブラリの urllib.request で(3行目)、課題1・課題2に追加のライブラリは要りません。

  1. 課題1:翻訳する前に「⇄ 入れ替え」を押すと、打った文が消える

    「⇄ 入れ替え」は、言語だけでなく左右のテキストも入れ替えます。右側がまだ空のときに押すと、左に打った文が読み取り専用の右へ移り、入力欄は空になります。

    期待結果:入力欄に おはよう と打つ(入力欄の下の文字数が 4 文字 になる)。翻訳せずに「⇄ 入れ替え」を押すと、翻訳元が「英語」・翻訳先が「日本語」に変わる。入力欄は空になり、右の「翻訳結果」に おはよう が入る。文字数は 4 文字 のまま。直したあとは、入力欄の おはよう が残る。

    合格条件

    • 翻訳結果が入っているときは、今までどおり左右のテキストも入れ替わる
    • 翻訳結果が空のときは、入力した文が消えない
    • どちらの場合も、翻訳元と翻訳先の言語は入れ替わる

    ヒント①(どこを触るか): 触るのは1か所です。_swap_langs() の中、self.input_text.delete("1.0", tk.END)(148行)の手前。
    ヒント②(使うもの): 言語の入れ替えは143-144行で終わっています。147行の dst_txt が空なら、そこで return するだけで入力欄はそのまま残ります。
    つまずきやすい点: 146-147行は strip() を通すので、入れ替えを繰り返すと前後の空行が落ちます。文字数のラベルは <KeyRelease> でしか更新されないため(90行)、コードから入力欄を書き換えても数字は古いままです。入れ替えのあとに数字もそろえたいときは、self._update_char_count()(217-219行)を呼びます。

  2. 課題2:ネットにつながらないときのエラーが、英語のまま出る

    通信の失敗は、例外の文面をそのままダイアログに出しています。何が起きたのか、どうすれば直るのかは書かれていません。

    期待結果:Wi-Fi を切るなどしてネットから外し、短い文を入れて「▶ 翻訳」を押す。「翻訳に失敗しました:」に続けて <urlopen error [Errno 11001] getaddrinfo failed> のような英語の1行が出る(文面は環境によって変わります)。直したあとは「インターネットに接続できませんでした」のように、原因と次の行動が分かる日本語が出る。

    合格条件

    • つながらないときは、日本語の案内が出る
    • サーバーが応答を返したときのエラーは、今までどおり中身が分かる形で出る
    • 翻訳に成功したときの動きは変わらない

    ヒント①(どこを触るか): 触るのは1か所です。_do_translate()except Exception as e:(187行)の手前。
    ヒント②(使うもの): urllib.error.URLError を先に受け止め、self.root.after(0, self._show_error, "…") に自分の文面を渡します(188行と同じ書き方)。urllib.request を読み込むと urllib.error も使える状態になりますが、3行目のあたりに import urllib.error を足しておくほうが読み手に親切です。
    つまずきやすい点: HTTPErrorURLError を継承しているため、except urllib.error.URLError を先に書くと HTTP のエラーもそこへ入ります。サーバーの応答を分けたいときは except urllib.error.HTTPError を先に置いてください。_show_error()(205-208行)はダイアログのあとステータス欄を エラーが発生しました に変えるので、渡す文面だけを差し替えれば足ります。「▶ 翻訳」は押すたびに新しいスレッドを立てるため(164-166行)、連打すると回数ぶんの通信が走ります。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
次のレベルへ進む参考書

このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。