Перемикач мови для сайту.
На цієї сторінці я розповім про свій перемикач мови для мультимовних сайтів. Це невеличка, але важлива рюшечка сайту.
Перші три кроку ви бачите на скрінах нище: беремо каталог з прапорцями. Беремо базу з кодами прапорців та ім'ями файлів і робимо DBML-файлік (мапер Linq-to-SQL).
Тут є невеличка проблема, у мене цей каталог і база кочують із проекта в проект, тому там коди країн якісь ліві, не ті, що приносить браузер у HttpContext.Current.Request.Headers("Accept-Language"), можна було б зробити додаткову колонку з кодами мов, що приносить браузер, аде робити це нема часу. Тому що мов у світі багато - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes, табличка велика, а потрібно мені звичайно дві-три мови. Але ця невеличка проблема вирішується просто - нище у коді (стрічка 23 классу TopInfo) я просто додав свій мапер коду, що приносить браузер у Accept-Language відносно ключа табличці з именами прапорців. Якщо робити це точно, то краще було в призначити в цій табличці окрему колонку с кодом Accept-Language.
-
Наступним, четвертим кроком - десь на плашці сайту знаходимо місце для прапорців (OCMR у данному випадку - це ім'я мого проекту та ім'я моєї DLL):
1: <%@ Master Language="VB" Inherits="System.Web.Mvc.ViewMasterPage" %>
2:
3: <!DOCTYPE html>
4: <html>
.....27: <td style="text-align: right; width:300px; vertical-align:top">
28: <form enctype="multipart/form-data" method="post" name="lang1" id="lang1" action="../../ChangeLang.ashx" >
29: <%: OCMR.TopInfo.GetLang()%>
30: </form>
31: </td>
....61: </body>
62: </html>
Моя загальна думка у цьому випадку полягає в тому, щоб сформувати приблизно ось такий код:
-
Наступнім, п'ятим кроком - робимо ось такий хандлер ChangeLang.ashx. Сенс його діі поки що незрозумілий, бо незрозуміло як формуються імена кнопок з прапорцями (які ставляться у куку) - це буде зрозуміло далі.
1: Imports System.Web
2: Imports System.Web.Services
3:
4: Public Class ChangeLang
5: Implements System.Web.IHttpHandler
6:
7:
8: 'context.Request.Form.AllKeys
9: ' (0): "US.x"
10: ' (1): "US.y"
11: ' (2): "__VIEWSTATE"
12: ' (3): "__VIEWSTATEGENERATOR"
13: Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
14: 'Определяем имя кнопки от которой прилетел постбек
15: If context.Request.Form.AllKeys.Length >= 2 Then
16: Dim Name1 As String = context.Request.Form.AllKeys(0).Replace(".x", "")
17: Dim Name2 As String = context.Request.Form.AllKeys(1).Replace(".y", "")
18: If Name1 = Name2 Then
19: 'и ставим куку для TopInfo.GetLang
20: Dim LangCook As HttpCookie = New HttpCookie("Lang", Name1)
21: LangCook.Domain = System.Configuration.ConfigurationManager.AppSettings("LoginDomain")
22: LangCook.Expires = Now.AddYears(1)
23: context.Response.Cookies.Add(LangCook)
24: context.Response.RedirectPermanent(context.Request.UrlReferrer.AbsolutePath)
25: End If
26: End If
27: End Sub
28:
29: ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
30: Get
31: Return False
32: End Get
33: End Property
34:
35: End Class
Тут ви бачите ссилки на конфіг сайту, що в ньому повинно бути - це зрозуміло:
1: <?xml version="1.0"?>
2:
3: <configuration>
4: <appSettings>
....17: <add key="LoginDomain" value="localhost"/>
18: <!-- Этот адрес нужен для активации логина и сброса пароля -->
19: <add key="HostingURL" value="localhost:52796"/>
20: <add key="NameURL" value="Open Community Media Room"/>
....34: <add key="webpages:Version" value="1.0.0.0"/>
35: <add key="ClientValidationEnabled" value="true"/>
36: <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
37: </appSettings>
38: <connectionStrings>
39: <remove name="LocalSqlServer"/>
40: <add name="OCMR_ConnectionStrings" connectionString="server=AAA.BBB.CCC.DDD;Initial Catalog=Ocmr;User ID=Ocmr;Password=XXXXXXXXXXX;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
41: <add name="OCMR_FS_ConnectionStrings" connectionString="server=AAA.BBB.CCC.DDD;Initial Catalog=Ocmr_FS;User ID=Ocmr_FS;Password=XXXXXXXXXXXX;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
42: </connectionStrings>
43: <system.web>
44: <compilation debug="true" targetFramework="4.0">
45:
46: </compilation>
47:
48: <authentication mode="Forms">
49: <forms loginUrl="~/Account/LogOn" timeout="2880" />
50: </authentication>
51:
52: <pages>
53: <namespaces>
54: <add namespace="System.Web.Helpers" />
55: <add namespace="System.Web.Mvc" />
56: <add namespace="System.Web.Mvc.Ajax" />
57: <add namespace="System.Web.Mvc.Html" />
58: <add namespace="System.Web.Routing" />
59: <add namespace="System.Web.WebPages"/>
60: </namespaces>
61: </pages>
62: </system.web>
63:
64: <system.webServer>
65: <validation validateIntegratedModeConfiguration="false"/>
66: <modules runAllManagedModulesForAllRequests="true"/>
67: </system.webServer>
68:
69: <runtime>
70: <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
71: <dependentAssembly>
72: <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
73: <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
74: </dependentAssembly>
75: </assemblyBinding>
76: </runtime>
77: </configuration>
-
Наступним, шостим кроком робимо головний класс TopInfo, що формує MvcHtmlString кнопки з прапорцями, кожну із своїм ім'ям, що є ключом в табличці прапорців (а ідеально це ім'я було б тим кодом, що приносить браузер у реквесті у Request.Headers("Accept-Language").
Цей код виходить з пропозиції, що браузер приносить у цьому параметрі стрічку, подібну "ru,en-US;q=0.7,en;q=0.3". Далее код анализирует куку, що ставить хандлер і прапорець з цим ім'ям виводиться звичайним стилем, а інші прапорці віводятсья напівпрозорим стилем з opacity.
1: Public Class TopInfo
2:
3:
4: Public Shared Function GetLang() As MvcHtmlString
5: 'сначала смотрим был ли установлен язык хандлером ChangeLang.ashx
6: Dim Cookies As String() = HttpContext.Current.Request.Cookies.AllKeys
7: Dim CurrentLang As String = "US"
8: If Cookies.Length > 0 Then
9: If HttpContext.Current.Request.Cookies("Lang") IsNot Nothing Then
10: If HttpContext.Current.Request.Cookies("Lang").Value <> "" Then
11: CurrentLang = HttpContext.Current.Request.Cookies("Lang").Value
12: End If
13: End If
14: End If
15:
16: Dim AcceptLanguageString As String = HttpContext.Current.Request.Headers("Accept-Language") '"ru,en-US;q=0.7,en;q=0.3"
17: If AcceptLanguageString IsNot Nothing Then
18: If AcceptLanguageString.Length > 0 Then
19: Dim AcceptLanguageArr = AcceptLanguageString.Split(";")
20: Dim SupportLangArr = AcceptLanguageArr(0).Split(",")
21: 'https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
22: Dim AllFlag As System.Collections.Generic.List(Of Global.OCMR.Flag) = HttpContext.Current.Application("Flags")
23: Dim AllSupportFlags As System.Collections.Generic.List(Of Global.OCMR.Flag) = (From X In AllFlag Select X Where X.Code = "UA" Or X.Code = "RU1" Or X.Code = "US").ToList
24: If AllSupportFlags.Count > 0 Then
25: Dim Result As New StringBuilder("")
26: For i As Integer = 0 To AllSupportFlags.Count - 1
27: If AllSupportFlags(i).Code = CurrentLang Then
28: Result.Append("<input type='image' name='" & AllSupportFlags(i).Code & "' src='/Flag/" & AllSupportFlags(i).Flag & "'><br>")
29: Else
30: Result.Append("<input type='image' name='" & AllSupportFlags(i).Code & "' src='/Flag/" & AllSupportFlags(i).Flag & "' style='opacity: 0.2 ! important;'><br>")
31: End If
32: Next
33: Return New MvcHtmlString(Result.ToString)
34: End If
35: End If
36: End If
37: Return New MvcHtmlString("")
38: End Function
39:
40: End Class
- І нарешті сьомий крок - це зробити кеш сайту з табличкою прапорців, щоб на кожному реквесті не смикати базу. Ще робиться ось так - в Global.asaх додаємо стрічку 29:
1: Public Class MvcApplication
2: Inherits System.Web.HttpApplication
3:
.....23: Sub Application_Start()
.....28: 'read cache data
29: ApplicationStart.GO()
30: End Sub
.....40:
41: Sub Session_Start()
42: 'redirect to permanent login user
43: SessionStart.GO()
44: End Sub
45:
46: End Class
І ось нарешті останній крок ціеї невеличкої головоломки - утворення кешу:
1: Public Class ApplicationStart
2: Public Shared Sub GO()
3: Dim db1 As New OCMRDataContext
4: Dim AllFlag = (db1.Flags).ToList
5: HttpContext.Current.Application("Flags") = AllFlag
6: End Sub
7: End Class
Ще одне невеличке пояснення застосованою мною технології - зверніть увагу що я сформував повний синхронний постбек, (на відміну від постбека AJAX!) - обробив його у хандлері і зробив редірект на Request.UrlReferrer.AbsolutePath, який приніс браузер. Щоб порозуміти цей мій вибір я поясню два альтернативних варіанту відправки постбеку, які не можна використовуватии у данному випадку. Як приклад використуемо html select / option одного мого сайту.
Взагалі повний постбек звичайно відправляють (по application/x-www-form-urlencoded) десь приблизно так:
1: <form enctype="application/x-www-form-urlencoded" method="post" name="Comm3" id="Comm3" action='/Home/ChangeCommunity' >
2: <% Dim Commnunity As System.Collections.Generic.List(Of System.Web.Mvc.SelectListItem) = Application("Community")%>
3: <%: Html.DropDownList("Community", Commnunity, New With {.style = "width:200px"})%>
4: </form>
5: <script language="javascript" type="text/javascript">
6: $('#Community').change(function () {
7: var selectedValue = $('#Community').val();
8: $('form#Comm3').submit();
9: });
10: </script>
І обробляють у контроллері десь приблизно так:
1: Namespace OCMR
2: Public Class HomeController
3: Inherits System.Web.Mvc.Controller
4:
5: Function Index() As ActionResult
6: Return View()
7: End Function
8:
9:
10: Function ChangeCommunity(Community As String) As ActionResult
11: If Community IsNot Nothing Then
12: Return RedirectToActionPermanent("Index")
13: End If
14: End Function
15:
Але таким чином незрозуміло на якому конкретно прапорці зроблено клік. Або тільки кожній прапорець загорнути у дужки <form> та зробити багато-багато Action у контролері. Кода вийде у сто разів більше, html буде забруднений тегами <form> - тобто це можливий, але дуже дурний шлях вирішення цієї проблеми - замість описанного тут - отримати координати кліка на прапорцях по multipart/form-data.
Альтернативний шлях - відправити Postback по AJAX:
1: <form enctype="application/x-www-form-urlencoded" method="post" name="Comm3" id="Comm3" action='/Home/ChangeCommunity' >
2: <% Dim Commnunity As System.Collections.Generic.List(Of System.Web.Mvc.SelectListItem) = Application("Community")%>
3: <%: Html.DropDownList("Community", Commnunity, New With {.style = "width:200px"})%>
4: </form>
5: <script language="javascript" type="text/javascript">
6: $('#Community').change(function () {
7: var selectedValue = $('#Community').val();
8: $.post('<%: html.Action("ChangeCommunity", "Home") %>', { Community: selectedValue }, function (data) {
9: //server response for AJAX request
10: });
11: });
12: </script>
Але справа у тому, що зробити повний редірект і відправити сторінку з самого початку з MVC-контроллера ASP.NET у відповідь на AJAX-реквест до серверу неможна. Буде добре знайоме усім програмистам повідомлення про заборону цього - Child actions are not allowed to perform redirect actions. - тобто у відповідь на такий AJAX-реквест до сервера можливо відправити або PartialView або Json.
Таким чином, шлях, описаний мною на ціей сторінці, мабуть, единий можливий для вирішення описанною тут проблеми.