Система перевода: Гибридный подход с тремя технологиями
В этой статье объясняется наша система перевода, которая объединяет три технологии — 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