
OpenAI API를 사용하여 웹사이트를 번역한 방법
- Articles, Stories
- Hugo , OpenAI , Translation , Automation
- 2025년 3월 19일
- 38 min read
소개
GoHugo.io 기반 웹사이트를 다국어로 만들기 위해 효율적이고 확장 가능하며 비용 효과적인 방법으로 번역을 생성하고자 했습니다. 각 페이지를 수동으로 번역하는 대신 OpenAI의 API를 활용하여 프로세스를 자동화했습니다. 이 글에서는 Hugo와 OpenAI API를 통합하여 Zeon Studio의 HugoPlate 테마를 사용하여 빠르고 정확하게 번역을 생성하는 방법을 설명합니다.
번역을 위해 OpenAI API를 선택한 이유
전통적인 번역 서비스는 종종 상당한 수작업이 필요하며, Google Translate와 같은 자동화 도구는 유용하지만 우리가 필요로 하는 수준의 맞춤화를 항상 제공하지는 않습니다. OpenAI의 API를 사용하여 다음을 수행할 수 있었습니다:
- 대량으로 번역 자동화
- 번역 스타일 맞춤화
- 품질에 대한 더 나은 제어 유지
- Hugo 기반 사이트와 원활하게 통합
- 개별 페이지를 재번역 대상으로 표시
단계별 프로세스
1. Hugo 웹사이트 준비
우리 사이트는 이미 다국어 기능을 지원하는 HugoPlate 테마를 사용하여 설정되어 있었습니다. 첫 번째 단계는 Hugo config/_default/languages.toml
파일에서 언어 지원을 활성화하는 것이었습니다:
################ English language ##################
[en]
languageName = "English"
languageCode = "en-us"
contentDir = "content/english"
weight = 1
################ Arabic language ##################
[ar]
languageName = "العربية"
languageCode = "ar"
contentDir = "content/arabic"
languageDirection = 'rtl'
weight = 2
이 구성은 Hugo가 콘텐츠의 별도 언어 버전을 생성할 수 있도록 보장합니다.
2. OpenAI API로 번역 자동화
Markdown 파일의 번역을 자동화하기 위해 Bash 스크립트를 개발했습니다. 이 스크립트는:
- 소스 디렉토리에서 영어
.md
파일을 읽습니다. - Markdown 형식을 유지하면서 OpenAI API를 사용하여 텍스트를 번역합니다.
- 번역된 콘텐츠를 적절한 언어 디렉토리에 작성합니다.
- JSON 파일을 사용하여 번역 상태를 추적합니다.
다음은 우리의 스크립트 개요입니다:
#!/bin/bash
# ===========================================
# Hugo Content Translation and Update Script (Sequential Processing & New-Language Cleanup)
# ===========================================
# This script translates Hugo Markdown (.md) files from English to all supported target languages
# sequentially (one file at a time). It updates a JSON status file after processing each file.
# At the end of the run, it checks translation_status.json and removes any language from
# translate_new_language.txt only if every file for that language is marked as "success".
# ===========================================
set -euo pipefail
# --- Simple Logging Function (writes to stderr) ---
log_step() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
# --- Environment Setup ---
export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
# (Removed "Script starting." log)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log_step "SCRIPT_DIR set to: $SCRIPT_DIR"
if [ -f "$SCRIPT_DIR/.env" ]; then
log_step "Loading environment variables from .env"
set -o allexport
source "$SCRIPT_DIR/.env"
set +o allexport
fi
# Load new languages from translate_new_language.txt (if available)
declare -a NEW_LANGUAGES=()
if [ -f "$SCRIPT_DIR/translate_new_language.txt" ]; then
while IFS= read -r line || [[ -n "$line" ]]; do
NEW_LANGUAGES+=("$line")
done <"$SCRIPT_DIR/translate_new_language.txt"
else
log_step "No new languages file found; proceeding with empty NEW_LANGUAGES."
fi
API_KEY="${OPENAI_API_KEY:-}"
if [ -z "$API_KEY" ]; then
log_step "❌ Error: OPENAI_API_KEY environment variable is not set."
exit 1
fi
# Supported Languages (full list)
SUPPORTED_LANGUAGES=("ar" "bg" "bn" "cs" "da" "de" "el" "es" "fa" "fi" "fr" "ha" "he" "hi" "hr" "hu" "id" "ig" "it" "ja" "ko" "ml" "mr" "ms" "nl" "no" "pa" "pl" "pt" "ro" "ru" "sk" "sn" "so" "sr" "sv" "sw" "ta" "te" "th" "tl" "tr" "uk" "vi" "xh" "yo" "zh" "zu")
STATUS_FILE="$SCRIPT_DIR/translation_status.json"
SRC_DIR="$SCRIPT_DIR/Content/english"
log_step "Source directory: $SRC_DIR"
# Check dependencies
for cmd in jq curl; do
if ! command -v "$cmd" >/dev/null 2>&1; then
log_step "❌ Error: '$cmd' is required. Please install it."
exit 1
fi
done
MAX_RETRIES=5
WAIT_TIME=2 # seconds
# Create/initialize status file if missing
if [ ! -f "$STATUS_FILE" ]; then
echo "{}" >"$STATUS_FILE"
log_step "Initialized status file at: $STATUS_FILE"
fi
# --- Locking for Status Updates ---
lock_status() {
local max_wait=10
local start_time
start_time=$(date +%s)
while ! mkdir "$STATUS_FILE.lockdir" 2>/dev/null; do
sleep 0.01
local now
now=$(date +%s)
if ((now - start_time >= max_wait)); then
log_step "WARNING: Lock wait exceeded ${max_wait}s. Forcibly removing stale lock."
rm -rf "$STATUS_FILE.lockdir"
fi
done
}
unlock_status() {
rmdir "$STATUS_FILE.lockdir"
}
update_status() {
local file_path="$1" lang="$2" status="$3"
lock_status
jq --arg file "$file_path" --arg lang "$lang" --arg status "$status" \
'.[$file][$lang] = $status' "$STATUS_FILE" >"$STATUS_FILE.tmp" && mv "$STATUS_FILE.tmp" "$STATUS_FILE"
unlock_status
}
# --- Translation Function ---
translate_text() {
local text="$1" lang="$2"
local retry_count=0
while [ "$retry_count" -lt "$MAX_RETRIES" ]; do
user_message="Translate the following text to $lang. Preserve all formatting exactly as in the original.
$text"
json_payload=$(jq -n \
--arg system "Translate from English to $lang. Preserve original formatting exactly." \
--arg user_message "$user_message" \
'{
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": $system},
{"role": "user", "content": $user_message}
],
"temperature": 0.3
}')
response=$(curl -s https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d "$json_payload")
log_step "📥 Received API response."
local error_type
error_type=$(echo "$response" | jq -r '.error.type // empty')
local error_message
error_message=$(echo "$response" | jq -r '.error.message // empty')
if [ "$error_type" == "insufficient_quota" ]; then
sleep "$WAIT_TIME"
retry_count=$((retry_count + 1))
elif [[ "$error_type" == "rate_limit_reached" || "$error_type" == "server_error" || "$error_type" == "service_unavailable" ]]; then
sleep "$WAIT_TIME"
retry_count=$((retry_count + 1))
elif [ "$error_type" == "invalid_request_error" ]; then
return 1
elif [ -z "$error_type" ]; then
if ! translated_text=$(echo "$response" | jq -r '.choices[0].message.content' 2>/dev/null); then
return 1
fi
if [ "$translated_text" == "null" ] || [ -z "$translated_text" ]; then
return 1
else
translated_text=$(echo "$translated_text" | sed -e 's/^```[[:space:]]*//; s/[[:space:]]*```$//')
echo "$translated_text"
return 0
fi
else
return 1
fi
done
return 1
}
# --- Process a Single File (Sequential Version) ---
process_file() {
local src_file="$1" target_file="$2" lang="$3" rel_src="$4"
# If target file exists and is non-empty, mark status as success.
if [ -s "$target_file" ]; then
update_status "$rel_src" "$lang" "success"
return 0
fi
content=$(<"$src_file")
if [[ "$content" =~ ^(---|\+\+\+)[[:space:]]*$ ]] && [[ "$content" =~ [[:space:]]*(---|\+\+\+\+)[[:space:]]*$ ]]; then
front_matter=$(echo "$content" | sed -n '/^\(---\|\+\+\+\)$/,/^\(---\|\+\+\+\)$/p')
body_content=$(echo "$content" | sed -n '/^\(---\|\+\+\+\)$/,/^\(---\|\+\+\+\)$/d')
else
front_matter=""
body_content="$content"
fi
log_step "Translating [$rel_src] to $lang..."
translated_body=$(translate_text "$body_content" "$lang")
if [ $? -ne 0 ]; then
update_status "$rel_src" "$lang" "failed"
return 1
fi
mkdir -p "$(dirname "$target_file")"
if [ -n "$front_matter" ]; then
echo -e "$front_matter
$translated_body" >"$target_file"
else
echo -e "$translated_body" >"$target_file"
fi
updated_content=$(echo "$content" | sed -E 's/^retranslate:\s*true/retranslate: false/')
echo "$updated_content" >"$src_file"
update_status "$rel_src" "$lang" "success"
}
# --- Main Sequential Processing ---
ALL_SUCCESS=true
for TARGET_LANG in "${SUPPORTED_LANGUAGES[@]}"; do
log_step "Processing language: $TARGET_LANG"
TARGET_DIR="$SCRIPT_DIR/Content/$TARGET_LANG"
while IFS= read -r -d '' src_file; do
rel_src="${src_file#$SCRIPT_DIR/}"
target_file="$TARGET_DIR/${src_file#$SRC_DIR/}"
# If file is marked not to retranslate, check that target file exists and is non-empty.
if ! [[ " ${NEW_LANGUAGES[@]:-} " =~ " ${TARGET_LANG} " ]] && grep -q '^retranslate:\s*false' "$src_file"; then
if [ -s "$target_file" ]; then
update_status "$rel_src" "$TARGET_LANG" "success"
else
update_status "$rel_src" "$TARGET_LANG" "failed"
fi
continue
fi
process_file "$src_file" "$target_file" "$TARGET_LANG" "$rel_src"
done < <(find "$SRC_DIR" -type f -name "*.md" -print0)
done
log_step "Translation run completed."
end_time=$(date +%s)
duration=$((end_time - $(date +%s)))
log_step "Execution Time: $duration seconds"
if [ "$ALL_SUCCESS" = true ]; then
log_step "🎉 Translation completed successfully for all supported languages!"
else
log_step "⚠️ Translation completed with some errors."
fi
# --- Clean Up Fully Translated New Languages ---
if [ -f "$SCRIPT_DIR/translate_new_language.txt" ]; then
log_step "Cleaning up fully translated new languages..."
for lang in "${NEW_LANGUAGES[@]:-}"; do
incomplete=$(jq --arg lang "$lang" 'to_entries[] | select(.value[$lang] != null and (.value[$lang] != "success")) | .key' "$STATUS_FILE")
if [ -z "$incomplete" ]; then
log_step "All translations for new language '$lang' are marked as success. Removing from translate_new_language.txt."
sed -E -i '' "/^[[:space:]]*$lang[[:space:]]*$/d" "$SCRIPT_DIR/translate_new_language.txt"
else
log_step "Language '$lang' still has incomplete translations."
fi
done
fi
3. 번역 상태 관리
중복 번역을 방지하고 진행 상황을 추적하기 위해 JSON 파일(translation_status.json
)을 사용했습니다. 스크립트는 각 문서를 처리한 후 이 파일을 업데이트하여 새로 추가되거나 업데이트된 콘텐츠만 번역되도록 합니다.
4. 오류 처리 및 API 속도 제한
우리는 속도 제한, API 실패 및 쿼터 문제를 처리하기 위해 재시도 및 오류 처리를 구현했습니다. OpenAI API가 rate_limit_reached
또는 service_unavailable
과 같은 오류를 반환하면 스크립트는 재시도하기 전에 대기합니다.
5. 배포
번역된 콘텐츠가 생성되면 hugo --minify
를 실행하여 다국어 정적 사이트를 빌드하고 배포할 준비를 합니다.
도전 과제 및 해결책
1. 번역 정확성
OpenAI의 번역은 일반적으로 정확했지만 일부 기술 용어는 수동 검토가 필요할 수 있습니다. 우리는 단 두 명의 팀이므로 최선을 바라며, 맥락과 톤을 유지하기 위해 프롬프트를 미세 조정했습니다.
2. 형식 문제
Markdown 구문이 번역 중에 변경되는 경우가 있었습니다. 이를 해결하기 위해 형식을 유지하기 위한 후처리 로직을 추가했습니다.
3. API 비용 최적화
비용을 줄이기 위해 변경되지 않은 콘텐츠를 다시 번역하지 않도록 캐싱을 구현했습니다.
4. 재번역 효율적으로 처리하기
특정 페이지를 재번역하기 위해 retranslate: true
프론트 매터 매개변수를 추가했습니다. 스크립트는 이 매개변수로 표시된 페이지만 재번역합니다. 이를 통해 전체 사이트를 재번역하지 않고도 필요에 따라 번역을 업데이트할 수 있습니다.
결론
OpenAI API를 Hugo와 통합함으로써 우리는 품질과 유연성을 유지하면서 웹사이트의 번역을 자동화했습니다. 이 접근 방식은 시간을 절약하고 일관성을 보장하며 손쉽게 확장할 수 있게 해주었습니다. Hugo 사이트를 다국어로 만들고자 한다면 OpenAI의 API는 강력한 솔루션을 제공합니다.