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).
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.
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
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):
The second is installing the extension i18n.POTGenerator.vsix
which generates a button in the Solution Explorer
for on-demand compilation:
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:
The best tool I know for editing these files is Poedit, which is free and also has paid version.
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.
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.
– Vinícius
@Vinicius Follow the issues. I’m writing about it right now.
– Leonel Sanches da Silva
I think that’s it. If I have any news I update.
– Leonel Sanches da Silva
@Ciganomorrisonmendez , I am in 2016 using Asp.net mvc5, this is still the best method to do internationalization?
– Alexandre Lima
@Alexandrelima Yes, they are the ones I teach in my course, by the way.
– Leonel Sanches da Silva
@Ciganomorrisonmendez , ok cool, mode 1, if the user does not click on any link, it picks up according to the language of the browser?
– Alexandre Lima
If the browser culture is implemented, yes. Otherwise, it takes the default.
– Leonel Sanches da Silva