Skip to main content

Custom Translators

BallonTranslator makes it easy to add custom translation services by implementing a simple interface. This guide shows you how to create your own translator module.

Overview

All translators in BallonTranslator inherit from the BaseTranslator class and implement two key methods:
  1. _setup_translator() - Initialize the translator and define language mappings
  2. _translate() - Perform the actual translation

BaseTranslator Interface

Here’s the core interface from modules/translators/base.py:
class BaseTranslator(BaseModule):
    
    concate_text = True
    cht_require_convert = False

    def __init__(self, 
                 lang_source: str, 
                 lang_target: str,
                 raise_unsupported_lang: bool = True,
                 **params) -> None:
        super().__init__(**params)
        self.name = ''  # Auto-populated from registry
        self.textblk_break = '\n##\n'
        self.lang_source: str = lang_source
        self.lang_target: str = lang_target
        self.lang_map: Dict = LANGMAP_GLOBAL.copy()
        
        # Your setup code runs here
        self.setup_translator()
        
        # Set source and target languages
        self.set_source(lang_source)
        self.set_target(lang_target)

    def _setup_translator(self):
        """Override this to initialize your translator"""
        raise NotImplementedError

    def _translate(self, src_list: List[str]) -> List[str]:
        """Override this to implement translation logic"""
        raise NotImplementedError

    def translate(self, text: Union[str, List]) -> Union[str, List]:
        """Main translation method (handles text concatenation)"""
        # Automatically calls _translate()
        pass

    @check_language_support(check_type='source')
    def set_source(self, lang: str):
        self.lang_source = lang

    @check_language_support(check_type='target')
    def set_target(self, lang: str):
        self.lang_target = lang

Key Attributes

AttributeTypeDescription
lang_mapDictMaps display language names to translator-specific codes
lang_sourcestrCurrent source language
lang_targetstrCurrent target language
concate_textboolWhether to concatenate text list into single string
cht_require_convertboolEnable Traditional Chinese via conversion from Simplified
textblk_breakstrSeparator for concatenated text blocks (default: \n##\n)
paramsDictCustom parameters (displayed in settings panel)
namestrTranslator name (auto-populated from registry)

Creating a Custom Translator

Step 1: Create the Translator File

Create a new file in modules/translators/, for example trans_mycustom.py:
from .base import *
import requests

@register_translator('MyCustom')
class TransMyCustom(BaseTranslator):

    concate_text = False  # Set to True if your API handles multiple texts
    params: Dict = {
        'api_key': '',
        'delay': 0.0,
        'description': 'My custom translation service'
    }

    def _setup_translator(self):
        """Initialize translator and set up language mappings"""
        # Define supported languages
        self.lang_map['Auto'] = 'auto'
        self.lang_map['English'] = 'en'
        self.lang_map['日本語'] = 'ja'
        self.lang_map['简体中文'] = 'zh-CN'
        self.lang_map['繁體中文'] = 'zh-TW'
        
        # Get API key from params if provided
        self.api_key = self.params.get('api_key', '')
        if not self.api_key:
            raise MissingTranslatorParams('MyCustom requires an API key')

    def _translate(self, src_list: List[str]) -> List[str]:
        """Translate a list of strings"""
        results = []
        
        source_lang = self.lang_map[self.lang_source]
        target_lang = self.lang_map[self.lang_target]
        
        for text in src_list:
            if not text.strip():
                results.append('')
                continue
                
            try:
                # Call your translation API
                response = requests.post(
                    'https://api.mycustom.com/translate',
                    json={
                        'text': text,
                        'source': source_lang,
                        'target': target_lang
                    },
                    headers={'Authorization': f'Bearer {self.api_key}'}
                )
                
                if response.status_code == 200:
                    translated = response.json()['translation']
                    results.append(translated)
                else:
                    LOGGER.error(f'Translation failed: {response.status_code}')
                    results.append('')
                    
            except Exception as e:
                LOGGER.error(f'Translation error: {e}')
                results.append('')
        
        return results

Step 2: Register the Translator

The @register_translator('MyCustom') decorator automatically registers your translator. The name you provide will appear in the UI.

Step 3: Import in __init__.py

Add your translator to modules/translators/__init__.py:
from .trans_mycustom import TransMyCustom

Step 4: Test Your Translator

Launch BallonTranslator and your translator should appear in the settings panel:
  1. Open Settings → Module
  2. Select “MyCustom” from the Translator dropdown
  3. Configure parameters (API key, etc.)
  4. Test translation

Real-World Example: Google Translator

Here’s a simplified version of the Google Translator from the source code:
from .base import *
import requests
import html

@register_translator("google")
class TransGoogle(BaseTranslator):

    concate_text = False
    params: Dict = {
        "delay": 0.0,
    }

    def _setup_translator(self):
        self.api_url = "https://translate-pa.googleapis.com/v1/translateHtml"
        self.api_key = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520"
        
        # Map UI language names to Google's language codes
        self.lang_map["Auto"] = "auto"
        self.lang_map["简体中文"] = "zh-CN"
        self.lang_map["繁體中文"] = "zh-TW"
        self.lang_map["日本語"] = "ja"
        self.lang_map["English"] = "en"
        self.lang_map["한국어"] = "ko"
        self.lang_map["Tiếng Việt"] = "vi"
        self.lang_map["Français"] = "fr"
        # ... more languages

    def _translate(self, src_list: List[str]) -> List[str]:
        if not src_list:
            return []

        results = []
        source_lang = self.lang_map.get(self.lang_source, "auto")
        target_lang = self.lang_map.get(self.lang_target, "en")

        for text in src_list:
            if not text.strip():
                results.append("")
                continue

            payload = [[[text], source_lang, target_lang], "wt_lib"]
            
            try:
                response = requests.post(
                    self.api_url,
                    json=payload,
                    headers={
                        "X-Goog-API-Key": self.api_key,
                        "Content-Type": "application/json+protobuf"
                    },
                    timeout=10
                )

                if response.status_code == 200:
                    data = response.json()
                    translated = data[0][0]
                    if isinstance(translated, str):
                        results.append(html.unescape(translated))
                    else:
                        results.append("")
                else:
                    results.append("")
                    
            except Exception as e:
                LOGGER.error(f"Google Translate error: {e}")
                results.append("")

        return results

Advanced Features

Text Concatenation

Some APIs work better with batch requests. Enable concatenation:
class TransBatch(BaseTranslator):
    concate_text = True  # Enable text concatenation
    
    def _translate(self, src_list: List[str]) -> List[str]:
        # src_list will contain a single concatenated string
        # separated by self.textblk_break (default: \n##\n)
        concatenated = src_list[0]
        
        # Translate the whole batch at once
        translated = self.api_call(concatenated)
        
        # Return as list with single element
        return [translated]
The base class will automatically split the result back into individual translations using self.textblk_break.

Custom Text Separator

If the default separator (\n##\n) doesn’t work for your API:
def __init__(self, lang_source, lang_target, **params):
    super().__init__(lang_source, lang_target, **params)
    self.textblk_break = '|||'  # Custom separator

Pre/Post Processing Hooks

Add custom processing before or after translation:
def my_preprocess_hook(translations, textblocks, translator, source_text):
    # Modify source text before translation
    for i, text in enumerate(source_text):
        source_text[i] = text.upper()  # Example: convert to uppercase

def my_postprocess_hook(translations, textblocks, translator):
    # Modify translations after translation
    for i, text in enumerate(translations):
        translations[i] = text.lower()  # Example: convert to lowercase

# Register hooks
TransMyCustom.register_preprocess_hooks({'uppercase': my_preprocess_hook})
TransMyCustom.register_postprocess_hooks({'lowercase': my_postprocess_hook})

Traditional Chinese Support

If your API doesn’t natively support Traditional Chinese but supports Simplified:
class TransMyCustom(BaseTranslator):
    cht_require_convert = True
    
    def _setup_translator(self):
        self.lang_map['简体中文'] = 'zh-CN'
        # Traditional Chinese will automatically use Simplified Chinese
        # and convert using OpenCC

Custom Parameters

Define parameters that appear in the settings UI:
params: Dict = {
    'api_key': '',
    'api_base': 'https://api.example.com',
    'model': 'default',
    'temperature': 0.7,
    'delay': 0.0,
    'description': 'My custom translator with configurable options'
}

def _setup_translator(self):
    # Access parameters
    self.api_key = self.params.get('api_key', '')
    self.api_base = self.params.get('api_base', 'https://api.example.com')
    self.model = self.params.get('model', 'default')
    self.temperature = float(self.params.get('temperature', 0.7))

Rate Limiting

Implement delays between API calls:
import time

def _translate(self, src_list: List[str]) -> List[str]:
    results = []
    delay = self.delay()  # Get delay from params
    
    for i, text in enumerate(src_list):
        if i > 0 and delay > 0:
            time.sleep(delay)
        
        # Translate text
        translation = self.api_call(text)
        results.append(translation)
    
    return results

Error Handling

Provide meaningful error messages:
from .exceptions import (
    TranslatorSetupFailure,
    MissingTranslatorParams,
    InvalidSourceOrTargetLanguage
)

def _setup_translator(self):
    api_key = self.params.get('api_key', '')
    if not api_key:
        raise MissingTranslatorParams(
            'MyCustom translator requires an API key. '
            'Please configure it in Settings → Translator'
        )
    
    # Test API connection
    try:
        self.test_connection()
    except Exception as e:
        raise TranslatorSetupFailure(
            f'Failed to connect to MyCustom API: {e}'
        )

def _translate(self, src_list: List[str]) -> List[str]:
    results = []
    for text in src_list:
        try:
            result = self.api_call(text)
            results.append(result)
        except Exception as e:
            LOGGER.error(f'Translation failed: {e}')
            results.append('')  # Return empty string on error
    return results

Language Mapping

The lang_map dictionary maps BallonTranslator’s standard language names to your API’s language codes.

Standard Language Names

LANGMAP_GLOBAL = {
    'Auto': '',
    '简体中文': '',      # Simplified Chinese
    '繁體中文': '',      # Traditional Chinese
    '日本語': '',        # Japanese
    'English': '',
    '한국어': '',        # Korean
    'Tiếng Việt': '',   # Vietnamese
    'čeština': '',      # Czech
    'Nederlands': '',   # Dutch
    'Français': '',     # French
    'Deutsch': '',      # German
    'magyar nyelv': '', # Hungarian
    'Italiano': '',     # Italian
    'Polski': '',       # Polish
    'Português': '',    # Portuguese
    'Brazilian Portuguese': '',
    'limba română': '',  # Romanian
    'русский язык': '',  # Russian
    'Español': '',       # Spanish
    'Türk dili': '',     # Turkish
    'украї́нська мо́ва': '',  # Ukrainian
    'Thai': '',
    'Arabic': '',
    'Hindi': '',
    'Malayalam': '',
    'Tamil': '',
}
Only populate the languages your translator supports:
def _setup_translator(self):
    # Only support these languages
    self.lang_map['English'] = 'en'
    self.lang_map['日本語'] = 'ja'
    self.lang_map['简体中文'] = 'zh'
    # Leave others empty - they won't appear in the UI

Testing Your Translator

Unit Test

Create test_mycustom.py:
import unittest
from modules.translators.trans_mycustom import TransMyCustom

class TestMyCustomTranslator(unittest.TestCase):
    
    def setUp(self):
        self.translator = TransMyCustom(
            lang_source='日本語',
            lang_target='English',
            api_key='your-test-api-key'
        )
    
    def test_single_translation(self):
        result = self.translator.translate('こんにちは')
        self.assertIsInstance(result, str)
        self.assertTrue(len(result) > 0)
    
    def test_batch_translation(self):
        texts = ['こんにちは', 'ありがとう', 'さようなら']
        results = self.translator.translate(texts)
        self.assertEqual(len(results), 3)
        for result in results:
            self.assertIsInstance(result, str)
    
    def test_empty_string(self):
        result = self.translator.translate('')
        self.assertEqual(result, '')

if __name__ == '__main__':
    unittest.main()

Manual Testing

  1. Launch BallonTranslator
  2. Open Settings → Module
  3. Select your translator
  4. Configure parameters
  5. Open a test project
  6. Run translation
  7. Check results

Best Practices

  1. Handle empty strings: Always check for empty input and return empty strings
  2. Error resilience: Return empty string on API errors, don’t crash
  3. Logging: Use LOGGER.error() for errors, LOGGER.info() for status
  4. Rate limiting: Respect API rate limits using the delay parameter
  5. Language validation: Only populate lang_map for supported languages
  6. Parameter validation: Check required parameters in _setup_translator()
  7. Timeout handling: Use reasonable timeouts for API calls
  8. Retry logic: Implement retries for transient failures
  9. Encoding: Handle Unicode properly, especially for CJK languages
  10. Documentation: Add a description in params['description']

Examples from BallonTranslator

DeepL Translator

@register_translator('DeepL')
class TransDeepL(BaseTranslator):
    params: Dict = {
        'api_key': '',
        'description': 'DeepL translation service'
    }
    # Implementation...

Sakura LLM Translator

@register_translator('Sakura-13B-Galgame')
class TransSakura(BaseTranslator):
    params: Dict = {
        'low_vram_mode': True,
        'description': 'Sakura-13B-Galgame local LLM translator'
    }
    # Implementation...

OpenAI-Compatible Translator

@register_translator('ChatGPT')
class TransChatGPT(BaseTranslator):
    params: Dict = {
        'api_key': '',
        'api_base': 'https://api.openai.com/v1',
        'model': 'gpt-4',
        'description': 'OpenAI ChatGPT translator'
    }
    # Implementation...

Troubleshooting

Translator Not Appearing in UI

  • Ensure you used @register_translator() decorator
  • Check that the file is imported in __init__.py
  • Look for Python syntax errors in your file

Language Not Available

  • Verify the language is in lang_map with a non-empty value
  • Check that the language name matches exactly (including Unicode characters)

Translation Returns Empty

  • Check API response format
  • Verify API credentials
  • Look for errors in logs (--debug mode)
  • Test API directly with curl/Postman

Next Steps