前言 最近的新型冠狀病毒流行讓很多人主動在家隔離,希望疫情能快點消退。武漢加油,中國必勝! Asp.Net Core 提供了內置的網站國際化(全球化與本地化)支持,微軟還內置了基於 resx 資源字元串的國際化服務組件。可以在入門教程中找到相關內容。 但是內置實現方式有一個明顯缺陷,resx 資源是 ...
前言
最近的新型冠狀病毒流行讓很多人主動在家隔離,希望疫情能快點消退。武漢加油,中國必勝!
Asp.Net Core 提供了內置的網站國際化(全球化與本地化)支持,微軟還內置了基於 resx 資源字元串的國際化服務組件。可以在入門教程中找到相關內容。
但是內置實現方式有一個明顯缺陷,resx 資源是要靜態編譯到程式集中的,無法在網站運行中臨時編輯,靈活性較差。幸好我找到了一個基於資料庫資源存儲的組件,這個組件完美解決了 resx 資源不靈活的缺陷,經過適當的設置,可以在第一次查找資源時順便創建資料庫記錄,而我們要做的就是訪問一次相應的網頁,讓組件創建好記錄,然後我們去編輯相應的翻譯欄位並刷新緩存即可。
但是!又是但是,經過一段時間的使用,發現基於資料庫的方式依然存在缺陷,開發中難免有需要刪除並重建資料庫,初始化環境。這時,之前辛辛苦苦編輯的翻譯就會一起灰飛煙滅 (╯‵□′)╯︵┻━┻ 。而 resx 資源卻完美避開了這個問題,這時我就在想,能不能讓他們同時工作,兼顧靈活性與穩定性,魚與熊掌兼得。
經過一番摸索,終於得以成功,在此開貼記錄分享。
正文
設置並啟用國際化服務組件
安裝 Nuget 包 Localization.SqlLocalizer,這個包依賴 EF Core 進行資料庫操作。然後在 Startup 的 ConfigureServices 方法中加入以下代碼註冊 EF Core 上下文:
1 services.AddDbContext<LocalizationModelContext>(options => 2 { 3 options.UseSqlServer(connectionString); 4 }, 5 ServiceLifetime.Singleton, 6 ServiceLifetime.Singleton);
註冊自製的混合國際化服務:
services.AddMixedLocalization(opts => { opts.ResourcesPath = "Resources"; }, options => options.UseSettings(true, false, true, true));
註冊請求本地化配置:
1 services.Configure<RequestLocalizationOptions>( 2 options => 3 { 4 var cultures = Configuration.GetSection("Internationalization").GetSection("Cultures") 5 .Get<List<string>>() 6 .Select(x => new CultureInfo(x)).ToList(); 7 var supportedCultures = cultures; 8 9 var defaultRequestCulture = cultures.FirstOrDefault() ?? new CultureInfo("zh-CN"); 10 options.DefaultRequestCulture = new RequestCulture(culture: defaultRequestCulture, uiCulture: defaultRequestCulture); 11 options.SupportedCultures = supportedCultures; 12 options.SupportedUICultures = supportedCultures; 13 });
註冊 MVC 本地化服務:
1 services.AddMvc() 2 //註冊視圖本地化服務 3 .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; }) 4 //註冊數據註解本地化服務 5 .AddDataAnnotationsLocalization();
在 appsettings.json 的根對象節點添加屬性:
"Internationalization": { "Cultures": [ "zh-CN", "en-US" ] }
在某個控制器加入以下動作:
1 public IActionResult SetLanguage(string lang) 2 { 3 var returnUrl = HttpContext.RequestReferer() ?? "/Home"; 4 5 Response.Cookies.Append( 6 CookieRequestCultureProvider.DefaultCookieName, 7 CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(lang)), 8 new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) } 9 ); 10 11 return Redirect(returnUrl); 12 }
準備一個頁面調用這個動作切換語言。然後,大功告成!
這個自製服務遵循以下規則:優先查找基於 resx 資源的翻譯數據,如果找到則直接使用,如果沒有找到,再去基於資料庫的資源中查找,如果找到則正常使用,如果沒有找到則按照對服務的配置決定是否在資料庫中生成記錄並使用。
自製混合國際化服務組件的實現
本體:
1 public interface IMiscibleStringLocalizerFactory : IStringLocalizerFactory 2 { 3 } 4 5 public class MiscibleResourceManagerStringLocalizerFactory : ResourceManagerStringLocalizerFactory, IMiscibleStringLocalizerFactory 6 { 7 public MiscibleResourceManagerStringLocalizerFactory(IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory) : base(localizationOptions, loggerFactory) 8 { 9 } 10 } 11 12 public class MiscibleSqlStringLocalizerFactory : SqlStringLocalizerFactory, IStringExtendedLocalizerFactory, IMiscibleStringLocalizerFactory 13 { 14 public MiscibleSqlStringLocalizerFactory(LocalizationModelContext context, DevelopmentSetup developmentSetup, IOptions<SqlLocalizationOptions> localizationOptions) : base(context, developmentSetup, localizationOptions) 15 { 16 } 17 } 18 19 public class MixedStringLocalizerFactory : IStringLocalizerFactory 20 { 21 private readonly IEnumerable<IMiscibleStringLocalizerFactory> _localizerFactories; 22 private readonly ILogger<MixedStringLocalizerFactory> _logger; 23 24 public MixedStringLocalizerFactory(IEnumerable<IMiscibleStringLocalizerFactory> localizerFactories, ILogger<MixedStringLocalizerFactory> logger) 25 { 26 _localizerFactories = localizerFactories; 27 _logger = logger; 28 } 29 30 public IStringLocalizer Create(string baseName, string location) 31 { 32 return new MixedStringLocalizer(_localizerFactories.Select(x => 33 { 34 try 35 { 36 return x.Create(baseName, location); 37 } 38 catch (Exception ex) 39 { 40 _logger.LogError(ex, ex.Message); 41 return null; 42 } 43 })); 44 } 45 46 public IStringLocalizer Create(Type resourceSource) 47 { 48 return new MixedStringLocalizer(_localizerFactories.Select(x => 49 { 50 try 51 { 52 return x.Create(resourceSource); 53 } 54 catch (Exception ex) 55 { 56 _logger.LogError(ex, ex.Message); 57 return null; 58 } 59 })); 60 } 61 } 62 63 public class MixedStringLocalizer : IStringLocalizer 64 { 65 private readonly IEnumerable<IStringLocalizer> _stringLocalizers; 66 67 public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers) 68 { 69 _stringLocalizers = stringLocalizers; 70 } 71 72 public virtual LocalizedString this[string name] 73 { 74 get 75 { 76 var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer); 77 var result = localizer?[name]; 78 if (!(result?.ResourceNotFound ?? true)) return result; 79 80 localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}"); 81 result = localizer[name]; 82 return result; 83 } 84 } 85 86 public virtual LocalizedString this[string name, params object[] arguments] 87 { 88 get 89 { 90 var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer); 91 var result = localizer?[name, arguments]; 92 if (!(result?.ResourceNotFound ?? true)) return result; 93 94 localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}"); 95 result = localizer[name, arguments]; 96 return result; 97 } 98 } 99 100 public virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) 101 { 102 var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer); 103 var result = localizer?.GetAllStrings(includeParentCultures); 104 if (!(result?.Any(x => x.ResourceNotFound) ?? true)) return result; 105 106 localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"沒有找到可用的 {nameof(IStringLocalizer)}"); 107 result = localizer?.GetAllStrings(includeParentCultures); 108 return result; 109 } 110 111 [Obsolete] 112 public virtual IStringLocalizer WithCulture(CultureInfo culture) 113 { 114 throw new NotImplementedException(); 115 } 116 } 117 118 public class MixedStringLocalizer<T> : MixedStringLocalizer, IStringLocalizer<T> 119 { 120 public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers) : base(stringLocalizers) 121 { 122 } 123 124 public override LocalizedString this[string name] => base[name]; 125 126 public override LocalizedString this[string name, params object[] arguments] => base[name, arguments]; 127 128 public override IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) 129 { 130 return base.GetAllStrings(includeParentCultures); 131 } 132 133 [Obsolete] 134 public override IStringLocalizer WithCulture(CultureInfo culture) 135 { 136 throw new NotImplementedException(); 137 } 138 }View Code
註冊輔助擴展:
1 public static class MixedLocalizationServiceCollectionExtensions 2 { 3 public static IServiceCollection AddMixedLocalization( 4 this IServiceCollection services, 5 Action<LocalizationOptions> setupBuiltInAction = null, 6 Action<SqlLocalizationOptions> setupSqlAction = null) 7 { 8 if (services == null) throw new ArgumentNullException(nameof(services)); 9 10 services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleResourceManagerStringLocalizerFactory>(); 11 12 services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleSqlStringLocalizerFactory>(); 13 services.TryAddSingleton<IStringExtendedLocalizerFactory, MiscibleSqlStringLocalizerFactory>(); 14 services.TryAddSingleton<DevelopmentSetup>(); 15 16 services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>)); 17 18 services.AddSingleton<IStringLocalizerFactory, MixedStringLocalizerFactory>(); 19 20 if (setupBuiltInAction != null) services.Configure(setupBuiltInAction); 21 if (setupSqlAction != null) services.Configure(setupSqlAction); 22 23 return services; 24 } 25 }View Code
原理簡介
服務組件利用了 DI 中可以為同一個服務類型註冊多個實現類型的特性,併在構造方法中註入服務集合,便可以將註冊的所有實現註入組件同時使用。要註意主控服務和工作服務不能註冊為同一個服務類型,不然會導致迴圈依賴。 內置的國際化框架已經指明瞭依賴 IStringLocalizerFatory ,必須將主控服務註冊為 IStringLocalizerFatory,工作服只能註冊為其他類型,不過依然要實現 IStringLocalizerFatory,所以最方便的辦法就是定義一個新服務類型作為工作服務類型並繼承 IStringLocalizerFatory。
想直接體驗效果的可以到文章底部訪問我的 Github 下載項目並運行。
結語
這個組件是在計劃集成 IdentityServer4 管理面板時發現那個組件使用了 resx 的翻譯,而我的現存項目已經使用了資料庫翻譯存儲,兩者又不相互相容的情況下產生的想法。
當時 Localization.SqlLocalizer 舊版本(2.0.4)還存在無法在視圖本地化時正常創建資料庫記錄的問題,也是我調試修複了 bug 並向原作者提交了拉取請求,原作者也在合併了我的修複後發佈了新版本。
這次在集成 IdentityServer4 管理面板時又發現了 bug,正準備聯繫原作者看怎麼處理。
轉載請完整保留以下內容併在顯眼位置標註,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!
本文地址:https://www.cnblogs.com/coredx/p/12271537.html
完整源代碼:Github
裡面有各種小東西,這隻是其中之一,不嫌棄的話可以Star一下。