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).
He visto proyectos implementados utilizando varios enfoques diferentes, cada uno tiene sus méritos e inconvenientes.
Yo diría que el método de recursos que ha elegido tiene mucho sentido. También sería interesante ver las respuestas de otras personas, ya que a menudo me pregunto si hay una mejor manera de hacer cosas como esta. He visto numerosos recursos que apuntan al método de uso de recursos, incluido uno aquí en SO .
fuente
No creo que haya una "mejor manera". Realmente dependerá de las tecnologías y el tipo de aplicación que esté creando.
Las aplicaciones web pueden almacenar la información en la base de datos como han sugerido otros carteles, pero recomiendo usar archivos de recursos separados. Es decir archivos de recursos separada de su fuente . Los archivos de recursos separados reducen la contención por los mismos archivos y, a medida que su proyecto crezca, es posible que la localización se realice por separado de la lógica empresarial. (Programadores y Traductores).
Los gurús de Microsoft WinForm y WPF recomiendan usar ensamblados de recursos separados personalizados para cada configuración regional.
La capacidad de WPF para ajustar el tamaño de los elementos de la interfaz de usuario al contenido reduce el trabajo de diseño requerido, por ejemplo: (las palabras en japonés son mucho más cortas que en inglés).
Si está considerando WPF: sugiero leer este artículo de msdn. Para ser sincero, las herramientas de localización de WPF: msbuild, locbaml (y tal vez una hoja de cálculo de Excel) son tediosas de usar, pero funcionan.
Algo solo ligeramente relacionado: un problema común al que me enfrento es la integración de sistemas heredados que envían mensajes de error (generalmente en inglés), no códigos de error. Esto obliga a realizar cambios en los sistemas heredados o asignar cadenas de backend a mis propios códigos de error y luego a cadenas localizadas ... Los códigos de error son amigos de las localizaciones
fuente
+1 Base de datos
Los formularios de su aplicación pueden incluso volver a traducirse sobre la marcha si se realizan correcciones en la base de datos.
Usamos un sistema en el que todos los controles se asignaban en un archivo XML (uno por formulario) a los ID de recursos de idioma, pero todos los ID estaban en la base de datos.
Básicamente, en lugar de que cada control contenga el ID (implementando una interfaz o usando la propiedad de la etiqueta en VB6), usamos el hecho de que en .NET, el árbol de control era fácilmente detectable mediante la reflexión. Un proceso en el que el formulario cargado generaría el archivo XML si faltaba. El archivo XML asignaría los controles a sus ID de recursos, por lo que simplemente era necesario completarlo y asignarlo a la base de datos. Esto significaba que no había necesidad de cambiar el binario compilado si algo no estaba etiquetado, o si era necesario dividirlo en otra ID (algunas palabras en inglés que podrían usarse como sustantivos y verbos podrían necesitar traducirse a dos palabras diferentes en el diccionario y no se puede volver a utilizar, pero es posible que no lo descubra durante la asignación inicial de ID).
Los únicos en los que la aplicación se involucra más es cuando se usa una fase con puntos de inserción.
El software de traducción de la base de datos era su pantalla básica de mantenimiento CRUD con varias opciones de flujo de trabajo para facilitar la revisión de las traducciones faltantes, etc.
fuente
He estado buscando y he encontrado esto:
Si usa WPF o Silverlight, su enfoque podría ser WPF LocalizationExtension por muchas razones.
Es de código abierto Es GRATIS (y seguirá siendo gratuito) está en un estado real
En una aplicación de Windows, puede hacer algo como esto:
public partial class App : Application { public App() { } protected override void OnStartup(StartupEventArgs e) { Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE"); ; Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("de-DE"); ; FrameworkElement.LanguageProperty.OverrideMetadata( typeof(FrameworkElement), new FrameworkPropertyMetadata( XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); base.OnStartup(e); } }
Y creo que en una página web el enfoque podría ser el mismo.
¡Buena suerte!
fuente
Yo iría con los archivos de recursos múltiples. No debería ser tan difícil de configurar. De hecho, recientemente respondí una pregunta similar sobre la configuración de archivos de recursos basados en un idioma global junto con los archivos de recursos del idioma de formulario.
Localización en Visual Studio 2008
Consideraría que es el mejor enfoque al menos para el desarrollo de WinForm.
fuente
Puede utilizar herramientas comerciales como Sisulizer . Creará un ensamblaje satélite para cada idioma. Lo único que debe prestar atención es no ofuscar los nombres de las clases de formularios (si usa ofuscator).
fuente
La mayoría de los proyectos de código abierto utilizan GetText para este propósito. No sé cómo y si alguna vez se ha utilizado en un proyecto .Net antes.
fuente
Usamos un proveedor personalizado para soporte en varios idiomas y colocamos todos los textos en una tabla de base de datos. Funciona bien, excepto que a veces nos enfrentamos a problemas de almacenamiento en caché al actualizar textos en la base de datos sin actualizar la aplicación web.
fuente
Los archivos de recursos estándar son más fáciles. Sin embargo, si tiene datos que dependen del idioma, como tablas de búsqueda, tendrá que administrar dos conjuntos de recursos.
No lo he hecho, pero en mi próximo proyecto implementaría un proveedor de recursos de base de datos. Encontré cómo hacerlo en MSDN:
http://msdn.microsoft.com/en-us/library/aa905797.aspx
También encontré esta implementación:
Proveedor de DBResource
fuente