LazZiya / XLocalizer.DB

DB extension pack for XLocalizer
http://docs.ziyad.info
5 stars 3 forks source link

string.Format -> Smart.Format for LocalizedString #9

Closed martinrulf closed 2 years ago

martinrulf commented 2 years ago

Hello again @LazZiya :)

I would like to ask you, if you could use rather

Smart.Format https://github.com/axuno/SmartFormat

instead of string.Format

in cases when you return the Microsoft.Extensions.Localization.LocalizedString

for example here

https://github.com/LazZiya/XLocalizer.DB/blob/db5b6736d3b9d580199bea6ec4c0d22abe45c33d/XLocalizer.DB/DbStringLocalizer.cs#L103

SmartFormat is a string composition library written in C# which is basically compatible with string.Format, it can replace it completely without breaking changes, so you don't have to do any further changes in your code. :)

In return you get support of named parameters in localization keys, that is something that I would like to use. But currently I can't do that even with a workaround.

Could you do that?

BR, Martin

LazZiya commented 2 years ago

Welcome @martinrulf :)

Thank you for your suggestion, I will check it and see the possibilities to integrate with XLocalizer.

Best, Ziya

LazZiya commented 2 years ago

Just did some tests and found how to use Smart.Format with XLocalizer:

1- Decorate the placeholders with double curly brackets {{Sample}} to avoid string format exception 2- Localize the text without providing parameters 3- Parse the locaized text to Smart.Format with relevant data 4- Make sure the localized text has the correct conditional values in English!

public class HomeController
    private readonly IStringLocalizer _loc;
    public HomeController(IStringLocalizer loc)
    {
        _loc = loc;
    }

    public IActionResult Index()
    {
        var data = new { Library = "XLocalizer" };

        // use double curly brackets to avoid string format exception.
        // don't parse parameters here.
        // make sure that you have {{Library}} in the localized text
        // e.g. Turkish: "{{Library}} ile lokalleştirildi."
        var localizedText = _loc["Localized with {{Library}}"];

        // parse the localized text to Smart.Format
        var str = Smart.Format(localizedText, data)

        // output str: "XLocalizer ile lokalleştirildi."
    }
}
martinrulf commented 2 years ago

Hello @LazZiya,

A good trick, thank you. :) in the meantime I resolved this a bit differently.

Basicaly I copied and modified your code for DbStringLocalizer (MyDbStringLocalizer)

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SmartFormat;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using XLocalizer.Common;
using XLocalizer.DB.Models;
using XLocalizer.Translate;

namespace XLocalizer.DB
{
    /// <summary>
    /// DbStringLocalizer
    /// </summary>
    /// <typeparam name="TResource"></typeparam>
    public class MyDbStringLocalizer<TResource> : IStringLocalizer, IStringLocalizer<TResource>
        where TResource : class, IXDbResource, new()
    {
        private readonly IDbResourceProvider _provider;
        private readonly ITranslator _translator;
        private readonly ExpressMemoryCache _cache;
        private readonly XLocalizerOptions _options;
        private readonly ILogger _logger;
        private string _transCulture;

        /// <summary>
        /// Initialize a new instance of DbStringLocalizer
        /// </summary>
        /// <param name="options"></param>
        /// <param name="provider"></param>
        /// <param name="translatorFactory"></param>
        /// <param name="cache"></param>
        /// <param name="loggerFactory"></param>
        /// <param name="localizationOptions"></param>
        public MyDbStringLocalizer(IDbResourceProvider provider,
            ITranslatorFactory translatorFactory,
            ExpressMemoryCache cache,
            IOptions<XLocalizerOptions> options,
            IOptions<RequestLocalizationOptions> localizationOptions,
            ILoggerFactory loggerFactory)
        {
            _options = options.Value;
            _provider = provider;
            _translator = translatorFactory.Create();
            _cache = cache;
            _logger = loggerFactory.CreateLogger<DbStringLocalizer<TResource>>();
            _transCulture = options.Value.TranslateFromCulture ?? localizationOptions.Value.DefaultRequestCulture.Culture.Name;
        }

        /// <summary>
        /// Get localized string
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public LocalizedString this[string name] => GetLocalizedString(name);

        /// <summary>
        /// Get localized string with arguments
        /// </summary>
        /// <param name="name"></param>
        /// <param name="arguments"></param>
        /// <returns></returns>
        public LocalizedString this[string name, params object[] arguments] => GetLocalizedString(name, arguments);

        /// <summary>
        /// NOT IMPLEMENTED
        /// </summary>
        /// <param name="includeParentCultures"></param>
        /// <returns></returns>
        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// NOT IMPLEMENTED! use <see cref="CultureSwitcher"/> instead.
        /// </summary>
        /// <param name="culture"></param>
        /// <returns></returns>
        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private LocalizedString GetLocalizedString(string name, params object[] arguments)
        {
            var targetCulture = CultureInfo.CurrentCulture.Name;
            var targetEqualSource = _transCulture.Equals(targetCulture, StringComparison.OrdinalIgnoreCase);

            // Option 0: Skip localization if:
            // LocalizeDefaultCulture == false and currentCulture == _transCulture
            if (!_options.LocalizeDefaultCulture && targetEqualSource)
            {
                return new LocalizedString(name, Smart.Format(name, arguments), resourceNotFound: true, searchedLocation: string.Empty);
            }

            // Option 1: Look in the cache
            bool availableInCache = _cache.TryGetValue<TResource>(name, out string value);
            if (availableInCache)
            {
                return new LocalizedString(name, Smart.Format(value, arguments), false, string.Empty);
            }

            // Option 2: Look in db source
            bool availableInSource = _provider.TryGetValue<TResource>(name, out value);
            if (availableInSource)
            {
                // Add to cache
                _cache.Set<TResource>(name, value);

                return new LocalizedString(name, Smart.Format(value, arguments), false, string.Empty);
            }

            // Option 3: Try online translation service
            //           and don't do online translation if target == source culture,
            //           because online tranlsation services requires two different cultures.
            var availableInTranslate = false;
            if (_options.AutoTranslate && !targetEqualSource)
            {
                var toLocalize = name?.Split("_")?.Last();
                availableInTranslate = _translator.TryTranslate(_transCulture, CultureInfo.CurrentCulture.Name, toLocalize, out value);
                if (availableInTranslate)
                {
                    // Add to cache
                    _cache.Set<TResource>(name, value);
                }
            }

            // Save to db when AutoAdd is anebled and:
            // A:translation success or, B: AutoTranslate is off or, C: Target and source cultures are same
            // option C: useful when we use code keys to localize defatul culture as well
            if (_options.AutoAddKeys && (availableInTranslate || !_options.AutoTranslate || targetEqualSource))
            {
                if ((value == name && value != null) || value == null)
                {
                    value = name?.Split("_")?.Last();
                }                  

                var res = new TResource
                {
                    Key = name,
                    Value = value,
                    Comment = "Created by XLocalizer",
                    CultureID = CultureInfo.CurrentCulture.Name,
                    IsActive = false
                };

                bool savedToResource = _provider.TrySetValue<TResource>(res);
                _logger.LogInformation($"Save resource to db, status: '{savedToResource}', key: '{name}', value: '{value ?? name}'");
            }

            return new LocalizedString(name, Smart.Format(value, arguments), !availableInTranslate, typeof(TResource).FullName);
        }
    }
}

Then I registered this in DI setup like this

image

This approach is handy, because I was able to further modify the logic there.

For example I use some prefixes for keys when I construct them.

Something like AppName_Controller_View_Key

name?.Split("_")?.Last();

But when the online translation is called (or the raw key is used as a default translation) I remove all the prefixes. i.e. only Key is stored/translated.

Anyway, thank you very much for your help, it is much appreciated.