번역 시스템: 세 가지 기술을 결합한 하이브리드 접근법

이 글은 Flask-Babel(템플릿용), TranslationManager(구문 테이블용), DeepSeek AI(콘텐츠 번역용) 세 가지 기술을 결합하여 포괄적인 다국어 지원을 제공하는 우리의 번역 시스템을 설명합니다.

문제: 복잡한 웹사이트 번역하기

우리 웹사이트에는 번역이 필요한 여러 유형의 콘텐츠가 있습니다:

  • 정적 템플릿: 네비게이션, 버튼, 라벨 (500개 이상의 문자열)

  • 동적 콘텐츠: 제품명, 설명, 기능 (10,000개 이상의 문자열)

  • 사용자 생성 콘텐츠: 리뷰, 댓글, 지원 티켓

  • 기술 용어: 브랜드명, SKU, URL (번역하면 안 됨)

단일 번역 접근법으로는 작동하지 않습니다:

  • 기계 번역: 빠르지만 기술 용어에 부정확함

  • 수동 번역: 정확하지만 느리고 비쌈

  • 템플릿만 사용: 동적 콘텐츠를 처리하지 못함

우리는 세 가지의 장점을 모두 결합한 하이브리드 접근법이 필요합니다.

번역 아키텍처

graph TD
    Request[사용자 요청
lang=hi] subgraph Templates Babel[Flask-Babel
.po 파일] Static[정적 문자열
버튼, 라벨] end subgraph Dynamic TM[TranslationManager
구문 테이블] Phrases[제품명
기능, 용어] end subgraph AI DS[DeepSeek API
AI 번역] Content[설명
기사, 긴 텍스트] end Request --> Babel Request --> TM Request --> DS Babel --> Static TM --> Phrases DS --> Content Static --> Response[번역된 페이지] Phrases --> Response Content --> Response

세 가지 기술

1. Flask-Babel: 템플릿 번역

Flask-Babelgettext .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 AI 모델은 장문 콘텐츠 번역을 처리합니다.

  • 사용 사례: 블로그 기사, 제품 설명, 마케팅 문구

  • 예시:

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)

이렇게 하면 여러 프로세스가 구문 테이블을 손상시키지 않습니다.

언어 감지

유니코드 범위와 불용어를 사용하여 쿼리 언어를 감지합니다:

유니코드 범위 감지

데바나가리 (힌디어, 마라티어):

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. 쿠키

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

3. 브라우저 Accept-Language 헤더

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

우선순위: URL 매개변수 > 쿠키 > 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μs)

  • 메모리: 언어당 ~5 MB (10,000개 구문)

언어 감지:

  • 시간: O(n), 여기서 n = 문자열 길이 (100자 기준 ~10μs)

  • 메모리: 무시할 수 있음

재귀적 번역:

  • 시간: O(n), 여기서 n = 문자열 수 (100개 문자열 기준 ~1ms)

  • 메모리: 데이터 구조 크기에 비례

파일 잠금:

  • 시간: 잠금 획득당 ~1ms

  • 경합: 드묾 (쓰기는 드물게 발생)

참고 자료

기술 개념