Skip to content

Commit e0791f2

Browse files
committed
Fix XLIFF backend encoding and structure issues
- Fix UTF-16/UTF-8 encoding mismatch by using MemoryStream instead of StringWriter - Always write target-language attribute to preserve bilingual structure - Always write both source (key) and target (value) elements - Exclude .lrm backup directory from file discovery - Fix reader to always prefer target element value
1 parent 1544391 commit e0791f2

3 files changed

Lines changed: 44 additions & 42 deletions

File tree

LocalizationManager.Core/Backends/Xliff/XliffResourceDiscovery.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ public List<LanguageInfo> DiscoverLanguages(string searchPath)
3838

3939
var languageMap = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
4040

41-
// Find all XLIFF files
41+
// Find all XLIFF files, excluding backup/metadata directories
4242
var xliffFiles = Directory.GetFiles(searchPath, "*.xliff", SearchOption.AllDirectories)
4343
.Concat(Directory.GetFiles(searchPath, "*.xlf", SearchOption.AllDirectories))
44+
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}.lrm{Path.DirectorySeparatorChar}"))
45+
.Where(f => !f.Contains($"{Path.AltDirectorySeparatorChar}.lrm{Path.AltDirectorySeparatorChar}"))
4446
.ToList();
4547

4648
foreach (var filePath in xliffFiles)
@@ -302,9 +304,15 @@ public XliffDiscoveryResult DiscoverConfiguration(string path)
302304
{
303305
var result = new XliffDiscoveryResult();
304306

305-
// Find XLIFF files
306-
var xliffFiles = Directory.GetFiles(path, "*.xliff", SearchOption.AllDirectories).ToList();
307-
var xlfFiles = Directory.GetFiles(path, "*.xlf", SearchOption.AllDirectories).ToList();
307+
// Find XLIFF files, excluding backup/metadata directories
308+
var xliffFiles = Directory.GetFiles(path, "*.xliff", SearchOption.AllDirectories)
309+
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}.lrm{Path.DirectorySeparatorChar}"))
310+
.Where(f => !f.Contains($"{Path.AltDirectorySeparatorChar}.lrm{Path.AltDirectorySeparatorChar}"))
311+
.ToList();
312+
var xlfFiles = Directory.GetFiles(path, "*.xlf", SearchOption.AllDirectories)
313+
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}.lrm{Path.DirectorySeparatorChar}"))
314+
.Where(f => !f.Contains($"{Path.AltDirectorySeparatorChar}.lrm{Path.AltDirectorySeparatorChar}"))
315+
.ToList();
308316

309317
result.FileExtension = xliffFiles.Count >= xlfFiles.Count ? ".xliff" : ".xlf";
310318

LocalizationManager.Core/Backends/Xliff/XliffResourceReader.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ private List<ResourceEntry> ParseXliff12(XDocument doc, LanguageInfo metadata)
188188
// Get state for translation status
189189
var state = unit.Element(ns + "target")?.Attribute("state")?.Value;
190190

191-
// For default/source language, return source value
192-
// For translation languages, return target with fallback to source
193-
var value = metadata.IsDefault ? (source ?? "") : (target ?? source ?? "");
191+
// Always prefer target value (which contains the localized text)
192+
// Fall back to source if target is not present
193+
var value = target ?? source ?? "";
194194

195195
return new ResourceEntry
196196
{
@@ -220,8 +220,8 @@ private List<ResourceEntry> ParseXliff12(XDocument doc, LanguageInfo metadata)
220220
var source = unit.Element(ns + "source")?.Value;
221221
var target = unit.Element(ns + "target")?.Value;
222222

223-
// For default/source language, use source value; for translations, use target
224-
var pluralValue = metadata.IsDefault ? source : (target ?? source);
223+
// Always prefer target value (which contains the localized text)
224+
var pluralValue = target ?? source;
225225

226226
if (!string.IsNullOrEmpty(pluralId) && pluralValue != null)
227227
{
@@ -314,8 +314,8 @@ private List<ResourceEntry> ParseXliff20(XDocument doc, LanguageInfo metadata)
314314

315315
if (directSource != null || directTarget != null)
316316
{
317-
// For default/source language, return source value
318-
var directValue = metadata.IsDefault ? (directSource ?? "") : (directTarget ?? directSource ?? "");
317+
// Always prefer target value (which contains the localized text)
318+
var directValue = directTarget ?? directSource ?? "";
319319
return new ResourceEntry
320320
{
321321
Key = id,
@@ -331,8 +331,8 @@ private List<ResourceEntry> ParseXliff20(XDocument doc, LanguageInfo metadata)
331331
{
332332
var target = segment.Element(ns + "target")?.Value;
333333
var source = segment.Element(ns + "source")?.Value;
334-
// For default/source language, return source value
335-
values.Add(metadata.IsDefault ? (source ?? "") : (target ?? source ?? ""));
334+
// Always prefer target value (which contains the localized text)
335+
values.Add(target ?? source ?? "");
336336
}
337337
var value = string.Join("", values);
338338

LocalizationManager.Core/Backends/Xliff/XliffResourceWriter.cs

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -117,20 +117,24 @@ public string SerializeToString(ResourceFile file)
117117
? CreateXliff20Document(file)
118118
: CreateXliff12Document(file);
119119

120+
// Use UTF-8 without BOM to avoid double-BOM issues when file is rewritten
121+
var utf8NoBom = new UTF8Encoding(false);
120122
var settings = new XmlWriterSettings
121123
{
122124
Indent = true,
123125
IndentChars = " ",
124-
Encoding = Encoding.UTF8,
126+
Encoding = utf8NoBom,
125127
OmitXmlDeclaration = false
126128
};
127129

128-
using var sw = new StringWriter();
129-
using (var writer = XmlWriter.Create(sw, settings))
130+
// Use MemoryStream instead of StringWriter to properly handle UTF-8 encoding
131+
// StringWriter always reports UTF-16 encoding regardless of XmlWriterSettings
132+
using var ms = new MemoryStream();
133+
using (var writer = XmlWriter.Create(ms, settings))
130134
{
131135
doc.Save(writer);
132-
} // XmlWriter is disposed and flushed here
133-
return sw.ToString();
136+
}
137+
return utf8NoBom.GetString(ms.ToArray());
134138
}
135139

136140
/// <summary>
@@ -140,17 +144,19 @@ private XDocument CreateXliff12Document(ResourceFile file)
140144
{
141145
var root = new XElement(Ns12 + "xliff",
142146
new XAttribute("version", "1.2"),
143-
new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"),
144147
new XAttribute("xmlns", Ns12.NamespaceName));
145148

149+
// For XLIFF, we always use the language code for both source and target
150+
// Even for default language files, we write target-language to preserve bilingual structure
146151
var sourceLanguage = file.Language.IsDefault ? file.Language.Code : "en";
147-
var targetLanguage = file.Language.IsDefault ? "" : file.Language.Code;
152+
var targetLanguage = file.Language.Code;
148153

149154
var fileElement = new XElement(Ns12 + "file",
150155
new XAttribute("original", file.Language.BaseName ?? "resources"),
151156
new XAttribute("source-language", sourceLanguage),
152157
new XAttribute("datatype", "plaintext"));
153158

159+
// Always write target-language attribute to preserve bilingual structure
154160
if (!string.IsNullOrEmpty(targetLanguage))
155161
{
156162
fileElement.Add(new XAttribute("target-language", targetLanguage));
@@ -187,22 +193,17 @@ private XDocument CreateXliff12Document(ResourceFile file)
187193

188194
/// <summary>
189195
/// Creates a trans-unit element for XLIFF 1.2.
196+
/// Always creates both source and target elements to preserve bilingual structure.
190197
/// </summary>
191198
private XElement CreateTransUnit12(ResourceEntry entry, bool isDefault)
192199
{
193200
var unit = new XElement(Ns12 + "trans-unit",
194201
new XAttribute("id", entry.Key));
195202

196-
// Source is the key for default language, or include bilingual content
197-
if (isDefault || _config.Bilingual)
198-
{
199-
unit.Add(new XElement(Ns12 + "source", entry.Value ?? ""));
200-
}
201-
else
202-
{
203-
unit.Add(new XElement(Ns12 + "source", entry.Key));
204-
unit.Add(new XElement(Ns12 + "target", entry.Value ?? ""));
205-
}
203+
// Always write both source (key) and target (value) for proper bilingual structure
204+
// Source contains the key identifier, target contains the translated value
205+
unit.Add(new XElement(Ns12 + "source", entry.Key));
206+
unit.Add(new XElement(Ns12 + "target", entry.Value ?? ""));
206207

207208
if (!string.IsNullOrEmpty(entry.Comment))
208209
{
@@ -272,6 +273,7 @@ private XDocument CreateXliff20Document(ResourceFile file)
272273

273274
/// <summary>
274275
/// Creates a unit element for XLIFF 2.0.
276+
/// Always creates both source and target elements to preserve bilingual structure.
275277
/// </summary>
276278
private XElement CreateUnit20(ResourceEntry entry, bool isDefault)
277279
{
@@ -299,18 +301,10 @@ private XElement CreateUnit20(ResourceEntry entry, bool isDefault)
299301
}
300302
else
301303
{
302-
var segment = new XElement(Ns20 + "segment");
303-
304-
if (isDefault)
305-
{
306-
segment.Add(new XElement(Ns20 + "source", entry.Value ?? ""));
307-
}
308-
else
309-
{
310-
segment.Add(new XElement(Ns20 + "source", entry.Key));
311-
segment.Add(new XElement(Ns20 + "target", entry.Value ?? ""));
312-
}
313-
304+
// Always write both source (key) and target (value) for proper bilingual structure
305+
var segment = new XElement(Ns20 + "segment",
306+
new XElement(Ns20 + "source", entry.Key),
307+
new XElement(Ns20 + "target", entry.Value ?? ""));
314308
unit.Add(segment);
315309
}
316310

0 commit comments

Comments
 (0)