closes #351 BREAKING CHANGE: The old preferences panel has been replaced with a brand new one Preferences are now much simpler, full of visual illustrations, and should provide a much better experience. Pick between 3 styles (thumbnails, app-icons, titles), sizes, dark/light themes, high visibility options, and more!
220 lines
10 KiB
Python
220 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import argparse
|
|
from googletrans import Translator
|
|
import time
|
|
|
|
comment = "/* No comment provided by engineer. */"
|
|
|
|
|
|
def get_language_code(directory_name):
|
|
"""
|
|
Convert the language folder name in the i18n directory to Google Translate language code.
|
|
"""
|
|
lang_map = {
|
|
'zh-Hans': 'zh-cn',
|
|
'zh-CN': 'zh-cn',
|
|
'zh-Hant': 'zh-tw',
|
|
'zh-TW': 'zh-tw',
|
|
'zh-HK': 'zh-tw',
|
|
}
|
|
return lang_map.get(directory_name, directory_name)
|
|
|
|
|
|
def read_localizable_strings_with_comments(file_path):
|
|
"""
|
|
Read the Localizable.strings file into a dictionary, including comments.
|
|
"""
|
|
translations = {}
|
|
comments = {}
|
|
current_comment = []
|
|
if os.path.isfile(file_path):
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('/*') and line.endswith('*/'):
|
|
current_comment.append(line)
|
|
elif line.startswith('/*'):
|
|
current_comment.append(line)
|
|
elif line.endswith('*/'):
|
|
current_comment.append(line)
|
|
elif line == "":
|
|
if current_comment:
|
|
current_comment.append(line)
|
|
elif '=' in line:
|
|
key, value = line.split('=', 1)
|
|
translations[key.strip()] = value.strip().strip(';').strip('"')
|
|
if current_comment:
|
|
comments[key.strip()] = "\n".join(current_comment)
|
|
current_comment = []
|
|
return translations, comments
|
|
|
|
|
|
def translate_text(translator, text, src_lang, dest_lang, max_retries=0):
|
|
"""
|
|
Translate text with retries in case of failure.
|
|
"""
|
|
for attempt in range(max_retries + 1):
|
|
try:
|
|
translated = translator.translate(text, src=src_lang, dest=dest_lang).text
|
|
return translated
|
|
except Exception as e:
|
|
print(f"Failed to translate '{text}' to '{dest_lang}' on attempt {attempt + 1}: {e}")
|
|
time.sleep(1) # Wait before retrying
|
|
print(f"Failed to translate '{text}' to '{dest_lang}' after {max_retries} attempts, fallback to English.")
|
|
return text # Fallback to original text if all retries fail
|
|
|
|
|
|
def translate_lines(original_translations, original_comments, language_dirs, original_language, replace_mode, i18n_dir,
|
|
failed_translations, retry_translate):
|
|
# Initialize the translator
|
|
translator = Translator()
|
|
total_lines = len(original_translations)
|
|
total_dirs = len(language_dirs)
|
|
|
|
for dir_idx, lang_dir in enumerate(language_dirs, start=1):
|
|
print(f'Translating directory {dir_idx}/{total_dirs}: {lang_dir}')
|
|
|
|
# Determine the target file path and language code
|
|
if lang_dir == f'{original_language}.lproj' or (original_language is None and lang_dir == 'en.lproj'):
|
|
target_file = os.path.join(i18n_dir, 'en.lproj', 'Localizable.strings')
|
|
google_code = 'en'
|
|
else:
|
|
lang_code = lang_dir.split('.')[0]
|
|
google_code = get_language_code(lang_code)
|
|
target_file = os.path.join(i18n_dir, lang_dir, 'Localizable.strings')
|
|
|
|
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
|
|
|
# Read the existing translations in the target language file
|
|
target_translations, target_comments = read_localizable_strings_with_comments(target_file)
|
|
|
|
# Open the target language file for writing
|
|
with open(target_file, 'w', encoding='utf-8') as f:
|
|
for idx, (key, original_value) in enumerate(original_translations.items(), start=1):
|
|
translated_value = target_translations.get(key)
|
|
original_comment = original_comments.get(key, comment)
|
|
|
|
# Check if the translation exists and contains no placeholders, or if replace mode is enabled
|
|
if translated_value and '{' not in translated_value and '}' not in translated_value and not replace_mode:
|
|
if retry_translate and translated_value == original_value:
|
|
translated = translate_text(translator, original_value, 'en', google_code)
|
|
# Print progress in one line
|
|
print(
|
|
f'Retrying translation for {google_code} ({idx}/{total_lines}): Original: {original_value}, Translated: {translated}')
|
|
# Write the translated content with default comment if none provided
|
|
f.write(f'{original_comment}\n{key} = "{translated}";\n\n')
|
|
else:
|
|
target_comment = target_comments.get(key, original_comment)
|
|
f.write(f'{target_comment}\n{key} = "{translated_value}";\n\n')
|
|
else:
|
|
translated = translate_text(translator, original_value, 'en', google_code)
|
|
# Print progress in one line
|
|
print(
|
|
f'Translating for {google_code} ({idx}/{total_lines}): Original: {original_value}, Translated: {translated}')
|
|
# Write the translated content with default comment if none provided
|
|
f.write(f'{original_comment}\n{key} = "{translated}";\n\n')
|
|
|
|
|
|
def translate_specific_content(specific_content, language_dirs, original_language, i18n_dir, failed_translations):
|
|
# Initialize the translator
|
|
translator = Translator()
|
|
total_dirs = len(language_dirs)
|
|
for dir_idx, lang_dir in enumerate(language_dirs, start=1):
|
|
print(f'Translating directory {dir_idx}/{total_dirs}: {lang_dir}')
|
|
if lang_dir == f'{original_language}.lproj' or lang_dir == 'en.lproj':
|
|
continue # Skip the original language
|
|
|
|
# Parse the language code
|
|
lang_code = lang_dir.split('.')[0]
|
|
google_code = get_language_code(lang_code)
|
|
|
|
# Generate the target language file path
|
|
target_file = os.path.join(i18n_dir, lang_dir, 'Localizable.strings')
|
|
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
|
|
|
translated = translate_text(translator, specific_content, 'en', google_code)
|
|
# Print progress in one line
|
|
print(
|
|
f'Translating for {google_code} ({dir_idx}/{total_dirs}): Original: {specific_content}, Translated: {translated}')
|
|
|
|
# Append the translated content to the end of the file with default comment if none provided
|
|
with open(target_file, 'a', encoding='utf-8') as f:
|
|
f.write(f'{comment}\n"{specific_content}" = "{translated}";\n\n')
|
|
|
|
|
|
def translate_files(i18n_dir, original_language=None, specific_content=None, replace_mode=False, retry_translate=False):
|
|
# Get all language subdirectories and sort them alphabetically
|
|
language_dirs = sorted([d for d in os.listdir(i18n_dir) if os.path.isdir(os.path.join(i18n_dir, d))])
|
|
failed_translations = []
|
|
|
|
# Determine the path of the original language file
|
|
if original_language:
|
|
original_file_path = os.path.join(i18n_dir, f'{original_language}.lproj', 'Localizable.strings')
|
|
else:
|
|
original_file_path = os.path.join(i18n_dir, 'Localizable.strings')
|
|
|
|
if not os.path.isfile(original_file_path):
|
|
print(f"Error: The original language file {original_file_path} does not exist.")
|
|
return
|
|
|
|
if specific_content is None:
|
|
# Read the original language file into a dictionary
|
|
original_translations, original_comments = read_localizable_strings_with_comments(original_file_path)
|
|
translate_lines(original_translations, original_comments, language_dirs, original_language, replace_mode,
|
|
i18n_dir, failed_translations, retry_translate)
|
|
else:
|
|
translate_specific_content(specific_content, language_dirs, original_language, i18n_dir, failed_translations)
|
|
|
|
print('Translation completed!')
|
|
|
|
if failed_translations:
|
|
print('The following languages failed to translate:')
|
|
for lang in set(failed_translations):
|
|
print(lang)
|
|
|
|
|
|
def main():
|
|
usage = """
|
|
Usage:
|
|
python translate_i18n_tools.py <i18n_dir> [--original_language <language_code>] [--specific_content <content>] [--replace] [--retry_translate]
|
|
|
|
Arguments:
|
|
<i18n_dir> Required. Path to the i18n directory.
|
|
--original_language Optional. Original language code. If not provided, the script will use the Localizable.strings file in the root of i18n_dir.
|
|
--specific_content Optional. Specific content to be translated and appended.
|
|
--replace Optional. If specified, force replace existing translations.
|
|
--retry_translate Optional. If specified, retry translation for target language if translated value is in English.
|
|
|
|
Examples:
|
|
python translate_i18n_tools.py path_to_your_i18n_directory
|
|
python translate_i18n_tools.py path_to_your_i18n_directory --original_language en
|
|
python translate_i18n_tools.py path_to_your_i18n_directory --specific_content "This is a specific content to be translated."
|
|
python translate_i18n_tools.py path_to_your_i18n_directory --original_language en --replace
|
|
python translate_i18n_tools.py path_to_your_i18n_directory --original_language en --retry_translate
|
|
"""
|
|
|
|
parser = argparse.ArgumentParser(description='Translate i18n files or specific content.', usage=usage)
|
|
parser.add_argument('i18n_dir', type=str, help='Path to the i18n directory')
|
|
parser.add_argument('--original_language', type=str, help='Original language code')
|
|
parser.add_argument('--specific_content', type=str, help='Specific content to be translated and appended')
|
|
parser.add_argument('--replace', action='store_true', help='Force replace existing translations')
|
|
parser.add_argument('--retry_translate', action='store_true',
|
|
help='Retry translation for target language if translated value is in English')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isdir(args.i18n_dir):
|
|
print(f"Error: The directory {args.i18n_dir} does not exist.")
|
|
parser.print_help()
|
|
return
|
|
|
|
translate_files(args.i18n_dir, args.original_language, args.specific_content, args.replace, args.retry_translate)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
# translate_files(i18n_dir="/Users/allsochen/AppCodeProjects/alt-tab-macos/resources/l10n",
|
|
# replace_mode=False, retry_translate=False)
|