Skip to content

Commit 6bd024f

Browse files
authored
Fix #731: Support language representation as LCID used by PFC projects (#734)
1 parent 90faad5 commit 6bd024f

19 files changed

Lines changed: 499 additions & 58 deletions

File tree

src/ResXManager.Infrastructure/CultureHelper.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Globalization;
67
using System.Linq;
78

89
using TomsToolbox.Essentials;
910

1011
public static class CultureHelper
1112
{
13+
[return: NotNullIfNotNull("languageName")]
14+
public static CultureInfo? CreateCultureInfo(string? languageName)
15+
{
16+
if (languageName is null)
17+
return null;
18+
19+
return int.TryParse(languageName, out var lcid) ? new CultureInfo(lcid) : new CultureInfo(languageName);
20+
}
21+
1222
public static bool IsValidCultureName(string? languageName)
1323
{
1424
try
@@ -21,7 +31,7 @@ public static bool IsValidCultureName(string? languageName)
2131
return true;
2232

2333
// #376: support Custom dialect resource
24-
var culture = new CultureInfo(languageName);
34+
var culture = CreateCultureInfo(languageName);
2535
while (!culture.IsNeutralCulture)
2636
{
2737
culture = culture.Parent;
@@ -79,4 +89,4 @@ private static CultureInfo[] GetSpecificCultures()
7989
}
8090
}
8191

82-
}
92+
}

src/ResXManager.Infrastructure/CultureKey.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33
using System;
44
using System.Globalization;
55

6+
public enum CultureRepresentation
7+
{
8+
/// <summary>
9+
/// BCP 47 language tag (e.g. "en-US")
10+
/// </summary>
11+
Name,
12+
/// <summary>
13+
/// Windows Language Code Identifier (Lcid)
14+
/// </summary>
15+
Lcid
16+
}
17+
18+
public sealed class CultureDefinition(CultureKey cultureKey, CultureRepresentation cultureRepresentation)
19+
{
20+
public CultureKey CultureKey { get; } = cultureKey;
21+
public CultureRepresentation CultureRepresentation { get; } = cultureRepresentation;
22+
}
23+
624
/// <summary>
725
/// A class encapsulating a <see cref="CultureInfo"/>, usable as a key to a dictionary to allow also indexing a <c>null</c> <see cref="CultureInfo"/>.
826
/// </summary>
@@ -29,9 +47,9 @@ public override string ToString()
2947
return ToString(string.Empty);
3048
}
3149

32-
public string ToString(string neutralCultureKey)
50+
public string ToString(string neutralCultureName)
3351
{
34-
return Culture != null ? "." + Culture.Name : neutralCultureKey;
52+
return Culture != null ? "." + Culture.Name : neutralCultureName;
3553
}
3654

3755
#region IComparable/IEquatable implementation
@@ -178,4 +196,4 @@ public static CultureKey Parse(object? item)
178196
_ => throw new InvalidOperationException("Unable to cast object to culture key: " + item)
179197
};
180198
}
181-
}
199+
}

src/ResXManager.Infrastructure/ExtensionMethods.cs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212

1313
public static class ExtensionMethods
1414
{
15+
public static string? NullIfEmpty(this string? value)
16+
{
17+
if (string.IsNullOrEmpty(value))
18+
return null;
19+
20+
return value;
21+
}
22+
1523
/// <summary>
1624
/// Converts the culture key name to the corresponding culture. The key name is the ieft language tag with an optional '.' prefix.
1725
/// </summary>
@@ -24,15 +32,12 @@ public static class ExtensionMethods
2432
{
2533
try
2634
{
27-
cultureKeyName = cultureKeyName?.TrimStart('.');
28-
29-
return cultureKeyName.IsNullOrEmpty() ? null : CultureInfo.GetCultureInfo(cultureKeyName);
35+
return CultureHelper.CreateCultureInfo(cultureKeyName?.TrimStart('.').NullIfEmpty());
3036
}
31-
catch (ArgumentException)
37+
catch
3238
{
39+
throw new InvalidOperationException("Error parsing language: " + cultureKeyName);
3340
}
34-
35-
throw new InvalidOperationException("Error parsing language: " + cultureKeyName);
3641
}
3742

3843
/// <summary>
@@ -46,12 +51,11 @@ public static class ExtensionMethods
4651
{
4752
try
4853
{
49-
cultureKeyName = cultureKeyName?.TrimStart('.');
50-
51-
return new CultureKey(cultureKeyName.IsNullOrEmpty() ? null : CultureInfo.GetCultureInfo(cultureKeyName));
54+
return ToCulture(cultureKeyName);
5255
}
53-
catch (ArgumentException)
56+
catch
5457
{
58+
// invalid culture, ignore...
5559
}
5660

5761
return null;
@@ -106,4 +110,4 @@ public static IEnumerable<FileInfo> EnumerateSourceFiles(this DirectoryInfo dire
106110
}
107111
}
108112
}
109-
}
113+
}

src/ResXManager.Model/ProjectFileExtensions.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Linq;
77

88
using ResXManager.Infrastructure;
9-
using TomsToolbox.Essentials;
109

1110
public static class ProjectFileExtensions
1211
{
@@ -52,7 +51,7 @@ public static bool IsResourceFile(this ProjectFile projectFile)
5251
return IsResourceFile(filePath, extension);
5352
}
5453

55-
public static CultureKey GetCultureKey(this ProjectFile projectFile, CultureInfo neutralResourcesLanguage)
54+
public static CultureDefinition GetCultureDefinition(this ProjectFile projectFile, CultureInfo neutralResourcesLanguage)
5655
{
5756
var extension = projectFile.Extension;
5857
var filePath = projectFile.FilePath;
@@ -62,13 +61,21 @@ public static CultureKey GetCultureKey(this ProjectFile projectFile, CultureInfo
6261
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
6362
var cultureName = Path.GetExtension(fileNameWithoutExtension).TrimStart('.');
6463

65-
if (cultureName.IsNullOrEmpty())
66-
return CultureKey.Neutral;
67-
6864
if (!CultureHelper.IsValidCultureName(cultureName))
69-
return CultureKey.Neutral;
65+
return new(CultureKey.Neutral, CultureRepresentation.Name);
66+
67+
var cultureKey = new CultureKey(cultureName);
68+
69+
if (!int.TryParse(cultureName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var lcid))
70+
return new(cultureKey, CultureRepresentation.Name);
71+
72+
if (Equals(neutralResourcesLanguage, CultureInfo.GetCultureInfo(lcid)))
73+
{
74+
return new(CultureKey.Neutral, CultureRepresentation.Lcid);
75+
}
76+
77+
return new(cultureKey, CultureRepresentation.Lcid);
7078

71-
return new CultureKey(cultureName);
7279
}
7380

7481
if (Resw.Equals(extension, StringComparison.OrdinalIgnoreCase))
@@ -80,7 +87,9 @@ public static CultureKey GetCultureKey(this ProjectFile projectFile, CultureInfo
8087

8188
var culture = cultureName.ToCulture();
8289

83-
return Equals(neutralResourcesLanguage, culture) ? CultureKey.Neutral : new CultureKey(culture);
90+
var cultureKey = Equals(neutralResourcesLanguage, culture) ? CultureKey.Neutral : new CultureKey(culture);
91+
92+
return new(cultureKey, CultureRepresentation.Name);
8493
}
8594

8695
throw new InvalidOperationException("Unsupported file format: " + extension);
@@ -101,14 +110,30 @@ public static string GetBaseName(this ProjectFile projectFile)
101110
return CultureHelper.IsValidCultureName(languageName) ? Path.GetFileNameWithoutExtension(name) : name;
102111
}
103112

104-
public static string GetLanguageFileName(this ProjectFile projectFile, CultureInfo culture)
113+
public static string GetLanguageFileName(this ResourceLanguage neutralLanguage, CultureInfo culture)
105114
{
115+
var projectFile = neutralLanguage.ProjectFile;
116+
106117
var extension = projectFile.Extension;
107118
var filePath = projectFile.FilePath;
108119

109120
if (Resx.Equals(extension, StringComparison.OrdinalIgnoreCase))
110121
{
111-
return Path.ChangeExtension(filePath, culture.ToString()) + @".resx";
122+
var baseName = Path.ChangeExtension(filePath, null);
123+
string cultureTag;
124+
125+
if (neutralLanguage.CultureRepresentation == CultureRepresentation.Lcid)
126+
{
127+
// Neutral LCID based resource also includes a language tag in the file, e.g. "Strings.1033.resx" - else we would not have classified it as LCID based.
128+
baseName = Path.ChangeExtension(baseName, null);
129+
cultureTag = culture.LCID.ToString("D", CultureInfo.InvariantCulture);
130+
}
131+
else
132+
{
133+
cultureTag = culture.Name;
134+
}
135+
136+
return $"{baseName}.{cultureTag}.resx";
112137
}
113138

114139
if (Resw.Equals(extension, StringComparison.OrdinalIgnoreCase))
@@ -134,4 +159,4 @@ public static bool IsCSharpFile(this ProjectFile projectFile)
134159
{
135160
return Path.GetExtension(projectFile.FilePath).Equals(".cs", StringComparison.OrdinalIgnoreCase);
136161
}
137-
}
162+
}

src/ResXManager.Model/ResourceEntity.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal ResourceEntity(ResourceManager container, string projectName, string ba
3636
RelativePath = GetRelativePath(files);
3737
DisplayName = projectName + @" - " + RelativePath + baseName;
3838

39-
NeutralProjectFile = files.FirstOrDefault(file => file.GetCultureKey(neutralResourcesLanguage) == CultureKey.Neutral);
39+
NeutralProjectFile = files.FirstOrDefault(file => file.GetCultureDefinition(neutralResourcesLanguage).CultureKey == CultureKey.Neutral);
4040
NeutralResourcesLanguage = neutralResourcesLanguage;
4141

4242
var entriesQuery = _languages.Values
@@ -56,7 +56,7 @@ internal bool Update(ICollection<ProjectFile> files, CultureInfo neutralResource
5656
if (!MergeItems(GetResourceLanguages(files, neutralResourcesLanguage, duplicateKeyHandling)))
5757
return false; // nothing has changed, no need to continue
5858

59-
var neutralProjectFile = files.FirstOrDefault(file => file.GetCultureKey(neutralResourcesLanguage) == CultureKey.Neutral);
59+
var neutralProjectFile = files.FirstOrDefault(file => file.GetCultureDefinition(neutralResourcesLanguage).CultureKey == CultureKey.Neutral);
6060

6161
UpdateResourceTableEntries();
6262

@@ -69,7 +69,7 @@ public bool Update(ProjectFile file, [NotNullWhen(true)] out ResourceLanguage? u
6969
{
7070
var duplicateKeyHandling = Container.Configuration.DuplicateKeyHandling;
7171

72-
updatedLanguage = new ResourceLanguage(this, file.GetCultureKey(NeutralResourcesLanguage), file, duplicateKeyHandling);
72+
updatedLanguage = new ResourceLanguage(this, file.GetCultureDefinition(NeutralResourcesLanguage), file, duplicateKeyHandling);
7373
if (!UpdateEntry(updatedLanguage))
7474
{
7575
updatedLanguage = null;
@@ -216,10 +216,10 @@ public void Remove(ResourceTableEntry item)
216216
/// <param name="file">The file.</param>
217217
public void AddLanguage(ProjectFile file)
218218
{
219-
var cultureKey = file.GetCultureKey(NeutralResourcesLanguage);
220-
var resourceLanguage = new ResourceLanguage(this, cultureKey, file, Container.Configuration.DuplicateKeyHandling);
219+
var cultureKeyInfo = file.GetCultureDefinition(NeutralResourcesLanguage);
220+
var resourceLanguage = new ResourceLanguage(this, cultureKeyInfo, file, Container.Configuration.DuplicateKeyHandling);
221221

222-
_languages.Add(cultureKey, resourceLanguage);
222+
_languages.Add(cultureKeyInfo.CultureKey, resourceLanguage);
223223
_resourceTableEntries.ForEach(entry => entry.Refresh());
224224

225225
Container.OnLanguageAdded(resourceLanguage, file);
@@ -295,11 +295,11 @@ internal bool EqualsAll(string? projectName, string? baseName, string? directory
295295

296296
private IDictionary<CultureKey, ResourceLanguage> GetResourceLanguages(IEnumerable<ProjectFile> files, CultureInfo neutralResourcesLanguage, DuplicateKeyHandling duplicateKeyHandling)
297297
{
298-
var languageQuery =
299-
from file in files
300-
let cultureKey = file.GetCultureKey(neutralResourcesLanguage)
301-
orderby cultureKey
302-
select new ResourceLanguage(this, cultureKey, file, duplicateKeyHandling);
298+
var languageQuery = files
299+
.Select(file => new { file, cultureKeyInfo = file.GetCultureDefinition(neutralResourcesLanguage) })
300+
.OrderBy(item => item.cultureKeyInfo.CultureKey.IsNeutral ? 0 : 1)
301+
.ThenBy(item => item.cultureKeyInfo.CultureKey)
302+
.Select(item => new ResourceLanguage(this, item.cultureKeyInfo, item.file, duplicateKeyHandling));
303303

304304
var languages = languageQuery.ToDictionary(language => language.CultureKey);
305305

@@ -356,4 +356,4 @@ private bool UpdateEntry(ResourceLanguage source)
356356
_languages[cultureKey] = source;
357357
return true;
358358
}
359-
}
359+
}

src/ResXManager.Model/ResourceLanguage.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,17 @@ public class ResourceLanguage
4343
/// Initializes a new instance of the <see cref="ResourceLanguage" /> class.
4444
/// </summary>
4545
/// <param name="container">The containing resource entity.</param>
46-
/// <param name="cultureKey">The culture key.</param>
46+
/// <param name="cultureDefinition">Culture key and representation.</param>
4747
/// <param name="file">The .resx file having all the localization.</param>
4848
/// <param name="duplicateKeyHandling">The duplicate key handling.</param>
4949
/// <exception cref="InvalidOperationException">
5050
/// </exception>
5151
/// <exception cref="InvalidOperationException"></exception>
52-
internal ResourceLanguage(ResourceEntity container, CultureKey cultureKey, ProjectFile file, DuplicateKeyHandling duplicateKeyHandling)
52+
internal ResourceLanguage(ResourceEntity container, CultureDefinition cultureDefinition, ProjectFile file, DuplicateKeyHandling duplicateKeyHandling)
5353
{
5454
Container = container;
55-
CultureKey = cultureKey;
55+
CultureKey = cultureDefinition.CultureKey;
56+
CultureRepresentation = cultureDefinition.CultureRepresentation;
5657
ProjectFile = file;
5758
_configuration = container.Container.Configuration;
5859

@@ -130,6 +131,8 @@ private void UpdateNodes(DuplicateKeyHandling duplicateKeyHandling)
130131

131132
public bool IsNeutralLanguage => Container.Languages.FirstOrDefault() == this;
132133

134+
public CultureRepresentation CultureRepresentation { get; }
135+
133136
public CultureKey CultureKey { get; }
134137

135138
public ResourceEntity Container { get; }
@@ -602,4 +605,4 @@ private XAttribute GetNameAttribute(XElement entry)
602605
return nameAttribute ?? throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.InvalidResourceFileNameAttributeMissingError, _owner.ProjectFile.FilePath));
603606
}
604607
}
605-
}
608+
}

src/ResXManager.Model/ResourceManagerExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ void EnumerationShouldContinue()
2727

2828
var allProjectFiles = fileInfos
2929
.Select(item => item.Intercept(_ => EnumerationShouldContinue()))
30-
.Select(fileInfo => new ProjectFile(fileInfo.FullName, solutionFolder.FullName, @"<unknown>", null))
30+
.Select(fileInfo => new ProjectFile(fileInfo.FullName, solutionFolder.FullName, "<unknown>", null))
3131
.Where(fileFilter.Matches)
3232
.ToList();
3333

@@ -87,4 +87,4 @@ void EnumerationShouldContinue()
8787

8888
return null;
8989
}
90-
}
90+
}

src/ResXManager.Scripting/Host.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ private bool CanEdit(ResourceEntity entity, CultureKey? cultureKey)
142142
if (neutralLanguage == null)
143143
return false;
144144

145-
var languageFileName = neutralLanguage.ProjectFile.GetLanguageFileName(culture);
145+
var languageFileName = neutralLanguage.GetLanguageFileName(culture);
146146

147147
if (!File.Exists(languageFileName))
148148
{

src/ResXManager.Tests/ResXManager.Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,10 @@
3434
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3535
</Content>
3636
</ItemGroup>
37+
38+
<ItemGroup>
39+
<Content Update="Resources\Lcid\Lcid.1033.resx">
40+
<Generator></Generator>
41+
</Content>
42+
</ItemGroup>
3743
</Project>

0 commit comments

Comments
 (0)