Skip to content

Commit

Permalink
Merge pull request #16 from msjogren/feature/json-v3-pluralization
Browse files Browse the repository at this point in the history
Fix #15 Pluralization and fallback languages
  • Loading branch information
DarkLiKally authored Apr 23, 2022
2 parents 5846638 + d7e5888 commit 6bf7b5e
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 64 deletions.
40 changes: 29 additions & 11 deletions src/I18Next.Net/Plugins/DefaultPluralResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, Func<int, int>> PluralizationFilters = new Dictionary<int, Func<int, int>>
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
78 changes: 26 additions & 52 deletions src/I18Next.Net/Plugins/DefaultTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,21 +151,6 @@ private string[] GetPostProcessorKeys(IDictionary<string, object> args)
return null;
}

private async Task<string> GetValueForFallbackAsync(string[] fallbackLanguages, string ns, string key, IDictionary<string, object> 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<string, object> args)
{
var postProcessorKeys = GetPostProcessorKeys(args);
Expand All @@ -186,7 +171,7 @@ private string HandlePostProcessing(string result, string key, IDictionary<strin
return result;
}

private void OnMissingKey(string language, string @namespace, string key, List<string> possibleKeys)
private async Task OnMissingKey(string language, string @namespace, string key, List<string> possibleKeys)
{
if (MissingKey == null && MissingKeyHandlers.Count == 0)
return;
Expand All @@ -196,29 +181,33 @@ private void OnMissingKey(string language, string @namespace, string key, List<s
MissingKey?.Invoke(this, args);

foreach (var missingKeyHandler in MissingKeyHandlers)
missingKeyHandler.HandleMissingKeyAsync(this, args);
await missingKeyHandler.HandleMissingKeyAsync(this, args);
}

private async Task<string> ResolveFallbackTranslationAsync(string ns, IDictionary<string, object> args, string[] fallbackLanguages,
IReadOnlyList<string> possibleKeys)
private async Task<string> ResolveTranslationAsync(string language, string ns, string key, IDictionary<string, object> args, TranslationOptions options)
{
string result = null;
var translationTree = await ResolveTranslationTreeAsync(language, ns);

for (var i = possibleKeys.Count - 1; i >= 0; i--)
async Task<string> 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<string> ResolveTranslationAsync(string language, string ns, string key, IDictionary<string, object> 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));

Expand Down Expand Up @@ -252,27 +241,6 @@ private async Task<string> 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<string> ResolveTranslationFromBackendAsync(string language, string ns, IDictionary<string, object> args,
List<string> 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
Expand All @@ -285,6 +253,12 @@ private async Task<string> ResolveTranslationFromBackendAsync(string language, s
break;
}

if (result == null)
await OnMissingKey(language, ns, key, possibleKeys);

if (result == null)
result = await ResolveTranslationFromFallbackLanguages();

return result;
}

Expand Down
9 changes: 9 additions & 0 deletions tests/I18Next.Net.Tests/I18NextFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down
87 changes: 86 additions & 1 deletion tests/I18Next.Net.Tests/Plugins/DefaultPluralResolverFixture.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
32 changes: 32 additions & 0 deletions tests/I18Next.Net.Tests/Plugins/DefaultTranslatorFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(0));
_interpolator.NestAsync(null, null, null, null).ReturnsForAnyArgs(c => c.ArgAt<string>(0));
Expand Down Expand Up @@ -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<ITranslationTree>();
_backend.LoadNamespaceAsync("ja-JP", "test").Returns(jpTranslationTree);
jpTranslationTree.GetValue(null, null).ReturnsForAnyArgs((string) null);
_translationTree.GetValue("test_2", Arg.Any<IDictionary<string, object>>()).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<IDictionary<string, object>>());
jpTranslationTree.DidNotReceive().GetValue("test_2", Arg.Any<IDictionary<string, object>>());
_translationTree.Received(1).GetValue("test_2", Arg.Any<IDictionary<string, object>>());
_translationTree.DidNotReceive().GetValue("test_0", Arg.Any<IDictionary<string, object>>());
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()
{
Expand Down

0 comments on commit 6bf7b5e

Please sign in to comment.