Skip to content

Commit

Permalink
Fix #4: implement support for fallback language
Browse files Browse the repository at this point in the history
  • Loading branch information
Shaddix committed Jun 20, 2019
1 parent 6ef8d61 commit ad90f9a
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 42 deletions.
6 changes: 4 additions & 2 deletions samples/Example.ConsoleApp.NetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ private static void SetupBackend()
var backend = new InMemoryBackend();

backend.AddTranslation("en", "translation", "exampleKey", "My English text.");
backend.AddTranslation("en", "translation", "exampleKey2", "My English fallback.");
backend.AddTranslation("de", "translation", "exampleKey", "Mein deutscher text.");

_backend = backend;
Expand Down Expand Up @@ -60,7 +61,7 @@ private static void SampleTwo()
var services = new ServiceCollection();

// Register I18Next.Net
services.AddI18NextLocalization(builder => builder.AddBackend(_backend));
services.AddI18NextLocalization(builder => builder.AddBackend(_backend).UseFallbackLanguage("en"));

using(var serviceProvider = services.BuildServiceProvider())
using (var scope = serviceProvider.CreateScope())
Expand Down Expand Up @@ -106,7 +107,8 @@ private static void SampleTwo()
Console.WriteLine("German translation:");
i18next.Language = "de";
Console.WriteLine(localizerGeneric["exampleKey"]);

Console.WriteLine(localizerGeneric["exampleKey2"]);

Console.WriteLine();
}
}
Expand Down
6 changes: 5 additions & 1 deletion samples/Example.ConsoleApp.NetFramework/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public static void Main(string[] args)
Console.WriteLine("German translation:");
i18next.Language = "de";
Console.WriteLine(i18next.T("exampleKey"));


i18next.SetFallbackLanguage("en");
Console.WriteLine(i18next.T("exampleKey2")); // should output "My English text." because of fallback language

Console.ReadKey();
}

Expand All @@ -33,6 +36,7 @@ private static void SetupBackend()
var backend = new InMemoryBackend();

backend.AddTranslation("en", "translation", "exampleKey", "My English text.");
backend.AddTranslation("en", "translation", "exampleKey2", "My English fallback.");
backend.AddTranslation("de", "translation", "exampleKey", "Mein deutscher text.");

_backend = backend;
Expand Down
2 changes: 1 addition & 1 deletion src/I18Next.Net.Abstractions/Plugins/ITranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public interface ITranslator
{
List<IPostProcessor> PostProcessors { get; }

Task<string> TranslateAsync(string language, string defaultNamespace, string key, IDictionary<string, object> args);
Task<string> TranslateAsync(string language, string defaultNamespace, string key, IDictionary<string, object> args, IList<string> fallbackLanguages);
}
}
12 changes: 12 additions & 0 deletions src/I18Next.Net.Extensions/Builder/I18NextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,18 @@ public I18NextBuilder UseDefaultLanguage(string language)
return this;
}

public I18NextBuilder UseFallbackLanguage(params string[] languages)
{
if (languages.Length == 0)
throw new ArgumentException("Please supply at least one fallback language", nameof(languages));
if (languages.Any(string.IsNullOrEmpty))
throw new ArgumentException("None of fallback languages can be null or empty.", nameof(languages));

Services.Configure<I18NextOptions>(options => options.FallbackLanguages = languages);

return this;
}

public I18NextBuilder UseDefaultNamespace(string @namespace)
{
if (string.IsNullOrEmpty(@namespace))
Expand Down
3 changes: 3 additions & 0 deletions src/I18Next.Net.Extensions/Builder/I18NextOptions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Collections.Generic;

namespace I18Next.Net.Extensions.Builder
{
public class I18NextOptions
{
public string DefaultLanguage { get; set; } = "en-US";

public string DefaultNamespace { get; set; } = "translation";
public IList<string> FallbackLanguages { get; set; }

public bool DetectLanguageOnEachTranslation { get; set; }
}
Expand Down
8 changes: 6 additions & 2 deletions src/I18Next.Net.Extensions/I18NextFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using I18Next.Net.Backends;
using I18Next.Net.Extensions.Builder;
using I18Next.Net.Plugins;
Expand Down Expand Up @@ -25,13 +26,16 @@ public I18NextFactory(ITranslationBackend backend, ITranslator translator, ILang

public II18Next CreateInstance()
{
return new I18NextNet(_backend, _translator, _languageDetector)
var instance = new I18NextNet(_backend, _translator, _languageDetector)
{
Language = _options.Value.DefaultLanguage,
DefaultNamespace = _options.Value.DefaultNamespace,
Logger = _logger,
DetectLanguageOnEachTranslation = _options.Value.DetectLanguageOnEachTranslation
DetectLanguageOnEachTranslation = _options.Value.DetectLanguageOnEachTranslation,
};
instance.SetFallbackLanguage(_options.Value.FallbackLanguages.ToArray());

return instance;
}
}
}
9 changes: 8 additions & 1 deletion src/I18Next.Net/I18NextNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public I18NextNet(ITranslationBackend backend, ITranslator translator, ILanguage

public string DefaultNamespace { get; set; }

public IList<string> FallbackLanguages { get; private set; } = new string[] { };

public bool DetectLanguageOnEachTranslation { get; set; }

public string Language
Expand Down Expand Up @@ -91,14 +93,19 @@ public async Task<string> Ta(string language, string defaultNamespace, string ke

var argsDict = args.ToDictionary();

return await Translator.TranslateAsync(language, defaultNamespace, key, argsDict);
return await Translator.TranslateAsync(language, defaultNamespace, key, argsDict, FallbackLanguages);
}

public void UseDetectedLanguage()
{
Language = LanguageDetector.GetLanguage();
}

public void SetFallbackLanguage(params string[] languages)
{
FallbackLanguages = languages;
}

private void OnLanguageChanged(LanguageChangedEventArgs e)
{
LanguageChanged?.Invoke(this, e);
Expand Down
38 changes: 29 additions & 9 deletions src/I18Next.Net/Plugins/DefaultTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public DefaultTranslator(ITranslationBackend backend, IInterpolator interpolator

public List<IPostProcessor> PostProcessors { get; } = new List<IPostProcessor>();

public virtual async Task<string> TranslateAsync(string language, string defaultNamespace, string key, IDictionary<string, object> args)
public virtual async Task<string> TranslateAsync(string language, string defaultNamespace, string key, IDictionary<string, object> args, IList<string> fallbackLanguages)
{
if (string.IsNullOrWhiteSpace(language))
throw new ArgumentNullException(nameof(language));
Expand All @@ -76,12 +76,12 @@ public virtual async Task<string> TranslateAsync(string language, string default
if (language.ToLower() == "cimode")
return $"{actualNamespace}:{key}";

var result = await ResolveTranslationAsync(language, actualNamespace, key, args);
var result = await ResolveTranslationAsync(language, actualNamespace, key, args, fallbackLanguages);

if (result == null)
return key;

return await ExtendTranslationAsync(result, actualNamespace, key, language, args);
return await ExtendTranslationAsync(result, actualNamespace, key, language, args, fallbackLanguages);
}

private static bool CheckForSpecialArg(IDictionary<string, object> args, string key, params Type[] allowedTypes)
Expand All @@ -105,7 +105,7 @@ private static bool CheckForSpecialArg(IDictionary<string, object> args, string
return false;
}

private async Task<string> ExtendTranslationAsync(string result, string ns, string key, string language, IDictionary<string, object> args)
private async Task<string> ExtendTranslationAsync(string result, string ns, string key, string language, IDictionary<string, object> args, IList<string> fallbackLanguages)
{
IDictionary<string, object> replaceArgs;

Expand All @@ -118,7 +118,7 @@ private async Task<string> ExtendTranslationAsync(string result, string ns, stri
result = await _interpolator.InterpolateAsync(result, key, language, replaceArgs);

if (AllowNesting && (!(args?.ContainsKey("nest") ?? false) || args["nest"] is bool nest && nest) && _interpolator.CanNest(result))
result = await _interpolator.NestAsync(result, language, replaceArgs, (lang2, key2, args2) => TranslateAsync(lang2, ns, key2, args2));
result = await _interpolator.NestAsync(result, language, replaceArgs, (lang2, key2, args2) => TranslateAsync(lang2, ns, key2, args2, fallbackLanguages));

if (AllowPostprocessing && PostProcessors.Count > 0)
result = HandlePostProcessing(result, key, args);
Expand Down Expand Up @@ -163,12 +163,29 @@ private string HandlePostProcessing(string result, string key, IDictionary<strin
return result;
}

private async Task<string> ResolveTranslationAsync(string language, string ns, string key, IDictionary<string, object> args)
private async Task<string> ResolveTranslationAsync(string language, string ns, string key, IDictionary<string, object> args, IList<string> fallbackLanguages)
{
var translationTree = await ResolveTranslationTreeAsync(language, ns);

if (translationTree == null)
async Task<string> ResolveTranslationFromFallbackLanguages()
{
if (fallbackLanguages?.Count > 0)
{
foreach (string fallbackLanguage in fallbackLanguages)
{
var fallbackResult = await ResolveTranslationAsync(fallbackLanguage, ns, key, args, null);
if (fallbackResult != null)
{
return fallbackResult;
}
}
}
return null;
}


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 All @@ -180,7 +197,7 @@ private async Task<string> ResolveTranslationAsync(string language, string ns, s

if (needsPluralHandling)
{
var count = (int) Convert.ChangeType(args["count"], typeof(int));
var count = (int)Convert.ChangeType(args["count"], typeof(int));
pluralSuffix = _pluralResolver.GetPluralSuffix(language, count);

// Fallback for plural if context was not found
Expand All @@ -191,7 +208,7 @@ private async Task<string> ResolveTranslationAsync(string language, string ns, s
// Get key for context if needed
if (needsContextHandling)
{
var context = (string) args["context"];
var context = (string)args["context"];
finalKey = $"{finalKey}{ContextSeparator}{context}";
possibleKeys.Push(finalKey);
}
Expand All @@ -214,6 +231,9 @@ private async Task<string> ResolveTranslationAsync(string language, string ns, s
break;
}

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

return result;
}

Expand Down
57 changes: 55 additions & 2 deletions tests/I18Next.Net.Tests/I18NextFixture.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,71 @@
using System;
using I18Next.Net.Backends;
using I18Next.Net.Plugins;
using NUnit.Framework;

namespace I18Next.Net.Tests
{
[TestFixture]
public class I18NextFixture
{
private InMemoryBackend _backend;
private I18NextNet _i18next;

[SetUp]
public void Setup()
{
SetupBackend();

var translator = new DefaultTranslator(_backend);
_i18next = new I18NextNet(_backend, translator);

}
private void SetupBackend()
{
var backend = new InMemoryBackend();

backend.AddTranslation("en", "translation", "exampleKey", "My English text.");
backend.AddTranslation("en", "translation", "exampleKey2", "My English fallback.");
backend.AddTranslation("de", "translation", "exampleKey", "Mein deutscher text.");

_backend = backend;
}

[Test]
public void English()
{
_i18next.Language = "en";
Assert.AreEqual("My English text.", _i18next.T("exampleKey"));
}

[Test]
public void German()
{
_i18next.Language = "de";
Assert.AreEqual("Mein deutscher text.", _i18next.T("exampleKey"));
}

[Test]
public void NoFallbackLanguage_MissingTranslation_ReturnsKey()
{
_i18next.Language = "de";
Assert.AreEqual("exampleKey2", _i18next.T("exampleKey2"));
}

[Test]
public void FallbackLanguageIsSet_MissingTranslation_ReturnsFallback()
{
_i18next.Language = "de";
_i18next.SetFallbackLanguage("en");
Assert.AreEqual("My English fallback.", _i18next.T("exampleKey2"));
}

[Test]
public void Test1()
public void MissingLanguage_ReturnsFallback()
{
Assert.Pass();
_i18next.Language = "jp";
_i18next.SetFallbackLanguage("en");
Assert.AreEqual("My English fallback.", _i18next.T("exampleKey2"));
}
}
}
Loading

0 comments on commit ad90f9a

Please sign in to comment.