網站如果希望能提供多語系版本,且網址規則如下該如何實作?
http://abcdefg.com/index 【預設繁體中文】http://abcdefg.com/zh-TW/index 【繁體中文】http://abcdefg.com/zh-CN/index 【簡體中文】http://abcdefg.com/en-US/index 【英文】
先從Action開始,挖個Route參數來抓目前使用者希望的語系為何,並且設定Culture(因為重點是多語系範例,所以就不考慮大小寫判斷那些,還請暫時忽略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [Route("~/index" ) ] [Route("~/{culture}/index" ) ] public ActionResult Index (string culture ) { switch (culture) { case "zh-CN" : break ; case "en-US" : break ; default : culture = "zh-TW" ; break ; } CultureInfo ci = new CultureInfo(culture); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name); return View(); }
接著新增語系檔資料夾App_GlobalResource 專案右鍵 > 加入 > 加入ASP.NET資料夾 > App_GlobalResource
[![](https://1.bp.blogspot.com/-X0T0qbHbM4g/Wg5DB8tdjCI/AAAAAAAAIQc/OFuD0klzB4sOBimU6auOHx-nduJNgxSagCLcBGAs/s640/1.png)](https://1.bp.blogspot.com/-X0T0qbHbM4g/Wg5DB8tdjCI/AAAAAAAAIQc/OFuD0klzB4sOBimU6auOHx-nduJNgxSagCLcBGAs/s1600/1.png)
新增對應語系的設定檔 App_GlobalResources右鍵 > 加入 > 資源檔
[![](https://2.bp.blogspot.com/-yDb53fw_k78/Wg5Dp4kiDQI/AAAAAAAAIQk/7nBQ-vbv1PcOVvNkoS1H6QpdHvBZIIV-ACLcBGAs/s640/2.png)](https://2.bp.blogspot.com/-yDb53fw_k78/Wg5Dp4kiDQI/AAAAAAAAIQk/7nBQ-vbv1PcOVvNkoS1H6QpdHvBZIIV-ACLcBGAs/s1600/2.png)
**
****
**
[![](https://2.bp.blogspot.com/-7b1G9a0T0Ec/Wg5EFbj8FoI/AAAAAAAAIQo/xi6QYst8RssqaLd6fxDr9avHWL9XwtCdACLcBGAs/s1600/3.png)](https://2.bp.blogspot.com/-7b1G9a0T0Ec/Wg5EFbj8FoI/AAAAAAAAIQo/xi6QYst8RssqaLd6fxDr9avHWL9XwtCdACLcBGAs/s1600/3.png) 這邊的資源檔名稱是固定的,不能任意更改
**
**
在各個設定檔設定簡單文字,用來判別是否有正確切換語系
[![](https://2.bp.blogspot.com/-_tlyjSac5Mw/Wg5FEUE-MPI/AAAAAAAAIQ4/EqScZ5b_GUMrvQiG6EU5Pg2qyAKmzs8cgCLcBGAs/s640/1.png)](https://2.bp.blogspot.com/-_tlyjSac5Mw/Wg5FEUE-MPI/AAAAAAAAIQ4/EqScZ5b_GUMrvQiG6EU5Pg2qyAKmzs8cgCLcBGAs/s1600/1.png)
[![](https://4.bp.blogspot.com/-SbOTT0V8vy0/Wg5FETAHr3I/AAAAAAAAIQ8/UIejGSO6TDYAihR8r5KtuUMIgV6ERa1ZQCLcBGAs/s640/2.png)](https://4.bp.blogspot.com/-SbOTT0V8vy0/Wg5FETAHr3I/AAAAAAAAIQ8/UIejGSO6TDYAihR8r5KtuUMIgV6ERa1ZQCLcBGAs/s1600/2.png)
[![](https://2.bp.blogspot.com/-7JPMV22L3uc/Wg5FEd-t4dI/AAAAAAAAIQ0/0DFlVpoEqPk2LdRgl-dCU_htY2lLaasPQCLcBGAs/s640/3.png)](https://2.bp.blogspot.com/-7JPMV22L3uc/Wg5FEd-t4dI/AAAAAAAAIQ0/0DFlVpoEqPk2LdRgl-dCU_htY2lLaasPQCLcBGAs/s1600/3.png)
View上面就簡單做,只顯示出對應語系的CultureNow
[![](https://1.bp.blogspot.com/-rkJslBeNSY4/Wg5FaG31wvI/AAAAAAAAIRA/hi6OuzPU7Mw9fmD-tGoDFJ_Z-shVMCpTgCLcBGAs/s400/1.png)](https://1.bp.blogspot.com/-rkJslBeNSY4/Wg5FaG31wvI/AAAAAAAAIRA/hi6OuzPU7Mw9fmD-tGoDFJ_Z-shVMCpTgCLcBGAs/s1600/1.png)
測試
[![](https://2.bp.blogspot.com/-jdlM9kSYyjM/Wg5G94F9tWI/AAAAAAAAIRg/E66m5xlgo64zLs9IcyAenpJUZcdiwm_YQCLcBGAs/s640/Webp.net-gifmaker.gif)](https://2.bp.blogspot.com/-jdlM9kSYyjM/Wg5G94F9tWI/AAAAAAAAIRg/E66m5xlgo64zLs9IcyAenpJUZcdiwm_YQCLcBGAs/s1600/Webp.net-gifmaker.gif)
好的做到這邊看起來沒什麼問題,但接下來的問題是,是否後續開發都要在每個Action加上這個RouteAttribute
1 2 3 4 5 [Route("~/{culture}/index" ) ] [Route("~/{culture}/home" ) ] [Route("~/{culture}/user" ) ] ......
且設定語系的段落勢必也要寫成ActionFilterAttribute,這樣其實增加了開發的困難之外,只要有人忘記加上,那新的頁面就會少了多語系功能。
工程師的美德就是懶,是否有辦法只做一次就讓全站Route都自動設定好呢? 這時候就要用到DefaultDirectRouteProvider 來解決這這個問題了
DefaultDirectProvider能幫我們做什麼?
它能幫我們在註冊全站Route Template的時候做一些邏輯的加工,在這邊的案例應用上, 我們希望在註冊全站的Route的時候都自動幫我們在Template最前面加上{culture} 來達到做一次多語系設定即可
讓我來實作看看,先新增一個CultureRouteProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class CultureRouteProvider : DefaultDirectRouteProvider { protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories (ActionDescriptor actionDescriptor ) { IReadOnlyList<IDirectRouteFactory> actionRouteFactories = base .GetActionRouteFactories(actionDescriptor); List<IDirectRouteFactory> actionDirectRouteFactories = new List<IDirectRouteFactory>(); foreach (IDirectRouteFactory routeFactory in actionRouteFactories) { RouteAttribute routeAttr = routeFactory as RouteAttribute; if (routeAttr != null && !string .IsNullOrEmpty(routeAttr.Template)) { var template = $"{routeAttr.Template} " ; var routeAttribute = new RouteAttribute(template) { Order = routeAttr.Order, Name = routeAttr.Name }; actionDirectRouteFactories.Add(routeAttribute); var includeLangTemplate = routeAttr.Template.Replace("~/" , string .Format(@"~/{{culture:regex(^(zh\-tw|en\-us|zh\-cn)$)}}/" )); var includeLangRouteAttribute = new RouteAttribute(includeLangTemplate); includeLangRouteAttribute.Order = routeAttr.Order + 1 ; includeLangRouteAttribute.Name = routeAttr.Name; actionDirectRouteFactories.Add(includeLangRouteAttribute); } } return actionDirectRouteFactories; } }
這邊需要特別注意一下,我在culture後面加上了RouteConstraint的正規表示法限制,目的是讓只有zh-tw , en-us , zh-cn才會落入這個模板的範圍,如果沒有這段限制,就會變成萬用Route,有點像是{Controller}/{action}/{id}那般,所有網址都會跑到這邊,造成網址大亂!!!
接著將這組CultureRouteProvider在RouteConfig註冊使用
1 2 3 4 5 6 7 8 9 10 11 12 public class RouteConfig { public static void RegisterRoutes (RouteCollection routes ) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}" ); var constraintsResolver = new DefaultInlineConstraintResolver(); RouteTable.Routes.MapMvcAttributeRoutes(new CultureRouteProvider()); } }
然後將原本Action那組多語系RouteAttribute拿掉試試看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [Route("~/index" ) ] public ActionResult Index (string culture ) { switch (culture) { case "zh-CN" : break ; case "en-US" : break ; default : culture = "zh-TW" ; break ; } CultureInfo ci = new CultureInfo(culture); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name); return View(); }
會發現多語系的功能依然存在,所以CultureRouteProvider有正確運作,自動的幫我們加上了多語系的Route Template。
接著是處理設定語系的段落,不應該讓設定語系的判斷落在每個Action裡面,所以將它獨立拉出來到自定義的CultureFilter中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class CultureFilter : IAuthorizationFilter { public List<string > AllowCultures = new List<string > { "zh-cn" ,"zh-tw" ,"en-us" }; public void OnAuthorization (AuthorizationContext filterContext ) { string culture = string .Empty; if (filterContext.RequestContext.HttpContext.Request.Url.Segments.Count() & gt; 1 ) { culture = filterContext.RequestContext.HttpContext.Request.Url.Segments[1 ].Replace("/" , string .Empty); } if (string .IsNullOrWhiteSpace(culture) || !AllowCultures.Any(x = > x.ToLower() == culture.ToLower())) { culture = "zh-tw" ; } CultureInfo ci = new CultureInfo(culture); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name); } }
在FilterConfig註冊CultureFilter
1 2 3 4 5 6 7 8 9 10 public class FilterConfig { public static void RegisterGlobalFilters (GlobalFilterCollection filters ) { filters.Add(new HandleErrorAttribute()); filters.Add(new CultureFilter()); } }
將原本的Action所有判斷拿掉會發現多語系的功能還是一切正常
1 2 3 4 5 6 [Route("~/index" ) ] public ActionResult Index ( ) { return View(); }
這樣就差不多大功告成,其實要優化的地方還很多,例如大小寫判斷,多語系應該拉成Enum方便擴充,Route Constraint應該跟隨著多語系的Enum去自動產生….等,因為這是獨立拉出來Demo的程式,就不搞得像Production Code一樣複雜了,知道自己注意一下就好XD