From d7e5888d373bde9f078818fb8ccdc9fb534fa900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Sj=C3=B6gren?= Date: Wed, 4 Aug 2021 13:29:10 +0200 Subject: [PATCH] Add support for Json format v3 pluralization rules. Fix pluralization fallback bug. Add pluralization tests. --- .../Plugins/DefaultPluralResolver.cs | 40 ++++++--- src/I18Next.Net/Plugins/DefaultTranslator.cs | 78 ++++++----------- tests/I18Next.Net.Tests/I18NextFixture.cs | 9 ++ .../Plugins/DefaultPluralResolverFixture.cs | 87 ++++++++++++++++++- .../Plugins/DefaultTranslatorFixture.cs | 32 +++++++ 5 files changed, 182 insertions(+), 64 deletions(-) diff --git a/src/I18Next.Net/Plugins/DefaultPluralResolver.cs b/src/I18Next.Net/Plugins/DefaultPluralResolver.cs index 5be6d07..739bc15 100644 --- a/src/I18Next.Net/Plugins/DefaultPluralResolver.cs +++ b/src/I18Next.Net/Plugins/DefaultPluralResolver.cs @@ -4,6 +4,13 @@ namespace I18Next.Net.Plugins { + public enum JsonFormat + { + Version1 = 1, + Version2 = 2, + Version3 = 3 + } + public class DefaultPluralResolver : IPluralResolver { private static readonly Dictionary> PluralizationFilters = new Dictionary> @@ -121,7 +128,7 @@ static DefaultPluralResolver() public string PluralSeparator { get; set; } = "_"; - public bool UseLegacyMode { get; set; } + public JsonFormat JsonFormatVersion { get; set; } = JsonFormat.Version3; public bool UseSimplePluralSuffixIfPossible { get; set; } = true; @@ -150,25 +157,36 @@ public string GetPluralSuffix(string language, int count) suffix = suffixNumber.ToString(); } - if (UseLegacyMode) + switch (JsonFormatVersion) { - if (suffixNumber == 1) - return string.Empty; + case JsonFormat.Version1: + if (suffixNumber == 1) + return string.Empty; - if (suffixNumber > 2) - return $"_plural_{suffixNumber.ToString()}"; + if (suffixNumber > 2) + return $"_plural_{suffixNumber.ToString()}"; - return $"_{suffix}"; - } + return $"_{suffix}"; - if (suffix == null) - return string.Empty; + case JsonFormat.Version2: + if (rule.Numbers.Length == 1 || suffix == null) + return string.Empty; + + return $"{PluralSeparator}{suffix}"; - return $"{PluralSeparator}{suffix}"; + default: + if (UseSimplePluralSuffixIfPossible && rule.Numbers.Length == 2 && rule.Numbers[0] == 1) + return suffix == null ? string.Empty : $"{PluralSeparator}{suffix}"; + else + return $"{PluralSeparator}{numberIndex}"; + } } public bool NeedsPlural(string language) { + if (JsonFormatVersion == JsonFormat.Version3) + return true; + var rule = GetRule(language); return rule != null && rule.Numbers.Length > 1; diff --git a/src/I18Next.Net/Plugins/DefaultTranslator.cs b/src/I18Next.Net/Plugins/DefaultTranslator.cs index 65affcf..9427eae 100644 --- a/src/I18Next.Net/Plugins/DefaultTranslator.cs +++ b/src/I18Next.Net/Plugins/DefaultTranslator.cs @@ -151,21 +151,6 @@ private string[] GetPostProcessorKeys(IDictionary args) return null; } - private async Task GetValueForFallbackAsync(string[] fallbackLanguages, string ns, string key, IDictionary args) - { - foreach (var fallbackLanguage in fallbackLanguages) - { - var translationTree = await ResolveTranslationTreeAsync(fallbackLanguage, ns); - - var result = translationTree.GetValue(key, args); - - if (result != null) - return result; - } - - return null; - } - private string HandlePostProcessing(string result, string key, IDictionary args) { var postProcessorKeys = GetPostProcessorKeys(args); @@ -186,7 +171,7 @@ private string HandlePostProcessing(string result, string key, IDictionary possibleKeys) + private async Task OnMissingKey(string language, string @namespace, string key, List possibleKeys) { if (MissingKey == null && MissingKeyHandlers.Count == 0) return; @@ -196,29 +181,33 @@ private void OnMissingKey(string language, string @namespace, string key, List ResolveFallbackTranslationAsync(string ns, IDictionary args, string[] fallbackLanguages, - IReadOnlyList possibleKeys) + private async Task ResolveTranslationAsync(string language, string ns, string key, IDictionary args, TranslationOptions options) { - string result = null; + var translationTree = await ResolveTranslationTreeAsync(language, ns); - for (var i = possibleKeys.Count - 1; i >= 0; i--) + async Task ResolveTranslationFromFallbackLanguages() { - var currentKey = possibleKeys[i]; - result = await GetValueForFallbackAsync(fallbackLanguages, ns, currentKey, args); - - if (result != null) - break; + if (options?.FallbackLanguages?.Length > 0) + { + foreach (string fallbackLanguage in options.FallbackLanguages) + { + var fallbackResult = await ResolveTranslationAsync(fallbackLanguage, ns, key, args, null); + if (fallbackResult != null) + { + return fallbackResult; + } + } + } + return null; } - return result; - } - private async Task ResolveTranslationAsync(string language, string ns, string key, IDictionary args, - TranslationOptions options) - { + if (translationTree == null) + return await ResolveTranslationFromFallbackLanguages(); + var needsPluralHandling = CheckForSpecialArg(args, "count", typeof(int), typeof(long)) && _pluralResolver.NeedsPlural(language); var needsContextHandling = CheckForSpecialArg(args, "context", typeof(string)); @@ -252,27 +241,6 @@ private async Task ResolveTranslationAsync(string language, string ns, s possibleKeys.Add(finalKey); } - // Try to resolve the translation from the backend - var result = await ResolveTranslationFromBackendAsync(language, ns, args, possibleKeys); - - if (result == null) - OnMissingKey(language, ns, key, possibleKeys); - - // Try to resolve the translation from the backend for all fallback langauges - if (result == null && options.FallbackLanguages != null && options.FallbackLanguages.Length > 0) - result = await ResolveFallbackTranslationAsync(ns, args, options.FallbackLanguages, possibleKeys); - - return result; - } - - private async Task ResolveTranslationFromBackendAsync(string language, string ns, IDictionary args, - List possibleKeys) - { - var translationTree = await ResolveTranslationTreeAsync(language, ns); - - if (translationTree == null) - return null; - string result = null; // Iterate over the possible keys starting with most specific pluralkey (-> contextkey only) -> singularkey only @@ -285,6 +253,12 @@ private async Task ResolveTranslationFromBackendAsync(string language, s break; } + if (result == null) + await OnMissingKey(language, ns, key, possibleKeys); + + if (result == null) + result = await ResolveTranslationFromFallbackLanguages(); + return result; } diff --git a/tests/I18Next.Net.Tests/I18NextFixture.cs b/tests/I18Next.Net.Tests/I18NextFixture.cs index 15253c9..9ae8e5f 100644 --- a/tests/I18Next.Net.Tests/I18NextFixture.cs +++ b/tests/I18Next.Net.Tests/I18NextFixture.cs @@ -25,6 +25,7 @@ private void SetupBackend() backend.AddTranslation("en", "translation", "exampleKey", "My English text."); backend.AddTranslation("en", "translation", "exampleKey2", "My English fallback."); + backend.AddTranslation("en", "translation", "exampleKey2_plural", "My English plural fallback {{count}}."); backend.AddTranslation("de", "translation", "exampleKey", "Mein deutscher text."); _backend = backend; @@ -60,6 +61,14 @@ public void MissingLanguage_ReturnsFallback() Assert.AreEqual("My English fallback.", _i18Next.T("exampleKey2")); } + [Test] + public void Pluralization_MissingLanguage_ReturnsFallback() + { + _i18Next.Language = "ja"; + _i18Next.SetFallbackLanguages("en"); + Assert.AreEqual("My English plural fallback 2.", _i18Next.T("exampleKey2", new { count = 2 })); + } + [Test] public void NoFallbackLanguage_MissingTranslation_ReturnsKey() { diff --git a/tests/I18Next.Net.Tests/Plugins/DefaultPluralResolverFixture.cs b/tests/I18Next.Net.Tests/Plugins/DefaultPluralResolverFixture.cs index 333d03c..a6f7010 100644 --- a/tests/I18Next.Net.Tests/Plugins/DefaultPluralResolverFixture.cs +++ b/tests/I18Next.Net.Tests/Plugins/DefaultPluralResolverFixture.cs @@ -1,6 +1,91 @@ -namespace I18Next.Net.Tests.Plugins +using I18Next.Net.Plugins; +using NUnit.Framework; + +namespace I18Next.Net.Tests.Plugins { + [TestFixture] public class DefaultPluralResolverFixture { + [TestCase(JsonFormat.Version1, ExpectedResult = "")] + [TestCase(JsonFormat.Version2, ExpectedResult = "")] + [TestCase(JsonFormat.Version3, ExpectedResult = "")] + public string GetPluralSuffix_OneInEnglish_ShouldReturnEmptyWhenUsingSimpleSuffix(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + UseSimplePluralSuffixIfPossible = true + }; + + return pluralResolver.GetPluralSuffix("en", 1); + } + + [TestCase(JsonFormat.Version1, ExpectedResult = "")] + [TestCase(JsonFormat.Version2, ExpectedResult = "_1")] + [TestCase(JsonFormat.Version3, ExpectedResult = "_0")] + public string GetPluralSuffix_OneInEnglish_ShouldReturnNumberWhenNotUsingSimpleSuffix(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + UseSimplePluralSuffixIfPossible = false + }; + + return pluralResolver.GetPluralSuffix("en", 1); + } + + [TestCase(JsonFormat.Version1, ExpectedResult = "_plural")] + [TestCase(JsonFormat.Version2, ExpectedResult = "_plural")] + [TestCase(JsonFormat.Version3, ExpectedResult = "_plural")] + public string GetPluralSuffix_TwoInEnglish_ShouldReturnPluralWhenUsingSimpleSuffix(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + UseSimplePluralSuffixIfPossible = true + }; + + return pluralResolver.GetPluralSuffix("en", 2); + } + + [TestCase(JsonFormat.Version1, ExpectedResult = "_2")] + [TestCase(JsonFormat.Version2, ExpectedResult = "_2")] + [TestCase(JsonFormat.Version3, ExpectedResult = "_1")] + public string GetPluralSuffix_TwoInEnglish_ShouldReturnNumberWhenNotUsingSimpleSuffix(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + UseSimplePluralSuffixIfPossible = false + }; + + return pluralResolver.GetPluralSuffix("en", 2); + } + + [TestCase(JsonFormat.Version1, ExpectedResult = "")] + [TestCase(JsonFormat.Version2, ExpectedResult = "")] + [TestCase(JsonFormat.Version3, ExpectedResult = "_0")] + public string GetPluralSuffix_OneInJapanese_ShouldReturnNumber(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + }; + + return pluralResolver.GetPluralSuffix("ja", 1); + } + + [TestCase(JsonFormat.Version1, ExpectedResult = "")] + [TestCase(JsonFormat.Version2, ExpectedResult = "")] + [TestCase(JsonFormat.Version3, ExpectedResult = "_0")] + public string GetPluralSuffix_TwoInJapanese_ShouldReturnNumber(JsonFormat jsonFormatVersion) + { + var pluralResolver = new DefaultPluralResolver() + { + JsonFormatVersion = jsonFormatVersion, + }; + + return pluralResolver.GetPluralSuffix("ja", 2); + } } } diff --git a/tests/I18Next.Net.Tests/Plugins/DefaultTranslatorFixture.cs b/tests/I18Next.Net.Tests/Plugins/DefaultTranslatorFixture.cs index 1fed411..dc4f873 100644 --- a/tests/I18Next.Net.Tests/Plugins/DefaultTranslatorFixture.cs +++ b/tests/I18Next.Net.Tests/Plugins/DefaultTranslatorFixture.cs @@ -51,7 +51,11 @@ public void OneTimeSetUp() _pluralResolver.GetPluralSuffix("en-US", 3).Returns("_3"); _pluralResolver.GetPluralSuffix("en-US", 4).Returns("_4"); _pluralResolver.GetPluralSuffix("en-US", 5).Returns("_5"); + _pluralResolver.GetPluralSuffix("ja-JP", 0).Returns("_0"); + _pluralResolver.GetPluralSuffix("ja-JP", 1).Returns("_0"); + _pluralResolver.GetPluralSuffix("ja-JP", 2).Returns("_0"); _pluralResolver.NeedsPlural("en-US").Returns(true); + _pluralResolver.NeedsPlural("ja-JP").Returns(true); _backend.LoadNamespaceAsync("en-US", "test").Returns(_translationTree); _interpolator.InterpolateAsync(null, null, null, null).ReturnsForAnyArgs(c => c.ArgAt(0)); _interpolator.NestAsync(null, null, null, null).ReturnsForAnyArgs(c => c.ArgAt(0)); @@ -538,6 +542,34 @@ public async Task TranslateAsync_WithCountButNoTranslation_ShouldUseFallback() _pluralResolver.Received(1).NeedsPlural("en-US"); } + [Test] + public async Task TranslateAsync_WithCountButNoTranslation_ShouldUseFallbackPluralRules() + { + var jpTranslationTree = Substitute.For(); + _backend.LoadNamespaceAsync("ja-JP", "test").Returns(jpTranslationTree); + jpTranslationTree.GetValue(null, null).ReturnsForAnyArgs((string) null); + _translationTree.GetValue("test_2", Arg.Any>()).Returns("translated"); + + _options.FallbackLanguages = new[] { "en-US" }; + var args = new { count = 2 }; + var result = await _translator.TranslateAsync("ja-JP", "test", args.ToDictionary(), _options); + + result.Should().Be("translated"); + + await _backend.Received(1).LoadNamespaceAsync("ja-JP", "test"); + await _backend.Received(1).LoadNamespaceAsync("en-US", "test"); + jpTranslationTree.Received(1).GetValue("test_0", Arg.Any>()); + jpTranslationTree.DidNotReceive().GetValue("test_2", Arg.Any>()); + _translationTree.Received(1).GetValue("test_2", Arg.Any>()); + _translationTree.DidNotReceive().GetValue("test_0", Arg.Any>()); + await _interpolator.ReceivedWithAnyArgs(1).InterpolateAsync(null, null, null, null); + _interpolator.Received(1).CanNest("translated"); + await _interpolator.ReceivedWithAnyArgs(0).NestAsync(null, null, null, null); + _pluralResolver.Received(1).GetPluralSuffix("ja-JP", 2); + _pluralResolver.Received(1).GetPluralSuffix("en-US", 2); + _pluralResolver.Received(1).NeedsPlural("en-US"); + } + [Test] public async Task TranslateAsync_WithDefaultNsAndSimpleString_ShouldTranslate() {