🍴入力特化翻訳。ほー いいじゃないか、こういのでいいんだよ こういうので（control＋Alt＋Dだけ翻訳）Oh, that's good. That's good. That's good.

## 開発の経緯
- 学生レベルの英語力で海外でBrSEをしていた時に非常に困った言語の壁・・・。
- 出来るだけ簡単に翻訳できるよう作ったものです
- Deeplのapikeyを取得してください。クレカあればすぐに作れます。月に50万文字まで無料で翻訳。https://www.deepl.com/ja/pro-api?cta=menu-pro-api
- 日本語は英語に英語は日本語に翻訳します。他の言語の場合deeplの対応言語であれば英語に翻訳されます。
## 工夫したとこ
- アプリ起動を意識せずに使用できるようショーカット翻訳にこだわりました。バックグラウンドで機能します。
- 目指したのは、
　ほー いいじゃないか
　　こういのでいいんだよ こういうので…

  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3808329/16afe5f9-d3e2-2966-4aed-f456a5d0bf1c.png)


## 使い方
- 翻訳したいテキストをコピー(control＋c)して(control＋Alt＋D)のショートカット操作で翻訳された文章がクリップボードに再格納されます。
- トリガーを変更したい場合はこのあたりを変更してください。
```python:hotkey.py
    def hotkey_listener(self):
        keyboard.on_release_key('d', self.deepl_hotkey_callback, suppress=True)
        keyboard.wait()
        
    def deepl_hotkey_callback(self, e):
        if keyboard.is_pressed('ctrl') and keyboard.is_pressed('alt'):
            threading.Thread(target=lambda: self.perform_translation("deepl"), daemon=True).start()
```

つまり、「英語が話せなくて苦労した」をコピー(control＋c)して、(control＋Alt＋D) ショートカット操作すると、「I had trouble speaking English.」がクリップボードに格納され、次のペーストで英語になっている！というものです。AIのAPIに変えればもっと良い翻訳になるかも？
その辺は適宜調整してください。、

ということで以下が本体スクリプトコードになります。
```python:ClipboardTranslator.py
# Ver5.6
import re
import json
import time
import os
import sys
import socket
import threading
import configparser
import webbrowser

import pyperclip
import keyboard
import requests
from googletrans import Translator
import tkinter as tk
from tkinter import messagebox
import langid

# DeepL API
DEEPL_API_KEY = 'ここにDeepLのAPI KEYを入力してください'
DEEPL_URL = 'https://api-free.deepl.com/v2/translate'

# Configuration
config = configparser.ConfigParser()
config_file = os.path.join(os.path.dirname(sys.executable), 'translation_app.ini')

translation_lock = threading.Lock()
clipboard_lock = threading.Lock()

class TranslationApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Ver5.6")
        self.geometry("200x300")
        
        self.text_area = tk.Text(
            self,
            wrap=tk.WORD,
            bg='#333333',
            padx=8,
            pady=8,
            fg='white',
            font=('Serif', 11),
            spacing1=2,
            spacing2=8,
            spacing3=8,
            # encoding='utf-8'  エンコーディングを指定
        )
        self.text_area.pack(expand=True, fill=tk.BOTH)
        self.text_area.configure(state='disabled')
        self.text_area.bind("<MouseWheel>", self.change_font_size)
        
        self.protocol("WM_DELETE_WINDOW", self.on_closing)
        
        self.check_expiration()
        
        self.hotkey_thread = threading.Thread(target=self.hotkey_listener, daemon=True)
        self.hotkey_thread.start()

        self.log_message("Script is running...")  # ここに追加
    
    def change_font_size(self, event):
        if event.state & 0x0004:  # コントロールキーが押されているか確認（0x0004はコントロールキーのステート）
            current_font = self.text_area.cget("font")
            font_family, font_size = current_font.split()[0], int(current_font.split()[1])
            new_size = font_size + (1 if event.delta > 0 else -1)  # スクロールアップでサイズアップ、ダウンでサイズダウン
            new_font = (font_family, new_size)
            self.text_area.config(font=new_font)
            return "break"  # 他のイベントハンドラへのイベントの伝播を停止
        
    def on_closing(self):
        self.save_window_position()
        self.quit()  # 追加: アプリケーションを終了
        
    def save_window_position(self):
        x = self.winfo_x()
        y = self.winfo_y()
        width = self.winfo_width()
        height = self.winfo_height()
        config['Window'] = {'x': x, 'y': y, 'width': width, 'height': height}
        
        with open(config_file, 'w') as f:
            config.write(f)
            
    def load_window_position(self):
        if os.path.exists(config_file):
            config.read(config_file)
            x = int(config['Window'].get('x', 0))
            y = int(config['Window'].get('y', 0))
            width = int(config['Window'].get('width', 800))
            height = int(config['Window'].get('height', 600))
            self.geometry(f"{width}x{height}+{x}+{y}")
                   
    def hotkey_listener(self):
        keyboard.on_release_key('d', self.deepl_hotkey_callback, suppress=True)
        keyboard.wait()
        
    def deepl_hotkey_callback(self, e):
        if keyboard.is_pressed('ctrl') and keyboard.is_pressed('alt'):
            threading.Thread(target=lambda: self.perform_translation("deepl"), daemon=True).start()
            
    def perform_translation(self, translator):
        with translation_lock:
            with clipboard_lock:
                text = pyperclip.paste()
            self.log_message(f"\n【Input】{text}")
            
            lang = self.detect_language(text)
            
            if lang == 'EN':
                target_lang = 'JA'
            elif lang == 'JA':
                target_lang = 'EN'
            else:
                target_lang = 'JA'
                
            while not self.is_connected():
                time.sleep(1)
                
            if translator == "deepl":
                translated_text = self.translate_with_deepl(text, target_lang)
                
            if not translated_text or translated_text == text:
                translated_text = self.translate_with_googletrans(text, target_lang)
                
            if translated_text:
                try:
                    with clipboard_lock:
                        pyperclip.copy(translated_text)
                    self.log_message(f"【Translated】\n{translated_text}")
                except Exception as e:
                    self.log_message(f"Error occurred during copying translated text to clipboard: {e}")
                    
    def detect_language(self, text):
        try:
            detected_lang, _ = langid.classify(text)
            if detected_lang == 'en':
                return 'EN'
            elif detected_lang == 'ja':
                return 'JA'
            else:
                return 'JA'  # translate to Japanese for other languages
        except Exception as e:
            self.log_message(f"Language detection failed: {e}")
            return 'EN'
        
    def translate_with_deepl(self, text, target_lang):
        try:
            source_lang = self.detect_language(text)
            target_lang = 'JA' if target_lang == 'JA' else 'EN'
            
            headers = {
                'Authorization': 'DeepL-Auth-Key ' + DEEPL_API_KEY,
                'Content-Type': 'application/x-www-form-urlencoded'
            }
            
            data = {
                'text': text,
                'source_lang': source_lang,
                'target_lang': target_lang
            }
            
            response = requests.post(DEEPL_URL, headers=headers, data=data, timeout=5)
            response_data = response.json()
            
            if response.status_code == 200:
                translated_text = response_data['translations'][0]['text']
                return translated_text
            else:
                self.log_message(f"Translation failed with status code {response.status_code}")
                return None
        except Exception as e:
            self.log_message(f"Error occurred during translation: {e}")
            return None
        
    def is_connected(self):
        try:
            socket.create_connection(("www.google.com", 80))
            return True
        except OSError:
            pass
        return False
    
    def translate_with_googletrans(self, text, target_lang):
        try:
            source_lang, _ = langid.classify(text)
            googletrans_translator = Translator(service_urls=['translate.google.com'])
            translated_text = googletrans_translator.translate(text, src=source_lang, dest=target_lang).text
            return translated_text
        except Exception as e:
            self.log_message(f"Error occurred during Googletrans translation: {e}")
            return None
        
    def log_message(self, message):
        self.text_area.configure(state='normal')
        
        # タグの設定（一度だけで良いため、関数の外や初期化時に移動しても良い）
        self.text_area.tag_configure('input_tag', foreground='#9597f7')
        self.text_area.tag_configure('translated_tag', foreground='#9597f7')
        
        # メッセージが【Input】または【Translated】で始まるかチェックし、該当する部分とそれ以外の部分に分けて挿入
        if "【Input】" in message:
            input_index_end = message.index("【Input】") + len("【Input】")
            for character in message[:input_index_end]:
                self.text_area.insert(tk.END, character, 'input_tag')
                time.sleep(0.0000001)  # Add this line
            for character in message[input_index_end:]:
                self.text_area.insert(tk.END, character)
                time.sleep(0.0000001)  # Add this line
        elif "【Translated】" in message:
            translated_index_end = message.index("【Translated】") + len("【Translated】")
            for character in message[:translated_index_end]:
                self.text_area.insert(tk.END, character, 'translated_tag')
                time.sleep(0.0000001)  # Add this line
            for character in message[translated_index_end:]:
                self.text_area.insert(tk.END, character)
                time.sleep(0.0000001)  # Add this line
        else:
            for character in message:
                self.text_area.insert(tk.END, character)
                time.sleep(0.0000001)  # Add this line
                
        self.text_area.insert(tk.END, '\n')
        self.text_area.configure(state='disabled')
        self.text_area.see(tk.END)

if __name__ == "__main__":
    app = TranslationApp()
    app.load_window_position()
    app.mainloop()
```
0.0000001はタイピングのような動きを付けたかった中二心ですwコメントアウトするとスマートです！