@@ -23,15 +23,117 @@ fn parse_standard_output_format(format: &str) -> Result<FormatType, String> {
2323 "strings" => Ok ( FormatType :: Strings ( None ) ) ,
2424 "android" | "androidstrings" => Ok ( FormatType :: AndroidStrings ( None ) ) ,
2525 "xcstrings" => Ok ( FormatType :: Xcstrings ) ,
26+ "xliff" => Ok ( FormatType :: Xliff ( None ) ) ,
2627 "csv" => Ok ( FormatType :: CSV ) ,
2728 "tsv" => Ok ( FormatType :: TSV ) ,
2829 _ => Err ( format ! (
29- "Unsupported output format: '{}'. Supported formats: strings, android, xcstrings, csv, tsv" ,
30+ "Unsupported output format: '{}'. Supported formats: strings, android, xcstrings, xliff, csv, tsv" ,
3031 format
3132 ) ) ,
3233 }
3334}
3435
36+ fn wants_named_output (
37+ output : & str ,
38+ output_format_hint : Option < & String > ,
39+ extension : & str ,
40+ format_name : & str ,
41+ ) -> bool {
42+ output. ends_with ( extension)
43+ || output_format_hint. is_some_and ( |hint| hint. eq_ignore_ascii_case ( format_name) )
44+ }
45+
46+ fn wants_xcstrings_output ( output : & str , output_format_hint : Option < & String > ) -> bool {
47+ wants_named_output ( output, output_format_hint, ".xcstrings" , "xcstrings" )
48+ }
49+
50+ fn wants_xliff_output ( output : & str , output_format_hint : Option < & String > ) -> bool {
51+ wants_named_output ( output, output_format_hint, ".xliff" , "xliff" )
52+ }
53+
54+ fn resolve_xliff_source_language (
55+ resources : & [ langcodec:: Resource ] ,
56+ explicit_source_language : Option < & String > ,
57+ target_language : & str ,
58+ ) -> Result < String , String > {
59+ if let Some ( explicit_source_language) = explicit_source_language {
60+ let trimmed = explicit_source_language. trim ( ) ;
61+ if trimmed. is_empty ( ) {
62+ return Err ( "--source-language cannot be empty for .xliff output" . to_string ( ) ) ;
63+ }
64+ return Ok ( trimmed. to_string ( ) ) ;
65+ }
66+
67+ let metadata_source_languages = resources
68+ . iter ( )
69+ . filter_map ( |resource| resource. metadata . custom . get ( "source_language" ) )
70+ . map ( |value| value. trim ( ) )
71+ . filter ( |value| !value. is_empty ( ) )
72+ . collect :: < std:: collections:: BTreeSet < _ > > ( ) ;
73+
74+ let available_languages = resources
75+ . iter ( )
76+ . map ( |resource| resource. metadata . language . trim ( ) )
77+ . filter ( |language| !language. is_empty ( ) )
78+ . collect :: < std:: collections:: BTreeSet < _ > > ( ) ;
79+
80+ if metadata_source_languages. len ( ) > 1 {
81+ return Err ( format ! (
82+ "Conflicting source_language metadata found for .xliff output: {}. Pass --source-language." ,
83+ metadata_source_languages
84+ . into_iter( )
85+ . collect:: <Vec <_>>( )
86+ . join( ", " )
87+ ) ) ;
88+ }
89+ if let Some ( source_language) = metadata_source_languages. iter ( ) . next ( ) {
90+ let extras = available_languages
91+ . iter ( )
92+ . filter ( |language| * * language != * source_language && * * language != target_language)
93+ . cloned ( )
94+ . collect :: < Vec < _ > > ( ) ;
95+
96+ if * source_language != target_language && extras. is_empty ( ) {
97+ return Ok ( ( * source_language) . to_string ( ) ) ;
98+ }
99+
100+ return Err ( format ! (
101+ "source_language metadata '{}' is ambiguous for .xliff output with available languages ({}). Pass --source-language." ,
102+ source_language,
103+ available_languages
104+ . iter( )
105+ . cloned( )
106+ . collect:: <Vec <_>>( )
107+ . join( ", " )
108+ ) ) ;
109+ }
110+
111+ if available_languages. is_empty ( ) {
112+ return Err ( "XLIFF output requires language metadata on the input resources" . to_string ( ) ) ;
113+ }
114+
115+ if available_languages. len ( ) == 1 {
116+ return Ok ( available_languages. iter ( ) . next ( ) . unwrap ( ) . to_string ( ) ) ;
117+ }
118+
119+ let non_target_languages = available_languages
120+ . iter ( )
121+ . filter ( |language| * * language != target_language)
122+ . cloned ( )
123+ . collect :: < Vec < _ > > ( ) ;
124+
125+ match non_target_languages. as_slice ( ) {
126+ [ source_language] => Ok ( ( * source_language) . to_string ( ) ) ,
127+ _ => Err ( format ! (
128+ "Could not infer the XLIFF source language from available languages ({}). Pass --source-language." ,
129+ available_languages
130+ . into_iter( )
131+ . collect:: <Vec <_>>( )
132+ . join( ", " )
133+ ) ) ,
134+ }
135+ }
136+
35137fn infer_output_path_language ( path : & str ) -> Option < String > {
36138 match langcodec:: infer_format_from_path ( path) {
37139 Some ( FormatType :: Strings ( Some ( lang) ) ) | Some ( FormatType :: AndroidStrings ( Some ( lang) ) ) => {
@@ -73,10 +175,21 @@ fn resolve_convert_output_format(
73175 }
74176 Ok ( output_format)
75177 }
178+ FormatType :: Xliff ( _) => {
179+ if let Some ( language) = output_lang {
180+ output_format = output_format. with_language ( Some ( language. clone ( ) ) ) ;
181+ Ok ( output_format)
182+ } else {
183+ Err (
184+ ".xliff output requires --output-lang to select the target language"
185+ . to_string ( ) ,
186+ )
187+ }
188+ }
76189 FormatType :: Xcstrings | FormatType :: CSV | FormatType :: TSV => {
77190 if let Some ( language) = output_lang {
78191 Err ( format ! (
79- "--output-lang '{}' is only supported for single-language outputs ( .strings, strings.xml) " ,
192+ "--output-lang '{}' is only supported for .strings, strings.xml, or .xliff output " ,
80193 language
81194 ) )
82195 } else {
@@ -92,6 +205,65 @@ pub fn run_unified_convert_command(
92205 options : ConvertOptions ,
93206 strict : bool ,
94207) {
208+ let wants_xliff = wants_xliff_output ( & output, options. output_format . as_ref ( ) ) ;
209+ if wants_xliff {
210+ println ! (
211+ "{}" ,
212+ ui:: status_line_stdout(
213+ ui:: Tone :: Info ,
214+ "Converting to XLIFF 1.2 with explicit source/target language selection..." ,
215+ )
216+ ) ;
217+ match read_resources_from_any_input ( & input, options. input_format . as_ref ( ) , strict) . and_then (
218+ |mut resources| {
219+ let output_format = resolve_convert_output_format (
220+ & output,
221+ options. output_format . as_ref ( ) ,
222+ options. output_lang . as_ref ( ) ,
223+ ) ?;
224+ let target_language =
225+ match & output_format {
226+ FormatType :: Xliff ( Some ( target_language) ) => target_language. clone ( ) ,
227+ _ => return Err (
228+ ".xliff output requires --output-lang to select the target language"
229+ . to_string ( ) ,
230+ ) ,
231+ } ;
232+ let source_language = resolve_xliff_source_language (
233+ & resources,
234+ options. source_language . as_ref ( ) ,
235+ & target_language,
236+ ) ?;
237+
238+ for resource in & mut resources {
239+ resource
240+ . metadata
241+ . custom
242+ . insert ( "source_language" . to_string ( ) , source_language. clone ( ) ) ;
243+ }
244+
245+ convert_resources_to_format ( resources, & output, output_format)
246+ . map_err ( |e| format ! ( "Error converting to xliff: {}" , e) )
247+ } ,
248+ ) {
249+ Ok ( ( ) ) => {
250+ println ! (
251+ "{}" ,
252+ ui:: status_line_stdout( ui:: Tone :: Success , "Successfully converted to xliff" , )
253+ ) ;
254+ return ;
255+ }
256+ Err ( e) => {
257+ println ! (
258+ "{}" ,
259+ ui:: status_line_stdout( ui:: Tone :: Error , "Conversion to xliff failed" )
260+ ) ;
261+ eprintln ! ( "Error: {}" , e) ;
262+ std:: process:: exit ( 1 ) ;
263+ }
264+ }
265+ }
266+
95267 if let Some ( output_lang) = options. output_lang . as_ref ( ) {
96268 if output. ends_with ( ".langcodec" ) {
97269 eprintln ! (
@@ -142,11 +314,7 @@ pub fn run_unified_convert_command(
142314
143315 // Special handling: when targeting xcstrings, ensure required metadata exists.
144316 // If source_language/version are missing, default to en/1.0 respectively.
145- let wants_xcstrings = output. ends_with ( ".xcstrings" )
146- || options
147- . output_format
148- . as_deref ( )
149- . is_some_and ( |s| s. eq_ignore_ascii_case ( "xcstrings" ) ) ;
317+ let wants_xcstrings = wants_xcstrings_output ( & output, options. output_format . as_ref ( ) ) ;
150318 if wants_xcstrings {
151319 println ! (
152320 "{}" ,
@@ -624,6 +792,7 @@ fn print_conversion_error(input: &str, output: &str) {
624792 eprintln ! ( "- .strings (Apple strings files)" ) ;
625793 eprintln ! ( "- .xml (Android strings files)" ) ;
626794 eprintln ! ( "- .xcstrings (Apple xcstrings files)" ) ;
795+ eprintln ! ( "- .xliff (Apple/Xcode XLIFF 1.2 files)" ) ;
627796 eprintln ! ( "- .csv (CSV files)" ) ;
628797 eprintln ! ( "- .tsv (TSV files)" ) ;
629798 eprintln ! ( "- .langcodec (Resource JSON array)" ) ;
@@ -634,6 +803,7 @@ fn print_conversion_error(input: &str, output: &str) {
634803 eprintln ! ( "- .strings (Apple strings files)" ) ;
635804 eprintln ! ( "- .xml (Android strings files)" ) ;
636805 eprintln ! ( "- .xcstrings (Apple xcstrings files)" ) ;
806+ eprintln ! ( "- .xliff (Apple/Xcode XLIFF 1.2 files)" ) ;
637807 eprintln ! ( "- .csv (CSV files)" ) ;
638808 eprintln ! ( "- .tsv (TSV files)" ) ;
639809 eprintln ! ( "- .langcodec (Resource JSON array)" ) ;
@@ -676,11 +846,12 @@ fn try_explicit_format_conversion(
676846 "strings" => langcodec:: formats:: FormatType :: Strings ( None ) ,
677847 "android" | "androidstrings" => langcodec:: formats:: FormatType :: AndroidStrings ( None ) ,
678848 "xcstrings" => langcodec:: formats:: FormatType :: Xcstrings ,
849+ "xliff" => langcodec:: formats:: FormatType :: Xliff ( None ) ,
679850 "csv" => langcodec:: formats:: FormatType :: CSV ,
680851 "tsv" => langcodec:: formats:: FormatType :: TSV ,
681852 _ => {
682853 return Err ( format ! (
683- "Unsupported input format: '{}'. Supported formats: strings, android, xcstrings, csv, tsv" ,
854+ "Unsupported input format: '{}'. Supported formats: strings, android, xcstrings, xliff, csv, tsv" ,
684855 input_format
685856 ) ) ;
686857 }
@@ -758,6 +929,7 @@ pub fn read_resources_from_any_input(
758929 Some ( langcodec:: formats:: FormatType :: AndroidStrings ( None ) )
759930 }
760931 "xcstrings" => Some ( langcodec:: formats:: FormatType :: Xcstrings ) ,
932+ "xliff" => Some ( langcodec:: formats:: FormatType :: Xliff ( None ) ) ,
761933 "csv" => Some ( langcodec:: formats:: FormatType :: CSV ) ,
762934 "tsv" => Some ( langcodec:: formats:: FormatType :: TSV ) ,
763935 _ => None ,
@@ -783,6 +955,7 @@ pub fn read_resources_from_any_input(
783955 if input. ends_with ( ".strings" )
784956 || input. ends_with ( ".xml" )
785957 || input. ends_with ( ".xcstrings" )
958+ || input. ends_with ( ".xliff" )
786959 || input. ends_with ( ".csv" )
787960 || input. ends_with ( ".tsv" )
788961 {
@@ -807,7 +980,7 @@ pub fn read_resources_from_any_input(
807980 }
808981
809982 return Err ( format ! (
810- "Unsupported input format or file extension: '{}'. Supported formats: .strings, .xml, .xcstrings, .csv, .tsv, .json, .yaml, .yml, .langcodec" ,
983+ "Unsupported input format or file extension: '{}'. Supported formats: .strings, .xml, .xcstrings, .xliff, . csv, .tsv, .json, .yaml, .yml, .langcodec" ,
811984 input
812985 ) ) ;
813986 }
@@ -821,6 +994,7 @@ pub fn read_resources_from_any_input(
821994 Some ( langcodec:: formats:: FormatType :: AndroidStrings ( None ) )
822995 }
823996 "xcstrings" => Some ( langcodec:: formats:: FormatType :: Xcstrings ) ,
997+ "xliff" => Some ( langcodec:: formats:: FormatType :: Xliff ( None ) ) ,
824998 "csv" => Some ( langcodec:: formats:: FormatType :: CSV ) ,
825999 "tsv" => Some ( langcodec:: formats:: FormatType :: TSV ) ,
8261000 _ => None ,
@@ -895,6 +1069,8 @@ pub fn read_resources_from_any_input(
8951069 Some ( langcodec:: formats:: FormatType :: AndroidStrings ( Some ( lang) ) )
8961070 } else if input. ends_with ( ".xcstrings" ) {
8971071 Some ( langcodec:: formats:: FormatType :: Xcstrings )
1072+ } else if input. ends_with ( ".xliff" ) {
1073+ Some ( langcodec:: formats:: FormatType :: Xliff ( None ) )
8981074 } else if input. ends_with ( ".csv" ) {
8991075 Some ( langcodec:: formats:: FormatType :: CSV )
9001076 } else if input. ends_with ( ".tsv" ) {
@@ -908,6 +1084,7 @@ pub fn read_resources_from_any_input(
9081084 codec
9091085 . read_file_by_type ( input, format_type)
9101086 . map_err ( |e2| format ! ( "{err_prefix}{e2}" ) ) ?;
1087+ return Ok ( codec. resources ) ;
9111088 }
9121089 } else {
9131090 eprintln ! ( "Standard format detection failed: {}" , e) ;
@@ -937,7 +1114,7 @@ pub fn read_resources_from_any_input(
9371114 }
9381115
9391116 Err ( format ! (
940- "Unsupported input format or file extension: '{}'. Supported formats: .strings, .xml, .xcstrings, .csv, .tsv, .json, .yaml, .yml, .langcodec" ,
1117+ "Unsupported input format or file extension: '{}'. Supported formats: .strings, .xml, .xcstrings, .xliff, . csv, .tsv, .json, .yaml, .yml, .langcodec" ,
9411118 input
9421119 ) )
9431120}
0 commit comments