Asp.Net Core 混合全球化與本地化支持


前言

最近的新型冠狀病毒流行讓很多人主動在家隔離,希望疫情能快點消退。武漢加油,中國必勝!

Asp.Net Core 提供了內置的網站國際化(全球化與本地化)支持,微軟還內置了基於 resx 資源字符串的國際化服務組件。可以在入門教程中找到相關內容。

但是內置實現方式有一個明顯缺陷,resx 資源是要靜態編譯到程序集中的,無法在網站運行中臨時編輯,靈活性較差。幸好我找到了一個基於數據庫資源存儲的組件,這個組件完美解決了 resx 資源不靈活的缺陷,經過適當的設置,可以在第一次查找資源時順便創建數據庫記錄,而我們要做的就是訪問一次相應的網頁,讓組件創建好記錄,然后我們去編輯相應的翻譯字段並刷新緩存即可。

但是!又是但是,經過一段時間的使用,發現基於數據庫的方式依然存在缺陷,開發中難免有需要刪除並重建數據庫,初始化環境。這時,之前辛辛苦苦編輯的翻譯就會一起灰飛煙滅 (╯‵□′)╯︵┻━┻ 。而 resx 資源卻完美避開了這個問題,這時我就在想,能不能讓他們同時工作,兼顧靈活性與穩定性,魚與熊掌兼得。

經過一番摸索,終於得以成功,在此開貼記錄分享。

正文

設置並啟用國際化服務組件

安裝 Nuget 包Localization.SqlLocalizer,這個包依賴 EF Core 進行數據庫操作。然后在 Startup 的 ConfigureServices 方法中加入以下代碼注冊  EF Core 上下文:

services.AddDbContext<LocalizationModelContext>(options =>
    {
        options.UseSqlServer(connectionString);
    },
    ServiceLifetime.Singleton,
    ServiceLifetime.Singleton
);

注冊自制的混合國際化服務:

services.AddMixedLocalization(opts =>
    {
        opts.ResourcesPath = "Resources";
    },
    options => options.UseSettings(true, false, true, true)
);

注冊請求本地化配置:

services.Configure<RequestLocalizationOptions>(
    options =>
    {
        var cultures =  Configuration.GetSection("Internationalization").GetSection("Cultures")
        .Get<List<string>>()
        .Select(x => new CultureInfo(x)).ToList();
        var supportedCultures = cultures;

        var defaultRequestCulture = cultures.FirstOrDefault() ?? new CultureInfo("zh-CN");
        options.DefaultRequestCulture = new RequestCulture(culture: defaultRequestCulture, uiCulture: defaultRequestCulture);
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;
    });

注冊 MVC 本地化服務:

services.AddMvc()
    //注冊視圖本地化服務
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; })
    //注冊數據注解本地化服務
    .AddDataAnnotationsLocalization();

appsettings.json的根對象節點添加屬性:

"Internationalization": {
  "Cultures": [
    "zh-CN",
    "en-US"
  ]
}

在某個控制器加入以下動作:

public IActionResult SetLanguage(string lang)
{
    var returnUrl = HttpContext.RequestReferer() ?? "/Home";

    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(lang)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return Redirect(returnUrl);
}

准備一個頁面調用這個動作切換語言。然后,大功告成!

這個自制服務遵循以下規則:優先查找基於 resx 資源的翻譯數據,如果找到則直接使用,如果沒有找到,再去基於數據庫的資源中查找,如果找到則正常使用,如果沒有找到則按照對服務的配置決定是否在數據庫中生成記錄並使用。

自制混合國際化服務組件的實現

本體:

public interface IMiscibleStringLocalizerFactory : IStringLocalizerFactory
{
}

public class MiscibleResourceManagerStringLocalizerFactory : ResourceManagerStringLocalizerFactory, IMiscibleStringLocalizerFactory
{
    public MiscibleResourceManagerStringLocalizerFactory(IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory) : base(localizationOptions, loggerFactory)
    {
    }
}

public class MiscibleSqlStringLocalizerFactory : SqlStringLocalizerFactory, IStringExtendedLocalizerFactory, IMiscibleStringLocalizerFactory
{
    public MiscibleSqlStringLocalizerFactory(LocalizationModelContext context, DevelopmentSetup developmentSetup, IOptions<SqlLocalizationOptions> localizationOptions) : base(context, developmentSetup, localizationOptions)
    {
    }
}

public class MixedStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly IEnumerable<IMiscibleStringLocalizerFactory> _localizerFactories;
    private readonly ILogger<MixedStringLocalizerFactory> _logger;

    public MixedStringLocalizerFactory(IEnumerable<IMiscibleStringLocalizerFactory> localizerFactories, ILogger<MixedStringLocalizerFactory> logger)
    {
        _localizerFactories = localizerFactories;
        _logger = logger;
    }

    public IStringLocalizer Create(string baseName, string location)
    {
        return new MixedStringLocalizer(_localizerFactories.Select(x =>
        {
            try
            {
                return x.Create(baseName, location);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, ex.Message);
                return null;
            }
        }));
    }

    public IStringLocalizer Create(Type resourceSource)
    {
        return new MixedStringLocalizer(_localizerFactories.Select(x =>
        {
            try
            {
                return x.Create(resourceSource);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, ex.Message);
                return null;
            }
        }));
    }
}

public class MixedStringLocalizer : IStringLocalizer
{
    private readonly IEnumerable<IStringLocalizer> _stringLocalizers;

    public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers)
    {
        _stringLocalizers = stringLocalizers;
    }

    public virtual LocalizedString this[string name]
    {
        get
        {
            var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
            var result = localizer?[name];
            if (!(result?.ResourceNotFound ?? true)) return result;

            localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}");
            result = localizer[name];
            return result;
        }
    }

    public virtual LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
            var result = localizer?[name, arguments];
            if (!(result?.ResourceNotFound ?? true)) return result;

            localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}");
            result = localizer[name, arguments];
            return result;
        }
    }

    public virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
        var result = localizer?.GetAllStrings(includeParentCultures);
        if (!(result?.Any(x => x.ResourceNotFound) ?? true)) return result;

        localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}");
        result = localizer?.GetAllStrings(includeParentCultures);
        return result;
    }

    [Obsolete]
    public virtual IStringLocalizer WithCulture(CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class MixedStringLocalizer<T> : MixedStringLocalizer, IStringLocalizer<T>
{
    public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers) : base(stringLocalizers)
    {
    }

    public override LocalizedString this[string name] => base[name];

    public override LocalizedString this[string name, params object[] arguments] => base[name, arguments];

    public override IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        return base.GetAllStrings(includeParentCultures);
    }

    [Obsolete]
    public override IStringLocalizer WithCulture(CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

注冊輔助擴展:

public static class MixedLocalizationServiceCollectionExtensions
{
    public static IServiceCollection AddMixedLocalization(
        this IServiceCollection services,
        Action<LocalizationOptions> setupBuiltInAction = null,
        Action<SqlLocalizationOptions> setupSqlAction = null)
    {
        if (services == null) throw new ArgumentNullException(nameof(services));

        services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleResourceManagerStringLocalizerFactory>();

        services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleSqlStringLocalizerFactory>();
        services.TryAddSingleton<IStringExtendedLocalizerFactory, MiscibleSqlStringLocalizerFactory>();
        services.TryAddSingleton<DevelopmentSetup>();

        services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));

        services.AddSingleton<IStringLocalizerFactory, MixedStringLocalizerFactory>();

        if (setupBuiltInAction != null) services.Configure(setupBuiltInAction);
        if (setupSqlAction != null) services.Configure(setupSqlAction);

        return services;
    }
}

原理簡介

服務組件利用了 DI 中可以為同一個服務類型注冊多個實現類型的特性,並在構造方法中注入服務集合,便可以將注冊的所有實現注入組件同時使用。要注意主控服務和工作服務不能注冊為同一個服務類型,不然會導致循環依賴。 內置的國際化框架已經指明了依賴IStringLocalizerFatory,必須將主控服務注冊為IStringLocalizerFatory,工作服只能注冊為其他類型,不過依然要實現IStringLocalizerFatory,所以最方便的辦法就是定義一個新服務類型作為工作服務類型並繼承IStringLocalizerFatory

想直接體驗效果的可以到文章底部訪問我的 Github 下載項目並運行。

結語

這個組件是在計划集成 IdentityServer4 管理面板時發現那個組件使用了 resx 的翻譯,而我的現存項目已經使用了數據庫翻譯存儲,兩者又不相互兼容的情況下產生的想法。

當時Localization.SqlLocalizer舊版本(2.0.4)還存在無法在視圖本地化時正常創建數據庫記錄的問題,也是我調試修復了 bug 並向原作者提交了拉取請求,原作者也在合並了我的修復后發布了新版本。

這次在集成 IdentityServer4 管理面板時又發現了 bug,正准備聯系原作者看怎么處理。

2020-2-15 更新:

實際上 IdentityServer4 是我集成中忘了一個步驟導致的,實際上沒有問題。項目已經修復。

轉載請完整保留以下內容並在顯眼位置標注,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!

本文地址:https://www.cnblogs.com/coredx/p/12271537.html

完整源代碼:Github

里面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM