Skip to content

Commit 7d3fe95

Browse files
Add NormalizeLocales task
* Add task to help work with Crowdin locales * Add MockBuildEngine to support testing * Add task into the documentation +semver:minor
1 parent 3ab87b9 commit 7d3fe95

6 files changed

Lines changed: 248 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616

1717
## [Unreleased]
1818

19+
## [2.3.0] - 2020-04-15
20+
21+
### Added
22+
23+
- Add `NormalizeLocales` Task to help work with Crowdin Localized files
24+
1925
### Added
2026

2127
- Create symbol nuget package

Documentation/SIL.BuildTasks.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ may be sent in clear.
7575

7676
### Example
7777

78+
## NormalizeLocales task
79+
80+
### Properties
81+
82+
- `L10nsDirectory`: The directory whose subdirectories are locale names and contain localization files
83+
7884
## NUnit task
7985

8086
Runs NUnit (v2) on a test assembly.

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Several useful msbuild tasks.
2323
[FileUpdate](Documentation/SIL.BuildTasks.md#fileupdate-task) |
2424
[MakePot](Documentation/SIL.BuildTasks.md#makepot-task) |
2525
[MakeWixForDirTree](Documentation/SIL.BuildTasks.md#makewixfordirtree-task) |
26+
[NormalizeLocales](Documentation/SIL.BuildTasks.md#normalizelocales-task) | Drops country code from directories and filenames to help work with Crowdin
2627
[NUnit](Documentation/SIL.BuildTasks.md#nunit-task) | Run NUnit (v2) on a test assembly.
2728
[NUnit3](Documentation/SIL.BuildTasks.md#nunit3-task) | Run NUnit3 on a test assembly.
2829
[Split](Documentation/SIL.BuildTasks.md#split-task) |
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Collections;
3+
using Microsoft.Build.Framework;
4+
5+
namespace SIL.BuildTasks.Tests
6+
{
7+
class MockBuildEngine : IBuildEngine
8+
{
9+
public void LogErrorEvent(BuildErrorEventArgs e)
10+
{
11+
}
12+
13+
public void LogWarningEvent(BuildWarningEventArgs e)
14+
{
15+
}
16+
17+
public void LogMessageEvent(BuildMessageEventArgs e)
18+
{
19+
}
20+
21+
public void LogCustomEvent(CustomBuildEventArgs e)
22+
{
23+
}
24+
25+
public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties,
26+
IDictionary targetOutputs)
27+
{
28+
throw new NotImplementedException();
29+
}
30+
31+
public bool ContinueOnError => false;
32+
public int LineNumberOfTaskNode => 0;
33+
public int ColumnNumberOfTaskNode => 0;
34+
public string ProjectFileOfTaskNode => throw new NotImplementedException();
35+
}
36+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) 2020 SIL International
2+
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)
3+
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Threading;
7+
using NUnit.Framework;
8+
9+
namespace SIL.BuildTasks.Tests
10+
{
11+
[TestFixture]
12+
public class NormalizeLocalesTests
13+
{
14+
private NormalizeLocales _task;
15+
private string _testDir;
16+
17+
[SetUp]
18+
public void TestSetup()
19+
{
20+
_testDir = Path.Combine(Path.GetTempPath(), GetType().Name);
21+
_task = new NormalizeLocales { BuildEngine = new MockBuildEngine(), L10nsDirectory = _testDir };
22+
23+
RecreateDirectory(_testDir);
24+
}
25+
26+
[TearDown]
27+
public void TestTeardown()
28+
{
29+
try
30+
{
31+
Directory.Delete(_testDir, true);
32+
}
33+
catch
34+
{
35+
Debug.WriteLine("Test could not clean up data directories.");
36+
}
37+
}
38+
39+
[Test]
40+
public void DoesntCrashWhenNoNameChange()
41+
{
42+
FileSystemSetup(new[] { "de", "en-US" });
43+
44+
_task.Execute();
45+
46+
// Verify that the already-normalized locale is still normalized
47+
VerifyLocale("de", "never-existed");
48+
// Verify that the locale with a region has been normalized
49+
VerifyLocale("en", "en-US");
50+
}
51+
52+
[Test]
53+
public void Works()
54+
{
55+
FileSystemSetup(new[] { "de-DE", "en-US", "zh-CN" });
56+
57+
_task.Execute();
58+
59+
// Verify that normal languages have no country codes
60+
VerifyLocale("de", "de-DE");
61+
VerifyLocale("en", "en-US");
62+
63+
// Verify that Chinese has the country code and that there is no regionless Chinese.
64+
VerifyLocale("zh-CN", "zh");
65+
}
66+
67+
private void FileSystemSetup(string[] locales)
68+
{
69+
foreach (var locale in locales)
70+
{
71+
var localeDir = Path.Combine(_testDir, locale);
72+
Directory.CreateDirectory(localeDir);
73+
File.WriteAllText(Path.Combine(localeDir, $"strings-{locale}.xml"), "some strings");
74+
File.WriteAllText(Path.Combine(localeDir, $"Palaso.{locale}.xlf"), "pretend this is xliff");
75+
var projectDir = Path.Combine(localeDir, "someProject");
76+
Directory.CreateDirectory(projectDir);
77+
File.WriteAllText(Path.Combine(projectDir, $"SomeFile.{locale}.resx"), "contents");
78+
}
79+
}
80+
81+
private void VerifyLocale(string expected, string not)
82+
{
83+
var localeDir = Path.Combine(_testDir, expected);
84+
AssertDirExists(localeDir);
85+
AssertDirDoesNotExist(Path.Combine(_testDir, not));
86+
87+
AssertFileExists(Path.Combine(localeDir, $"strings-{expected}.xml"));
88+
AssertFileDoesNotExist(Path.Combine(localeDir, $"strings-{not}.xml"));
89+
AssertFileExists(Path.Combine(localeDir, $"Palaso.{expected}.xlf"));
90+
AssertFileDoesNotExist(Path.Combine(localeDir, $"Palaso.{not}.xlf"));
91+
AssertFileExists(Path.Combine(localeDir, "someProject", $"SomeFile.{expected}.resx"));
92+
AssertFileDoesNotExist(Path.Combine(localeDir, "someProject", $"SomeFile.{not}.resx"));
93+
}
94+
95+
private static void AssertDirExists(string path)
96+
{
97+
Assert.That(Directory.Exists(path), $"Expected the directory {path} to exist, but it did not.");
98+
}
99+
100+
private static void AssertDirDoesNotExist(string path)
101+
{
102+
Assert.That(!Directory.Exists(path), $"Expected the directory {path} not to exist, but it did.");
103+
}
104+
105+
private static void AssertFileExists(string path)
106+
{
107+
Assert.That(File.Exists(path), $"Expected the file {path} to exist, but it did not.");
108+
}
109+
110+
private static void AssertFileDoesNotExist(string path)
111+
{
112+
Assert.That(!File.Exists(path), $"Expected the file {path} not to exist, but it did.");
113+
}
114+
public static void RecreateDirectory(string path)
115+
{
116+
if (Directory.Exists(path))
117+
{
118+
try
119+
{
120+
Directory.Delete(path, true);
121+
Thread.Sleep(1000); // wait for the filesystem to finish deleting
122+
}
123+
catch
124+
{
125+
Assert.Fail("Couldn't delete old test data from aborted runs");
126+
}
127+
}
128+
Directory.CreateDirectory(path);
129+
}
130+
}
131+
}

SIL.BuildTasks/NormalizeLocales.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2020 SIL International
2+
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)
3+
4+
using System.IO;
5+
using System.Linq;
6+
using Microsoft.Build.Framework;
7+
using Microsoft.Build.Utilities;
8+
9+
namespace SIL.BuildTasks
10+
{
11+
/// <summary>
12+
/// Crowdin includes country codes either always or never. Country codes are required for Chinese, but mostly get in the way for other languages.
13+
/// This task removes country codes from all locales except Chinese. It expects to work with paths in the form of /%locale%/filename.%locale.extension
14+
/// </summary>
15+
public class NormalizeLocales : Task
16+
{
17+
/// <summary>The directory whose subdirectories are locale names and contain localizations</summary>
18+
[Required]
19+
public string L10nsDirectory { get; set; }
20+
21+
public override bool Execute()
22+
{
23+
var locales = Directory.GetDirectories(L10nsDirectory).Select(Path.GetFileName);
24+
foreach (var locale in locales)
25+
{
26+
RenameLocale(locale, Normalize(locale));
27+
}
28+
return true;
29+
}
30+
31+
/// <summary>
32+
/// Normalizes a locale in the form %language_code%-%region_code%, assuming the language is in the default region.
33+
/// That is, strip the region codes from all languages except Chinese (which must always have a region code).
34+
/// </summary>
35+
public static string Normalize(string locale)
36+
{
37+
return locale.Equals("zh-CN") ? locale : locale.Split('-')[0];
38+
}
39+
40+
private void RenameLocale(string source, string dest)
41+
{
42+
if (source == dest)
43+
{
44+
Log.LogMessage($"Not normalizing '{source}'.");
45+
return;
46+
}
47+
48+
var sourceDir = Path.Combine(L10nsDirectory, source);
49+
var destDir = Path.Combine(L10nsDirectory, dest);
50+
Directory.Move(sourceDir, destDir);
51+
52+
foreach (var file in Directory.EnumerateFiles(destDir, "*", SearchOption.AllDirectories))
53+
{
54+
var nameNoExt = Path.GetFileNameWithoutExtension(file);
55+
// ReSharper disable once PossibleNullReferenceException - no files are null
56+
if (nameNoExt.EndsWith(source))
57+
{
58+
var lengthToKeep = nameNoExt.Length - source.Length;
59+
var dir = Path.GetDirectoryName(file);
60+
var ext = Path.GetExtension(file);
61+
// ReSharper disable once AssignNullToNotNullAttribute - no files are null
62+
var newName = Path.Combine(dir, $"{nameNoExt.Substring(0, lengthToKeep)}{dest}{ext}");
63+
File.Move(file, newName);
64+
}
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)