diff --git a/Gu.Localization.Tests/Gu.Localization.Tests.csproj b/Gu.Localization.Tests/Gu.Localization.Tests.csproj index 071eadb6..58ee216f 100644 --- a/Gu.Localization.Tests/Gu.Localization.Tests.csproj +++ b/Gu.Localization.Tests/Gu.Localization.Tests.csproj @@ -53,8 +53,11 @@ - - + + + + + @@ -82,6 +85,7 @@ ResXFileCodeGenerator Resources.Designer.cs + Designer diff --git a/Gu.Localization.Tests/Internals/ResourceManagerExtTests.cs b/Gu.Localization.Tests/Internals/ResourceManagerExtTests.cs index 6cb343c1..f42995c7 100644 --- a/Gu.Localization.Tests/Internals/ResourceManagerExtTests.cs +++ b/Gu.Localization.Tests/Internals/ResourceManagerExtTests.cs @@ -6,6 +6,12 @@ public class ResourceManagerExtTests { + [OneTimeSetUp] + public void OneTimeSetUp() + { + Properties.Resources.ResourceManager.ReleaseAllResources(); + } + [TestCase(null, true)] [TestCase("sv", true)] [TestCase("it", false)] @@ -30,8 +36,6 @@ public void HasKey(string key, string cultureName, bool expected) : CultureInfo.GetCultureInfo(cultureName); var resourceManager = Properties.Resources.ResourceManager; - resourceManager.GetString(key, culture); // warmup - Assert.AreEqual(expected, resourceManager.HasKey(key, culture)); Assert.IsNull(resourceManager.GetResourceSet(culture, false, false)); } diff --git a/Gu.Localization.Tests/Properties/Resources.Designer.cs b/Gu.Localization.Tests/Properties/Resources.Designer.cs index d6f3da1c..1473cf8c 100644 --- a/Gu.Localization.Tests/Properties/Resources.Designer.cs +++ b/Gu.Localization.Tests/Properties/Resources.Designer.cs @@ -87,6 +87,15 @@ internal static string first___0___second__1_ { } } + /// + /// Looks up a localized string similar to Value: {0}. + /// + internal static string InvalidFormat__0__ { + get { + return ResourceManager.GetString("InvalidFormat__0__", resourceCulture); + } + } + /// /// Looks up a localized string similar to So neutral. /// @@ -106,11 +115,29 @@ internal static string NoTranslation { } /// - /// Looks up a localized string similar to Value: {0}. + /// Looks up a localized string similar to Neutral: {0}. + /// + internal static string ValidFormat__0__ { + get { + return ResourceManager.GetString("ValidFormat__0__", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Neutral first: {0}, second {1:F2}. + /// + internal static string ValidFormat__0__1__ { + get { + return ResourceManager.GetString("ValidFormat__0__1__", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Neutral first: {0}, second {1:F2}, third: {2}. /// - internal static string Value___0_ { + internal static string ValidFormat__0__1__2__ { get { - return ResourceManager.GetString("Value___0_", resourceCulture); + return ResourceManager.GetString("ValidFormat__0__1__2__", resourceCulture); } } } diff --git a/Gu.Localization.Tests/Properties/Resources.de.resx b/Gu.Localization.Tests/Properties/Resources.de.resx index d2ab12f4..a987ebdc 100644 --- a/Gu.Localization.Tests/Properties/Resources.de.resx +++ b/Gu.Localization.Tests/Properties/Resources.de.resx @@ -123,4 +123,13 @@ zuerst: {0}, die zweite: {1} + + Wert: {0} + + + zuerst: {0}, die zweite: {1:F2} + + + zuerst: {0}, die zweite: {1:F2} meh {2} + \ No newline at end of file diff --git a/Gu.Localization.Tests/Properties/Resources.en.resx b/Gu.Localization.Tests/Properties/Resources.en.resx index f275ea74..6593b8e6 100644 --- a/Gu.Localization.Tests/Properties/Resources.en.resx +++ b/Gu.Localization.Tests/Properties/Resources.en.resx @@ -126,7 +126,16 @@ first: {0}, second:{1} - - Value: {0} {1} + + Value: {0} {2} + + + Value: {0} + + + English first: {0}, second {1:F2} + + + English first: {0}, second {1:F2}, third: {2} \ No newline at end of file diff --git a/Gu.Localization.Tests/Properties/Resources.resx b/Gu.Localization.Tests/Properties/Resources.resx index 9ddaafcc..6e94805f 100644 --- a/Gu.Localization.Tests/Properties/Resources.resx +++ b/Gu.Localization.Tests/Properties/Resources.resx @@ -132,7 +132,16 @@ first: {0}, second:{1} - + Value: {0} + + Neutral: {0} + + + Neutral first: {0}, second {1:F2} + + + Neutral first: {0}, second {1:F2}, third: {2} + \ No newline at end of file diff --git a/Gu.Localization.Tests/Properties/Resources.sv.resx b/Gu.Localization.Tests/Properties/Resources.sv.resx index ffa0212c..c41f7e3b 100644 --- a/Gu.Localization.Tests/Properties/Resources.sv.resx +++ b/Gu.Localization.Tests/Properties/Resources.sv.resx @@ -123,7 +123,16 @@ första: {0}, andra:{1} - + Värde: + + Värde: {0} + + + första: {0}, andra: {1:F2} + + + första: {0}, andra: {1:F2}, tredje: {2} + \ No newline at end of file diff --git a/Gu.Localization.Tests/TranslatorTests.OneParameter.cs b/Gu.Localization.Tests/TranslatorTests.OneParameter.cs new file mode 100644 index 00000000..e7428421 --- /dev/null +++ b/Gu.Localization.Tests/TranslatorTests.OneParameter.cs @@ -0,0 +1,70 @@ +namespace Gu.Localization.Tests +{ + using System; + using System.Globalization; + + using NUnit.Framework; + + public partial class TranslatorTests + { + public class OneParameter + { + [TestCase("en", 1, "Value: 1")] + [TestCase("sv", 1, "Värde: 1")] + [TestCase(null, 1, "Neutral: 1")] + public void HappyPath(string cultureName, object arg, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.ValidFormat__0__); + + Translator.CurrentCulture = culture; + var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg); + Assert.AreEqual(expected, actual); + + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg); + Assert.AreEqual(expected, actual); + } + + [TestCase("en", 1, "Invalid format string: \"Value: {0} {2}\".")] + [TestCase("sv", 1, "Invalid format string: \"Värde: \" for the single argument: 1.")] + public void Throws(string cultureName, object arg, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.InvalidFormat__0__); + + Translator.CurrentCulture = culture; + var actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, arg)); + Assert.AreEqual(expected, actual.Message); + + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg)); + Assert.AreEqual(expected, actual.Message); + } + + [TestCase("en", 1, "{\"Value: {0} {2}\" : 1}")] + [TestCase("sv", 1, "{\"Värde: \" : 1}")] + public void ReturnsInfo(string cultureName, object arg, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.InvalidFormat__0__); + + Translator.ErrorHandling = ErrorHandling.ReturnErrorInfo; + Translator.CurrentCulture = culture; + var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg); + Assert.AreEqual(expected, actual); + + Translator.ErrorHandling = ErrorHandling.Throw; + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg, ErrorHandling.ReturnErrorInfo); + Assert.AreEqual(expected, actual); + } + } + } +} diff --git a/Gu.Localization.Tests/TranslatorTests.Parameters.cs b/Gu.Localization.Tests/TranslatorTests.Parameters.cs deleted file mode 100644 index f80833f4..00000000 --- a/Gu.Localization.Tests/TranslatorTests.Parameters.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Gu.Localization.Tests -{ - using System.Globalization; - - using NUnit.Framework; - - public partial class TranslatorTests - { - public class Parameters - { - [TestCase("en", 1, "Value: 1")] - [TestCase("sv", 1, "Värde: 1")] - [TestCase(null, 1, "Neutral: 1")] - public void TranslateOneParameterThrow(string cultureName, object arg, string expected) - { - Assert.Inconclusive(); - var culture = cultureName != null - ? CultureInfo.GetCultureInfo(cultureName) - : CultureInfo.InvariantCulture; - Translator.CurrentCulture = culture; - var actual = Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.Value___0_), arg); - Assert.AreEqual(expected, actual); - - Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); - actual = Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.Value___0_), culture, arg); - Assert.AreEqual(expected, actual); - } - - [Test] - public void TranslateOneParameterReturnInfo() - { - Assert.Inconclusive(); - } - } - } -} diff --git a/Gu.Localization.Tests/TranslatorTests.Params.cs b/Gu.Localization.Tests/TranslatorTests.Params.cs new file mode 100644 index 00000000..7739fbf2 --- /dev/null +++ b/Gu.Localization.Tests/TranslatorTests.Params.cs @@ -0,0 +1,70 @@ +////namespace Gu.Localization.Tests +////{ +//// using System; +//// using System.Globalization; + +//// using NUnit.Framework; + +//// public partial class TranslatorTests +//// { +//// public class Params +//// { +//// [TestCase("en", 1, 2.0, 3, "English first: 1, second 2.00, third: 3")] +//// [TestCase("sv", 1, 2.0, 3, "första: 1, andra: 2,00, tredje: 3")] +//// [TestCase(null, 1, 2.0, 3, "Neutral first: 1, second 2.00, third: 3")] +//// public void HappyPath(string cultureName, object arg0, object arg1, object arg2, string expected) +//// { +//// var culture = cultureName != null +//// ? CultureInfo.GetCultureInfo(cultureName) +//// : CultureInfo.InvariantCulture; +//// var key = nameof(Properties.Resources.ValidFormat__0__1__2__); + +//// Translator.CurrentCulture = culture; +//// var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1, arg2); +//// Assert.AreEqual(expected, actual); + +//// Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); +//// actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg0, arg1, arg2); +//// Assert.AreEqual(expected, actual); +//// } + +//// [TestCase("en", 1, 2, 3, "Invalid format string: \"Value: {0} {2}\".")] +//// [TestCase("sv", 1, 2,3, "Invalid format string: \"Värde: \" for the two arguments: 1, 2, 3.")] +//// public void Throws(string cultureName, object arg0, object arg1, object arg2, string expected) +//// { +//// var culture = cultureName != null +//// ? CultureInfo.GetCultureInfo(cultureName) +//// : CultureInfo.InvariantCulture; +//// var key = nameof(Properties.Resources.InvalidFormat__0__); + +//// Translator.CurrentCulture = culture; +//// var actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1, arg2)); +//// Assert.AreEqual(expected, actual.Message); + +//// Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); +//// actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg0, arg1, arg2)); +//// Assert.AreEqual(expected, actual.Message); +//// } + +//// [TestCase("en", 1, 2, 3, "{\"Value: {0} {2}\" : 1, 2, 3}")] +//// [TestCase("sv", 1, 2, 3, "{\"Värde: \" : 1, 2, 3}")] +//// public void ReturnsInfo(string cultureName, object arg0, object arg1, object arg2, string expected) +//// { +//// var culture = cultureName != null +//// ? CultureInfo.GetCultureInfo(cultureName) +//// : CultureInfo.InvariantCulture; +//// var key = nameof(Properties.Resources.InvalidFormat__0__); + +//// Translator.ErrorHandling = ErrorHandling.ReturnErrorInfo; +//// Translator.CurrentCulture = culture; +//// var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1, arg2); +//// Assert.AreEqual(expected, actual); + +//// Translator.ErrorHandling = ErrorHandling.Throw; +//// Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); +//// actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, ErrorHandling.ReturnErrorInfo, arg0, arg1, arg2); +//// Assert.AreEqual(expected, actual); +//// } +//// } +//// } +////} \ No newline at end of file diff --git a/Gu.Localization.Tests/TranslatorTests.TwoParameters.cs b/Gu.Localization.Tests/TranslatorTests.TwoParameters.cs new file mode 100644 index 00000000..e9cbd509 --- /dev/null +++ b/Gu.Localization.Tests/TranslatorTests.TwoParameters.cs @@ -0,0 +1,70 @@ +namespace Gu.Localization.Tests +{ + using System; + using System.Globalization; + + using NUnit.Framework; + + public partial class TranslatorTests + { + public class TwoParameters + { + [TestCase("en", 1, 2.0, "English first: 1, second 2.00")] + [TestCase("sv", 1, 2.0, "första: 1, andra: 2,00")] + [TestCase(null, 1, 2.0, "Neutral first: 1, second 2.00")] + public void HappyPath(string cultureName, object arg0, object arg1, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.ValidFormat__0__1__); + + Translator.CurrentCulture = culture; + var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1); + Assert.AreEqual(expected, actual); + + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg0, arg1); + Assert.AreEqual(expected, actual); + } + + [TestCase("en", 1, 2, "Invalid format string: \"Value: {0} {2}\".")] + [TestCase("sv", 1, 2, "Invalid format string: \"Värde: \" for the two arguments: 1, 2.")] + public void Throws(string cultureName, object arg0, object arg1, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.InvalidFormat__0__); + + Translator.CurrentCulture = culture; + var actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1)); + Assert.AreEqual(expected, actual.Message); + + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Assert.Throws(() => Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg0, arg1)); + Assert.AreEqual(expected, actual.Message); + } + + [TestCase("en", 1, 2, "{\"Value: {0} {2}\" : 1, 2}")] + [TestCase("sv", 1, 2, "{\"Värde: \" : 1, 2}")] + public void ReturnsInfo(string cultureName, object arg0, object arg1, string expected) + { + var culture = cultureName != null + ? CultureInfo.GetCultureInfo(cultureName) + : CultureInfo.InvariantCulture; + var key = nameof(Properties.Resources.InvalidFormat__0__); + + Translator.ErrorHandling = ErrorHandling.ReturnErrorInfo; + Translator.CurrentCulture = culture; + var actual = Translator.Translate(Properties.Resources.ResourceManager, key, arg0, arg1); + Assert.AreEqual(expected, actual); + + Translator.ErrorHandling = ErrorHandling.Throw; + Translator.CurrentCulture = CultureInfo.GetCultureInfo("it"); + actual = Translator.Translate(Properties.Resources.ResourceManager, key, culture, arg0, arg1, ErrorHandling.ReturnErrorInfo); + Assert.AreEqual(expected, actual); + } + } + } +} \ No newline at end of file diff --git a/Gu.Localization.Tests/ValidateTests.Formats.cs b/Gu.Localization.Tests/ValidateTests.Formats.cs new file mode 100644 index 00000000..7556454e --- /dev/null +++ b/Gu.Localization.Tests/ValidateTests.Formats.cs @@ -0,0 +1,70 @@ +namespace Gu.Localization.Tests +{ + using System; + using NUnit.Framework; + + public partial class ValidateTests + { + public class Formats + { + [TestCase("First: {0}")] + [TestCase("First: {0:N}")] + [TestCase("First: {0}, again: {0}")] + [TestCase("First: {0:F2}, Second: {0:F3}")] + public void OneArgumentHappyPath(string format) + { + Assert.DoesNotThrow(() => Validate.Format(format, 1)); + Assert.IsTrue(Validate.IsValidFormat(format, 1)); + } + + [TestCase("First: {0} Second: {1}")] + [TestCase("First: {0:N}, second: {1:F3}")] + [TestCase("First: {0}, again: {0:F2}, second: {1}")] + [TestCase("First: {1}, again: {0:F2}, second: {0}")] + public void TwoArgumentsHappyPath(string format) + { + Assert.DoesNotThrow(() => Validate.Format(format, 1, 2)); + Assert.IsTrue(Validate.IsValidFormat(format, 1, 2)); + } + + [TestCase("First: {0} second: {1}, third: {2}")] + [TestCase("First: {0:N}, second: {1:F3}, third: {2:G}")] + public void ParamsHappyPath(string format) + { + Assert.DoesNotThrow(() => Validate.Format(format, 1, 2, 3)); + Assert.IsTrue(Validate.IsValidFormat(format, 1, 2, 3)); + } + + [TestCase("Hej", "Invalid format string: \"Hej\" for the single argument: 1.")] + [TestCase("First: {1}", "Invalid format string: \"First: {1}\".")] + [TestCase("First: {0}, Second: {1}", "Invalid format string: \"First: {0}, Second: {1}\" for the single argument: 1.")] + public void OneArgumentWithError(string format, string expected) + { + var exception = Assert.Throws(() => Validate.Format(format, 1)); + Assert.AreEqual(expected, exception.Message); + Assert.IsFalse(Validate.IsValidFormat(format, 1)); + } + + [TestCase("Hej", "Invalid format string: \"Hej\" for the two arguments: 1, 2.")] + [TestCase("First: {1}", "Invalid format string: \"First: {1}\".")] + [TestCase("First: {0}, second: {1}, third: {2}", "Invalid format string: \"First: {0}, second: {1}, third: {2}\" for the two arguments: 1, 2.")] + public void TwoArgumentsWithError(string format, string expected) + { + var exception = Assert.Throws(() => Validate.Format(format, 1, 2)); + Assert.AreEqual(expected, exception.Message); + Assert.IsFalse(Validate.IsValidFormat(format, 1, 2)); + } + + + [TestCase("Hej", "Invalid format string: \"Hej\" for the arguments: 1, 2, 3.")] + [TestCase("First: {1}", "Invalid format string: \"First: {1}\".")] + [TestCase("First: {0}, second: {1}, third: {2} {3}", "Invalid format string: \"First: {0}, second: {1}, third: {2} {3}\" for the arguments: 1, 2, 3.")] + public void ParamsWithError(string format, string expected) + { + var exception = Assert.Throws(() => Validate.Format(format, 1, 2, 3)); + Assert.AreEqual(expected, exception.Message); + Assert.IsFalse(Validate.IsValidFormat(format, 1, 2, 3)); + } + } + } +} diff --git a/Gu.Localization.Tests/ValidateTests.Translations.cs b/Gu.Localization.Tests/ValidateTests.Translations.cs new file mode 100644 index 00000000..7c1c26d4 --- /dev/null +++ b/Gu.Localization.Tests/ValidateTests.Translations.cs @@ -0,0 +1,115 @@ +namespace Gu.Localization.Tests +{ + using System; + using System.Globalization; + using System.Text; + + using Gu.Localization.Tests.Errors; + + using NUnit.Framework; + + public partial class ValidateTests + { + public class Translations + { + [Test] + public void ResourceManager() + { + var errors = Validate.Translations(Properties.Resources.ResourceManager); + Assert.IsFalse(errors.IsEmpty); + var expectedKeys = new[] { "InvalidFormat__0__", "NeutralOnly", "EnglishOnly", "NoTranslation" }; + CollectionAssert.AreEqual(expectedKeys, errors.Keys); + var builder = new StringBuilder(); + builder.AppendLine("Key: InvalidFormat__0__") + .AppendLine(" Has format errors, the formats are:") + .AppendLine(" Value: {0}") + .AppendLine(" null") + .AppendLine(" Value: {0} {2}") + .AppendLine(" Värde: ") + .AppendLine(" Missing for: { de }") + .AppendLine("Key: NeutralOnly") + .AppendLine(" Missing for: { de, en, sv }") + .AppendLine("Key: EnglishOnly") + .AppendLine(" Missing for: { de, sv }") + .AppendLine("Key: NoTranslation") + .AppendLine(" Missing for: { de, en, sv }"); + var expected = builder.ToString(); + var actual = errors.ToString(" ", Environment.NewLine); + Assert.AreEqual(expected, actual); + } + + [Test] + public void ResourceManagerExplicitCultures() + { + var cultures = new[] { CultureInfo.GetCultureInfo("sv"), CultureInfo.GetCultureInfo("en") }; + var errors = Validate.Translations(Properties.Resources.ResourceManager, cultures); + Assert.IsFalse(errors.IsEmpty); + var expectedKeys = new[] { "InvalidFormat__0__", "NeutralOnly", "EnglishOnly", "NoTranslation" }; + CollectionAssert.AreEqual(expectedKeys, errors.Keys); + var builder = new StringBuilder(); + builder.AppendLine("Key: InvalidFormat__0__") + .AppendLine(" Has format errors, the formats are:") + .AppendLine(" Värde: ") + .AppendLine(" Value: {0} {2}") + .AppendLine("Key: NeutralOnly") + .AppendLine(" Missing for: { sv, en }") + .AppendLine("Key: EnglishOnly") + .AppendLine(" Missing for: { sv }") + .AppendLine("Key: NoTranslation") + .AppendLine(" Missing for: { sv, en }"); + var expected = builder.ToString(); + var actual = errors.ToString(" ", Environment.NewLine); + Assert.AreEqual(expected, actual); + } + + [Test] + public void EnumTranslations() + { + var errors = Validate.EnumTranslations(Properties.Resources.ResourceManager); + Assert.IsFalse(errors.IsEmpty); + CollectionAssert.AreEqual(new[] { DummyEnum.MissingTranslation.ToString() }, errors.Keys); + Assert.AreEqual( + "Key: MissingTranslation Missing for: { invariant, de, en, sv } ", + errors.ToString("", " ")); + } + + [Test] + public void EnumTranslationsExplicitCultures() + { + var cultures = new[] { CultureInfo.GetCultureInfo("sv"), CultureInfo.GetCultureInfo("en") }; + var errors = Validate.EnumTranslations(Properties.Resources.ResourceManager, cultures); + Assert.IsFalse(errors.IsEmpty); + CollectionAssert.AreEqual(new[] { DummyEnum.MissingTranslation.ToString() }, errors.Keys); + Assert.AreEqual("Key: MissingTranslation Missing for: { sv, en } ", errors.ToString("", " ")); + } + + [TestCase(nameof(Properties.Resources.AllLanguages))] + [TestCase(nameof(Properties.Resources.ValidFormat__0__))] + [TestCase(nameof(Properties.Resources.ValidFormat__0__1__))] + public void KeyWhenNoErrors(string key) + { + var resourceManager = Properties.Resources.ResourceManager; + var errors = Validate.Translations(resourceManager, key); + CollectionAssert.IsEmpty(errors); + + var cultures = new[] + { + CultureInfo.InvariantCulture, + CultureInfo.GetCultureInfo("en"), + CultureInfo.GetCultureInfo("sv") + }; + errors = Validate.Translations(resourceManager, key, cultures); + CollectionAssert.IsEmpty(errors); + } + + [TestCase(nameof(Properties.Resources.InvalidFormat__0__))] + [TestCase(nameof(Properties.Resources.EnglishOnly))] + public void KeyWithErrors(string key) + { + var resourceManager = Properties.Resources.ResourceManager; + var errors = Validate.Translations(resourceManager, key); + CollectionAssert.IsNotEmpty(errors); + } + } + } +} diff --git a/Gu.Localization.Tests/ValidateTests.cs b/Gu.Localization.Tests/ValidateTests.cs deleted file mode 100644 index e9bc1862..00000000 --- a/Gu.Localization.Tests/ValidateTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -namespace Gu.Localization.Tests -{ - using System; - using System.Globalization; - using System.Text; - - using Gu.Localization.Tests.Errors; - - using NUnit.Framework; - - public class ValidateTests - { - [Test] - public void TranslationsForResourceManager() - { - var errors = Validate.Translations(Properties.Resources.ResourceManager); - Assert.IsFalse(errors.IsEmpty); - CollectionAssert.AreEqual(new[] { "NeutralOnly", "EnglishOnly", "NoTranslation", "Value___0_" }, errors.Keys); - var builder = new StringBuilder(); - builder.AppendLine("Key: NeutralOnly") - .AppendLine(" Missing for: { de, en, sv }") - .AppendLine("Key: EnglishOnly") - .AppendLine(" Missing for: { de, sv }") - .AppendLine("Key: NoTranslation") - .AppendLine(" Missing for: { de, en, sv }") - .AppendLine("Key: Value___0_") - .AppendLine(" Has format errors, the formats are:") - .AppendLine(" Value: {0}") - .AppendLine(" null") - .AppendLine(" Value: {0} {1}") - .AppendLine(" Värde: ") - .AppendLine(" Missing for: { de }"); - var expected = builder.ToString(); - var actual = errors.ToString(" ", Environment.NewLine); - Assert.AreEqual(expected, actual); - } - - [Test] - public void TranslationsForResourceManagerExplicitCultures() - { - var cultures = new[] { CultureInfo.GetCultureInfo("sv"), CultureInfo.GetCultureInfo("en") }; - var errors = Validate.Translations(Properties.Resources.ResourceManager, cultures); - Assert.IsFalse(errors.IsEmpty); - CollectionAssert.AreEqual(new[] { "NeutralOnly", "EnglishOnly", "NoTranslation", "Value___0_" }, errors.Keys); - var builder = new StringBuilder(); - builder.AppendLine("Key: NeutralOnly") - .AppendLine(" Missing for: { sv, en }") - .AppendLine("Key: EnglishOnly") - .AppendLine(" Missing for: { sv }") - .AppendLine("Key: NoTranslation") - .AppendLine(" Missing for: { sv, en }") - .AppendLine("Key: Value___0_") - .AppendLine(" Has format errors, the formats are:") - .AppendLine(" Värde: ") - .AppendLine(" Value: {0} {1}"); - var expected = builder.ToString(); - var actual = errors.ToString(" ", Environment.NewLine); - Assert.AreEqual(expected, actual); - } - - [Test] - public void EnumTranslations() - { - var errors = Validate.EnumTranslations(Properties.Resources.ResourceManager); - Assert.IsFalse(errors.IsEmpty); - CollectionAssert.AreEqual(new[] { DummyEnum.MissingTranslation.ToString() }, errors.Keys); - Assert.AreEqual("Key: MissingTranslation Missing for: { invariant, de, en, sv } ", errors.ToString("", " ")); - } - - [Test] - public void EnumTranslationsExplicitCultures() - { - var cultures = new[] { CultureInfo.GetCultureInfo("sv"), CultureInfo.GetCultureInfo("en") }; - var errors = Validate.EnumTranslations(Properties.Resources.ResourceManager, cultures); - Assert.IsFalse(errors.IsEmpty); - CollectionAssert.AreEqual(new[] { DummyEnum.MissingTranslation.ToString() }, errors.Keys); - Assert.AreEqual("Key: MissingTranslation Missing for: { sv, en } ", errors.ToString("", " ")); - } - - [Test] - public void FormatsHappyPath() - { - var errors = Validate.Translations(Properties.Resources.ResourceManager, nameof(Properties.Resources.first___0___second__1_)); - CollectionAssert.IsEmpty(errors); - - var cultures = new[] { CultureInfo.InvariantCulture, CultureInfo.GetCultureInfo("en"), CultureInfo.GetCultureInfo("sv") }; - errors = Validate.Translations(Properties.Resources.ResourceManager, nameof(Properties.Resources.first___0___second__1_), cultures); - CollectionAssert.IsEmpty(errors); - } - - [Test] - public void FormatsWithErrors() - { - var errors = Validate.Translations(Properties.Resources.ResourceManager, Properties.Resources.Value___0_); - CollectionAssert.IsNotEmpty(errors); - } - - [Test] - public void TranslationsForKeyWhenNoErrors() - { - var errors = Validate.Translations(Properties.Resources.ResourceManager, nameof(Properties.Resources.AllLanguages)); - CollectionAssert.IsEmpty(errors); - - var cultures = new[] { CultureInfo.InvariantCulture, CultureInfo.GetCultureInfo("en"), CultureInfo.GetCultureInfo("sv") }; - errors = Validate.Translations(Properties.Resources.ResourceManager, nameof(Properties.Resources.AllLanguages), cultures); - CollectionAssert.IsEmpty(errors); - } - } -} diff --git a/Gu.Localization/Gu.Localization.csproj b/Gu.Localization/Gu.Localization.csproj index 7072693e..8931c5de 100644 --- a/Gu.Localization/Gu.Localization.csproj +++ b/Gu.Localization/Gu.Localization.csproj @@ -71,6 +71,7 @@ + diff --git a/Gu.Localization/Internals/ResourceManagerExt.cs b/Gu.Localization/Internals/ResourceManagerExt.cs index d3e37aac..d11d49e8 100644 --- a/Gu.Localization/Internals/ResourceManagerExt.cs +++ b/Gu.Localization/Internals/ResourceManagerExt.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; - using System.Reflection; using System.Resources; internal static class ResourceManagerExt @@ -21,10 +20,18 @@ internal static class ResourceManagerExt /// True if a translation exists internal static bool HasKey(this ResourceManager resourceManager, string key, CultureInfo culture) { - using (var resourceSet = resourceManager.GetTempResourceSet(culture)) + using (var clone = resourceManager.Clone()) { - return resourceSet?.ResourceSet.OfType() - .Any(x => Equals(x.Key, key)) == true; + if (clone?.ResourceManager == null) + { + return false; + } + + using (var resourceSet = clone.ResourceManager.GetResourceSet(culture, true, false)) + { + return resourceSet?.OfType() + .Any(x => Equals(x.Key, key)) == true; + } } } @@ -38,56 +45,47 @@ internal static bool HasKey(this ResourceManager resourceManager, string key, Cu /// True if a translation exists internal static bool HasCulture(this ResourceManager resourceManager, CultureInfo culture) { - using (var resourceSet = resourceManager.GetTempResourceSet(culture)) + using (var clone = resourceManager.Clone()) { - return resourceSet != null; + return clone?.ResourceManager?.GetResourceSet(culture, true, false) != null; } } - // Clones the resourcemanager and gets the resource set for the culture + // Clones the resourcemanager // This is slow and backwards but can't think of another way that does not load a the resourceset into memory. // Also calling resourceManager.ReleaseAllResources() feels really nasty in a lib like this. // Keeping it slow and dumb until something better. - private static Disposer GetTempResourceSet(this ResourceManager resourceManager, CultureInfo culture) + internal static ResourceManagerClone Clone(this ResourceManager resourceManager) { - var type = AppDomain.CurrentDomain.GetAssemblies() - .Select(x => x.GetType(resourceManager.BaseName)) - .SingleOrDefault(x => x != null && - x.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) - .Any(p => p.PropertyType == typeof(ResourceManager))); - if (type == null) - { - return null; - } - - var clone = new ResourceManager(resourceManager.BaseName, type.Assembly); - var resourceSet = clone.GetResourceSet(culture, true, false); - if (resourceSet == null) - { - resourceManager.ReleaseAllResources(); - return null; - } + return new ResourceManagerClone(resourceManager); + } - return new Disposer(resourceManager, resourceSet); + internal static Type ContainingType(this ResourceManager resourceManager) + { + return ResourceManagers.TypeManagerCache.GetOrAdd(resourceManager); } - private class Disposer : IDisposable + /// Creates a clone of the passed in. Releases all resources on dispose. + internal sealed class ResourceManagerClone : IDisposable { - internal readonly ResourceSet ResourceSet; - private readonly ResourceManager resourceManager; + internal readonly ResourceManager ResourceManager; - public Disposer(ResourceManager resourceManager, ResourceSet resourceSet) + public ResourceManagerClone(ResourceManager source) { - Debug.Assert(resourceManager != null, "resourceManager == null"); - Debug.Assert(resourceSet != null, "resourceSet == null"); - this.resourceManager = resourceManager; - this.ResourceSet = resourceSet; + Debug.Assert(source != null, "resourceManager == null"); + var containingType = source.ContainingType(); + Debug.Assert(containingType != null, "containingType == null"); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse want this check in release build + if (containingType != null) + { + this.ResourceManager = new ResourceManager(source.BaseName, containingType.Assembly); + } } public void Dispose() { - this.resourceManager.ReleaseAllResources(); - this.ResourceSet.Dispose(); + this.ResourceManager?.ReleaseAllResources(); } } } diff --git a/Gu.Localization/Internals/ResourceManagers.cs b/Gu.Localization/Internals/ResourceManagers.cs index 94f7c638..0637aa5a 100644 --- a/Gu.Localization/Internals/ResourceManagers.cs +++ b/Gu.Localization/Internals/ResourceManagers.cs @@ -2,21 +2,21 @@ namespace Gu.Localization { using System; using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; using System.Reflection; using System.Resources; /// A cache for resourcemanagers. - internal static class ResourceManagers + internal static class ResourceManagers { - private static readonly ConcurrentDictionary TypeManagerMap = new ConcurrentDictionary(); - /// Tries to get from cache or create a for /// Ex. typeof(Properties.Resources) /// The /// True if a could be created for internal static bool TryGetForType(Type resourcesType, out ResourceManager result) { - result = TypeManagerMap.GetOrAdd(resourcesType, CreateManagerForType); + result = TypeManagerCache.GetOrAdd(resourcesType, CreateManagerForTypeOrDefault); return result != null; } @@ -25,7 +25,7 @@ internal static bool TryGetForType(Type resourcesType, out ResourceManager resul /// A resource manager internal static ResourceManager ForType(Type resourcesType) { - var resourceManager = TypeManagerMap.GetOrAdd(resourcesType, CreateManagerForType); + var resourceManager = TypeManagerCache.GetOrAdd(resourcesType, CreateManagerForType); if (resourceManager == null) { var message = $"{nameof(resourcesType)} must have a property named ResourceManager of type ResourceManager"; @@ -35,15 +35,88 @@ internal static ResourceManager ForType(Type resourcesType) return resourceManager; } + private static ResourceManager CreateManagerForTypeOrDefault(Type type) + { + var property = GetResourceManagerProperty(type); + return property?.GetValue(null) as ResourceManager; + } + private static ResourceManager CreateManagerForType(Type type) { - var property = type.GetProperty(nameof(Properties.Resources.ResourceManager), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + var property = GetResourceManagerProperty(type); if (property == null || !typeof(ResourceManager).IsAssignableFrom(property.PropertyType)) { - return null; + var message = $"{nameof(type)} must have a property named ResourceManager of type ResourceManager"; + throw new ArgumentException(message); } return (ResourceManager)property.GetValue(null); } + + private static PropertyInfo GetResourceManagerProperty(this Type type) + { + var property = type.GetProperty( + nameof(Properties.Resources.ResourceManager), + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + return property; + } + + internal static class TypeManagerCache + { + private static readonly ConcurrentDictionary TypeManagerMap = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary ManagerTypeMap = new ConcurrentDictionary(ResourceManagerComparer.Default); + + internal static ResourceManager GetOrAdd(Type type, Func create) + { + var manager = TypeManagerMap.GetOrAdd(type, create); + if (manager != null) + { + ManagerTypeMap.TryAdd(manager, type); + } + + return manager; + } + + internal static Type GetOrAdd(ResourceManager resourceManager) + { + var type = ManagerTypeMap.GetOrAdd(resourceManager, ContainingType); + if (type != null) + { + TypeManagerMap.TryAdd(type, resourceManager); + } + + return type; + } + + private static Type ContainingType(ResourceManager resourceManager) + { + var resourcesType = AppDomain.CurrentDomain.GetAssemblies() + .Select(x => x.GetType(resourceManager.BaseName)) + .SingleOrDefault(x => x != null && + x.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) + .Any(p => p.PropertyType == typeof(ResourceManager))); + return resourcesType; + } + + private class ResourceManagerComparer : IEqualityComparer + { + public static readonly ResourceManagerComparer Default = new ResourceManagerComparer(); + private static readonly StringComparer StringComparer = StringComparer.Ordinal; + + private ResourceManagerComparer() + { + } + + public bool Equals(ResourceManager x, ResourceManager y) + { + return StringComparer.Equals(x.BaseName, y.BaseName); + } + + public int GetHashCode(ResourceManager obj) + { + return StringComparer.GetHashCode(obj.BaseName); + } + } + } } } \ No newline at end of file diff --git a/Gu.Localization/Properties/Resources.Designer.cs b/Gu.Localization/Properties/Resources.Designer.cs index e7ab0a63..04cd5b55 100644 --- a/Gu.Localization/Properties/Resources.Designer.cs +++ b/Gu.Localization/Properties/Resources.Designer.cs @@ -60,6 +60,15 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to {{"{0}" : {1}}}. + /// + public static string InvalidFormat { + get { + return ResourceManager.GetString("InvalidFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to ~{0}~. /// diff --git a/Gu.Localization/Properties/Resources.resx b/Gu.Localization/Properties/Resources.resx index 962ab73f..7304fa62 100644 --- a/Gu.Localization/Properties/Resources.resx +++ b/Gu.Localization/Properties/Resources.resx @@ -135,4 +135,7 @@ #{0}# + + {{"{0}" : {1}}} + \ No newline at end of file diff --git a/Gu.Localization/Translator.Parameters.cs b/Gu.Localization/Translator.Parameters.cs new file mode 100644 index 00000000..28ea9139 --- /dev/null +++ b/Gu.Localization/Translator.Parameters.cs @@ -0,0 +1,185 @@ +namespace Gu.Localization +{ + using System; + using System.Globalization; + using System.Resources; + + public static partial class Translator + { + /// + /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /// + /// The type of generic to avoid boxing + /// The containing translations. + /// The key in + /// The argument will be used as string.Format(format, ) + /// Specifies how to handle errors. + /// The key translated to the + public static string Translate(ResourceManager resourceManager, string key, T arg0, ErrorHandling errorHandling = ErrorHandling.Default) + { + return Translate(resourceManager, key, CurrentCulture, arg0, errorHandling); + } + + /// + /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /// + /// The type of generic to avoid boxing + /// The containing translations. + /// The key in + /// The culture. + /// The argument will be used as string.Format(format, ) + /// Specifies how to handle errors. + /// The key translated to the + public static string Translate(ResourceManager resourceManager, string key, CultureInfo culture, T arg0, ErrorHandling errorHandling = ErrorHandling.Default) + { + string format; + if (!TryTranslateOrThrow(resourceManager, key, culture, errorHandling, out format)) + { + return format; + } + + if (ShouldThrow(errorHandling)) + { + Validate.Format(format, arg0); + return string.Format(culture, format, arg0); + } + + if (!Validate.IsValidFormat(format, arg0)) + { + return string.Format(culture, Properties.Resources.InvalidFormat, format, arg0); + } + + try + { + return string.Format(format, arg0); + } + catch (Exception) + { + return string.Format(culture, Properties.Resources.InvalidFormat, format, arg0); + } + } + + /// + /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /// + /// The type of generic to avoid boxing + /// The type of generic to avoid boxing + /// The containing translations. + /// The key in + /// The argument will be used as first arguyment in string.Format(culture, format, , ) + /// The argument will be used as second argument string.Format(culture, format, , ) + /// Specifies how to handle errors. + /// The key translated to the + public static string Translate(ResourceManager resourceManager, string key, T0 arg0, T1 arg1, ErrorHandling errorHandling = ErrorHandling.Default) + { + return Translate(resourceManager, key, CurrentCulture, arg0, arg1, errorHandling); + } + + /// + /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /// + /// The type of generic to avoid boxing + /// The type of generic to avoid boxing + /// The containing translations. + /// The key in + /// The culture. + /// The argument will be used as first arguyment in string.Format(culture, format, , ) + /// The argument will be used as second argument string.Format(culture, format, , ) + /// Specifies how to handle errors. + /// The key translated to the + public static string Translate(ResourceManager resourceManager, string key, CultureInfo culture, T0 arg0, T1 arg1, ErrorHandling errorHandling = ErrorHandling.Default) + { + string format; + if (!TryTranslateOrThrow(resourceManager, key, culture, errorHandling, out format)) + { + return format; + } + + if (ShouldThrow(errorHandling)) + { + Validate.Format(format, arg0, arg1); + return string.Format(culture, format, arg0, arg1); + } + + if (!Validate.IsValidFormat(format, arg0, arg1)) + { + return string.Format(culture, Properties.Resources.InvalidFormat, format, string.Join(", ", arg0, arg1)); + } + + try + { + return string.Format(format, arg0, arg1); + } + catch (Exception) + { + return string.Format(culture, Properties.Resources.InvalidFormat, format, string.Join(", ", arg0, arg1)); + } + } + + /////// + /////// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /////// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /////// + /////// The containing translations. + /////// The key in + /////// Specifies how to handle errors. + /////// The arguments will be used as first arguyment in string.Format(culture, format, ) + /////// The key translated to the + ////public static string Translate( + //// ResourceManager resourceManager, + //// string key, + //// ErrorHandling errorHandling = ErrorHandling.Default, + //// params object[] args) + ////{ + //// return Translate(resourceManager, key, CurrentCulture, errorHandling, args); + ////} + + /////// + /////// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); + /////// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. + /////// + /////// The containing translations. + /////// The key in + /////// The culture. + /////// Specifies how to handle errors. + /////// The arguments will be used as first arguyment in string.Format(culture, format, ) + /////// The key translated to the + ////public static string Translate( + //// ResourceManager resourceManager, + //// string key, + //// CultureInfo culture, + //// ErrorHandling errorHandling = ErrorHandling.Default, + //// params object[] args) + ////{ + //// string format; + //// if (!TryTranslateOrThrow(resourceManager, key, culture, errorHandling, out format)) + //// { + //// return format; + //// } + + //// if (ShouldThrow(errorHandling)) + //// { + //// Validate.Format(format, args); + //// return string.Format(culture, format, args); + //// } + + //// if (!Validate.IsValidFormat(format, args)) + //// { + //// return string.Format(culture, Properties.Resources.InvalidFormat, format, string.Join(", ", args)); + //// } + + //// try + //// { + //// return string.Format(format, args); + //// } + //// catch (Exception) + //// { + //// return string.Format(culture, Properties.Resources.InvalidFormat, format, string.Join(", ", args)); + //// } + ////} + } +} \ No newline at end of file diff --git a/Gu.Localization/Translator.cs b/Gu.Localization/Translator.cs index 5f2f3d45..f31cb3d6 100644 --- a/Gu.Localization/Translator.cs +++ b/Gu.Localization/Translator.cs @@ -10,7 +10,7 @@ using System.Threading; /// Class for translating resources - public static class Translator + public static partial class Translator { private static CultureInfo currentCulture = Thread.CurrentThread.CurrentUICulture; private static DirectoryInfo resourceDirectory = ResourceCultures.DefaultResourceDirectory(); @@ -124,47 +124,6 @@ public static string Translate( return result; } - /// - /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); - /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. - /// - /// The containing translations. - /// The key in - /// The argument will be used as string.Format(format, ) - /// Specifies how to handle errors. - /// The key translated to the - internal static string Translate(ResourceManager resourceManager, string key, object arg, ErrorHandling errorHandling = ErrorHandling.Default) - { - return Translate(resourceManager, key, CurrentCulture, arg, errorHandling); - } - - /// - /// Translator.Translate(Properties.Resources.ResourceManager, nameof(Properties.Resources.SomeKey)); - /// This assumes that the resource is something like 'Value: {0}' i.e. having one format parameter. - /// - /// The containing translations. - /// The key in - /// The culture. - /// The argument will be used as string.Format(format, ) - /// Specifies how to handle errors. - /// The key translated to the - internal static string Translate(ResourceManager resourceManager, string key, CultureInfo culture, object arg, ErrorHandling errorHandling = ErrorHandling.Default) - { - string format; - if (!TryTranslateOrThrow(resourceManager, key, culture, errorHandling, out format)) - { - return format; - } - - if (ShouldThrow(errorHandling)) - { - Validate.Format(format, arg); - return string.Format(format, arg); - } - - throw new NotImplementedException("message"); - } - private static bool TryTranslateOrThrow( ResourceManager resourceManager, string key, diff --git a/Gu.Localization/Validate.Formats.cs b/Gu.Localization/Validate.Formats.cs index 13e35432..51b1aecc 100644 --- a/Gu.Localization/Validate.Formats.cs +++ b/Gu.Localization/Validate.Formats.cs @@ -1,22 +1,131 @@ -namespace Gu.Localization +// ReSharper disable UnusedParameter.Global +namespace Gu.Localization { using System; + /// Methods for validating format resources. public partial class Validate { - internal static void Format(string format, object arg) + /// + /// Call with Validate.IsValidFormat("First: {0:N}", 1.2); + /// Throws a if error(s) are found. + /// + /// The type of generic to avoid boxing + /// The format string ex: 'First: {0:N} + /// The argument + public static void Format(string format, T0 arg0) { int count; bool? anyItemHasFormat; if (!FormatString.IsValidFormat(format, out count, out anyItemHasFormat)) { - throw new FormatException($"Invalid format string: {format}."); + throw new FormatException($"Invalid format string: \"{format}\"."); } + // not sure if we should bother with checking individual format items here if (count != 1) { - throw new FormatException($"Invalid format string: {format} for the single argument: {arg}."); + throw new FormatException($"Invalid format string: \"{format}\" for the single argument: {arg0}."); } } + + /// + /// Call with Validate.IsValidFormat("First: {0:N}", 1.2); + /// Throws a if error(s) are found. + /// + /// The type of generic to avoid boxing + /// The type of generic to avoid boxing + /// The format string ex: 'First: {0:N} + /// The first argument. + /// The second argument. + public static void Format(string format, T0 arg0, T1 arg1) + { + int count; + bool? anyItemHasFormat; + if (!FormatString.IsValidFormat(format, out count, out anyItemHasFormat)) + { + throw new FormatException($"Invalid format string: \"{format}\"."); + } + + // not sure if we should bother with checking individual format items here + if (count != 2) + { + throw new FormatException($"Invalid format string: \"{format}\" for the two arguments: {arg0}, {arg1}."); + } + } + + /// + /// Call with Validate.IsValidFormat("First: {0:N}", 1, 2, 3..); + /// Throws a if error(s) are found. + /// + /// The format string ex: 'First: {0:N} + /// The arguments. + public static void Format(string format, params object[] args) + { + int count; + bool? anyItemHasFormat; + if (!FormatString.IsValidFormat(format, out count, out anyItemHasFormat)) + { + throw new FormatException($"Invalid format string: \"{format}\"."); + } + + if (args == null || args.Length == 0) + { + if (count == 0) + { + return; + } + + throw new FormatException($"Invalid format string: \"{format}\" when no arguments."); + } + + if (count != args.Length) + { + throw new FormatException($"Invalid format string: \"{format}\" for the arguments: {string.Join(", ", args)}."); + } + } + + /// Call with Validate.IsValidFormat("First: {0:N}", 1.2); + /// The type of generic to avoid boxing + /// The format string ex: 'First: {0:N} + /// The argument + /// True if is valid for the argument + public static bool IsValidFormat(string format, T arg0) + { + return IsValidFormat(format, 1); + } + + /// Call with Validate.IsValidFormat("First: {0:N}, Second: {1}", 1, 2); + /// The type of generic to avoid boxing + /// The type of generic to avoid boxing + /// The format string ex: 'First: {0:N} + /// The first argument. + /// The second argument. + /// True if is valid for the two arguments and + public static bool IsValidFormat(string format, T0 arg0, T1 arg1) + { + return IsValidFormat(format, 2); + } + + /// Call with Validate.IsValidFormat("First: {0:N}, Second: {1}", 2); + /// The format string ex: 'First: {0:N} + /// The arguments. + /// True if is valid for . + public static bool IsValidFormat(string format, params object[] args) + { + return IsValidFormat(format, args?.Length ?? 0); + } + + private static bool IsValidFormat(string format, int argumentCount) + { + int count; + bool? anyItemHasFormat; + if (!FormatString.IsValidFormat(format, out count, out anyItemHasFormat)) + { + return false; + } + + return count == argumentCount; + } } } diff --git a/Gu.Localization/Validate.Translations.cs b/Gu.Localization/Validate.Translations.cs index 3ddcc72a..9bd8a5dc 100644 --- a/Gu.Localization/Validate.Translations.cs +++ b/Gu.Localization/Validate.Translations.cs @@ -45,28 +45,31 @@ public static TranslationErrors Translations(ResourceManager resourceManager) /// An with all errors found in public static TranslationErrors Translations(ResourceManager resourceManager, IEnumerable cultures) { - var resources = GetResources(resourceManager, cultures); - var keys = GetKeys(resourceManager); - Dictionary> errors = null; - foreach (var key in keys) + using (var clone = resourceManager.Clone()) { - var keyErrors = Translations(resources, key); - if (keyErrors.Count == 0) + var resources = GetResources(clone, cultures); + var keys = GetKeys(resourceManager); + Dictionary> errors = null; + foreach (var key in keys) { - continue; - } - - if (errors == null) - { - errors = new Dictionary>(); + var keyErrors = Translations(resources, key); + if (keyErrors.Count == 0) + { + continue; + } + + if (errors == null) + { + errors = new Dictionary>(); + } + + errors.Add(key, keyErrors); } - errors.Add(key, keyErrors); + return errors == null + ? TranslationErrors.Empty + : new TranslationErrors(errors); } - - return errors == null - ? TranslationErrors.Empty - : new TranslationErrors(errors); } /// @@ -97,27 +100,30 @@ public static TranslationErrors EnumTranslations(ResourceManager resourceMana public static TranslationErrors EnumTranslations(ResourceManager resourceManager, IEnumerable cultures) where T : struct, IComparable, IFormattable, IConvertible { - var resources = GetResources(resourceManager, cultures); - Dictionary> errors = null; - foreach (var key in Enum.GetNames(typeof(T))) + using (var clone = resourceManager.Clone()) { - var keyErrors = Translations(resources, key); - if (keyErrors.Count == 0) + var resources = GetResources(clone, cultures); + Dictionary> errors = null; + foreach (var key in Enum.GetNames(typeof(T))) { - continue; + var keyErrors = Translations(resources, key); + if (keyErrors.Count == 0) + { + continue; + } + + if (errors == null) + { + errors = new Dictionary>(); + } + + errors.Add(key, keyErrors); } - if (errors == null) - { - errors = new Dictionary>(); - } - - errors.Add(key, keyErrors); + return errors == null + ? TranslationErrors.Empty + : new TranslationErrors(errors); } - - return errors == null - ? TranslationErrors.Empty - : new TranslationErrors(errors); } /// @@ -152,8 +158,11 @@ public static IReadOnlyList Translations(ResourceManager resou /// A list with all errors for the key or an empty list if no errors. public static IReadOnlyList Translations(ResourceManager resourceManager, string key, IEnumerable cultures) { - var resources = GetResources(resourceManager, cultures); - return Translations(resources, key); + using (var clone = resourceManager.Clone()) + { + var resources = GetResources(clone, cultures); + return Translations(resources, key); + } } private static IReadOnlyList Translations(IReadOnlyDictionary resources, string key) @@ -214,9 +223,14 @@ private static IReadOnlyList GetKeys(ResourceManager resourceManager) .ToArray(); } - private static IReadOnlyDictionary GetResources(ResourceManager resourceManager, IEnumerable cultures) + private static IReadOnlyDictionary GetResources(ResourceManagerExt.ResourceManagerClone clone, IEnumerable cultures) { - return cultures.ToDictionary(c => c, c => resourceManager.GetResourceSet(c, true, false), CultureInfoComparer.Default); + if (clone == null || clone.ResourceManager == null) + { + return EmptyReadOnlyDictionary.Default; + } + + return cultures.ToDictionary(c => c, c => clone.ResourceManager.GetResourceSet(c, true, false), CultureInfoComparer.Default); } } }