-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WebAppLocalization: PO localization implemented
- Loading branch information
Showing
31 changed files
with
1,132 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"version": 1, | ||
"isRoot": true, | ||
"tools": { | ||
"karambolo.aspnetskeleton.potools": { | ||
"version": "1.0.0", | ||
"commands": [ | ||
"po" | ||
] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Threading; | ||
|
||
namespace WebApp | ||
{ | ||
public class AppLocalizationOptions | ||
{ | ||
private static CultureInfo[]? s_defaultSupportedCultureInfos; | ||
private static CultureInfo[] DefaultSupportedCultureInfos => LazyInitializer.EnsureInitialized(ref s_defaultSupportedCultureInfos, () => new[] { new CultureInfo("en-US") }); | ||
|
||
public string[]? SupportedCultures | ||
{ | ||
get => _supportedCultureInfos?.Select(cultureInfo => cultureInfo.Name).ToArray(); | ||
set => _supportedCultureInfos = value?.Length > 0 ? value.Select(culture => new CultureInfo(culture)).ToArray() : null; | ||
} | ||
|
||
private CultureInfo[]? _supportedCultureInfos; | ||
public IReadOnlyList<CultureInfo> SupportedCultureInfos => _supportedCultureInfos ?? DefaultSupportedCultureInfos; | ||
|
||
public CultureInfo DefaultCultureInfo => SupportedCultureInfos[0]; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
samples/WebAppLocalization/Infrastructure/AssociatedAssemblyNameAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Reflection; | ||
|
||
namespace WebApp.Infrastructure | ||
{ | ||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] | ||
public sealed class AssociatedAssemblyNameAttribute : Attribute | ||
{ | ||
private static readonly ConcurrentDictionary<Type, AssociatedAssemblyNameAttribute?> s_attributeCache = new ConcurrentDictionary<Type, AssociatedAssemblyNameAttribute?>(); | ||
|
||
public static AssociatedAssemblyNameAttribute? GetCachedFor(Type type) => s_attributeCache.GetOrAdd(type, type => type.GetCustomAttribute<AssociatedAssemblyNameAttribute>()); | ||
|
||
public AssociatedAssemblyNameAttribute(string assemblyName) | ||
{ | ||
AssemblyName = new AssemblyName(assemblyName); | ||
} | ||
|
||
public AssemblyName AssemblyName { get; } | ||
} | ||
} |
88 changes: 88 additions & 0 deletions
88
samples/WebAppLocalization/Infrastructure/Localization/DefaultTranslationsProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using Karambolo.PO; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public class DefaultTranslationsProvider : ITranslationsProvider | ||
{ | ||
private const string FileNamePattern = "*.po"; | ||
|
||
private static readonly POParserSettings s_parserSettings = new POParserSettings | ||
{ | ||
SkipComments = true, | ||
SkipInfoHeaders = true, | ||
StringDecodingOptions = new POStringDecodingOptions { KeepKeyStringsPlatformIndependent = true } | ||
}; | ||
|
||
private readonly ILogger _logger; | ||
|
||
private readonly string _translationsBasePath; | ||
private readonly IReadOnlyDictionary<(string Location, string Culture), POCatalog> _catalogs; | ||
|
||
public DefaultTranslationsProvider(ILogger<DefaultTranslationsProvider> logger) | ||
{ | ||
_logger = logger ?? (ILogger)NullLogger.Instance; | ||
|
||
_translationsBasePath = Path.Combine(AppContext.BaseDirectory, "Translations"); | ||
_catalogs = LoadFiles(); | ||
} | ||
|
||
private Dictionary<(string Location, string Culture), POCatalog> LoadFiles() | ||
{ | ||
return Directory.GetFiles(_translationsBasePath, FileNamePattern, SearchOption.AllDirectories) | ||
.Select(LoadFile) | ||
.Where(item => item != null) | ||
.ToDictionary(item => (item!.Value.Location, item.Value.Culture), item => item!.Value.Catalog); | ||
} | ||
|
||
private (POCatalog Catalog, string Location, string Culture)? LoadFile(string filePath) | ||
{ | ||
var relativeFilePath = Path.GetRelativePath(_translationsBasePath, filePath); | ||
|
||
var culture = Path.GetDirectoryName(relativeFilePath); | ||
if (string.IsNullOrEmpty(culture) || !string.IsNullOrEmpty(Path.GetDirectoryName(culture))) | ||
return null; | ||
|
||
var location = Path.GetFileNameWithoutExtension(relativeFilePath); | ||
|
||
var catalog = LoadTranslations(filePath); | ||
if (catalog == null) | ||
return null; | ||
|
||
return (catalog, location, culture); | ||
} | ||
|
||
private POCatalog? LoadTranslations(string filePath) | ||
{ | ||
FileStream fileStream; | ||
try { fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); } | ||
catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) | ||
{ | ||
_logger.LogWarning(ex, "Translation file \"{PATH}\" cannot be accessed.", filePath); | ||
return null; | ||
} | ||
|
||
POParseResult parseResult; | ||
using (fileStream) | ||
parseResult = new POParser(s_parserSettings).Parse(fileStream); | ||
|
||
if (!parseResult.Success) | ||
{ | ||
var diagnosticMessages = parseResult.Diagnostics | ||
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); | ||
|
||
_logger.LogWarning("Translation file \"{PATH}\" has errors: {ERRORS}", filePath, string.Join(Environment.NewLine, diagnosticMessages)); | ||
return null; | ||
} | ||
|
||
return parseResult.Catalog; | ||
} | ||
|
||
public IReadOnlyDictionary<(string Location, string Culture), POCatalog> GetCatalogs() => _catalogs; | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
samples/WebAppLocalization/Infrastructure/Localization/ExtendedHtmlLocalizer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public sealed class ExtendedHtmlLocalizer : HtmlLocalizer | ||
{ | ||
private readonly IExtendedStringLocalizer _stringLocalizer; | ||
|
||
public ExtendedHtmlLocalizer(IExtendedStringLocalizer stringLocalizer) : base(stringLocalizer) | ||
{ | ||
_stringLocalizer = stringLocalizer; | ||
} | ||
|
||
public override LocalizedHtmlString this[string name] | ||
{ | ||
get | ||
{ | ||
var translation = _stringLocalizer.GetTranslation(name, default, default, out var _, out var resourceNotFound); | ||
return new LocalizedHtmlString(name, translation, resourceNotFound); | ||
} | ||
} | ||
|
||
public override LocalizedHtmlString this[string name, params object[] arguments] | ||
{ | ||
get | ||
{ | ||
var (plural, context) = LocalizationHelper.GetSpecialArgs(arguments); | ||
var translation = _stringLocalizer.GetTranslation(name, plural, context, out var _, out var resourceNotFound); | ||
return new LocalizedHtmlString(name, translation, resourceNotFound, arguments); | ||
} | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
samples/WebAppLocalization/Infrastructure/Localization/ExtendedHtmlLocalizerFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
using System; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
using Microsoft.Extensions.Localization; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public sealed class ExtendedHtmlLocalizerFactory : IHtmlLocalizerFactory | ||
{ | ||
private readonly IStringLocalizerFactory _stringLocalizerFactory; | ||
|
||
public ExtendedHtmlLocalizerFactory(IStringLocalizerFactory stringLocalizerFactory) | ||
{ | ||
_stringLocalizerFactory = stringLocalizerFactory ?? throw new ArgumentNullException(nameof(stringLocalizerFactory)); | ||
} | ||
|
||
private static IHtmlLocalizer CreateHtmlLocalizer(IStringLocalizer stringLocalizer) => | ||
stringLocalizer is IExtendedStringLocalizer extendedStringLocalizer ? | ||
new ExtendedHtmlLocalizer(extendedStringLocalizer) : | ||
new HtmlLocalizer(stringLocalizer); | ||
|
||
public IHtmlLocalizer Create(string baseName, string location) => CreateHtmlLocalizer(_stringLocalizerFactory.Create(baseName, location)); | ||
|
||
public IHtmlLocalizer Create(Type resourceSource) => CreateHtmlLocalizer(_stringLocalizerFactory.Create(resourceSource)); | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
samples/WebAppLocalization/Infrastructure/Localization/ExtendedViewLocalizer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Text; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
using Microsoft.AspNetCore.Mvc.Razor; | ||
using Microsoft.AspNetCore.Mvc.Rendering; | ||
using Microsoft.AspNetCore.Mvc.ViewFeatures; | ||
using Microsoft.Extensions.Localization; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public sealed class ExtendedViewLocalizer : IViewLocalizer, IViewContextAware | ||
{ | ||
private readonly IHtmlLocalizerFactory _localizerFactory; | ||
private readonly string _applicationName; | ||
private IHtmlLocalizer? _localizer; | ||
|
||
public ExtendedViewLocalizer(IHtmlLocalizerFactory localizerFactory, IWebHostEnvironment hostingEnvironment) | ||
{ | ||
_localizerFactory = localizerFactory ?? throw new ArgumentNullException(nameof(localizerFactory)); | ||
_applicationName = (hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment))).ApplicationName; | ||
} | ||
|
||
public LocalizedHtmlString this[string key] => _localizer![key]; | ||
|
||
public LocalizedHtmlString this[string key, params object[] arguments] => _localizer![key, arguments]; | ||
|
||
public LocalizedString GetString(string name) => _localizer!.GetString(name); | ||
|
||
public LocalizedString GetString(string name, params object[] values) => _localizer!.GetString(name, values); | ||
|
||
#if !NET5_0_OR_GREATER | ||
[Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] | ||
public IHtmlLocalizer WithCulture(CultureInfo culture) => _localizer!.WithCulture(culture); | ||
#endif | ||
|
||
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => | ||
_localizer!.GetAllStrings(includeParentCultures); | ||
|
||
public void Contextualize(ViewContext viewContext) | ||
{ | ||
if (viewContext == null) | ||
throw new ArgumentNullException(nameof(viewContext)); | ||
|
||
// Given a view path "/Views/Home/Index.cshtml" we want a baseName like "MyApplication.Views.Home.Index" | ||
var path = viewContext.ExecutingFilePath; | ||
|
||
if (string.IsNullOrEmpty(path)) | ||
{ | ||
path = viewContext.View.Path; | ||
} | ||
|
||
Debug.Assert(!string.IsNullOrEmpty(path), "Couldn't determine a path for the view"); | ||
|
||
var location = | ||
(viewContext.View is RazorView razorView ? AssociatedAssemblyNameAttribute.GetCachedFor(razorView.RazorPage.GetType())?.AssemblyName.Name : null) ?? | ||
_applicationName; | ||
|
||
_localizer = _localizerFactory.Create(BuildBaseName(path, location), location); | ||
} | ||
|
||
private static string BuildBaseName(string path, string location) | ||
{ | ||
var extension = Path.GetExtension(path); | ||
var startIndex = path[0] == '/' || path[0] == '\\' ? 1 : 0; | ||
var length = path.Length - startIndex - extension.Length; | ||
var capacity = length + location.Length + 1; | ||
var builder = new StringBuilder(path, startIndex, length, capacity); | ||
|
||
builder.Replace('/', '.').Replace('\\', '.'); | ||
|
||
// Prepend the application name | ||
builder.Insert(0, '.'); | ||
builder.Insert(0, location); | ||
|
||
return builder.ToString(); | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
samples/WebAppLocalization/Infrastructure/Localization/IExtendedStringLocalizer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
using Karambolo.Common.Localization; | ||
using Microsoft.Extensions.Localization; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public interface IExtendedStringLocalizer : IStringLocalizer | ||
{ | ||
string GetTranslation(string name, Plural plural, TextContext context, out string? searchedLocation, out bool resourceNotFound); | ||
bool TryGetTranslation(string name, Plural plural, TextContext context, out string? searchedLocation, [MaybeNullWhen(false)] out string value); | ||
|
||
bool TryLocalize(string name, out string? searchedLocation, [MaybeNullWhen(false)] out string value); | ||
bool TryLocalize(string name, object[] arguments, out string? searchedLocation, [MaybeNullWhen(false)] out string value); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
samples/WebAppLocalization/Infrastructure/Localization/ITranslationsProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using System.Collections.Generic; | ||
using Karambolo.PO; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public interface ITranslationsProvider | ||
{ | ||
IReadOnlyDictionary<(string Location, string Culture), POCatalog> GetCatalogs(); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
samples/WebAppLocalization/Infrastructure/Localization/LocalizationHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Globalization; | ||
using Karambolo.Common.Localization; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace WebApp.Infrastructure.Localization | ||
{ | ||
public static class LocalizationHelper | ||
{ | ||
private static readonly ConcurrentDictionary<(string, string?), object?> s_unavailableTranslations = new ConcurrentDictionary<(string, string?), object?>(); | ||
|
||
internal static void TranslationNotAvailable(this ILogger logger, string name, CultureInfo culture, string? searchedLocation) | ||
{ | ||
if (s_unavailableTranslations.TryAdd((name, searchedLocation), default)) | ||
logger.LogWarning("Translation for '{NAME}' in culture '{CULTURE}' was not found at the following location(s): {LOCATION}.", name, culture.Name, searchedLocation ?? "(n/a)"); | ||
} | ||
|
||
public static (Plural, TextContext) GetSpecialArgs(object[] args) | ||
{ | ||
var plural = (Plural?)Array.Find(args, arg => arg is Plural); | ||
var context = args.Length > 0 ? args[^1] as TextContext? : null; | ||
|
||
return (plural ?? default, context ?? default); | ||
} | ||
} | ||
} |
Oops, something went wrong.