modified: translate_content.py
This commit is contained in:
parent
17ec4e853c
commit
8e974f30c1
1 changed files with 270 additions and 103 deletions
|
|
@ -1,28 +1,41 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
# Main script to translate .rst files into multiple languages, converting them to both .rst and .md formats.
|
||||||
import hashlib
|
# Supports progress tracking with tqdm and a debug mode for streaming API responses.
|
||||||
import json
|
|
||||||
from openai import OpenAI, OpenAIError
|
|
||||||
import pandoc
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from tqdm import tqdm
|
|
||||||
import sys
|
|
||||||
import tiktoken
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
# Parse command-line arguments
|
import os # For file and directory operations
|
||||||
parser = argparse.ArgumentParser(description="Translate .rst files with progress and debugging.")
|
import hashlib # For generating content hashes
|
||||||
parser.add_argument("--debug", action="store_true", help="Enable debug mode with streaming output.")
|
import json # For reading/writing hash files
|
||||||
|
from openai import OpenAI, OpenAIError # OpenAI API client and error type
|
||||||
|
import pandoc # For converting .rst to .md
|
||||||
|
import re # For regex pattern matching
|
||||||
|
import time # For retry delays
|
||||||
|
from tqdm import tqdm # For progress bars
|
||||||
|
import sys # For stderr output
|
||||||
|
import tiktoken # For token counting
|
||||||
|
import argparse # For command-line argument parsing
|
||||||
|
from typing import Dict, Tuple, List, Optional # Type hints for better code clarity
|
||||||
|
|
||||||
|
# Parse command-line arguments to enable optional debug mode
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Translate .rst files with progress and debugging."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug", action="store_true", help="Enable debug mode with streaming output."
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# AI client setup
|
# AI client setup using environment variables for flexibility and security
|
||||||
client = OpenAI(base_url="https://hermes.ai.unturf.com/v1", api_key="choose-any-value")
|
# Defaults are provided for local testing, but should be overridden in production
|
||||||
MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
|
base_url = os.environ.get("OPENAI_BASE_URL", "https://hermes.ai.unturf.com/v1")
|
||||||
|
api_key = os.environ.get("OPENAI_API_KEY", "choose-any-value")
|
||||||
|
client = OpenAI(base_url=base_url, api_key=api_key)
|
||||||
|
MODEL = os.environ.get("OPENAI_MODEL", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")
|
||||||
|
|
||||||
# Tokenizer setup (used only once per file)
|
# Tokenizer setup for counting tokens in .rst content (used once per file)
|
||||||
TOKENIZER = tiktoken.get_encoding("cl100k_base")
|
TOKENIZER = tiktoken.get_encoding("cl100k_base")
|
||||||
|
|
||||||
|
# Dictionary of top languages to translate into, mapping codes to full names
|
||||||
|
# Excludes languages like Tamil and Kuku Yalanji due to known looping issues
|
||||||
TOP_20_LANGS = {
|
TOP_20_LANGS = {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"zh": "Chinese (Simplified)",
|
"zh": "Chinese (Simplified)",
|
||||||
|
|
@ -41,30 +54,48 @@ TOP_20_LANGS = {
|
||||||
"mr": "Marathi",
|
"mr": "Marathi",
|
||||||
"te": "Telugu",
|
"te": "Telugu",
|
||||||
"tr": "Turkish",
|
"tr": "Turkish",
|
||||||
# "ta": "Tamil",
|
|
||||||
"zh-tw": "Chinese (Traditional)",
|
"zh-tw": "Chinese (Traditional)",
|
||||||
"ko": "Korean",
|
"ko": "Korean",
|
||||||
}
|
}
|
||||||
|
|
||||||
CONTENT_DIR = "content"
|
# Directory and file constants
|
||||||
HASH_FILE = "content/translation_hashes.json"
|
CONTENT_DIR = "content" # Root directory for .rst files
|
||||||
|
HASH_FILE = "content/translation_hashes.json" # File to store content hashes
|
||||||
|
|
||||||
|
|
||||||
def replace_special_content(content):
|
def replace_special_content(content: str) -> Tuple[str, Dict[str, str]]:
|
||||||
"""Replace code blocks, URIs, and images with unique placeholders."""
|
"""Replace special RST content (code blocks, URIs, images) with unique placeholders.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The raw .rst content to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of (replaced content, dictionary mapping placeholders to original text).
|
||||||
|
"""
|
||||||
|
# Regex patterns to identify special content in RST
|
||||||
patterns = [
|
patterns = [
|
||||||
|
# Code directives (single-line and multi-line)
|
||||||
(re.compile(r"(\.\.\s+code::\s+[^\n]*(?:\n\s+[^\n]+)*)"), "__CODE_{}__"),
|
(re.compile(r"(\.\.\s+code::\s+[^\n]*(?:\n\s+[^\n]+)*)"), "__CODE_{}__"),
|
||||||
|
(re.compile(r"(\.\.\s+code-block::\s+[^\n]*(?:\n\s+[^\n]+)*)"), "__CODE_{}__"),
|
||||||
|
(re.compile(r"(\.\.\s+highlight::\s+[^\n]*(?:\n\s+[^\n]+)*)"), "__CODE_{}__"),
|
||||||
|
# Raw content directive
|
||||||
|
(re.compile(r"(\.\.\s+raw::\s+[^\n]*(?:\n\s+[^\n]+)*)"), "__RAW_{}__"),
|
||||||
|
# Inline literals (double and single backticks)
|
||||||
|
(re.compile(r"``([^`]+?)``"), "__CODE_{}__"),
|
||||||
(re.compile(r"`([^`]+?)`(?![`_])"), "__CODE_{}__"),
|
(re.compile(r"`([^`]+?)`(?![`_])"), "__CODE_{}__"),
|
||||||
|
# URI patterns (links with optional inline code)
|
||||||
(re.compile(r"`([^`]+?)\s+<(https?://[^\s>]+)>`_"), "__URI_{}__"),
|
(re.compile(r"`([^`]+?)\s+<(https?://[^\s>]+)>`_"), "__URI_{}__"),
|
||||||
(re.compile(r"(`[^`]+?`_\s+)?<(https?://[^\s>]+)>"), "__URI_{}__"),
|
(re.compile(r"(`[^`]+?`_\s+)?<(https?://[^\s>]+)>"), "__URI_{}__"),
|
||||||
(re.compile(r"(\.\.\s+_.*?:(?:\s+https?://[^\s]+)?)"), "__URI_{}__"),
|
(re.compile(r"(\.\.\s+_.*?:(?:\s+https?://[^\s]+)?)"), "__URI_{}__"),
|
||||||
(re.compile(r"(\.\.\s+image::\s+[^\s]+(?:\s*\n\s+:.*?)*)"), "__IMG_{}__"),
|
# Image directive with optional attributes
|
||||||
|
(re.compile(r"(\.\.\s+image::\s+[^\s]+(?:\n\s+:.*?)*)"), "__IMG_{}__"),
|
||||||
]
|
]
|
||||||
|
|
||||||
placeholders = {}
|
placeholders: Dict[str, str] = {} # Store original content for restoration
|
||||||
counter = {"code": 0, "uri": 0, "img": 0}
|
counter = {"code": 0, "uri": 0, "img": 0, "raw": 0} # Track placeholder indices
|
||||||
|
|
||||||
def replacer(match, placeholder_template, key):
|
def replacer(match, placeholder_template: str, key: str):
|
||||||
|
"""Helper to replace matched content with a placeholder."""
|
||||||
nonlocal counter
|
nonlocal counter
|
||||||
full_match = match.group(0)
|
full_match = match.group(0)
|
||||||
placeholder = placeholder_template.format(counter[key])
|
placeholder = placeholder_template.format(counter[key])
|
||||||
|
|
@ -74,8 +105,7 @@ def replace_special_content(content):
|
||||||
|
|
||||||
replaced_content = content
|
replaced_content = content
|
||||||
for pattern, template in patterns:
|
for pattern, template in patterns:
|
||||||
for pattern, template in patterns:
|
# Apply replacements based on placeholder type
|
||||||
if "CODE" in template:
|
|
||||||
if "CODE" in template:
|
if "CODE" in template:
|
||||||
current_template = template # Create a local variable that will be properly captured
|
current_template = template # Create a local variable that will be properly captured
|
||||||
replaced_content = pattern.sub(
|
replaced_content = pattern.sub(
|
||||||
|
|
@ -91,19 +121,97 @@ def replace_special_content(content):
|
||||||
replaced_content = pattern.sub(
|
replaced_content = pattern.sub(
|
||||||
lambda m: replacer(m, current_template, "img"), replaced_content
|
lambda m: replacer(m, current_template, "img"), replaced_content
|
||||||
)
|
)
|
||||||
|
elif "RAW" in template:
|
||||||
|
replaced_content = pattern.sub(
|
||||||
|
lambda m: replacer(m, template, "raw"), replaced_content
|
||||||
|
)
|
||||||
|
|
||||||
return replaced_content, placeholders
|
return replaced_content, placeholders
|
||||||
|
|
||||||
|
|
||||||
def restore_special_content(content, placeholders):
|
def restore_special_content(content: str, placeholders: Dict[str, str]) -> str:
|
||||||
"""Restore original code, URIs, and images from placeholders."""
|
"""Restore original special content from placeholders.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The content with placeholders.
|
||||||
|
placeholders: Dictionary mapping placeholders to original text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The content with placeholders replaced by original text.
|
||||||
|
"""
|
||||||
for placeholder, original in placeholders.items():
|
for placeholder, original in placeholders.items():
|
||||||
content = content.replace(placeholder, original)
|
content = content.replace(placeholder, original)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
def translate_text(text, target_lang_full, retries=3, delay=5, timeout=100):
|
def _handle_translation_error(
|
||||||
"""Translate text with optional streaming debug mode."""
|
e: Exception,
|
||||||
|
attempt: int,
|
||||||
|
retries: int,
|
||||||
|
delay: float,
|
||||||
|
target_lang_full: str,
|
||||||
|
debug: bool = False,
|
||||||
|
) -> bool:
|
||||||
|
"""Handle translation errors with retries and optional debug output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: The caught exception.
|
||||||
|
attempt: Current attempt number (0-based).
|
||||||
|
retries: Total number of retries.
|
||||||
|
delay: Delay between retries in seconds.
|
||||||
|
target_lang_full: The target language name.
|
||||||
|
debug: Whether to print debug messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if retrying should continue, False if retries are exhausted.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If retries are exhausted, with chained context.
|
||||||
|
"""
|
||||||
|
if debug:
|
||||||
|
error_type = "OpenAI API" if isinstance(e, OpenAIError) else "Unexpected"
|
||||||
|
print(
|
||||||
|
f"\nDEBUG: {error_type} error on attempt {attempt + 1}/{retries}: {str(e)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if attempt < retries - 1:
|
||||||
|
if debug:
|
||||||
|
print(f"DEBUG: Retrying in {delay} seconds...", file=sys.stderr)
|
||||||
|
time.sleep(delay)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
error_type = (
|
||||||
|
"Failed to translate to" if isinstance(e, OpenAIError) else "Translation to"
|
||||||
|
)
|
||||||
|
raise Exception(
|
||||||
|
f"{error_type} {target_lang_full} after {retries} attempts: {str(e)}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def translate_text(
|
||||||
|
text: str,
|
||||||
|
target_lang_full: str,
|
||||||
|
retries: int = 3,
|
||||||
|
delay: float = 5,
|
||||||
|
timeout: float = 100,
|
||||||
|
) -> str:
|
||||||
|
"""Translate RST content into the target language using the OpenAI API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The RST content to translate.
|
||||||
|
target_lang_full: The full name of the target language.
|
||||||
|
retries: Number of retry attempts for API calls.
|
||||||
|
delay: Delay between retries in seconds.
|
||||||
|
timeout: Timeout for blocking API calls in seconds.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The translated RST content.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If translation fails after all retries.
|
||||||
|
"""
|
||||||
|
# Construct the prompt for the OpenAI API
|
||||||
prompt = (
|
prompt = (
|
||||||
f"Translate the following reStructuredText (.rst) content into {target_lang_full}. "
|
f"Translate the following reStructuredText (.rst) content into {target_lang_full}. "
|
||||||
"The input is in English and formatted as valid reStructuredText, which includes metadata "
|
"The input is in English and formatted as valid reStructuredText, which includes metadata "
|
||||||
|
|
@ -118,16 +226,23 @@ def translate_text(text, target_lang_full, retries=3, delay=5, timeout=100):
|
||||||
messages = [{"role": "user", "content": prompt}]
|
messages = [{"role": "user", "content": prompt}]
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
# Streaming mode with no newline between chunks
|
# Debug mode: Stream the API response for real-time debugging
|
||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
print(f"\nDEBUG: Streaming attempt {attempt + 1}/{retries} for {target_lang_full}", file=sys.stderr)
|
print(
|
||||||
print(f"DEBUG: Translation output in {target_lang_full}: ", end="", file=sys.stderr)
|
f"\nDEBUG: Streaming attempt {attempt + 1}/{retries} for {target_lang_full}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"DEBUG: Translation output in {target_lang_full}: ",
|
||||||
|
end="",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
stream = client.chat.completions.create(
|
stream = client.chat.completions.create(
|
||||||
model=MODEL,
|
model=MODEL,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
max_tokens=60000,
|
max_tokens=16000, # Lower limit for streaming to avoid overload
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
result = ""
|
result = ""
|
||||||
|
|
@ -135,65 +250,79 @@ def translate_text(text, target_lang_full, retries=3, delay=5, timeout=100):
|
||||||
if chunk.choices[0].delta.content is not None:
|
if chunk.choices[0].delta.content is not None:
|
||||||
content = chunk.choices[0].delta.content
|
content = chunk.choices[0].delta.content
|
||||||
result += content
|
result += content
|
||||||
print(content, end="", file=sys.stderr) # No newline, continuous stream
|
print(content, end="", file=sys.stderr)
|
||||||
sys.stderr.flush() # Ensure immediate output
|
sys.stderr.flush() # Ensure immediate output
|
||||||
print(f"\nDEBUG: Streaming completed for {target_lang_full}", file=sys.stderr)
|
print(
|
||||||
|
f"\nDEBUG: Streaming completed for {target_lang_full}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
except OpenAIError as e:
|
except (OpenAIError, Exception) as e:
|
||||||
print(f"\nDEBUG: OpenAI API error on attempt {attempt + 1}/{retries}: {str(e)}", file=sys.stderr)
|
if not _handle_translation_error(
|
||||||
if attempt < retries - 1:
|
e, attempt, retries, delay, target_lang_full, debug=True
|
||||||
print(f"DEBUG: Retrying in {delay} seconds...", file=sys.stderr)
|
):
|
||||||
time.sleep(delay)
|
continue
|
||||||
else:
|
|
||||||
raise Exception(f"Failed to translate to {target_lang_full} after {retries} attempts: {str(e)}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\nDEBUG: Unexpected error on attempt {attempt + 1}/{retries}: {str(e)}", file=sys.stderr)
|
|
||||||
if attempt < retries - 1:
|
|
||||||
print(f"DEBUG: Retrying in {delay} seconds...", file=sys.stderr)
|
|
||||||
time.sleep(delay)
|
|
||||||
else:
|
|
||||||
raise Exception(f"Translation to {target_lang_full} failed after {retries} attempts: {str(e)}")
|
|
||||||
else:
|
else:
|
||||||
# Non-debug, blocking mode
|
# Normal mode: Blocking API call with timeout
|
||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model=MODEL,
|
model=MODEL,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=0.5,
|
temperature=0.5,
|
||||||
max_tokens=60000,
|
max_tokens=60000, # Higher limit for full responses
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
return response.choices[0].message.content
|
return response.choices[0].message.content
|
||||||
except OpenAIError as e:
|
except (OpenAIError, Exception) as e:
|
||||||
if attempt < retries - 1:
|
if not _handle_translation_error(
|
||||||
time.sleep(delay)
|
e, attempt, retries, delay, target_lang_full
|
||||||
else:
|
):
|
||||||
raise Exception(f"Failed to translate to {target_lang_full} after {retries} attempts: {str(e)}")
|
continue
|
||||||
except Exception as e:
|
|
||||||
if attempt < retries - 1:
|
|
||||||
time.sleep(delay)
|
|
||||||
else:
|
|
||||||
raise Exception(f"Translation to {target_lang_full} failed after {retries} attempts: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def load_hashes():
|
def load_hashes() -> Dict[str, Dict[str, str]]:
|
||||||
|
"""Load translation hashes from the hash file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary of hashes, empty if the file doesn’t exist.
|
||||||
|
"""
|
||||||
if os.path.exists(HASH_FILE):
|
if os.path.exists(HASH_FILE):
|
||||||
with open(HASH_FILE, "r") as f:
|
with open(HASH_FILE, "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def save_hashes(hashes):
|
def save_hashes(hashes: Dict[str, Dict[str, str]]) -> None:
|
||||||
with open(HASH_FILE, "w") as f:
|
"""Save translation hashes to the hash file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hashes: The dictionary of hashes to save.
|
||||||
|
"""
|
||||||
|
with open(HASH_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(hashes, f, indent=2)
|
json.dump(hashes, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
def get_content_hash(content):
|
def get_content_hash(content: str) -> str:
|
||||||
|
"""Generate a SHA-256 hash of the content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The content to hash.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The hexadecimal hash string.
|
||||||
|
"""
|
||||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def process_file(file_path, lang_pbar):
|
def process_file(file_path: str, lang_pbar) -> None:
|
||||||
|
"""Process a single .rst file, translating it into all target languages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: Path to the .rst file.
|
||||||
|
lang_pbar: tqdm progress bar for language processing.
|
||||||
|
"""
|
||||||
|
# Read the .rst file and split into header and content
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
header_end = next(
|
header_end = next(
|
||||||
|
|
@ -202,29 +331,32 @@ def process_file(file_path, lang_pbar):
|
||||||
header = lines[:header_end]
|
header = lines[:header_end]
|
||||||
content = "".join(lines[header_end:])
|
content = "".join(lines[header_end:])
|
||||||
|
|
||||||
# Count tokens for the entire .rst content once
|
# Count tokens once for the entire content
|
||||||
total_tokens = len(TOKENIZER.encode(content))
|
total_tokens = len(TOKENIZER.encode(content))
|
||||||
|
|
||||||
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
slug = base_name
|
slug = base_name
|
||||||
source_hash = get_content_hash(content)
|
source_hash = get_content_hash(content)
|
||||||
|
|
||||||
|
# Load existing hashes to check for updates
|
||||||
hashes = load_hashes()
|
hashes = load_hashes()
|
||||||
file_hashes = hashes.get(file_path, {})
|
file_hashes = hashes.get(file_path, {})
|
||||||
needs_update = file_hashes.get("source") != source_hash
|
needs_update = file_hashes.get("source") != source_hash
|
||||||
|
|
||||||
# Replace code, URIs, and images with placeholders
|
# Replace special content with placeholders
|
||||||
replaced_content, placeholders = replace_special_content(content)
|
replaced_content, placeholders = replace_special_content(content)
|
||||||
|
|
||||||
|
# Process each language
|
||||||
for lang_code, lang_full in TOP_20_LANGS.items():
|
for lang_code, lang_full in TOP_20_LANGS.items():
|
||||||
lang_pbar.set_description(f"File: {slug}.rst | Lang: {lang_full} ({lang_code}) | Total Tokens: {total_tokens}")
|
lang_pbar.set_description(
|
||||||
|
f"File: {slug}.rst | Lang: {lang_full} ({lang_code}) | Total Tokens: {total_tokens}"
|
||||||
|
)
|
||||||
lang_dir = f"{CONTENT_DIR}/{lang_code}"
|
lang_dir = f"{CONTENT_DIR}/{lang_code}"
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
|
|
||||||
rst_file = f"{lang_dir}/{slug}.rst"
|
rst_file = f"{lang_dir}/{slug}.rst"
|
||||||
md_file = f"{lang_dir}/{slug}.md"
|
md_file = f"{lang_dir}/{slug}.md"
|
||||||
|
|
||||||
# Update header with lang_code
|
# Update header with language code
|
||||||
new_header = []
|
new_header = []
|
||||||
lang_set = False
|
lang_set = False
|
||||||
for line in header:
|
for line in header:
|
||||||
|
|
@ -236,12 +368,11 @@ def process_file(file_path, lang_pbar):
|
||||||
if not lang_set:
|
if not lang_set:
|
||||||
new_header.append(f":lang: {lang_code}\n")
|
new_header.append(f":lang: {lang_code}\n")
|
||||||
|
|
||||||
# Check existing hashes for this language
|
# Check if translation already exists and is up-to-date
|
||||||
lang_hashes = file_hashes.get(lang_code, {})
|
lang_hashes = file_hashes.get(lang_code, {})
|
||||||
rst_hash = lang_hashes.get("rst")
|
rst_hash = lang_hashes.get("rst")
|
||||||
md_hash = lang_hashes.get("md")
|
md_hash = lang_hashes.get("md")
|
||||||
|
|
||||||
# Check if files exist and match hashes
|
|
||||||
rst_exists = os.path.exists(rst_file)
|
rst_exists = os.path.exists(rst_file)
|
||||||
md_exists = os.path.exists(md_file)
|
md_exists = os.path.exists(md_file)
|
||||||
if rst_exists and md_exists and not needs_update:
|
if rst_exists and md_exists and not needs_update:
|
||||||
|
|
@ -253,11 +384,21 @@ def process_file(file_path, lang_pbar):
|
||||||
lang_pbar.update(1)
|
lang_pbar.update(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Translate or use original content
|
# Translate content (English is copied directly)
|
||||||
if lang_code == "en":
|
if lang_code == "en":
|
||||||
translated_replaced_content = replaced_content
|
translated_replaced_content = replaced_content
|
||||||
else:
|
else:
|
||||||
translated_replaced_content = translate_text(replaced_content, lang_full)
|
try:
|
||||||
|
translated_replaced_content = translate_text(
|
||||||
|
replaced_content, lang_full
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error translating {slug} to {lang_full}: {str(e)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
lang_pbar.update(1)
|
||||||
|
continue
|
||||||
|
|
||||||
# Restore special content
|
# Restore special content
|
||||||
translated_content = restore_special_content(
|
translated_content = restore_special_content(
|
||||||
|
|
@ -265,22 +406,29 @@ def process_file(file_path, lang_pbar):
|
||||||
)
|
)
|
||||||
full_rst_content = "".join(new_header) + translated_content
|
full_rst_content = "".join(new_header) + translated_content
|
||||||
|
|
||||||
# Save .rst and calculate hash
|
# Save translated .rst and convert to .md
|
||||||
with open(rst_file, "w", encoding="utf-8") as f:
|
try:
|
||||||
f.write(full_rst_content)
|
with open(rst_file, "w", encoding="utf-8") as f:
|
||||||
rst_hash = get_content_hash(full_rst_content)
|
f.write(full_rst_content)
|
||||||
|
rst_hash = get_content_hash(full_rst_content)
|
||||||
|
|
||||||
# Convert to .md and calculate hash
|
replaced_full_rst = "".join(new_header) + translated_replaced_content
|
||||||
replaced_full_rst = "".join(new_header) + translated_replaced_content
|
md_content = pandoc.read(replaced_full_rst, format="rst")
|
||||||
md_content = pandoc.read(replaced_full_rst, format="rst")
|
md_replaced = pandoc.write(md_content, format="markdown")
|
||||||
md_replaced = pandoc.write(md_content, format="markdown")
|
md_final = restore_special_content(md_replaced, placeholders)
|
||||||
md_final = restore_special_content(md_replaced, placeholders)
|
|
||||||
|
|
||||||
with open(md_file, "w", encoding="utf-8") as f:
|
with open(md_file, "w", encoding="utf-8") as f:
|
||||||
f.write(md_final)
|
f.write(md_final)
|
||||||
md_hash = get_content_hash(md_final)
|
md_hash = get_content_hash(md_final)
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Error converting/saving {slug} to {lang_full}: {str(e)}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
lang_pbar.update(1)
|
||||||
|
continue
|
||||||
|
|
||||||
# Update hashes for this language
|
# Update hashes
|
||||||
if file_path not in hashes:
|
if file_path not in hashes:
|
||||||
hashes[file_path] = {}
|
hashes[file_path] = {}
|
||||||
hashes[file_path]["source"] = source_hash
|
hashes[file_path]["source"] = source_hash
|
||||||
|
|
@ -289,28 +437,47 @@ def process_file(file_path, lang_pbar):
|
||||||
hashes[file_path][lang_code]["rst"] = rst_hash
|
hashes[file_path][lang_code]["rst"] = rst_hash
|
||||||
hashes[file_path][lang_code]["md"] = md_hash
|
hashes[file_path][lang_code]["md"] = md_hash
|
||||||
|
|
||||||
# Save hashes after each language
|
|
||||||
save_hashes(hashes)
|
save_hashes(hashes)
|
||||||
lang_pbar.update(1)
|
lang_pbar.update(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
|
"""Main function to process all .rst files in the content directory."""
|
||||||
|
# Collect all source .rst files (excluding language subdirectories)
|
||||||
rst_files = [
|
rst_files = [
|
||||||
os.path.join(root, file)
|
os.path.join(root, file)
|
||||||
for root, _, files in os.walk(CONTENT_DIR)
|
for root, _, files in os.walk(CONTENT_DIR)
|
||||||
for file in files
|
for file in files
|
||||||
if file.endswith(".rst")
|
if file.endswith(".rst")
|
||||||
and not any(f"/{lang}/" in root for lang in TOP_20_LANGS.keys())
|
and not any(f"/{lang}/" in root for lang in TOP_20_LANGS)
|
||||||
]
|
]
|
||||||
|
|
||||||
with tqdm(total=len(rst_files), desc="Total Files", unit="file", file=sys.stderr) as file_pbar:
|
# Use nested progress bars for files and languages
|
||||||
with tqdm(total=len(TOP_20_LANGS), desc="File: N/A | Lang: N/A | Total Tokens: N/A", unit="lang", file=sys.stderr) as lang_pbar:
|
with tqdm(
|
||||||
for file_path in rst_files:
|
total=len(rst_files), desc="Total Files", unit="file", file=sys.stderr
|
||||||
file_pbar.set_description(f"Total Files (Current: {os.path.basename(file_path)})")
|
) as file_pbar, tqdm(
|
||||||
lang_pbar.reset()
|
total=len(TOP_20_LANGS),
|
||||||
process_file(file_path, lang_pbar)
|
desc="File: N/A | Lang: N/A | Total Tokens: N/A",
|
||||||
file_pbar.update(1)
|
unit="lang",
|
||||||
|
file=sys.stderr,
|
||||||
|
) as lang_pbar:
|
||||||
|
for file_path in rst_files:
|
||||||
|
file_pbar.set_description(
|
||||||
|
f"Total Files (Current: {os.path.basename(file_path)})"
|
||||||
|
)
|
||||||
|
lang_pbar.reset()
|
||||||
|
process_file(file_path, lang_pbar)
|
||||||
|
file_pbar.update(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Verify Pandoc availability before starting
|
||||||
|
try:
|
||||||
|
pandoc.read("test", format="rst")
|
||||||
|
except Exception:
|
||||||
|
print(
|
||||||
|
"Pandoc is not installed or not functioning correctly. Install Pandoc to enable .md conversion.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue