Skip to content

Commit 471658a

Browse files
authored
Merge pull request #589 from krzychu124/fix-csv-translation-parser-with-tests
Fix CSV multi-line translation
2 parents 8e0120a + c15fdd6 commit 471658a

File tree

8 files changed

+468
-17
lines changed

8 files changed

+468
-17
lines changed

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,8 @@
6161
#*.PDF diff=astextplain
6262
#*.rtf diff=astextplain
6363
#*.RTF diff=astextplain
64+
65+
###############################################################################
66+
# Test files should not be normalized
67+
###############################################################################
68+
*.test binary

TLM/TLM/UI/Localization/LookupTable.cs

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,17 @@ private void Load() {
6767
string filename = $"{Translation.RESOURCES_PREFIX}Translations.{Name}.csv";
6868
Log.Info($"Loading translations: {filename}");
6969

70-
string[] lines;
70+
string firstLine;
71+
string dataBlock;
7172
using (Stream st = Assembly.GetExecutingAssembly()
7273
.GetManifestResourceStream(filename)) {
7374
using (var sr = new StreamReader(st, Encoding.UTF8)) {
74-
lines = sr.ReadToEnd()
75-
.Split(new[] { "\n", "\r\n" }, StringSplitOptions.None);
75+
ReadLines(sr, out firstLine, out dataBlock);
7676
}
7777
}
7878

7979
// Read each line as CSV line
8080
// Read language order in the first line
81-
string firstLine = lines[0];
8281
var languageCodes = new List<string>();
8382
using (var sr = new StringReader(firstLine)) {
8483
ReadCsvCell(sr); // skip
@@ -96,19 +95,38 @@ private void Load() {
9695
}
9796
}
9897

98+
CollectTranslations(dataBlock, languageCodes, out AllLanguages);
99+
100+
#if DUMP_TRANSLATIONS
101+
DumpTranslationsToCsv();
102+
#endif
103+
Log._Debug($"Loaded {AllLanguages.Count} different languages for {Name}");
104+
}
105+
106+
/// <summary>
107+
/// Collects translations to map - collection of keyValue pairs for each language code
108+
/// </summary>
109+
/// <param name="dataBlock">block of data (all rows excluding first)</param>
110+
/// <param name="languageCodes">list of language codes</param>
111+
/// <param name="allLanguages">result dictionary where all translation string will be collected</param>
112+
private static void CollectTranslations(string dataBlock,
113+
List<string> languageCodes,
114+
out Dictionary<string, Dictionary<string, string>> allLanguages) {
115+
allLanguages = new Dictionary<string, Dictionary<string, string>>();
99116
// Initialize empty dicts for each language
100117
foreach (string lang in languageCodes) {
101-
AllLanguages[lang] = new Dictionary<string, string>();
118+
allLanguages[lang] = new Dictionary<string, string>();
102119
}
103120

104121
// first column is the translation key
105122
// Following columns are languages, following the order in AvailableLanguageCodes
106-
foreach (string line in lines.Skip(1)) {
107-
using (var sr = new StringReader(line)) {
123+
using (var sr = new StringReader(dataBlock)) {
124+
while (true) {
108125
string key = ReadCsvCell(sr);
109126
if (key.Length == 0) {
110127
break; // last line is empty
111128
}
129+
112130
foreach (string lang in languageCodes) {
113131
string cell = ReadCsvCell(sr);
114132
// Empty translations are not accepted for all languages other than English
@@ -117,15 +135,23 @@ private void Load() {
117135
lang != Translation.DEFAULT_LANGUAGE_CODE) {
118136
continue;
119137
}
120-
AllLanguages[lang][key] = cell;
138+
139+
allLanguages[lang][key] = cell;
121140
}
122141
}
123142
}
143+
}
124144

125-
#if DUMP_TRANSLATIONS
126-
DumpTranslationsToCsv();
127-
#endif
128-
Log._Debug($"Loaded {AllLanguages.Count} different languages for {Name}");
145+
/// <summary>
146+
/// Split stream of data on first row and remaining dataBlock
147+
/// </summary>
148+
/// <param name="sr">stream to read from</param>
149+
/// <param name="firstLine">first line of tranlation - row with language code names</param>
150+
/// <param name="dataBlock">string block of data (all remaining lines)</param>
151+
/// <returns>collection of valid translation rows</returns>
152+
private static void ReadLines(StreamReader sr, out string firstLine, out string dataBlock) {
153+
firstLine = sr.ReadLine();
154+
dataBlock = sr.ReadToEnd();
129155
}
130156

131157
/// <summary>
@@ -162,18 +188,27 @@ private static string ReadCsvCell(StringReader sr) {
162188
break;
163189
}
164190
case '\"': {
165-
// Found a '""', or a '",'
191+
// Found a '""', or a '",', or a '"/r', or a '"/r'
166192
int peek = sr.Peek();
167193
switch (peek) {
168194
case '\"': {
169195
sr.Read(); // consume the double quote
170196
sb.Append("\"");
171197
break;
172198
}
199+
case '\r':
200+
//Followed by a \r then \n or just \n - end-of-string
201+
sr.Read();// consume double quote
202+
sr.Read();// consume \r
203+
if (sr.Peek() == '\n') {
204+
sr.Read(); // consume \n
205+
}
206+
return sb.ToString();
207+
case '\n':
173208
case ',':
174209
case -1: {
175210
// Followed by a comma or end-of-string
176-
sr.Read(); // Consume the comma
211+
sr.Read(); // Consume the comma or newLine(LF)
177212
return sb.ToString();
178213
}
179214
default: {
@@ -191,11 +226,15 @@ private static string ReadCsvCell(StringReader sr) {
191226
}
192227
}
193228
} else {
194-
// Simple reading rules apply, read to the next comma or end-of-string
229+
// Simple reading rules apply, read to the next comma, LF sequence or end-of-string
195230
while (true) {
196231
int next = sr.Read();
197-
if (next == -1 || next == ',') {
198-
break; // end-of-string or a comma
232+
if (next == -1 || next == ',' || next == '\n') {
233+
break; // end-of-string, a newLine or a comma
234+
}
235+
if (next == '\r' && sr.Peek() == '\n') {
236+
sr.Read(); //consume LF(\n) to complete CRLF(\r\n) newLine escape sequence
237+
break;
199238
}
200239

201240
sb.Append((char)next, 1);

TLM/TMPE.UnitTest/TMPE.UnitTest.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
</Choose>
9191
<ItemGroup>
9292
<Compile Include="Properties\AssemblyInfo.cs" />
93+
<Compile Include="Translation\CsvParser.cs" />
9394
<Compile Include="Util\TinyDictionaryUnitTest.cs" />
9495
<Compile Include="Util\LogicUtilUnitTest.cs" />
9596
</ItemGroup>
@@ -111,6 +112,22 @@
111112
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.2\analyzers\dotnet\cs\StyleCop.Analyzers.CodeFixes.dll" />
112113
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.2\analyzers\dotnet\cs\StyleCop.Analyzers.dll" />
113114
</ItemGroup>
115+
<ItemGroup>
116+
<Content Include="Translation\TestFiles\MultilineTestBlock_LF.test">
117+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
118+
</Content>
119+
<Content Include="Translation\TestFiles\MultilineTestBlock_CRLF.test">
120+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
121+
</Content>
122+
<Content Include="Translation\TestFiles\TestBlock_CRLF.test">
123+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
124+
</Content>
125+
</ItemGroup>
126+
<ItemGroup>
127+
<Content Include="Translation\TestFiles\TestBlock_LF.test">
128+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
129+
</Content>
130+
</ItemGroup>
114131
<Choose>
115132
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
116133
<ItemGroup>

0 commit comments

Comments
 (0)