Система перевода: Гибридный подход с тремя технологиями

В этой статье объясняется наша система перевода, которая объединяет три технологии — Flask-Babel для шаблонов, TranslationManager для таблиц фраз и DeepSeek AI для перевода контента — чтобы обеспечить комплексную поддержку многоязычности.

Проблема: Перевод сложного веб-сайта

Наш сайт содержит несколько типов контента, требующих перевода:

  • Статические шаблоны: Навигация, кнопки, метки (500+ строк)

  • Динамический контент: Названия продуктов, описания, характеристики (10 000+ строк)

  • Пользовательский контент: Отзывы, комментарии, обращения в поддержку

  • Технические термины: Названия брендов, артикулы (SKU), URL-адреса (переводу НЕ подлежат)

Единый подход к переводу не работает:

  • Машинный перевод: Быстрый, но неточный для технических терминов

  • Ручной перевод: Точный, но медленный и дорогой

  • Только шаблоны: Не работает с динамическим контентом

Нам нужен гибридный подход, сочетающий лучшее из всех трёх.

Архитектура перевода

graph TD
    Request[Запрос пользователя
lang=hi] subgraph Templates Babel[Flask-Babel
.po files] Static[Статические строки
кнопки, метки] end subgraph Dynamic TM[TranslationManager
phrase tables] Phrases[Названия продуктов
характеристики, термины] end subgraph AI DS[DeepSeek API
AI translation] Content[Описания
статьи, длинный текст] end Request --> Babel Request --> TM Request --> DS Babel --> Static TM --> Phrases DS --> Content Static --> Response[Переведённая страница] Phrases --> Response Content --> Response

Три технологии

1. Flask-Babel: Перевод шаблонов

Flask-Babel обрабатывает статические строки шаблонов с использованием файлов gettext .po.

  • Область применения: Навигация, кнопки, метки, сообщения об ошибках

  • Пример:

# Шаблон
{{ _('Add to Cart') }}

# .po файл (Хинди)
msgid "Add to Cart"
msgstr "कार्ट में जोड़ें"

Преимущества:

  • ✅ Стандартный подход i18n

  • ✅ Поддержка инструментов перевода (Poedit, Weblate)

  • ✅ Валидация на этапе компиляции

  • ✅ Быстрый поиск (скомпилированные .mo файлы)

Ограничения:

  • ❌ Работает только со статическими строками

  • ❌ Требует изменений кода для добавления новых строк

  • ❌ Не поддерживает динамический контент

2. TranslationManager: Таблицы фраз

Наш собственный класс TranslationManager обрабатывает динамический контент с использованием JSON-таблиц фраз.

  • Область применения: Названия продуктов, характеристики, спецификации, описания

  • Пример:

{
  "Mini PC": "मिनी पीसी",
  "Intel N100": "Intel N100",
  "8GB RAM": "8GB RAM",
  "256GB SSD": "256GB SSD"
}

Преимущества:

  • ✅ Поддержка динамического контента

  • ✅ Обновления на лету (не требует перезапуска)

  • ✅ Правила сохранения (названия брендов, SKU)

  • ✅ Региональное форматирование (десятичные разделители)

  • ✅ Очередь переводов (отсутствующие строки)

Ограничения:

  • ❌ Ручное управление фразами

  • ❌ Нет контекста для переводчиков

  • ❌ Требует начального перевода

3. DeepSeek AI: Перевод контента

Модель ИИ DeepSeek обрабатывает перевод длинного контента.

  • Область применения: Статьи блога, описания продуктов, маркетинговые тексты

  • Пример:

prompt = f"Translate to {lang}: {text}"
response = deepseek.generate(prompt)

Преимущества:

  • ✅ Работает с длинным контентом (1000+ слов)

  • ✅ Перевод с учётом контекста

  • ✅ Естественный язык на выходе

  • ✅ Пакетная обработка

Ограничения:

  • ❌ Стоимость API за каждый перевод

  • ❌ Требует подключения к интернету

  • ❌ Может некорректно переводить технические термины

Архитектура TranslationManager

Структура таблиц фраз

Таблицы фраз хранятся в виде JSON-файлов для каждого языка:

app/shared/translation/phrase_tables/

Каждый файл сопоставляет английские фразы с переводами:

{
  "Mini PC": "मिनी पीसी",
  "Thin Client": "थिन क्लाइंट",
  "Industrial PC": "औद्योगिक पीसी",
  "All-in-One": "ऑल-इन-वन"
}

Поиск перевода

Метод get() выполняет многоуровневый поиск:

1. Проверка на сохранение:

if self._should_skip_translation(text, lang):
    return text  # Не переводить названия брендов, SKU, URL

2. Разделение по запятым (для длинных строк):

if len(text) > 150 and "," in text:
    parts = text.split(",")
    return ", ".join(self.get(p, lang) for p in parts)

3. Поиск в таблице фраз:

if text in self.phrase_tables[lang]:
    return self.phrase_tables[lang][text]

4. Продвижение из кэша устаревшей системы:

if text_hash in self.legacy_caches[lang]:
    translation = self.legacy_caches[lang][text_hash]
    self.set(text, lang, translation)  # Добавить в таблицу фраз
    return translation

5. Постановка в очередь, если отсутствует:

if queue_if_missing:
    self.add_to_queue(text, lang)
return None  # Перевод не найден

Правила сохранения

Мы сохраняем определённые типы контента:

Названия брендов:

BRAND_NAMES_PRESERVE = [
    "Thinvent", "Intel", "AMD", "Microsoft", "Ubuntu",
    "Windows", "Linux", "WiFi", "Bluetooth"
]

Технические термины:

TECHNICAL_TERMS_PRESERVE = [
    "HDMI", "DisplayPort", "VGA", "USB", "Ethernet",
    "DDR4", "SSD", "NVMe", "PCIe", "SATA"
]

SKU (на основе шаблонов):

if not " " in text and text.count("-") >= 2:
    return True  # Сохранить "Treo-N100-8-256-2H-W6-11P"

URL-адреса:

if text.startswith(("/", "http://", "https://")):
    return True

Региональное форматирование

Для европейских локалей (немецкий, французский, испанский) применяем региональное форматирование:

Десятичный разделитель:

# Английский: 34.00
# Немецкий:   34,00
text = text.replace(".", ",")

Напряжение/Ток:

# Английский: 12.5V
# Немецкий:   12,5V
text = re.sub(r"(\d+)\.(\d+)V", r"\1,\2V", text)

Это гарантирует корректное отображение чисел для каждой локали.

Очередь переводов

Отсутствующие переводы помещаются в очередь для пакетной обработки:

[
  {
    "text": "Compact Desktop",
    "lang": "hi",
    "hash": "a1b2c3d4e5f6"
  },
  {
    "text": "Fanless Design",
    "lang": "hi",
    "hash": "f6e5d4c3b2a1"
  }
]

Фоновый скрипт обрабатывает очередь с помощью DeepSeek AI:

for item in queue:
    translation = deepseek.translate(item["text"], item["lang"])
    translation_manager.set(item["text"], item["lang"], translation)

Блокировка файлов

Мы используем блокировку файлов fcntl, чтобы предотвратить одновременную запись:

with open(lock_path, "w") as lock_file:
    fcntl.flock(lock_file, fcntl.LOCK_EX)
    try:
        # Чтение, изменение, запись таблицы фраз
        with open(table_path, "r+") as f:
            data = json.load(f)
            data.update(new_translations)
            f.seek(0)
            json.dump(data, f)
            f.truncate()
    finally:
        fcntl.flock(lock_file, fcntl.LOCK_UN)

Это гарантирует, что несколько процессов не повредят таблицы фраз.

Определение языка

Мы определяем язык запроса с помощью диапазонов Unicode и стоп-слов:

Определение по диапазонам Unicode

Деванагари (Хинди, Маратхи):

if 0x0900 <= ord(char) <= 0x097F:
    return "hi"

Бенгальский:

if 0x0980 <= ord(char) <= 0x09FF:
    return "bn"

Арабский:

if 0x0600 <= ord(char) <= 0x06FF:
    return "ar"

Кириллица (Русский):

if 0x0400 <= ord(char) <= 0x04FF:
    return "ru"

CJK (Китайский, Японский, Корейский):

if 0x4E00 <= ord(char) <= 0x9FFF:  # Кандзи/Ханьцзы
    has_cjk = True
if 0x3040 <= ord(char) <= 0x30FF:  # Хирагана/Катакана
    return "ja"
if 0xAC00 <= ord(char) <= 0xD7AF:  # Хангыль
    return "ko"

Определение по стоп-словам

Для языков на основе латиницы используем стоп-слова:

Испанский:

SPANISH_STOPWORDS = {
    "el", "la", "los", "las", "un", "una", "para", "con",
    "en", "por", "que", "es", "su", "y", "del", "al"
}

Французский:

FRENCH_STOPWORDS = {
    "le", "la", "les", "des", "du", "un", "une", "pour",
    "avec", "en", "est", "sur", "et", "au", "dans"
}

Немецкий:

GERMAN_STOPWORDS = {
    "der", "die", "das", "ein", "eine", "für", "mit",
    "ist", "und", "auf", "den", "dem", "bei", "von"
}

Если совпадает любое стоп-слово, возвращаем этот язык.

Динамическое определение языка

Пользователи могут указать язык через:

1. Параметр URL

/p/Treo-N100-8-256-2H-W6-11P?lang=hi

2. Cookie

response.set_cookie("lang", "hi", max_age=31536000)

3. Заголовок Accept-Language браузера

lang = request.accept_languages.best_match(["en", "hi", "es", "fr", "de"])

Приоритет: Параметр URL > Cookie > Accept-Language > По умолчанию (en)

Рекурсивный перевод

Мы рекурсивно переводим вложенные структуры данных:

def translate_data(obj, lang):
    if isinstance(obj, dict):
        return {k: translate_data(v, lang) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [translate_data(item, lang) for item in obj]
    elif isinstance(obj, str):
        return self.get(obj, lang) or obj
    return obj

Пример:

data = {
    "title": "Mini PC",
    "features": {
        "RAM": "8GB",
        "Storage": "256GB SSD"
    },
    "price": 25000
}

translated = translate_data(data, "hi")
# Результат:
# {
#     "title": "मिनी पीसी",
#     "features": {
#         "RAM": "8GB",
#         "Storage": "256GB SSD"
#     },
#     "price": 25000
# }

Перевод характеристик

Характеристики продуктов требуют особой обработки:

def translate_features(features, lang):
    translated = {}
    for heading, feature_dict in features.items():
        # Перевести заголовок
        translated_heading = self.get(heading, lang) or heading

        # Перевести названия и значения характеристик
        translated_features = {}
        for name, value in feature_dict.items():
            translated_name = self.get(name, lang) or name
            translated_value = self.get(value, lang) or value
            translated_features[translated_name] = translated_value

        translated[translated_heading] = translated_features

    return translated

Пример:

features = {
    "Processing": {
        "Processor": "Intel N100",
        "Cores": "4",
        "RAM": "8GB"
    }
}

translated = translate_features(features, "hi")
# Результат:
# {
#     "प्रोसेसिंग": {
#         "प्रोसेसर": "Intel N100",
#         "कोर": "4",
#         "RAM": "8GB"
#     }
# }

Интеграция с SEO-пайплайном

Система перевода интегрирована с SEO-пайплайном:

Передача языка запроса

Когда пользователь ищет на хинди, мы передаём язык в связанные поиски:

if SEO_ENABLE_LANGUAGE_PROPAGATION:
    lang = detect_query_language(query)
    if lang != "en":
        url = f"{url}?lang={lang}"

Это гарантирует, что пользователи остаются на предпочитаемом языке при переходе по связанным поискам.

Переведённые страницы запросов

Страницы запросов генерируются на нескольких языках:

/q/mini-pc          (Английский)
/q/mini-pc?lang=hi  (Хинди)
/q/mini-pc?lang=es  (Испанский)

Контент переводится с помощью TranslationManager.

Характеристики производительности

Поиск в таблице фраз:

  • Время: O(1) поиск в словаре (~1 мкс)

  • Память: ~5 МБ на язык (10 000 фраз)

Определение языка:

  • Время: O(n), где n = длина строки (~10 мкс для 100 символов)

  • Память: Незначительное

Рекурсивный перевод:

  • Время: O(n), где n = количество строк (~1 мс для 100 строк)

  • Память: Пропорционально размеру структуры данных

Блокировка файлов:

  • Время: ~1 мс на получение блокировки

  • Конкуренция: Редкая (запись происходит нечасто)

Ссылки

Технические концепции

  • [Интернационализация (i18n)](https://en.wikipedia.org