注意 - 本页面内容由英文自动翻译,可能存在不准确之处。翻译仅供方便使用。建议查看英文原版以获得最准确的内容。
我们如何使用 OpenAI API 翻译我们的网站

我们如何使用 OpenAI API 翻译我们的网站

介绍

当我们着手将我们的 GoHugo.io 基础网站变为多语言时,我们希望找到一种高效、可扩展且具有成本效益的翻译生成方式。我们没有手动翻译每个页面,而是利用 OpenAI 的 API 来自动化这个过程。本文将介绍我们如何将 OpenAI API 与 Hugo 集成,使用 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 自动化翻译

我们开发了一个 Bash 脚本 来自动化 Markdown 文件的翻译。该脚本:

  • 从源目录读取英语 .md 文件。
  • 使用 OpenAI API 翻译文本,同时保留 Markdown 格式。
  • 将翻译后的内容写入相应的语言目录。
  • 使用 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_reachedservice_unavailable 等错误,脚本会在重试之前等待。

5. 部署

一旦生成翻译内容,运行 hugo --minify 将构建多语言静态网站,准备部署。

挑战与解决方案

1. 翻译准确性

虽然 OpenAI 的翻译通常是准确的,但某些技术术语可能需要手动审核,但我们只有两个人的团队,所以我们希望一切顺利。我们微调了提示以保持上下文和语气。

2. 格式问题

Markdown 语法在翻译中有时会被更改。为了解决这个问题,我们添加了后处理逻辑以保留格式。

3. API 成本优化

为了降低成本,我们实现了缓存,以避免重新翻译未更改的内容。

4. 高效处理重新翻译

为了重新翻译特定页面,我们添加了一个 retranslate: true 前置参数。脚本仅重新翻译标记了该参数的页面。这使我们能够根据需要更新翻译,而无需重新翻译整个网站。

结论

通过将 OpenAI API 与 Hugo 集成,我们自动化了网站的翻译,同时保持了质量和灵活性。这种方法节省了时间,确保了一致性,并使我们能够轻松扩展。如果您希望使您的 Hugo 网站支持多语言,OpenAI 的 API 提供了一个强大的解决方案。

Share This Page:

相关文章

通过翻译扩展访问

通过翻译扩展访问

我们想首先表示歉意,如果我们的任何翻译未能达到您的期望。在EVnSteven,我们致力于让尽可能多的人可以访问我们的内容,这就是为什么我们启用了多种语言的翻译。然而,我们知道AI生成的翻译可能无法始终准确捕捉每一个细微差别,如果有任何内容让您感到不妥或不清晰,我们深表歉意。

由于我们的翻译是通过AI工具完成的,我们没有资源逐篇更新每种语言的文章。相反,我们计划随着AI翻译工具的改进,定期重新翻译我们的整个库。在此之前,如果某些翻译不完全准确,我们真诚地感谢您的耐心和理解。

您可能会想知道为什么我们要预先翻译整个网站,而不是简单地允许按需的浏览器翻译。通过提供这些预翻译的页面,我们允许Google和其他搜索引擎索引每个语言版本。这意味着您可以更轻松地用母语搜索找到我们,帮助我们更有效地与全球观众联系。

我们唯一会立即更改的情况是,如果某个翻译看起来具有冒犯性。由于我们没有完美的方法自行检查这一点,我们欢迎您的帮助。如果您发现任何不当或冒犯的语言,请通过website.translations@evnsteven.app告知我们。您的反馈确保我们的内容对每个人都保持尊重和可访问。

感谢您的理解,因为我们致力于建设一个更具包容性的全球社区!


阅读更多