1+ using Csv ;
2+ using System ;
3+ using System . Collections . Generic ;
4+ using System . CommandLine ;
5+ using System . CommandLine . Parsing ;
6+ using System . Data ;
7+ using System . IO ;
8+ using System . Linq ;
9+ using System . Text ;
10+
11+ List < string > Warnings = new ( ) ;
12+ List < string > Errors = new ( ) ;
13+
14+ var srcOption = new Option < string > ( "--src" , "The source folder containing the .csv files" ) { IsRequired = true } ;
15+ var outOption = new Option < string > ( "--out" , "The output folder for .resw files" ) { IsRequired = true } ;
16+ var languagesOption = new Option < IEnumerable < string > > ( "--languages" , "All languages for translation" ) { IsRequired = true , AllowMultipleArgumentsPerToken = true } ;
17+ var defaultLanguageOption = new Option < string > ( "--default-language" , ( ) => "" , "Default language of the app" ) ;
18+ defaultLanguageOption . AddValidator ( result =>
19+ {
20+ IEnumerable < string > languages = result . GetValueForOption ( languagesOption ) ! ;
21+ string defaultLanguage = result . GetValueForOption ( defaultLanguageOption ) ! ;
22+ if ( defaultLanguage != "" && ! languages . Contains ( defaultLanguage ) )
23+ result . ErrorMessage = "Default language must be in the list of languages" ;
24+ } ) ;
25+
26+ var rootCommand = new RootCommand ( "Convert .csv files to .resw files for UWP/WinUI localization" ) ;
27+ rootCommand . AddOption ( srcOption ) ;
28+ rootCommand . AddOption ( outOption ) ;
29+ rootCommand . AddOption ( languagesOption ) ;
30+ rootCommand . AddOption ( defaultLanguageOption ) ;
31+ rootCommand . SetHandler ( ConvertCsvToResw , srcOption , outOption , languagesOption , defaultLanguageOption ) ;
32+ rootCommand . Invoke ( args ) ;
33+
34+ void ConvertCsvToResw ( string srcPath , string outPath , IEnumerable < string > languages , string defaultLanguage )
35+ {
36+ DirectoryInfo srcFolder = new ( srcPath ) ;
37+ DirectoryInfo outFolder = new ( outPath ) ;
38+
39+ // Init string resource table (key=language code, value=translated string resources)
40+ var strings = new Dictionary < string , Dictionary < string , string > > ( ) ;
41+ foreach ( string lang in languages )
42+ {
43+ strings [ lang ] = new ( ) ;
44+ }
45+
46+ // Enumerate and parse all CSV files
47+ foreach ( FileInfo file in srcFolder . EnumerateFiles ( "*.csv" , SearchOption . AllDirectories ) )
48+ {
49+ string relativePath = Path . GetRelativePath ( srcFolder . FullName , file . FullName ) ;
50+ foreach ( var str in ParseCsv ( file , relativePath , languages ) )
51+ {
52+ foreach ( string lang in languages )
53+ {
54+ string resourceId = relativePath [ 0 ..^ ".csv" . Length ] . Replace ( Path . DirectorySeparatorChar , '_' ) + "_" + str . GetName ( ) ;
55+ strings [ lang ] [ resourceId ] = str . Translations [ lang ] ;
56+ }
57+ }
58+
59+ }
60+
61+ // Print errors (invalid CSV files)
62+ Console . ForegroundColor = ConsoleColor . Red ;
63+
64+ foreach ( var item in Errors )
65+ Console . WriteLine ( item ) ;
66+
67+ if ( Errors . Count > 0 )
68+ {
69+ Console . WriteLine ( $ "Failed to generate .resw files due to { Errors . Count } errors.") ;
70+ Environment . Exit ( - 1 ) ;
71+ }
72+
73+ // Print warnings (missing translations)
74+ Console . ForegroundColor = ConsoleColor . Yellow ;
75+
76+ foreach ( var item in Warnings )
77+ Console . WriteLine ( item ) ;
78+
79+ Console . ForegroundColor = ConsoleColor . Green ;
80+
81+ // Generate .resw files
82+ if ( ! Directory . Exists ( outFolder . FullName ) )
83+ Directory . CreateDirectory ( outFolder . FullName ) ;
84+
85+ foreach ( string lang in languages )
86+ {
87+ // Build .resw file
88+ var reswBuilder = new StringBuilder ( ) ;
89+
90+ reswBuilder . AppendLine ( """
91+ <?xml version="1.0" encoding="utf-8"?>
92+ <root>
93+ <resheader name="resmimetype">
94+ <value>text/microsoft-resx</value>
95+ </resheader>
96+ <resheader name="version">
97+ <value>2.0</value>
98+ </resheader>
99+ <resheader name="reader">
100+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
101+ </resheader>
102+ <resheader name="writer">
103+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
104+ </resheader>
105+ """ ) ;
106+
107+ foreach ( ( string key , string translatedString ) in strings [ lang ] )
108+ {
109+ reswBuilder . AppendLine ( $ """
110+ <data name="{ key } " xml:space="preserve">
111+ <value>{ translatedString } </value>
112+ </data>
113+ """ ) ;
114+ }
115+
116+ reswBuilder . AppendLine ( """
117+ </root>
118+ """ ) ;
119+
120+ // Write to file
121+
122+ string outputPath = lang == defaultLanguage
123+ ? Path . Combine ( outFolder . FullName , $ "Resources.resw")
124+ : Path . Combine ( outFolder . FullName , $ "Resources.lang-{ lang } .resw") ;
125+ var outputFile = new FileInfo ( outputPath ) ;
126+ File . WriteAllText ( outputFile . FullName , reswBuilder . ToString ( ) ) ;
127+ Console . WriteLine ( $ "[INFO] Generated translation for { lang } : { outputFile . FullName } ") ;
128+ }
129+ Console . WriteLine ( $ "Successfully generated { strings . First ( ) . Value . Count } translations for { languages . Count ( ) } languages.") ;
130+ }
131+
132+
133+ // Parse a CSV file
134+ IEnumerable < StringResource > ParseCsv ( FileInfo csvFile , string relativePath , IEnumerable < string > languages )
135+ {
136+ var csvLines = CsvReader . ReadFromText ( File . ReadAllText ( csvFile . FullName ) ) ;
137+
138+ // Check CSV headers
139+ var line = csvLines . FirstOrDefault ( ) ;
140+ if ( line is null ) // Empty file
141+ return [ ] ;
142+
143+ bool invalid = false ;
144+ if ( ! line . HasColumn ( "Id" ) )
145+ {
146+ Errors . Add ( $ "[ERROR] { relativePath } : Missing column \" Id\" ") ;
147+ invalid = true ;
148+ }
149+
150+ if ( ! line . HasColumn ( "Property" ) )
151+ {
152+ Errors . Add ( $ "[ERROR] { relativePath } : Missing column \" Property\" ") ;
153+ invalid = true ;
154+ }
155+
156+ foreach ( string lang in languages )
157+ {
158+ if ( ! line . HasColumn ( lang ) )
159+ {
160+ Errors . Add ( $ "[ERROR] { relativePath } : Missing column for translation to { lang } ") ;
161+ invalid = true ;
162+ }
163+ }
164+
165+ if ( invalid ) return [ ] ;
166+
167+ // Parse lines
168+ IEnumerable < StringResource > lines = csvLines
169+ . Select ( line => ParseLine ( line , relativePath , languages ) )
170+ . Where ( x => x is not null ) ! ;
171+ return lines ;
172+ }
173+
174+ // Parse a line in the CSV file
175+ StringResource ? ParseLine ( ICsvLine line , string relativePath , IEnumerable < string > languages )
176+ {
177+ // Error checking
178+ if ( string . IsNullOrWhiteSpace ( line [ "Id" ] ) )
179+ {
180+ Errors . Add ( $ "[ERROR] { relativePath } , Line { line . Index } : Id must not be empty") ;
181+ return null ;
182+ }
183+
184+ if ( line [ "Id" ] . StartsWith ( '_' ) && ! string . IsNullOrEmpty ( line [ "Property" ] ) )
185+ {
186+ Errors . Add ( $ "[ERROR] { relativePath } , Line { line . Index } : Property must be empty for strings for code-behind") ;
187+ return null ;
188+ }
189+
190+ // Parse translations
191+ Dictionary < string , string > translations = new ( ) ;
192+
193+ foreach ( string lang in languages )
194+ {
195+ if ( line [ lang ] == "" ) // Missing translation
196+ {
197+ Warnings . Add ( $ "[WARNING] { relativePath } , Line { line . Index } : Missing translation to { lang } ") ;
198+ }
199+
200+ translations [ lang ] = line [ lang ] ;
201+ }
202+
203+ var resource = new StringResource
204+ {
205+ Uid = line [ "Id" ] ,
206+ Property = line [ "Property" ] ,
207+ Translations = translations
208+ } ;
209+
210+ return resource ;
211+ }
212+
213+
214+ /// <summary>
215+ /// Represents a string resource with translations for different languages
216+ /// </summary>
217+ record class StringResource
218+ {
219+ /// <summary>
220+ /// x:Uid of the component (if used in XAML) or ID of the resource (if used in code behind)
221+ /// </summary>
222+ public required string Uid { get ; init ; }
223+
224+ /// <summary>
225+ /// Property name of the component (if used in XAML)
226+ /// </summary>
227+ public required string Property { get ; init ; }
228+
229+ /// <summary>
230+ /// Translations for different languages (key=language code, value=translated string)
231+ /// </summary>
232+ public required Dictionary < string , string > Translations { get ; init ; }
233+
234+ public string GetName ( )
235+ {
236+ if ( Uid . StartsWith ( '_' ) )
237+ return Uid ;
238+
239+ return $ "{ Uid } .{ Property } ";
240+ }
241+ }
0 commit comments