Skip to content

Commit 075cea5

Browse files
authored
Merge pull request #8 from session-foundation/share-more-logic
Share more logic between platforms
2 parents 385bfb6 + 9861f8d commit 075cea5

7 files changed

+157
-156
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
.DS_Store
33

44
# Exclude venv folder
5-
venv
5+
venv
6+
7+
8+
__pycache__
9+
.vscode/
10+
do_not_commit.sh

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.yml
2+

crowdin/download_translations_from_crowdin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def check_error(response):
4343
print(f"{Fore.BLUE}Response: {json.dumps(response.json(), indent=2)}{Style.RESET_ALL}")
4444
sys.exit(1)
4545

46-
def download_file(url, output_path):
46+
def download_file(url: str, output_path: str):
4747
"""
4848
Function to download a file from Crowdin
4949
"""

crowdin/generate_android_strings.py

Lines changed: 26 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import os
2-
import json
32
import xml.etree.ElementTree as ET
43
import sys
54
import argparse
65
import re
76
from pathlib import Path
87
from colorama import Fore, Style
8+
from generate_shared import load_glossary_dict, clean_string, setup_generation
99

1010
# Variables that should be treated as numeric (using %d)
1111
NUMERIC_VARIABLES = ['count', 'found_count', 'total_count']
1212

13+
AUTO_REPLACE_STATIC_STRINGS = False
14+
1315

1416
# Parse command-line arguments
1517
parser = argparse.ArgumentParser(description='Convert a XLIFF translation files to Android XML.')
@@ -66,22 +68,8 @@ def repl(match):
6668

6769
return re.sub(r'\{([^}]+)\}', repl, text)
6870

69-
def clean_string(text):
70-
# Note: any changes done for all platforms needs most likely to be done on crowdin side.
71-
# So we don't want to replace -> with → for instance, we want the crowdin strings to not have those at all.
72-
# We can use standard XML escaped characters for most things (since XLIFF is an XML format) but
73-
# want the following cases escaped in a particular way
74-
text = text.replace("'", r"\'")
75-
text = text.replace(""", "\"")
76-
text = text.replace("\"", "\\\"")
77-
text = text.replace("&lt;b&gt;", "<b>")
78-
text = text.replace("&lt;/b&gt;", "</b>")
79-
text = text.replace("&lt;/br&gt;", "\\n")
80-
text = text.replace("<br/>", "\\n")
81-
text = text.replace("&", "&amp;") # Assume any remaining ampersands are desired
82-
return text.strip() # Strip whitespace
83-
84-
def generate_android_xml(translations, app_name):
71+
72+
def generate_android_xml(translations, app_name, glossary_dict):
8573
sorted_translations = sorted(translations.items())
8674
result = '<?xml version="1.0" encoding="utf-8"?>\n'
8775
result += '<resources>\n'
@@ -93,25 +81,26 @@ def generate_android_xml(translations, app_name):
9381
if isinstance(target, dict): # It's a plural group
9482
result += f' <plurals name="{resname}">\n'
9583
for form, value in target.items():
96-
escaped_value = clean_string(convert_placeholders(value))
84+
escaped_value = clean_string(convert_placeholders(value), True, glossary_dict, {})
9785
result += f' <item quantity="{form}">{escaped_value}</item>\n'
9886
result += ' </plurals>\n'
9987
else: # It's a regular string (for these we DON'T want to convert the placeholders)
100-
escaped_target = clean_string(target)
88+
escaped_target = clean_string(target, True, glossary_dict, {})
10189
result += f' <string name="{resname}">{escaped_target}</string>\n'
10290

10391
result += '</resources>'
10492

10593
return result
10694

107-
def convert_xliff_to_android_xml(input_file, output_dir, source_locale, locale, app_name):
95+
def convert_xliff_to_android_xml(input_file, output_dir, source_locale, locale, glossary_dict):
10896
if not os.path.exists(input_file):
10997
raise FileNotFoundError(f"Could not find '{input_file}' in raw translations directory")
11098

11199
# Parse the XLIFF and convert to XML (only include the 'app_name' entry in the source language)
112100
is_source_language = locale == source_locale
113101
translations = parse_xliff(input_file)
114-
output_data = generate_android_xml(translations, app_name if is_source_language else None)
102+
app_name = glossary_dict['app_name']
103+
output_data = generate_android_xml(translations, app_name if is_source_language else None, glossary_dict if AUTO_REPLACE_STATIC_STRINGS else {})
115104

116105
# android is pretty smart to resolve resources for translations, see the example here:
117106
# https://developer.android.com/guide/topics/resources/multilingual-support#resource-resolution-examples
@@ -131,17 +120,10 @@ def convert_xliff_to_android_xml(input_file, output_dir, source_locale, locale,
131120

132121

133122
def convert_non_translatable_strings_to_kotlin(input_file, output_path):
134-
if not os.path.exists(input_file):
135-
raise FileNotFoundError(f"Could not find '{input_file}' in raw translations directory")
123+
glossary_dict = load_glossary_dict(input_file)
136124

137-
# Process the non-translatable string input
138-
non_translatable_strings_data = {}
139-
with open(input_file, 'r', encoding="utf-8") as file:
140-
non_translatable_strings_data = json.load(file)
141-
142-
entries = non_translatable_strings_data['data']
143-
max_key_length = max(len(entry['data']['note'].upper()) for entry in entries)
144-
app_name = None
125+
max_key_length = max(len(key) for key in glossary_dict)
126+
app_name = glossary_dict['app_name']
145127

146128
# Output the file in the desired format
147129
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
@@ -151,42 +133,24 @@ def convert_non_translatable_strings_to_kotlin(input_file, output_path):
151133
file.write('\n')
152134
file.write('// Non-translatable strings for use with the UI\n')
153135
file.write("object NonTranslatableStringConstants {\n")
154-
for entry in entries:
155-
key = entry['data']['note'].upper()
156-
text = entry['data']['text']
136+
for key_lowercase in glossary_dict:
137+
key = key_lowercase.upper()
138+
text = glossary_dict[key_lowercase]
157139
file.write(f' const val {key:<{max_key_length}} = "{text}"\n')
158140

159-
if key == 'APP_NAME':
160-
app_name = text
161-
162141
file.write('}\n')
163142
file.write('\n')
164143

165-
return app_name
166-
167-
def convert_all_files(input_directory):
168-
# Extract the project information
169-
print(f"\033[2K{Fore.WHITE}⏳ Processing project info...{Style.RESET_ALL}", end='\r')
170-
project_info_file = os.path.join(input_directory, "_project_info.json")
171-
if not os.path.exists(project_info_file):
172-
raise FileNotFoundError(f"Could not find '{project_info_file}' in raw translations directory")
173-
174-
project_details = {}
175-
with open(project_info_file, 'r', encoding="utf-8") as file:
176-
project_details = json.load(file)
177-
178-
# Extract the language info and sort the target languages alphabetically by locale
179-
source_language = project_details['data']['sourceLanguage']
180-
target_languages = project_details['data']['targetLanguages']
181-
target_languages.sort(key=lambda x: x['locale'])
182-
num_languages = len(target_languages)
183-
print(f"\033[2K{Fore.GREEN}✅ Project info processed, {num_languages} languages will be converted{Style.RESET_ALL}")
184-
185-
# Convert the non-translatable strings to the desired format
186-
print(f"\033[2K{Fore.WHITE}⏳ Generating static strings file...{Style.RESET_ALL}", end='\r')
187-
non_translatable_strings_file = os.path.join(input_directory, "_non_translatable_strings.json")
188-
app_name = convert_non_translatable_strings_to_kotlin(non_translatable_strings_file, NON_TRANSLATABLE_STRINGS_OUTPUT_PATH)
144+
if not app_name:
145+
raise ValueError("could not find app_name in glossary_dict")
146+
147+
def convert_all_files(input_directory: str ):
148+
setup_values = setup_generation(input_directory)
149+
source_language, rtl_languages, non_translatable_strings_file, target_languages = setup_values.values()
150+
151+
convert_non_translatable_strings_to_kotlin(non_translatable_strings_file, NON_TRANSLATABLE_STRINGS_OUTPUT_PATH)
189152
print(f"\033[2K{Fore.GREEN}✅ Static string generation complete{Style.RESET_ALL}")
153+
glossary_dict = load_glossary_dict(non_translatable_strings_file)
190154

191155
# Convert the XLIFF data to the desired format
192156
print(f"\033[2K{Fore.WHITE}⏳ Converting translations to target format...{Style.RESET_ALL}", end='\r')
@@ -199,7 +163,7 @@ def convert_all_files(input_directory):
199163
continue
200164
print(f"\033[2K{Fore.WHITE}⏳ Converting translations for {lang_locale} to target format...{Style.RESET_ALL}", end='\r')
201165
input_file = os.path.join(input_directory, f"{lang_locale}.xliff")
202-
convert_xliff_to_android_xml(input_file, TRANSLATIONS_OUTPUT_DIRECTORY, source_locale, lang_locale, app_name)
166+
convert_xliff_to_android_xml(input_file, TRANSLATIONS_OUTPUT_DIRECTORY, source_locale, lang_locale, glossary_dict)
203167
print(f"\033[2K{Fore.GREEN}✅ All conversions complete{Style.RESET_ALL}")
204168

205169
if __name__ == "__main__":

crowdin/generate_desktop_strings.py

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import os
22
import json
3+
from typing import Dict, List
34
import xml.etree.ElementTree as ET
45
import sys
56
import argparse
6-
import html
77
from pathlib import Path
8-
from colorama import Fore, Style, init
8+
from colorama import Fore, Style
9+
from generate_shared import clean_string, load_glossary_dict, setup_generation
910

1011
# Customizable mapping for output folder hierarchy
1112
# Add entries here to customize the output path for specific locales
@@ -37,6 +38,7 @@
3738
TRANSLATIONS_OUTPUT_DIRECTORY = args.translations_output_directory
3839
NON_TRANSLATABLE_STRINGS_OUTPUT_PATH = args.non_translatable_strings_output_path
3940

41+
4042
def parse_xliff(file_path):
4143
tree = ET.parse(file_path)
4244
root = tree.getroot()
@@ -69,26 +71,20 @@ def parse_xliff(file_path):
6971

7072
return translations
7173

72-
def clean_string(text):
73-
# Note: any changes done for all platforms needs most likely to be done on crowdin side.
74-
# So we don't want to replace -&gt; with → for instance, we want the crowdin strings to not have those at all.
75-
text = html.unescape(text) # Unescape any HTML escaping
76-
return text.strip() # Strip whitespace
7774

78-
def generate_icu_pattern(target):
75+
def generate_icu_pattern(target, glossary_dict : Dict[str,str]):
7976
if isinstance(target, dict): # It's a plural group
8077
pattern_parts = []
8178
for form, value in target.items():
8279
if form in ['zero', 'one', 'two', 'few', 'many', 'other', 'exact', 'fractional']:
83-
# Replace {count} with #
84-
value = clean_string(value.replace('{count}', '#'))
80+
value = clean_string(value, False, glossary_dict, {})
8581
pattern_parts.append(f"{form} [{value}]")
8682

8783
return "{{count, plural, {0}}}".format(" ".join(pattern_parts))
8884
else: # It's a regular string
89-
return clean_string(target)
85+
return clean_string(target, False, glossary_dict, {})
9086

91-
def convert_xliff_to_json(input_file, output_dir, locale, locale_two_letter_code):
87+
def convert_xliff_to_json(input_file, output_dir, locale, locale_two_letter_code, glossary_dict):
9288
if not os.path.exists(input_file):
9389
raise FileNotFoundError(f"Could not find '{input_file}' in raw translations directory")
9490

@@ -98,7 +94,7 @@ def convert_xliff_to_json(input_file, output_dir, locale, locale_two_letter_code
9894
converted_translations = {}
9995

10096
for resname, target in sorted_translations:
101-
converted_translations[resname] = generate_icu_pattern(target)
97+
converted_translations[resname] = generate_icu_pattern(target, glossary_dict)
10298

10399
# Generate output files
104100
output_locale = LOCALE_PATH_MAPPING.get(locale, LOCALE_PATH_MAPPING.get(locale_two_letter_code, locale_two_letter_code))
@@ -112,16 +108,10 @@ def convert_xliff_to_json(input_file, output_dir, locale, locale_two_letter_code
112108
file.write('\n')
113109
return output_locale
114110

115-
def convert_non_translatable_strings_to_type_script(input_file, output_path, exported_locales, rtl_languages):
116-
if not os.path.exists(input_file):
117-
raise FileNotFoundError(f"Could not find '{input_file}' in raw translations directory")
118111

119-
# Process the non-translatable string input
120-
non_translatable_strings_data = {}
121-
with open(input_file, 'r', encoding="utf-8") as file:
122-
non_translatable_strings_data = json.load(file)
123112

124-
entries = non_translatable_strings_data['data']
113+
def convert_non_translatable_strings_to_type_script(input_file: str, output_path: str, exported_locales: List[str], rtl_languages: List[str]):
114+
glossary_dict = load_glossary_dict(input_file)
125115
rtl_locales = sorted([lang["twoLettersCode"] for lang in rtl_languages])
126116

127117
# Output the file in the desired format
@@ -132,9 +122,8 @@ def convert_non_translatable_strings_to_type_script(input_file, output_path, exp
132122

133123
with open(output_path, 'w', encoding='utf-8') as file:
134124
file.write('export enum LOCALE_DEFAULTS {\n')
135-
for entry in entries:
136-
key = entry['data']['note']
137-
text = entry['data']['text']
125+
for key in glossary_dict:
126+
text = glossary_dict[key]
138127
file.write(f" {key} = '{text}',\n")
139128

140129
file.write('}\n')
@@ -151,40 +140,27 @@ def convert_non_translatable_strings_to_type_script(input_file, output_path, exp
151140
file.write('\n')
152141

153142

154-
def convert_all_files(input_directory):
155-
# Extract the project information
156-
print(f"\033[2K{Fore.WHITE}⏳ Processing project info...{Style.RESET_ALL}", end='\r')
157-
project_info_file = os.path.join(input_directory, "_project_info.json")
158-
if not os.path.exists(project_info_file):
159-
raise FileNotFoundError(f"Could not find '{project_info_file}' in raw translations directory")
160-
161-
project_details = {}
162-
with open(project_info_file, 'r', encoding="utf-8") as file:
163-
project_details = json.load(file)
164-
165-
# Extract the language info and sort the target languages alphabetically by locale
166-
source_language = project_details['data']['sourceLanguage']
167-
target_languages = project_details['data']['targetLanguages']
168-
target_languages.sort(key=lambda x: x['locale'])
169-
num_languages = len(target_languages)
170-
print(f"\033[2K{Fore.GREEN}✅ Project info processed, {num_languages} languages will be converted{Style.RESET_ALL}")
143+
def convert_all_files(input_directory: str):
144+
setup_values = setup_generation(input_directory)
145+
source_language, rtl_languages, non_translatable_strings_file, target_languages = setup_values.values()
171146

172147
# Convert the XLIFF data to the desired format
173148
print(f"\033[2K{Fore.WHITE}⏳ Converting translations to target format...{Style.RESET_ALL}", end='\r')
174149
exported_locales = []
150+
glossary_dict = load_glossary_dict(non_translatable_strings_file)
151+
175152
for language in [source_language] + target_languages:
176153
lang_locale = language['locale']
177154
lang_two_letter_code = language['twoLettersCode']
178155
print(f"\033[2K{Fore.WHITE}⏳ Converting translations for {lang_locale} to target format...{Style.RESET_ALL}", end='\r')
179156
input_file = os.path.join(input_directory, f"{lang_locale}.xliff")
180-
exported_as = convert_xliff_to_json(input_file, TRANSLATIONS_OUTPUT_DIRECTORY, lang_locale, lang_two_letter_code)
157+
exported_as = convert_xliff_to_json(input_file, TRANSLATIONS_OUTPUT_DIRECTORY, lang_locale, lang_two_letter_code, glossary_dict)
181158
exported_locales.append(exported_as)
182159
print(f"\033[2K{Fore.GREEN}✅ All conversions complete{Style.RESET_ALL}")
183160

184161
# Convert the non-translatable strings to the desired format
185162
print(f"\033[2K{Fore.WHITE}⏳ Generating static strings file...{Style.RESET_ALL}", end='\r')
186-
non_translatable_strings_file = os.path.join(input_directory, "_non_translatable_strings.json")
187-
rtl_languages = [lang for lang in target_languages if lang["textDirection"] == "rtl"]
163+
188164
convert_non_translatable_strings_to_type_script(non_translatable_strings_file, NON_TRANSLATABLE_STRINGS_OUTPUT_PATH, exported_locales, rtl_languages)
189165
print(f"\033[2K{Fore.GREEN}✅ Static string generation complete{Style.RESET_ALL}")
190166

0 commit comments

Comments
 (0)