@@ -13,16 +13,133 @@ pub struct ConvertOptions {
1313 pub output_format : Option < String > ,
1414 pub source_language : Option < String > ,
1515 pub version : Option < String > ,
16+ pub output_lang : Option < String > ,
1617 pub exclude_lang : Vec < String > ,
1718 pub include_lang : Vec < String > ,
1819}
1920
21+ fn parse_standard_output_format ( format : & str ) -> Result < FormatType , String > {
22+ match format. to_lowercase ( ) . as_str ( ) {
23+ "strings" => Ok ( FormatType :: Strings ( None ) ) ,
24+ "android" | "androidstrings" => Ok ( FormatType :: AndroidStrings ( None ) ) ,
25+ "xcstrings" => Ok ( FormatType :: Xcstrings ) ,
26+ "csv" => Ok ( FormatType :: CSV ) ,
27+ "tsv" => Ok ( FormatType :: TSV ) ,
28+ _ => Err ( format ! (
29+ "Unsupported output format: '{}'. Supported formats: strings, android, xcstrings, csv, tsv" ,
30+ format
31+ ) ) ,
32+ }
33+ }
34+
35+ fn infer_output_path_language ( path : & str ) -> Option < String > {
36+ match langcodec:: infer_format_from_path ( path) {
37+ Some ( FormatType :: Strings ( Some ( lang) ) ) | Some ( FormatType :: AndroidStrings ( Some ( lang) ) ) => {
38+ Some ( lang)
39+ }
40+ _ => None ,
41+ }
42+ }
43+
44+ fn resolve_convert_output_format (
45+ output : & str ,
46+ output_format_hint : Option < & String > ,
47+ output_lang : Option < & String > ,
48+ ) -> Result < FormatType , String > {
49+ let mut output_format = if let Some ( format_hint) = output_format_hint {
50+ parse_standard_output_format ( format_hint) ?
51+ } else {
52+ langcodec:: infer_format_from_path ( output)
53+ . or_else ( || langcodec:: infer_format_from_extension ( output) )
54+ . ok_or_else ( || format ! ( "Cannot infer output format from extension: {}" , output) ) ?
55+ } ;
56+
57+ let path_language = infer_output_path_language ( output) ;
58+
59+ match & output_format {
60+ FormatType :: Strings ( _) | FormatType :: AndroidStrings ( _) => {
61+ if let Some ( language) = output_lang {
62+ if let Some ( path_language) = path_language
63+ && path_language != * language
64+ {
65+ return Err ( format ! (
66+ "--output-lang '{}' conflicts with language '{}' implied by output path '{}'" ,
67+ language, path_language, output
68+ ) ) ;
69+ }
70+ output_format = output_format. with_language ( Some ( language. clone ( ) ) ) ;
71+ } else if let Some ( path_language) = path_language {
72+ output_format = output_format. with_language ( Some ( path_language) ) ;
73+ }
74+ Ok ( output_format)
75+ }
76+ FormatType :: Xcstrings | FormatType :: CSV | FormatType :: TSV => {
77+ if let Some ( language) = output_lang {
78+ Err ( format ! (
79+ "--output-lang '{}' is only supported for single-language outputs (.strings, strings.xml)" ,
80+ language
81+ ) )
82+ } else {
83+ Ok ( output_format)
84+ }
85+ }
86+ }
87+ }
88+
2089pub fn run_unified_convert_command (
2190 input : String ,
2291 output : String ,
2392 options : ConvertOptions ,
2493 strict : bool ,
2594) {
95+ if let Some ( output_lang) = options. output_lang . as_ref ( ) {
96+ if output. ends_with ( ".langcodec" ) {
97+ eprintln ! (
98+ "Error: --output-lang '{}' is not supported for .langcodec output. Use --include-lang instead." ,
99+ output_lang
100+ ) ;
101+ std:: process:: exit ( 1 ) ;
102+ }
103+
104+ println ! (
105+ "{}" ,
106+ ui:: status_line_stdout(
107+ ui:: Tone :: Info ,
108+ & format!( "Converting with explicit output language '{}'" , output_lang) ,
109+ )
110+ ) ;
111+ match read_resources_from_any_input ( & input, options. input_format . as_ref ( ) , strict) . and_then (
112+ |resources| {
113+ let output_format = resolve_convert_output_format (
114+ & output,
115+ options. output_format . as_ref ( ) ,
116+ options. output_lang . as_ref ( ) ,
117+ ) ?;
118+ convert_resources_to_format ( resources, & output, output_format)
119+ . map_err ( |e| format ! ( "Error converting to output format: {}" , e) )
120+ } ,
121+ ) {
122+ Ok ( ( ) ) => {
123+ println ! (
124+ "{}" ,
125+ ui:: status_line_stdout(
126+ ui:: Tone :: Success ,
127+ "Successfully converted with explicit output language" ,
128+ )
129+ ) ;
130+ return ;
131+ }
132+ Err ( e) => {
133+ println ! (
134+ "{}" ,
135+ ui:: status_line_stdout( ui:: Tone :: Error , "Conversion failed" )
136+ ) ;
137+ eprintln ! ( "Error: {}" , e) ;
138+ std:: process:: exit ( 1 ) ;
139+ }
140+ }
141+ }
142+
26143 // Special handling: when targeting xcstrings, ensure required metadata exists.
27144 // If source_language/version are missing, default to en/1.0 respectively.
28145 let wants_xcstrings = output. ends_with ( ".xcstrings" )
@@ -230,7 +347,13 @@ pub fn run_unified_convert_command(
230347 "Strict mode: converting with explicit format hints only..." ,
231348 )
232349 ) ;
233- if let Err ( e) = try_explicit_format_conversion ( & input, & output, input_fmt, output_fmt) {
350+ if let Err ( e) = try_explicit_format_conversion (
351+ & input,
352+ & output,
353+ input_fmt,
354+ output_fmt,
355+ options. output_lang . as_ref ( ) ,
356+ ) {
234357 println ! (
235358 "{}" ,
236359 ui:: status_line_stdout( ui:: Tone :: Error , "Strict conversion failed" )
@@ -257,7 +380,13 @@ pub fn run_unified_convert_command(
257380 "Strict mode: converting custom format without fallback..." ,
258381 )
259382 ) ;
260- if let Err ( e) = try_custom_format_conversion ( & input, & output, & options. input_format ) {
383+ if let Err ( e) = try_custom_format_conversion (
384+ & input,
385+ & output,
386+ & options. input_format ,
387+ options. output_format . as_ref ( ) ,
388+ options. output_lang . as_ref ( ) ,
389+ ) {
261390 println ! (
262391 "{}" ,
263392 ui:: status_line_stdout( ui:: Tone :: Error , "Strict conversion failed" )
@@ -326,7 +455,7 @@ pub fn run_unified_convert_command(
326455 ui:: status_line_stdout( ui:: Tone :: Info , "Trying standard JSON format detection..." , )
327456 ) ;
328457 // Try to use the standard format detection which will show proper JSON parsing errors
329- if let Err ( e ) = convert_auto ( & input, & output) {
458+ if convert_auto ( & input, & output) . is_err ( ) {
330459 println ! (
331460 "{}" ,
332461 ui:: status_line_stdout(
@@ -335,32 +464,48 @@ pub fn run_unified_convert_command(
335464 )
336465 ) ;
337466 // If standard detection fails, try custom formats
338- if let Ok ( ( ) ) = try_custom_format_conversion ( & input, & output, & options. input_format )
339- {
340- println ! (
341- "{}" ,
342- ui:: status_line_stdout(
343- ui:: Tone :: Success ,
344- "Successfully converted using custom JSON format" ,
345- )
346- ) ;
347- return ;
467+ match try_custom_format_conversion (
468+ & input,
469+ & output,
470+ & options. input_format ,
471+ options. output_format . as_ref ( ) ,
472+ options. output_lang . as_ref ( ) ,
473+ ) {
474+ Ok ( ( ) ) => {
475+ println ! (
476+ "{}" ,
477+ ui:: status_line_stdout(
478+ ui:: Tone :: Success ,
479+ "Successfully converted using custom JSON format" ,
480+ )
481+ ) ;
482+ return ;
483+ }
484+ Err ( custom_error) => {
485+ // If both fail, show the custom conversion error because it is usually
486+ // more actionable than the initial extension-based failure.
487+ println ! (
488+ "{}" ,
489+ ui:: status_line_stdout( ui:: Tone :: Error , "Conversion failed" )
490+ ) ;
491+ eprintln ! ( "Error: {}" , custom_error) ;
492+ std:: process:: exit ( 1 ) ;
493+ }
348494 }
349- // If both fail, show the standard error message
350- println ! (
351- "{}" ,
352- ui:: status_line_stdout( ui:: Tone :: Error , "Conversion failed" )
353- ) ;
354- eprintln ! ( "Error: {}" , e) ;
355- std:: process:: exit ( 1 ) ;
356495 }
357496 } else {
358497 // For YAML and langcodec files, try custom formats directly
359498 println ! (
360499 "{}" ,
361500 ui:: status_line_stdout( ui:: Tone :: Info , "Converting using custom format..." )
362501 ) ;
363- if let Err ( e) = try_custom_format_conversion ( & input, & output, & options. input_format ) {
502+ if let Err ( e) = try_custom_format_conversion (
503+ & input,
504+ & output,
505+ & options. input_format ,
506+ options. output_format . as_ref ( ) ,
507+ options. output_lang . as_ref ( ) ,
508+ ) {
364509 println ! (
365510 "{}" ,
366511 ui:: status_line_stdout( ui:: Tone :: Error , "Custom format conversion failed" , )
@@ -387,7 +532,13 @@ pub fn run_unified_convert_command(
387532 "{}" ,
388533 ui:: status_line_stdout( ui:: Tone :: Info , "Converting with explicit format hints..." )
389534 ) ;
390- if let Err ( e) = try_explicit_format_conversion ( & input, & output, & input_fmt, & output_fmt) {
535+ if let Err ( e) = try_explicit_format_conversion (
536+ & input,
537+ & output,
538+ & input_fmt,
539+ & output_fmt,
540+ options. output_lang . as_ref ( ) ,
541+ ) {
391542 println ! (
392543 "{}" ,
393544 ui:: status_line_stdout( ui:: Tone :: Error , "Explicit format conversion failed" , )
@@ -418,6 +569,8 @@ fn try_custom_format_conversion(
418569 input : & str ,
419570 output : & str ,
420571 input_format : & Option < String > ,
572+ output_format : Option < & String > ,
573+ output_lang : Option < & String > ,
421574) -> Result < ( ) , String > {
422575 // Validate custom format file
423576 validate_custom_format_file ( input) ?;
@@ -443,8 +596,7 @@ fn try_custom_format_conversion(
443596 }
444597
445598 // Get output format type
446- let output_format_type = langcodec:: infer_format_from_extension ( output)
447- . ok_or_else ( || format ! ( "Cannot infer output format from extension: {}" , output) ) ?;
599+ let output_format_type = resolve_convert_output_format ( output, output_format, output_lang) ?;
448600
449601 // Convert to target format
450602 convert_resources_to_format ( resources, output, output_format_type)
@@ -511,6 +663,7 @@ fn try_explicit_format_conversion(
511663 output : & str ,
512664 input_format : & str ,
513665 output_format : & str ,
666+ output_lang : Option < & String > ,
514667) -> Result < ( ) , String > {
515668 // Validate input file exists
516669 validation:: validate_file_path ( input) ?;
@@ -543,19 +696,8 @@ fn try_explicit_format_conversion(
543696 write_resources_as_langcodec ( & codec. resources , output)
544697 } else {
545698 // Parse output format
546- let output_format_type = match output_format. to_lowercase ( ) . as_str ( ) {
547- "strings" => langcodec:: formats:: FormatType :: Strings ( None ) ,
548- "android" | "androidstrings" => langcodec:: formats:: FormatType :: AndroidStrings ( None ) ,
549- "xcstrings" => langcodec:: formats:: FormatType :: Xcstrings ,
550- "csv" => langcodec:: formats:: FormatType :: CSV ,
551- "tsv" => langcodec:: formats:: FormatType :: TSV ,
552- _ => {
553- return Err ( format ! (
554- "Unsupported output format: '{}'. Supported formats: strings, android, xcstrings, csv, tsv" ,
555- output_format
556- ) ) ;
557- }
558- } ;
699+ let output_format_type =
700+ resolve_convert_output_format ( output, Some ( & output_format. to_string ( ) ) , output_lang) ?;
559701
560702 // Use the lib crate's convert function
561703 langcodec:: convert ( input, input_format_type, output, output_format_type)
@@ -748,23 +890,25 @@ pub fn read_resources_from_any_input(
748890 let err_prefix = format ! ( "Failed to read input with language '{}': " , lang) ;
749891
750892 let format_type = if input. ends_with ( ".strings" ) {
751- langcodec:: formats:: FormatType :: Strings ( Some ( lang) )
893+ Some ( langcodec:: formats:: FormatType :: Strings ( Some ( lang) ) )
752894 } else if input. ends_with ( ".xml" ) {
753- langcodec:: formats:: FormatType :: AndroidStrings ( Some ( lang) )
895+ Some ( langcodec:: formats:: FormatType :: AndroidStrings ( Some ( lang) ) )
754896 } else if input. ends_with ( ".xcstrings" ) {
755- langcodec:: formats:: FormatType :: Xcstrings
897+ Some ( langcodec:: formats:: FormatType :: Xcstrings )
756898 } else if input. ends_with ( ".csv" ) {
757- langcodec:: formats:: FormatType :: CSV
899+ Some ( langcodec:: formats:: FormatType :: CSV )
758900 } else if input. ends_with ( ".tsv" ) {
759- langcodec:: formats:: FormatType :: TSV
901+ Some ( langcodec:: formats:: FormatType :: TSV )
760902 } else {
761- return Err ( format ! ( "Unsupported file extension for input: {}" , input ) ) ;
903+ None
762904 } ;
763905
764- let mut codec = Codec :: new ( ) ;
765- codec
766- . read_file_by_type ( input, format_type)
767- . map_err ( |e2| format ! ( "{err_prefix}{e2}" ) ) ?;
906+ if let Some ( format_type) = format_type {
907+ let mut codec = Codec :: new ( ) ;
908+ codec
909+ . read_file_by_type ( input, format_type)
910+ . map_err ( |e2| format ! ( "{err_prefix}{e2}" ) ) ?;
911+ }
768912 } else {
769913 eprintln ! ( "Standard format detection failed: {}" , e) ;
770914 }
0 commit comments