Internationalization using ASP.Net MVC

Asked

Viewed 8,090 times

31

How can I implement an internationalization system using Microsoft’s MVC technology? Is there something similar to Android so that my website can support several languages? Is there also some way to get the language of the browser the user is accessing, to configure the default language of this starting point?

1 answer

35


There are some ways:

1. Using a File of Resource, manual mode

This method is the simplest to internationalize your client-side and server-side application and usage. Has median complexity regarding configuration.

Initial Configuration

Create a directory in your project called Resources. Within it, create a Resource File by the name of Language.resx. This will be the base file for creating the other internationalized files.

Write your system always referencing Resources.Language. When finished, duplicate your file Language.resx varying the suffix before the extension according to the culture to which it refers. For example:

Language.en.resx
Language.es.resx
Language.pt-BR.resx

Use

To internationalize elements of your Model, decorate each property with the following Attribute:

[Display(Name = "Name", ResourceType = typeof(Resources.Language))]
public String Name { get; set; }

This means that in order for the property to be translated correctly, there must be inside the file Language.resx one String whose name is Name and the value is "Name" (in Portuguese, for example).

Editor de Resources

For translations directly into Views, just include in the View the following using:

@using SeuProjeto.Resources

Then just use as follows:

@Language.Name

Alternatively, you can refer to the namespace of Resources in the section system.web.webPages.razor/pages/namespaces as below, avoiding the @using SeuProjeto.Resources in all Views:

<configuration>
  ...
  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
        <add namespace="SeuProjeto" />
        <add namespace="SeuProjeto.Resources" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>
  ...
</configuration>

Similarly to the example of Model, to View will spell "Name".

Changing culture at runtime

In MVC4

Create in your Controller common the following method:

public ActionResult ChangeCulture(string lang, string returnUrl)
{
    Session["Culture"] = new CultureInfo(lang);
    return Redirect(returnUrl);
}

In any View, you can change the language using the following ActionLink:

@Html.ActionLink(Language.Spanish, "ChangeCulture", new { lang = "es", returnUrl = this.Request.RawUrl }, null)
@Html.ActionLink(Language.Portuguese, "ChangeCulture", new { lang = "pt-BR", returnUrl = this.Request.RawUrl }, null)
@Html.ActionLink(Language.English, "ChangeCulture", new { lang = "en", returnUrl = this.Request.RawUrl }, null)

In MVC5 (also for MVC4)

With the architectural change of order of request processing in ASP.NET MVC5, just change the culture in Session has no effect because the change of culture can only be made before the call of Action.

Not only that, the exchange of culture implies the change of behavior of the Modelbinder. For example, dates and numbers representing financial values or decimal and scientific notation need to be treated differently.

Therefore, it is necessary to overwrite the method that precedes the processing of the entire request, BeginExecuteCore, as follows:

public class Controller : System.Web.Mvc.Controller
{
    ...
    protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
    {
        string cultureName = RouteData.Values["culture"] as string;

        if (cultureName == null)
            cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ? Request.UserLanguages[0] : null; // obtain it from HTTP header AcceptLanguages

        cultureName = CultureHelper.GetImplementedCulture(cultureName); // Veja mais abaixo na resposta

        if (RouteData.Values["culture"] as string != cultureName)
        {
            // Força uma cultura válida na URL
            RouteData.Values["culture"] = cultureName.ToLowerInvariant();
            Response.RedirectToRoute(RouteData.Values);
        }

        Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
        Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

        return base.BeginExecuteCore(callback, state);
    }
    ...
}

CultureHelper is a Helper that checks the culture passed in the URL and the normatiza. If there is a problem in the culture-language pair, for example, the Helper performs regression for the nearest crop:

public static class CultureHelper
{
    // Culturas válidas
    private static readonly List<string> _validCultures = new List<string> { "af", "af-ZA", "sq", "sq-AL", "gsw-FR", "am-ET", "ar", "ar-DZ", "ar-BH", "ar-EG", "ar-IQ", "ar-JO", "ar-KW",
        "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SA", "ar-SY", "ar-TN", "ar-AE", "ar-YE", "hy", "hy-AM", "as-IN", "az", "az-Cyrl-AZ", "az-Latn-AZ", "ba-RU", "eu", "eu-ES", "be",
        "be-BY", "bn-BD", "bn-IN", "bs-Cyrl-BA", "bs-Latn-BA", "br-FR", "bg", "bg-BG", "ca", "ca-ES", "zh-HK", "zh-MO", "zh-CN", "zh-Hans", "zh-SG", "zh-TW", "zh-Hant", "co-FR", "hr", "hr-HR",
        "hr-BA", "cs", "cs-CZ", "da", "da-DK", "prs-AF", "div", "div-MV", "nl", "nl-BE", "nl-NL", "en", "en-AU", "en-BZ", "en-CA", "en-029", "en-IN", "en-IE", "en-JM", "en-MY", "en-NZ", "en-PH",
        "en-SG", "en-ZA", "en-TT", "en-GB", "en-US", "en-ZW", "et", "et-EE", "fo", "fo-FO", "fil-PH", "fi", "fi-FI", "fr", "fr-BE", "fr-CA", "fr-FR", "fr-LU", "fr-MC", "fr-CH", "fy-NL", "gl",
        "gl-ES", "ka", "ka-GE", "de", "de-AT", "de-DE", "de-LI", "de-LU", "de-CH", "el", "el-GR", "kl-GL", "gu", "gu-IN", "ha-Latn-NG", "he", "he-IL", "hi", "hi-IN", "hu", "hu-HU", "is", "is-IS",
        "ig-NG", "id", "id-ID", "iu-Latn-CA", "iu-Cans-CA", "ga-IE", "xh-ZA", "zu-ZA", "it", "it-IT", "it-CH", "ja", "ja-JP", "kn", "kn-IN", "kk", "kk-KZ", "km-KH", "qut-GT", "rw-RW", "sw", "sw-KE",
        "kok", "kok-IN", "ko", "ko-KR", "ky", "ky-KG", "lo-LA", "lv", "lv-LV", "lt", "lt-LT", "wee-DE", "lb-LU", "mk", "mk-MK", "ms", "ms-BN", "ms-MY", "ml-IN", "mt-MT", "mi-NZ", "arn-CL", "mr",
        "mr-IN", "moh-CA", "mn", "mn-MN", "mn-Mong-CN", "ne-NP", "no", "nb-NO", "nn-NO", "oc-FR", "or-IN", "ps-AF", "fa", "fa-IR", "pl", "pl-PL", "pt", "pt-BR", "pt-PT", "pa", "pa-IN", "quz-BO",
        "quz-EC", "quz-PE", "ro", "ro-RO", "rm-CH", "ru", "ru-RU", "smn-FI", "smj-NO", "smj-SE", "se-FI", "se-NO", "se-SE", "sms-FI", "sma-NO", "sma-SE", "sa", "sa-IN", "sr", "sr-Cyrl-BA",
        "sr-Cyrl-SP", "sr-Latn-BA", "sr-Latn-SP", "nso-ZA", "tn-ZA", "si-LK", "sk", "sk-SK", "sl", "sl-SI", "es", "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-SV", "es-GT",
        "es-HN", "es-MX", "es-NI", "es-PA", "es-PY", "es-PE", "es-PR", "es-ES", "es-US", "es-UY", "es-VE", "sv", "sv-FI", "sv-SE", "syr", "syr-SY", "tg-Cyrl-TJ", "tzm-Latn-DZ", "ta", "ta-IN", "tt",
        "tt-RU", "te", "te-IN", "th", "th-TH", "bo-CN", "tr", "tr-TR", "tk-TM", "ug-CN", "uk", "uk-UA", "wen-DE", "ur", "ur-PK", "uz", "uz-Cyrl-UZ", "uz-Latn-UZ", "vi", "vi-VN", "cy-GB", "wo-SN",
        "sah-RU", "ii-CN", "yo-NG" };

    // Inclua aqui apenas as culturas que estarão disponíveis no sistema.
    private static readonly List<string> _cultures = new List<string> {
        "pt-BR", // Default
        "en-US"//, "es"
    };

    /// <summary>
    /// Returna true se a linguagem for lida da direita para a esquerda, como o árabe.
    /// </summary>
    public static bool IsRighToLeft()
    {
        return Thread.CurrentThread.CurrentCulture.TextInfo.IsRightToLeft;
    }

    /// <summary>
    /// Retorna um nome válido de cultura baseado no parâmetro "name". Se "name" não é válido, retorna "en-US".
    /// </summary>
    /// <param name="name" />Nome da cultura (ex. en-US)</param>
    public static string GetImplementedCulture(string name)
    {
        if (string.IsNullOrEmpty(name))
            return GetDefaultCulture(); 

        if (_validCultures.Where(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)
            return GetDefaultCulture(); 

        if (_cultures.Where(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() > 0)
            return name; 

        // Se até aqui não foi encontrada uma cultura adequada, tenta retornar uma "Cultura Neutra". 
        // Por exemplo, "pt" é a cultura neutra do Português do Brasil (pt-BR) e de Portugal (pt-PT). 
        var n = GetNeutralCulture(name);
        foreach (var c in _cultures)
            if (c.StartsWith(n))
                return c;

        return GetDefaultCulture(); // Se nada deu certo, retorna a primeira cultura definida no array de culturas.
    }

    /// <summary>
    /// Retorna o primeiro nome de cultura entre todas as culturas definidas.
    /// </summary>
    /// <returns></returns>
    public static string GetDefaultCulture()
    {
        return _cultures[0]; // return Default culture
    }

    public static string GetCurrentCulture()
    {
        return Thread.CurrentThread.CurrentCulture.Name;
    }

    public static string GetCurrentNeutralCulture()
    {
        return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
    }

    public static string GetNeutralCulture(string name)
    {
        if (!name.Contains("-")) return name;

        return name.Split('-')[0]; // Read first part only. E.g. "en", "es"
    }
}

I removed this Helper from here.

Finally, specify a special treatment for routes initiated by the crop as follows:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "DefaultLocalized",
            url: "{culture}/{controller}/{action}/{id}",
            defaults: new
            {
                controller = "Home",
                action = "Index",
                id = UrlParameter.Optional,
                culture = "pt-BR"
            },
            constraints: new { culture = "[a-z]{2}-[A-Z]{2}" }
        );

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

How can I translate the files simultaneously?

There is an editor called Zeta Resource Editor, which is free, where you can open all the files at the same time, organized by strings of Resource (if they are equal). The visual looks like an Excel table. It is excellent for translating in several languages.

Zeta Resource Editor

Done all these steps, the view of the translated page can be seen so:

http://teste:12345/Produtos
http://teste:12345/en-US/Produtos
http://teste:12345/pt-BR/Produtos

2. I18n

Based on gettext/PO ecosystem, The I18n library is more robust in the amount of automatic functionality, requiring a slightly smaller configuration effort, but its gains are better seen in very large, highly complex systems. The documentation is poorly organized and clear and some add-ons, such as an extension to Visual Studio, receive little maintenance (I even needed to make a).

Setup

Web.config

<configuration>
  ...
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="i18n.DirectoriesToScan" value=".." />
    <add key="i18n.WhiteList" value="*.cs;*.cshtml;*.sitemap" />
    <add key="i18n.BlackList" value=".\js\kendo;.\js\angular" />
    <add key="i18n.AvailableLanguages" value="pt-BR;en-US" />
  </appSettings>
  ...
  <system.web>
    ...
    <httpModules>
      ...
      <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" />
      ...
    </httpModules>
  </system.web>
  <system.webServer>
    <modules>
      ...
      <add name="i18n.LocalizingModule" type="i18n.LocalizingModule, i18n" />
      ...
    </modules>
    ...
  </system.webServer>
  ...
</configuration>

Global.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

        // Change from the default of 'en'.
        i18n.LocalizedApplication.Current.DefaultLanguage = "pt";

        // Change from the of temporary redirects during URL localization
        i18n.LocalizedApplication.Current.PermanentRedirects = true;

        // This line can be used to disable URL Localization.
        //i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Void;

        // Change the URL localization scheme from Scheme1.
        i18n.UrlLocalizer.UrlLocalizationScheme = i18n.UrlLocalizationScheme.Scheme2;

        // Blacklist certain URLs from being 'localized' via a callback.
        i18n.UrlLocalizer.IncomingUrlFilters += delegate (Uri url) {
            if (url.LocalPath.EndsWith("sitemap.xml", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
            return true;
        };

        // Blacklist certain URLs from being translated using a regex pattern. The default setting is:
        i18n.LocalizedApplication.Current.UrlsToExcludeFromProcessing = new Regex(@"(?:\.(?:less|css)(?:\?|$))|(?i:i18nSkip|glimpse|trace|elmah)");

        // Whitelist content types to translate. The default setting is:
        i18n.LocalizedApplication.Current.ContentTypesToLocalize = new Regex(@"^(?:(?:(?:text|application)/(?:plain|html|xml|javascript|x-javascript|json|x-json))(?:\s*;.*)?)$");

        // Change the types of async postback blocks that are localized
        i18n.LocalizedApplication.Current.AsyncPostbackTypesToTranslate = "updatePanel,scriptStartupBlock,pageTitle";
    }
}

Use

In Views

@{
    ViewBag.Title = "[[[Index]]]";
}

<h2>[[[Index]]]</h2>

<p>
    @Html.ActionLink("[[[Create New]]]", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.ProdutoCategoria.Nome)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Nome)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Preco)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.ProdutoCategoria.Nome)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Nome)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Preco)
        </td>
        <td>
            @Html.ActionLink("[[[Edit]]]", "Edit", new { id=item.ProdutoId }) |
            @Html.ActionLink("[[[Details]]]", "Details", new { id=item.ProdutoId }) |
            @Html.ActionLink("[[[Delete]]]", "Delete", new { id=item.ProdutoId })
        </td>
    </tr>
}

</table>

In Models

[Table("Produtos")]
[DisplayColumn("Nome")]
public class Produto
{
    [Key]
    public int ProdutoId { get; set; }
    public int ProdutoCategoriaId { get; set; }

    [Required]
    [Display(Name = "[[[Nome]]]")]
    public String Nome { get; set; }
    [Required]
    [Display(Name = "[[[Preco]]]")]
    public decimal Preco { get; set; }

    public virtual ProdutoCategoria ProdutoCategoria { get; set; }
}

In Controllers

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "[[[Bem vindo ao ASP.NET MVC!]]]";

            return View();
        }
    }

Compiling the PO files.

There are two methods: the first is configuring your post-build to generate the PO files (right click on the project > Properties > Build Events):

Post-Build I18n

The second is installing the extension i18n.POTGenerator.vsix which generates a button in the Solution Explorer for on-demand compilation:

Botão I18n

A directory will be generated in the solution locale with the PO files in their respective culture directories. You will need to include them in the solution to versioning them:

Arquivos PO

The best tool I know for editing these files is Poedit, which is free and also has paid version.

POEdit

Done all these steps, the view of the translated page can be seen as well (as in the first method):

http://teste:12345/Produtos
http://teste:12345/en-US/Produtos
http://teste:12345/pt-BR/Produtos

3. Using Code52 i18n, using Javascript

There is a Nuget package that is integrated with jQuery Globalize, but at the time (2013/2014) I tried to use it didn’t work very well, which is the Code52 i18n:

http://code52.org/aspnet-internationalization/tutorial.html

From what I’ve researched, there is still no version of it for the MVC5 (and it probably won’t even exist, since the Github of it is not updated since 2012). It is possible to risk the version for MVC4, but the result may be unpredictable.


For all intents and purposes, I use the first option, which is homemade but which has not let me down by then, and is very simple to understand.

  • 1

    Okay Gypsy, thank you for the answer. What about internationalization within cshtml, which does not directly involve the model? The contents of a Section for example, or the menu.

  • 1

    @Vinicius Follow the issues. I’m writing about it right now.

  • 2

    I think that’s it. If I have any news I update.

  • @Ciganomorrisonmendez , I am in 2016 using Asp.net mvc5, this is still the best method to do internationalization?

  • @Alexandrelima Yes, they are the ones I teach in my course, by the way.

  • @Ciganomorrisonmendez , ok cool, mode 1, if the user does not click on any link, it picks up according to the language of the browser?

  • If the browser culture is implemented, yes. Otherwise, it takes the default.

Show 2 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.