1. 程式人生 > >Asp.Net Core 混合全球化與本地化支援

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

前言

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

       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一