8
8
from colorama import Fore , Style , init
9
9
from datetime import datetime
10
10
11
+ # It seems that Xcode uses different language codes and doesn't support all of the languages we get from Crowdin
12
+ # (at least in the variants that Crowdin is specifying them in) so need to map/exclude them in order to build correctly
13
+ LANGUAGE_MAPPING = {
14
+ 'kmr' : 'ku-TR' , # Explicitly Kurmanji in Türkiye, `ku-TR` is the general language code for Kurdish in Türkiye
15
+ 'no' : 'nb-NO' , # Norwegian general, `nb-NO` is Norwegian Bokmål in Norway and is apparently seen as the standard
16
+ 'sr-CS' : 'sr-Latn' , # Serbian (Latin)
17
+ 'tl' : None , # Tagalog (not supported, we have Filipino which might have to be enough for now)
18
+ }
19
+
11
20
# Parse command-line arguments
12
21
parser = argparse .ArgumentParser (description = 'Convert a XLIFF translation files to Apple String Catalog.' )
13
22
parser .add_argument ('raw_translations_directory' , help = 'Directory which contains the raw translation files' )
19
28
TRANSLATIONS_OUTPUT_DIRECTORY = args .translations_output_directory
20
29
NON_TRANSLATABLE_STRINGS_OUTPUT_PATH = args .non_translatable_strings_output_path
21
30
31
+ def filter_and_map_language_ids (target_languages ):
32
+ result = []
33
+ for lang in target_languages :
34
+ if lang ['id' ] in LANGUAGE_MAPPING :
35
+ mapped_value = LANGUAGE_MAPPING [lang ['id' ]]
36
+ if mapped_value is not None :
37
+ lang ['mapped_id' ] = mapped_value
38
+ result .append (lang )
39
+ else :
40
+ lang ['mapped_id' ] = lang ['id' ]
41
+ result .append (lang )
42
+ return result
43
+
22
44
def parse_xliff (file_path ):
23
45
tree = ET .parse (file_path )
24
46
root = tree .getroot ()
@@ -32,39 +54,53 @@ def parse_xliff(file_path):
32
54
target_language = file_elem .get ('target-language' )
33
55
if target_language is None :
34
56
raise ValueError (f"Missing target-language in file: { file_path } " )
57
+
58
+ if target_language in LANGUAGE_MAPPING :
59
+ target_language = LANGUAGE_MAPPING [target_language ]
35
60
36
- for trans_unit in root .findall ('.//ns:trans-unit' , namespaces = namespace ):
37
- resname = trans_unit .get ('resname' ) or trans_unit .get ('id' )
38
- if resname is None :
39
- continue # Skip entries without a resname or id
40
-
41
- target = trans_unit .find ('ns:target' , namespaces = namespace )
42
- source = trans_unit .find ('ns:source' , namespaces = namespace )
43
-
44
- if target is not None and target .text :
45
- translations [resname ] = target .text
46
- elif source is not None and source .text :
47
- # If target is missing or empty, use source as a fallback
48
- translations [resname ] = source .text
49
- print (f"Warning: Using source text for '{ resname } ' as target is missing or empty" )
50
-
51
- # Handle plural groups
61
+ # Handle plural groups first (want to make sure any warnings shown are correctly attributed to plurals or non-plurals)
52
62
for group in root .findall ('.//ns:group[@restype="x-gettext-plurals"]' , namespaces = namespace ):
53
63
plural_forms = {}
54
64
resname = None
55
65
for trans_unit in group .findall ('ns:trans-unit' , namespaces = namespace ):
56
66
if resname is None :
57
67
resname = trans_unit .get ('resname' ) or trans_unit .get ('id' )
68
+
58
69
target = trans_unit .find ('ns:target' , namespaces = namespace )
70
+ source = trans_unit .find ('ns:source' , namespaces = namespace )
59
71
context_group = trans_unit .find ('ns:context-group' , namespaces = namespace )
72
+
60
73
if context_group is not None :
61
74
plural_form = context_group .find ('ns:context[@context-type="x-plural-form"]' , namespaces = namespace )
62
- if target is not None and target . text and plural_form is not None :
75
+ if plural_form is not None :
63
76
form = plural_form .text .split (':' )[- 1 ].strip ().lower ()
64
- plural_forms [form ] = target .text
77
+
78
+ if target is not None and target .text :
79
+ plural_forms [form ] = target .text
80
+ elif source is not None and source .text :
81
+ # If target is missing or empty, use source as a fallback
82
+ plural_forms [form ] = source .text
83
+ print (f"Warning: Using source text for plural form '{ form } ' of '{ resname } ' in '{ target_language } ' as target is missing or empty" )
84
+
65
85
if resname and plural_forms :
66
86
translations [resname ] = plural_forms
67
87
88
+ # Then handle non-plurals (ignore any existing values as they are plurals)
89
+ for trans_unit in root .findall ('.//ns:trans-unit' , namespaces = namespace ):
90
+ resname = trans_unit .get ('resname' ) or trans_unit .get ('id' )
91
+ if resname is None or resname in translations :
92
+ continue # Skip entries without a resname/id and entries which already exist (ie. plurals)
93
+
94
+ target = trans_unit .find ('ns:target' , namespaces = namespace )
95
+ source = trans_unit .find ('ns:source' , namespaces = namespace )
96
+
97
+ if target is not None and target .text :
98
+ translations [resname ] = target .text
99
+ elif source is not None and source .text :
100
+ # If target is missing or empty, use source as a fallback
101
+ translations [resname ] = source .text
102
+ print (f"Warning: Using source text for '{ resname } ' in '{ target_language } ' as target is missing or empty" )
103
+
68
104
return translations , target_language
69
105
70
106
def clean_string (text ):
@@ -87,11 +123,13 @@ def convert_xliff_to_string_catalog(input_dir, output_dir, source_language, targ
87
123
"strings" : {},
88
124
"version" : "1.0"
89
125
}
126
+ target_mapped_languages = filter_and_map_language_ids (target_languages )
127
+ source_language ['mapped_id' ] = source_language ['id' ]
90
128
91
129
# We need to sort the full language list (if the source language comes first rather than in alphabetical order
92
130
# then the output will differ from what Xcode generates)
93
- all_languages = [source_language ] + target_languages
94
- sorted_languages = sorted (all_languages , key = lambda x : x ['id ' ])
131
+ all_languages = [source_language ] + target_mapped_languages
132
+ sorted_languages = sorted (all_languages , key = lambda x : x ['mapped_id ' ])
95
133
96
134
for language in sorted_languages :
97
135
lang_locale = language ['locale' ]
@@ -103,7 +141,7 @@ def convert_xliff_to_string_catalog(input_dir, output_dir, source_language, targ
103
141
try :
104
142
translations , target_language = parse_xliff (input_file )
105
143
except Exception as e :
106
- raise ValueError (f"Error processing file { filename } : { str (e )} " )
144
+ raise ValueError (f"Error processing locale { lang_locale } : { str (e )} " )
107
145
108
146
print (f"\033 [2K{ Fore .WHITE } ⏳ Converting translations for { target_language } to target format...{ Style .RESET_ALL } " , end = '\r ' )
109
147
sorted_translations = sorted (translations .items ())
0 commit comments