Skip to content

Commit d56aeea

Browse files
committed
1 parent 92ce39f commit d56aeea

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

translate-messages/__main__.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
'''
2+
Name: translate-en-messages
3+
Version: 2026.2.10.29
4+
Author: Adam Lui
5+
Description: Translate en/messages.json to other locales
6+
Homepage: https://github.com/adamlui/python-utils
7+
Support: https://github.com/adamlui/python-utils/issues
8+
Sponsor: https://github.com/sponsors/adamlui
9+
Notes: Use --help to print CLI args.
10+
'''
11+
12+
import argparse, os, json
13+
import lib.init as init
14+
from sys import stdout
15+
from translate import Translator
16+
from urllib.request import urlopen
17+
18+
cli = init.cli() ; init.configFile(cli, __file__)
19+
20+
# Parse CLI args
21+
parser = argparse.ArgumentParser(description='Translate en/messages.json to other locales')
22+
parser.add_argument('--include-langs', type=str, help='Languages to include (e.g. "en,es,fr")')
23+
parser.add_argument('--exclude-langs', type=str, help='Languages to exclude (e.g. "en,es")')
24+
parser.add_argument('--ignore-keys', type=str, help='Keys to ignore (e.g. "appName,author")')
25+
parser.add_argument('--locales-dir', type=str, help='Name of folder containing locales')
26+
parser.add_argument('--provider', type=str, help='Name of provider to use for translation')
27+
parser.add_argument('--init', action='store_true', help='Create .config.json file to store defaults')
28+
args = parser.parse_args()
29+
locales_dir = args.locales_dir or cli.config_data.get('locales_dir', '') or '_locales'
30+
provider = args.provider or cli.config_data.get('provider', '')
31+
32+
if args.init: # create config file
33+
if os.path.exists(cli.config_path):
34+
print(f'Config already exists at {cli.config_path}')
35+
else:
36+
try: # try to fetch template from jsDelivr
37+
jsd_url = f'{cli.urls.jsdelivr}/{cli.name}/{cli.config_filename}'
38+
with urlopen(jsd_url) as resp:
39+
if resp.status == 200 : cli.config_data = json.loads(resp.read().decode('utf-8'))
40+
except Exception : pass
41+
with open(cli.config_path, 'w', encoding='utf-8') as configFile:
42+
json.dump(cli.config_data, configFile, indent=2)
43+
print(f'Default config created at {cli.config_path}')
44+
exit()
45+
46+
# Init target_locales
47+
def parse_csv_val(val) : return [item.strip() for item in val.split(',') if item.strip()]
48+
include_arg = args.include_langs or cli.config_data.get('include_langs', '')
49+
exclude_arg = args.exclude_langs or cli.config_data.get('exclude_langs', '')
50+
target_locales = parse_csv_val(include_arg) or cli.default_target_locales
51+
exclude_langs = set(parse_csv_val(exclude_arg))
52+
target_locales = [lang for lang in target_locales if lang not in exclude_langs]
53+
54+
# UI initializations
55+
try:
56+
terminal_width = os.get_terminal_size()[0]
57+
except OSError:
58+
terminal_width = 80
59+
def print_trunc(msg, end='\n'):
60+
truncated_lines = [
61+
line if len(line) < terminal_width else line[:terminal_width -4] + '...' for line in msg.splitlines() ]
62+
print('\n'.join(truncated_lines), end=end)
63+
def overwrite_print(msg) : stdout.write('\r' + msg.ljust(terminal_width)[:terminal_width])
64+
65+
print('')
66+
67+
# Prompt user for keys to ignore
68+
ignore_keys = parse_csv_val(args.ignore_keys or cli.config_data.get('ignore_keys', ''))
69+
while True:
70+
if ignore_keys : print('Ignored key(s):', ignore_keys)
71+
key = input('Enter key to ignore (or ENTER if done): ')
72+
if not key : break
73+
ignore_keys.append(key)
74+
75+
# Determine closest locales dir
76+
print_trunc(f'\nSearching for { locales_dir }...')
77+
script_dir = os.path.abspath(os.path.dirname(__file__))
78+
for root, dirs, files in os.walk(script_dir): # search script dir recursively
79+
if locales_dir in dirs:
80+
locales_dir = os.path.join(root, locales_dir) ; break
81+
else: # search script parent dirs recursively
82+
parent_dir = os.path.dirname(script_dir)
83+
while parent_dir and parent_dir != script_dir:
84+
for root, dirs, files in os.walk(parent_dir):
85+
if locales_dir in dirs:
86+
locales_dir = os.path.join(root, locales_dir) ; break
87+
if locales_dir : break
88+
parent_dir = os.path.dirname(parent_dir)
89+
else : locales_dir = None
90+
91+
# Print result
92+
if locales_dir : print_trunc(f'_locales directory found!\n\n>> { locales_dir }\n')
93+
else : print_trunc(f'Unable to locate a { locales_dir } directory.') ; exit()
94+
95+
# Load en/messages.json
96+
msgs_filename = 'messages.json'
97+
en_msgs_path = os.path.join(locales_dir, 'en', msgs_filename)
98+
with open(en_msgs_path, 'r', encoding='utf-8') as en_file:
99+
en_messages = json.load(en_file)
100+
101+
# Combine [target_locales] w/ languages discovered in _locales
102+
output_langs = list(set(target_locales)) # remove duplicates
103+
for root, dirs, files in os.walk(locales_dir):
104+
for folder in dirs:
105+
folder_path = os.path.join(root, folder)
106+
msgs_path = os.path.join(folder_path, msgs_filename)
107+
discovered_lang = folder.replace('_', '-')
108+
if os.path.exists(msgs_path) and discovered_lang not in output_langs : output_langs.append(discovered_lang)
109+
output_langs.sort() # alphabetize languages
110+
111+
# Create/update/translate [[output_langs]/messages.json]
112+
langs_added, langs_skipped, langs_translated, langs_not_translated = [], [], [], []
113+
for lang_code in output_langs:
114+
lang_added, lang_skipped, lang_translated = False, False, False
115+
folder = lang_code.replace('-', '_') ; translated_msgs = {}
116+
if '-' in lang_code: # cap suffix
117+
sep_idx = folder.index('_')
118+
folder = folder[:sep_idx] + '_' + folder[sep_idx+1:].upper()
119+
120+
# Skip English locales
121+
if lang_code.startswith('en'):
122+
print_trunc(f'Skipped {folder}/messages.json...')
123+
langs_skipped.append(lang_code) ; langs_not_translated.append(lang_code) ; continue
124+
125+
# Initialize target locale folder
126+
folder_path = os.path.join(locales_dir, folder)
127+
if not os.path.exists(folder_path): # if missing, create folder
128+
os.makedirs(folder_path) ; langs_added.append(lang_code) ; lang_added = True
129+
130+
# Initialize target messages
131+
msgs_path = os.path.join(folder_path, msgs_filename)
132+
if os.path.exists(msgs_path):
133+
with open(msgs_path, 'r', encoding='utf-8') as messages_file : messages = json.load(messages_file)
134+
else : messages = {}
135+
136+
# Attempt translations
137+
print_trunc(f"{ 'Adding' if not messages else 'Updating' } { folder }/messages.json...", end='')
138+
stdout.flush()
139+
en_keys = list(en_messages.keys())
140+
fail_flags = ['INVALID TARGET LANGUAGE', 'TOO MANY REQUESTS', 'MYMEMORY']
141+
for key in en_keys:
142+
if key in ignore_keys:
143+
translated_msg = en_messages[key]['message']
144+
translated_msgs[key] = { 'message': translated_msg }
145+
continue
146+
if key not in messages:
147+
original_msg = translated_msg = en_messages[key]['message']
148+
try:
149+
translator = Translator(provider=provider, to_lang=lang_code)
150+
translated_msg = translator.translate(original_msg).replace('&quot;', "'").replace('&#39;', "'")
151+
if any(flag in translated_msg for flag in fail_flags):
152+
translated_msg = original_msg
153+
except Exception as e:
154+
print_trunc(f'Translation failed for key "{key}" in {lang_code}/messages.json: {e}')
155+
translated_msg = original_msg
156+
translated_msgs[key] = { 'message': translated_msg }
157+
else : translated_msgs[key] = messages[key]
158+
159+
# Format messages
160+
formatted_msgs = '{\n'
161+
for idx, (key, message_data) in enumerate(translated_msgs.items()):
162+
formatted_msg = json.dumps(message_data, ensure_ascii=False) \
163+
.replace('{', '{ ').replace('}', ' }') # add spacing
164+
formatted_msgs += ( f' "{key}": {formatted_msg}'
165+
+ ( ',\n' if idx < len(translated_msgs) -1 else '\n' )) # terminate line
166+
formatted_msgs += '}'
167+
with open(msgs_path, 'w', encoding='utf-8') as output_file : output_file.write(formatted_msgs + '\n')
168+
169+
# Print file summary
170+
if translated_msgs == messages : langs_skipped.append(lang_code) ; lang_skipped = True
171+
elif translated_msgs != messages : langs_translated.append(lang_code) ; lang_translated = True
172+
if not lang_translated : langs_not_translated.append(lang_code)
173+
overwrite_print(f"{ 'Added' if lang_added else 'Skipped' if lang_skipped else 'Updated' } { folder }/messages.json")
174+
175+
# Print final summary
176+
print_trunc('\nAll messages.json files updated successfully!\n')
177+
lang_data = [langs_translated, langs_skipped, langs_added, langs_not_translated]
178+
for data in lang_data:
179+
if data:
180+
list_name = next(name for name, value in globals().items() if value is data)
181+
status = list_name.split('langs_')[-1].replace('_', ' ')
182+
print(f'Languages {status}: {len(data)}\n') # print tally
183+
print('[ ' + ', '.join(data) + ' ]\n') # list languages

0 commit comments

Comments
 (0)