88from colorama import Fore , Style , init
99from datetime import datetime
1010
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+
1120# Parse command-line arguments
1221parser = argparse .ArgumentParser (description = 'Convert a XLIFF translation files to Apple String Catalog.' )
1322parser .add_argument ('raw_translations_directory' , help = 'Directory which contains the raw translation files' )
1928TRANSLATIONS_OUTPUT_DIRECTORY = args .translations_output_directory
2029NON_TRANSLATABLE_STRINGS_OUTPUT_PATH = args .non_translatable_strings_output_path
2130
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+
2244def parse_xliff (file_path ):
2345 tree = ET .parse (file_path )
2446 root = tree .getroot ()
@@ -32,39 +54,53 @@ def parse_xliff(file_path):
3254 target_language = file_elem .get ('target-language' )
3355 if target_language is None :
3456 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 ]
3560
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)
5262 for group in root .findall ('.//ns:group[@restype="x-gettext-plurals"]' , namespaces = namespace ):
5363 plural_forms = {}
5464 resname = None
5565 for trans_unit in group .findall ('ns:trans-unit' , namespaces = namespace ):
5666 if resname is None :
5767 resname = trans_unit .get ('resname' ) or trans_unit .get ('id' )
68+
5869 target = trans_unit .find ('ns:target' , namespaces = namespace )
70+ source = trans_unit .find ('ns:source' , namespaces = namespace )
5971 context_group = trans_unit .find ('ns:context-group' , namespaces = namespace )
72+
6073 if context_group is not None :
6174 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 :
6376 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+
6585 if resname and plural_forms :
6686 translations [resname ] = plural_forms
6787
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+
68104 return translations , target_language
69105
70106def clean_string (text ):
@@ -87,11 +123,13 @@ def convert_xliff_to_string_catalog(input_dir, output_dir, source_language, targ
87123 "strings" : {},
88124 "version" : "1.0"
89125 }
126+ target_mapped_languages = filter_and_map_language_ids (target_languages )
127+ source_language ['mapped_id' ] = source_language ['id' ]
90128
91129 # We need to sort the full language list (if the source language comes first rather than in alphabetical order
92130 # 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 ' ])
95133
96134 for language in sorted_languages :
97135 lang_locale = language ['locale' ]
@@ -103,7 +141,7 @@ def convert_xliff_to_string_catalog(input_dir, output_dir, source_language, targ
103141 try :
104142 translations , target_language = parse_xliff (input_file )
105143 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 )} " )
107145
108146 print (f"\033 [2K{ Fore .WHITE } ⏳ Converting translations for { target_language } to target format...{ Style .RESET_ALL } " , end = '\r ' )
109147 sorted_translations = sorted (translations .items ())
0 commit comments