번역 시스템: 세 가지 기술을 결합한 하이브리드 접근법
이 글은 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-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 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
-
경합: 드묾 (쓰기는 드물게 발생)