Use un proyecto separado con Recursos
Puedo decir esto por nuestra experiencia, teniendo una solución actual con 12 24 proyectos que incluye API, MVC, Bibliotecas de proyectos (funcionalidades principales), WPF, UWP y Xamarin. Vale la pena leer este extenso post porque creo que es la mejor manera de hacerlo. Con la ayuda de herramientas VS fácilmente exportables e importables para ser enviadas a agencias de traducción o revisadas por otras personas.
EDITAR 02/2018: Aún siendo fuerte, convertirlo a una biblioteca .NET Standard hace posible incluso usarlo en .NET Framework y NET Core. Agregué una sección adicional para convertirlo a JSON para que, por ejemplo, angular pueda usarlo.
EDITAR 2019: en el futuro con Xamarin, esto todavía funciona en todas las plataformas. Por ejemplo, Xamarin.Forms recomienda usar archivos resx también. (Todavía no desarrollé una aplicación en Xamarin.Forms, pero la documentación, que es muy detallada para comenzar, la cubre: Documentación de Xamarin.Forms ). Al igual que convertirlo a JSON, también podemos convertirlo en un archivo .xml para Xamarin.Android.
EDITAR 2019 (2): Al actualizar a UWP desde WPF, encontré que en UWP prefieren usar otro tipo de archivo .resw
, que es idéntico en términos de contenido pero el uso es diferente. Encontré una forma diferente de hacer esto que, en mi opinión, funciona mejor que la solución predeterminada .
EDITAR 2020: se actualizaron algunas sugerencias para proyectos más grandes (modulares) que pueden requerir proyectos en varios idiomas.
Vamos a por ello.
Pro's
- Fuertemente mecanografiado en casi todas partes.
- En WPF no tienes que lidiar con
ResourceDirectories
.
- Compatible con ASP.NET, bibliotecas de clases, WPF, Xamarin, .NET Core, .NET Standard hasta donde he probado.
- No se necesitan bibliotecas de terceros adicionales.
- Admite respaldo cultural: en-US -> en.
- No solo back-end, también funciona en XAML para WPF y Xamarin.Forms, en .cshtml para MVC.
- Manipule fácilmente el idioma cambiando el
Thread.CurrentThread.CurrentCulture
- Los motores de búsqueda pueden rastrear en diferentes idiomas y el usuario puede enviar o guardar URL específicas del idioma.
Contras
- WPF XAML a veces tiene errores, las cadenas recién agregadas no se muestran directamente. Reconstruir es la solución temporal (frente a 2015).
- UWP XAML no muestra sugerencias de intellisense y no muestra el texto durante el diseño.
- Dime.
Preparar
Cree un proyecto de lenguaje en su solución, asígnele un nombre como MyProject.Language . Agregue una carpeta llamada Recursos y, en esa carpeta, cree dos archivos de Recursos (.resx). Uno llamado Resources.resx y otro llamado Resources.en.resx (o .en-GB.resx para específicos). En mi implementación, tengo el idioma NL (holandés) como idioma predeterminado, por lo que va en mi primer archivo y el inglés en mi segundo archivo.
La configuración debería verse así:
Las propiedades de Resources.resx deben ser:
Asegúrese de que el espacio de nombres de la herramienta personalizada esté configurado en el espacio de nombres de su proyecto. La razón de esto es que en WPF, no puede hacer referencia a Resources
dentro de XAML.
Y dentro del archivo de recursos, establezca el modificador de acceso en Público:
Si tiene una aplicación tan grande (digamos diferentes módulos), puede considerar la posibilidad de crear varios proyectos como el anterior. En ese caso, puede prefijar sus claves y clases de recursos con el módulo en particular. Utilice el mejor editor de idioma que existe para Visual Studio para combinar todos los archivos en una sola descripción general.
Usar en otro proyecto
Referencia a su proyecto: haga clic con el botón derecho en Referencias -> Agregar referencia -> Proyectos \ Soluciones.
Use el espacio de nombres en un archivo: using MyProject.Language;
Úselo así en el back-end:
string someText = Resources.orderGeneralError;
si hay algo más llamado Recursos, simplemente ingrese el espacio de nombres completo.
Usando en MVC
En MVC puede hacer lo que quiera para configurar el idioma, pero utilicé URL parametrizadas, que se pueden configurar así:
RouteConfig.cs
debajo de las otras asignaciones
routes.MapRoute(
name: "Locolized",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = @"(\w{2})|(\w{2}-\w{2})" },
defaults: new { controller = "shop", action = "index", id = UrlParameter.Optional }
);
FilterConfig.cs (es posible que deba agregarse, si es así, agréguelo FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
al Application_start()
método enGlobal.asax
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ErrorHandler.AiHandleErrorAttribute());
filters.Add(new LocalizationAttribute("nl-NL"), 0);
}
}
LocalizationAttribute
public class LocalizationAttribute : ActionFilterAttribute
{
private string _DefaultLanguage = "nl-NL";
private string[] allowedLanguages = { "nl", "en" };
public LocalizationAttribute(string defaultLanguage)
{
_DefaultLanguage = defaultLanguage;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string lang = (string) filterContext.RouteData.Values["lang"] ?? _DefaultLanguage;
LanguageHelper.SetLanguage(lang);
}
}
LanguageHelper solo establece la información de Cultura.
public static void SetLanguage(LanguageEnum language)
{
string lang = "";
switch (language)
{
case LanguageEnum.NL:
lang = "nl-NL";
break;
case LanguageEnum.EN:
lang = "en-GB";
break;
case LanguageEnum.DE:
lang = "de-DE";
break;
}
try
{
NumberFormatInfo numberInfo = CultureInfo.CreateSpecificCulture("nl-NL").NumberFormat;
CultureInfo info = new CultureInfo(lang);
info.NumberFormat = numberInfo;
info.DateTimeFormat.DateSeparator = "/";
info.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
Thread.CurrentThread.CurrentUICulture = info;
Thread.CurrentThread.CurrentCulture = info;
}
catch (Exception)
{
}
}
Uso en .cshtml
@using MyProject.Language;
<h3>@Resources.w_home_header</h3>
o si no desea definir usos, simplemente complete todo el espacio de nombres O puede definir el espacio de nombres en /Views/web.config:
<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="MyProject.Language" />
</namespaces>
</pages>
</system.web.webPages.razor>
Este tutorial de la fuente de implementación de mvc: blog de tutoriales impresionante
Uso de bibliotecas de clases para modelos
El uso de back-end es el mismo, pero solo un ejemplo para usar en atributos
using MyProject.Language;
namespace MyProject.Core.Models
{
public class RegisterViewModel
{
[Required(ErrorMessageResourceName = "accountEmailRequired", ErrorMessageResourceType = typeof(Resources))]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
}
}
Si tiene remodelador, comprobará automáticamente si existe el nombre del recurso dado. Si prefiere la seguridad de tipos, puede usar plantillas T4 para generar una enumeración
Utilizando en WPF.
Por supuesto, agregue una referencia a su espacio de nombres MyProject.Language , sabemos cómo usarlo en el back-end.
En XAML, dentro del encabezado de una ventana o UserControl, agregue una referencia de espacio de nombres llamada lang
así:
<UserControl x:Class="Babywatcher.App.Windows.Views.LoginView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyProject.App.Windows.Views"
xmlns:lang="clr-namespace:MyProject.Language;assembly=MyProject.Language" <!--this one-->
mc:Ignorable="d"
d:DesignHeight="210" d:DesignWidth="300">
Luego, dentro de una etiqueta:
<Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>
Dado que está fuertemente tipado, está seguro de que la cadena de recursos existe. Es posible que deba volver a compilar el proyecto a veces durante la configuración, WPF a veces tiene errores con nuevos espacios de nombres.
Una cosa más para WPF, configure el idioma dentro de App.xaml.cs
. Puede hacer su propia implementación (elegir durante la instalación) o dejar que el sistema decida.
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
SetLanguageDictionary();
}
private void SetLanguageDictionary()
{
switch (Thread.CurrentThread.CurrentCulture.ToString())
{
case "nl-NL":
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("nl-NL");
break;
case "en-GB":
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
break;
default:
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
break;
}
}
}
Utilizando en UWP
En UWP, Microsoft usa esta solución , lo que significa que deberá crear nuevos archivos de recursos. Además, tampoco puede reutilizar el texto porque quieren que establezca el valor x:Uid
de su control en XAML en una clave en sus recursos. Y en tus recursos tienes que hacer Example.Text
para completar un TextBlock
texto. No me gustó esa solución en absoluto porque quiero reutilizar mis archivos de recursos. Finalmente se me ocurrió la siguiente solución. Me acabo de enterar de esto hoy (2019-09-26), así que podría volver con algo más si resulta que esto no funciona como se desea.
Agregue esto a su proyecto:
using Windows.UI.Xaml.Resources;
public class MyXamlResourceLoader : CustomXamlResourceLoader
{
protected override object GetResource(string resourceId, string objectType, string propertyName, string propertyType)
{
return MyProject.Language.Resources.ResourceManager.GetString(resourceId);
}
}
Agregue esto App.xaml.cs
en el constructor:
CustomXamlResourceLoader.Current = new MyXamlResourceLoader();
Donde quiera que quiera en su aplicación, use esto para cambiar el idioma:
ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());
La última línea es necesaria para actualizar la interfaz de usuario. Mientras todavía estoy trabajando en este proyecto, noté que necesitaba hacer esto 2 veces. Podría terminar con una selección de idioma la primera vez que el usuario inicia. Pero dado que esto se distribuirá a través de la Tienda Windows, el idioma suele ser el mismo que el del sistema.
Luego use en XAML:
<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>
Usándolo en Angular (convertir a JSON)
Hoy en día es más común tener un marco como Angular en combinación con componentes, por lo que sin cshtml. Las traducciones se almacenan en archivos json, no voy a explicar cómo funciona, solo recomendaría encarecidamente ngx-translate en lugar de la traducción múltiple angular. Entonces, si desea convertir traducciones a un archivo JSON, es bastante fácil, utilizo un script de plantilla T4 que convierte el archivo de recursos en un archivo json. Recomiendo instalar el editor T4 para leer la sintaxis y usarlo correctamente porque necesita hacer algunas modificaciones.
Solo una cosa a tener en cuenta: no es posible generar los datos, copiarlos, limpiar los datos y generarlos para otro idioma. Por lo tanto, debe copiar el código a continuación tantas veces como idiomas tenga y cambiar la entrada antes de '// elegir idioma aquí'. Actualmente no hay tiempo para arreglar esto, pero probablemente se actualice más tarde (si está interesado).
Ruta: MyProject.Language / T4 / CreateLocalizationEN.tt
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".json" #>
<#
var fileNameNl = "../Resources/Resources.resx";
var fileNameEn = "../Resources/Resources.en.resx";
var fileNameDe = "../Resources/Resources.de.resx";
var fileNameTr = "../Resources/Resources.tr.resx";
var fileResultName = "../T4/CreateLocalizationEN.json";
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
var fileNameDestNl = "nl.json";
var fileNameDestEn = "en.json";
var fileNameDestDe = "de.json";
var fileNameDestTr = "tr.json";
var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();
string[] fileNamesResx = new string[] {fileNameEn };
string[] fileNamesDest = new string[] {fileNameDestEn };
for(int x = 0; x < fileNamesResx.Length; x++)
{
var currentFileNameResx = fileNamesResx[x];
var currentFileNameDest = fileNamesDest[x];
var currentPathResx = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", currentFileNameResx);
var currentPathDest =pathBaseDestination + "/MyProject.Web/ClientApp/app/i18n/" + currentFileNameDest;
using(var reader = new ResXResourceReader(currentPathResx))
{
reader.UseResXDataNodes = true;
#>
{
<#
foreach(DictionaryEntry entry in reader)
{
var name = entry.Key;
var node = (ResXDataNode)entry.Value;
var value = node.GetValue((ITypeResolutionService) null);
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
#>
"<#=name#>": "<#=value#>",
<#
}
#>
"WEBSHOP_LASTELEMENT": "just ignore this, for testing purpose"
}
<#
}
File.Copy(fileResultPath, currentPathDest, true);
}
#>
Si tiene una aplicación modulair y siguió mi sugerencia de crear proyectos en varios idiomas, tendrá que crear un archivo T4 para cada uno de ellos. Asegúrese de que los archivos json estén definidos lógicamente, no tiene que estarlo en.json
, también puede estarlo example-en.json
. Para combinar varios archivos json para usar con ngx-translate , siga las instrucciones aquí
Usar en Xamarin.Android
Como se explicó anteriormente en las actualizaciones, uso el mismo método que he hecho con Angular / JSON. Pero Android usa archivos XML, así que escribí un archivo T4 que genera esos archivos XML.
Ruta: MyProject.Language / T4 / CreateAppLocalizationEN.tt
#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Windows.Forms" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Resources" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.ComponentModel.Design" #>
<#@ output extension=".xml" #>
<#
var fileName = "../Resources/Resources.en.resx";
var fileResultName = "../T4/CreateAppLocalizationEN.xml";
var fileResultRexPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileName);
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
var fileNameDest = "strings.xml";
var pathBaseDestination = Directory.GetParent(Directory.GetParent(this.Host.ResolvePath("")).ToString()).ToString();
var currentPathDest =pathBaseDestination + "/MyProject.App.AndroidApp/Resources/values-en/" + fileNameDest;
using(var reader = new ResXResourceReader(fileResultRexPath))
{
reader.UseResXDataNodes = true;
#>
<resources>
<#
foreach(DictionaryEntry entry in reader)
{
var name = entry.Key;
var node = (ResXDataNode)entry.Value;
var value = node.GetValue((ITypeResolutionService) null);
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\n", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("\r", "");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("&", "&");
if (!String.IsNullOrEmpty(value.ToString())) value = value.ToString().Replace("<<", "");
#>
<string name="<#=name#>">"<#=value#>"</string>
<#
}
#>
<string name="WEBSHOP_LASTELEMENT">just ignore this</string>
<#
#>
</resources>
<#
File.Copy(fileResultPath, currentPathDest, true);
}
#>
Android funciona con values-xx
carpetas, por lo que lo anterior es para inglés en la values-en
carpeta. Pero también debe generar un valor predeterminado que vaya a la values
carpeta. Simplemente copie la plantilla T4 y cambie la carpeta en el código anterior.
Ahí lo tienes, ahora puedes usar un solo archivo de recursos para todos tus proyectos. Esto hace que sea muy fácil exportar todo a un documento excl y dejar que alguien lo traduzca e importe nuevamente.
Un agradecimiento especial a esta increíble extensión VS que funciona de maravilla con resx
archivos. Considere donarle por su increíble trabajo (no tengo nada que ver con eso, me encanta la extensión).