0%

【MVC】多語系

網站如果希望能提供多語系版本,且網址規則如下該如何實作?

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
/// <summary>
/// 多語系Route Provider
/// </summary>
/// <seealso cref="System.Web.Mvc.Routing.DefaultDirectRouteProvider" />
public class CultureRouteProvider: DefaultDirectRouteProvider
{
/// <summary>
/// 取得所指定之動作描述元的一組路由 Factory。
/// </summary>
/// <param name="actionDescriptor">動作描述元。</param>
/// <returns>
/// 一組路由 Factory。
/// </returns>
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))
{
//每個Route Template原本的樣子
//已剛剛的Action為例,就是 "~/index"
var template = $"{routeAttr.Template}";

var routeAttribute = new RouteAttribute(template)
{
Order = routeAttr.Order,
Name = routeAttr.Name
};
actionDirectRouteFactories.Add(routeAttribute);

//替每組Action都多加上一組{culture}/RouterTemplateLanguage的多語系RouteMap
//EX: "~/index" 多一組 "~/{culture}/index"
var includeLangTemplate = routeAttr.Template.Replace("~/", string.Format(@"~/{{culture:regex(^(zh\-tw|en\-us|zh\-cn)$)}}/"));

//註冊這組多語系的Route Template
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}");

//註冊CultureRouteProvider
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
//拿掉多語系RouteAttribute
[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