Use um projeto separado com recursos
Posso dizer isso por experiência própria, tendo uma solução atual com 12 24 projetos que inclui API, MVC, Bibliotecas de Projetos (funcionalidades do Core), WPF, UWP e Xamarin. Vale a pena ler este longo post, pois acho que é a melhor maneira de fazer isso. Com a ajuda de ferramentas VS facilmente exportáveis e importáveis para enviar para agências de tradução ou revisão por outras pessoas.
EDITAR 02/2018: Ainda forte, convertê-lo em uma biblioteca .NET Standard torna possível até mesmo usá-lo em .NET Framework e NET Core. Eu adicionei uma seção extra para convertê-lo em JSON para que, por exemplo, o angular possa usá-lo.
EDITAR 2019: Avançando com o Xamarin, ainda funciona em todas as plataformas. Por exemplo, conselhos do Xamarin.Forms para usar arquivos resx também. (Eu não desenvolvi um aplicativo em Xamarin.Forms ainda, mas a documentação, que é detalhada para começar, cobre: Documentação do Xamarin.Forms ). Assim como convertê-lo para JSON, também podemos convertê-lo em um arquivo .xml para Xamarin.Android.
EDIT 2019 (2): Ao atualizar para UWP a partir do WPF, descobri que na UWP eles preferem usar outro tipo de arquivo .resw
, que é idêntico em termos de conteúdo, mas o uso é diferente. Eu encontrei uma maneira diferente de fazer isso que, na minha opinião, funciona melhor do que a solução padrão .
EDIT 2020: Atualizado algumas sugestões para projetos maiores (modulair) que podem exigir vários projetos de idioma.
Então vamos fazer isso.
Pro's
- Fortemente digitado em quase todos os lugares.
- No WPF você não tem que lidar
ResourceDirectories
.
- Compatível com ASP.NET, Bibliotecas de classes, WPF, Xamarin, .NET Core, .NET Standard até onde testei.
- Nenhuma biblioteca extra de terceiros necessária.
- Suporta fallback de cultura: en-US -> en.
- Não apenas back-end, funciona também em XAML para WPF e Xamarin.Forms, em .cshtml para MVC.
- Manipule facilmente o idioma alterando o
Thread.CurrentThread.CurrentCulture
- Os mecanismos de pesquisa podem rastrear em diferentes idiomas e o usuário pode enviar ou salvar urls específicas de um idioma.
Con's
- Às vezes, o WPF XAML apresenta erros, as cadeias de caracteres recém-adicionadas não aparecem diretamente. Reconstruir é a correção temporária (vs2015).
- O UWP XAML não mostra sugestões de intellisense e não mostra o texto durante o projeto.
- Conte-me.
Configuração
Crie um projeto de linguagem em sua solução, dê a ele um nome como MyProject.Language . Adicione uma pasta chamada Recursos e, nessa pasta, crie dois arquivos de Recursos (.resx). Um denominado Resources.resx e outro denominado Resources.en.resx (ou .en-GB.resx para específico). Na minha implementação, tenho o idioma NL (holandês) como o idioma padrão, então isso vai no meu primeiro arquivo e o inglês vai no meu segundo arquivo.
A configuração deve ser semelhante a esta:
As propriedades de Resources.resx devem ser:
Certifique-se de que o namespace da ferramenta personalizada esteja definido como o namespace do seu projeto. A razão para isso é que no WPF, você não pode fazer referência a Resources
dentro do XAML.
E dentro do arquivo de recurso, defina o modificador de acesso como Público:
Se você tiver um aplicativo tão grande (digamos, módulos diferentes), você pode considerar a criação de vários projetos como acima. Nesse caso, você pode prefixar suas Chaves e classes de recursos com o Módulo específico. Use o melhor editor de linguagem existente para o Visual Studio para combinar todos os arquivos em uma única visão geral.
Usando em outro projeto
Referência ao seu projeto: Clique com o botão direito em References -> Add Reference -> Prjects \ Solutions.
Use namespace em um arquivo: using MyProject.Language;
Use-o assim no back-end:
string someText = Resources.orderGeneralError;
se houver outra coisa chamada Recursos, basta inserir o namespace inteiro.
Usando em MVC
Em MVC você pode fazer o que quiser para definir o idioma, mas eu usei urls parametrizadas, que podem ser configuradas da seguinte forma:
RouteConfig.cs
Abaixo dos outros mapeamentos
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 (pode precisar ser adicionado, se assim for, adicione FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
ao Application_start()
método emGlobal.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 apenas define as informações 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 em .cshtml
@using MyProject.Language;
<h3>@Resources.w_home_header</h3>
ou se você não quiser definir usings, basta preencher todo o namespace OU você pode definir o namespace em /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 da fonte de implementação do mvc: Blog de tutorial incrível
Usando bibliotecas de classe para modelos
O uso de back-end é o mesmo, mas apenas um exemplo de uso em 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; }
}
}
Se você tiver remodelado, ele verificará automaticamente se o nome do recurso fornecido existe. Se você preferir segurança de tipo, você pode usar modelos T4 para gerar um enum
Usando em WPF.
Claro, adicione uma referência ao seu namespace MyProject.Language , sabemos como usá-lo no back-end.
Em XAML, dentro do cabeçalho de uma janela ou UserControl, adicione uma referência de namespace chamada lang
assim:
<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">
Então, dentro de um rótulo:
<Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>
Uma vez que é fortemente tipado, você tem certeza de que a string de recurso existe. Pode ser necessário recompilar o projeto às vezes durante a configuração, às vezes o WPF apresenta erros com novos namespaces.
Mais uma coisa para o WPF, defina o idioma dentro do App.xaml.cs
. Você pode fazer sua própria implementação (escolha durante a instalação) ou deixar o sistema decidir.
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;
}
}
}
Usando em UWP
Na UWP, a Microsoft usa essa solução , o que significa que você precisará criar novos arquivos de recursos. Além disso, você não pode reutilizar o texto porque eles querem que você defina o x:Uid
de seu controle em XAML como uma chave em seus recursos. E em seus recursos você tem que fazer Example.Text
para preencher um TextBlock
texto de. Eu não gostei dessa solução porque quero reutilizar meus arquivos de recursos. Eventualmente, encontrei a seguinte solução. Acabei de descobrir isso hoje (26/09/2019), então posso voltar com outra coisa, se isso não funcionar como desejado.
Adicione isto ao seu projeto:
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);
}
}
Adicione isto a App.xaml.cs
no construtor:
CustomXamlResourceLoader.Current = new MyXamlResourceLoader();
Sempre que você quiser em seu aplicativo, use isto para alterar o idioma:
ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());
A última linha é necessária para atualizar a IU. Enquanto ainda estou trabalhando neste projeto, percebi que precisava fazer isso 2 vezes. Posso acabar com uma seleção de idioma na primeira vez que o usuário iniciar. Mas, como será distribuído por meio da Windows Store, o idioma geralmente é igual ao idioma do sistema.
Em seguida, use em XAML:
<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>
Usando no Angular (converter para JSON)
Hoje em dia é mais comum ter um framework como o Angular em combinação com componentes, portanto, sem cshtml. As traduções são armazenadas em arquivos json, não vou cobrir como isso funciona, eu recomendo fortemente o ngx-translate em vez da multi-tradução angular. Portanto, se você deseja converter traduções em um arquivo JSON, é muito fácil, eu uso um script de modelo T4 que converte o arquivo de Recursos em um arquivo json. Eu recomendo instalar o editor T4 para ler a sintaxe e usá-lo corretamente porque você precisa fazer algumas modificações.
Apenas uma coisa a se notar: não é possível gerar os dados, copiá-los, limpar os dados e gerá-los para outro idioma. Portanto, você deve copiar o código abaixo tantas vezes quanto os idiomas que possui e alterar a entrada antes de '// escolher o idioma aqui'. Atualmente não há tempo para consertar isso, mas provavelmente atualizarei mais tarde (se estiver interessado).
Caminho: 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);
}
#>
Se você tem um aplicativo modulair e seguiu minha sugestão de criar projetos em vários idiomas, terá que criar um arquivo T4 para cada um deles. Certifique-se de que os arquivos json estão definidos logicamente, não precisa ser en.json
, também pode ser example-en.json
. Para combinar vários arquivos json para usar com ngx-translate , siga as instruções aqui
Use no Xamarin.Android
Conforme explicado acima nas atualizações, eu uso o mesmo método que usei com Angular / JSON. Mas o Android usa arquivos XML, então escrevi um arquivo T4 que gera esses arquivos XML.
Caminho: 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);
}
#>
O Android funciona com values-xx
pastas, portanto, acima é para Inglês para na values-en
pasta. Mas você também deve gerar um padrão que vai para a values
pasta. Basta copiar o modelo T4 acima e alterar a pasta no código acima.
Pronto, agora você pode usar um único arquivo de recurso para todos os seus projetos. Isso torna muito fácil exportar tudo para um documento excl e permitir que alguém o traduza e importe novamente.
Agradecimentos especiais a esta extensão VS incrível que funciona muito bem com resx
arquivos. Considere fazer uma doação a ele por seu trabalho incrível (não tenho nada a ver com isso, simplesmente amo a extensão).
Já vi projetos implementados usando uma série de abordagens diferentes, cada uma com seus méritos e desvantagens.
Eu diria que o método de recurso que você escolheu faz muito sentido. Seria interessante ver as respostas de outras pessoas também, pois muitas vezes me pergunto se há uma maneira melhor de fazer coisas assim. Eu vi vários recursos que apontam para o método de recursos de uso, incluindo um aqui no SO .
fonte
Não acho que haja uma "melhor maneira". Realmente dependerá das tecnologias e do tipo de aplicativo que você está construindo.
Os aplicativos da Web podem armazenar as informações no banco de dados como sugeriram outros pôsteres, mas eu recomendo o uso de arquivos de recursos separados. Ou seja , os arquivos de recursos são separados da sua fonte . Arquivos de recursos separados reduzem a contenção pelos mesmos arquivos e, à medida que seu projeto cresce, você pode descobrir que a localização será feita separadamente da lógica de negócios. (Programadores e Tradutores).
Os gurus do Microsoft WinForm e do WPF recomendam o uso de assemblies de recursos separados, personalizados para cada local.
A capacidade do WPF de dimensionar elementos da IU para o conteúdo diminui o trabalho de layout necessário, por exemplo: (palavras em japonês são muito mais curtas do que em inglês).
Se você está considerando o WPF: Eu sugiro a leitura deste artigo do msdn. Para ser sincero, achei as ferramentas de localização do WPF: msbuild, locbaml (e talvez uma planilha do Excel) tediosas de usar, mas funcionam.
Algo apenas ligeiramente relacionado: um problema comum que enfrento é a integração de sistemas legados que enviam mensagens de erro (geralmente em inglês), não códigos de erro. Isso força a mudança para sistemas legados ou mapeamento de strings de back-end para meus próprios códigos de erro e, em seguida, para strings localizadas ... yech. Códigos de erro são amigos de localizações
fonte
+1 Banco de dados
Os formulários em seu aplicativo podem até se traduzir novamente em tempo real se as correções forem feitas no banco de dados.
Usamos um sistema em que todos os controles foram mapeados em um arquivo XML (um por formulário) para IDs de recursos de linguagem, mas todos os IDs estavam no banco de dados.
Basicamente, em vez de fazer com que cada controle contenha o ID (implementando uma interface ou usando a propriedade tag no VB6), usamos o fato de que no .NET a árvore de controle era facilmente descoberta por meio de reflexão. Um processo quando o formulário carregado iria construir o arquivo XML se estivesse faltando. O arquivo XML mapearia os controles para seus IDs de recursos, portanto, isso simplesmente precisava ser preenchido e mapeado para o banco de dados. Isso significa que não há necessidade de alterar o binário compilado se algo não estiver marcado ou se precisar ser dividido em outro ID (algumas palavras em inglês que podem ser usadas como substantivos e verbos podem precisar ser traduzidas para duas palavras diferentes no dicionário e não ser reutilizado, mas você pode não descobrir isso durante a atribuição inicial de IDs).
Os únicos em que o aplicativo fica mais envolvido é quando uma fase com pontos de inserção é usada.
O software de tradução de banco de dados era sua tela de manutenção CRUD básica com várias opções de fluxo de trabalho para facilitar a passagem pelas traduções que faltavam, etc.
fonte
Estive pesquisando e encontrei o seguinte:
Se você está usando WPF ou Silverlight, sua abordagem pode usar WPF LocalizationExtension por muitos motivos.
Código aberto de TI É GRATUITO (e permanecerá gratuito) está em um estado real de estabilidade
Em um aplicativo do Windows, você pode fazer algo assim:
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); } }
E eu acho que em uma página Wep a abordagem poderia ser a mesma.
Boa sorte!
fonte
Eu iria com os vários arquivos de recursos. Não deve ser tão difícil de configurar. Na verdade, recentemente respondi a uma pergunta semelhante sobre a configuração de arquivos de recursos baseados em linguagem global em conjunto com arquivos de recursos de linguagem de formulário.
Localização no Visual Studio 2008
Eu consideraria essa a melhor abordagem pelo menos para o desenvolvimento do WinForm.
fonte
Você pode usar ferramentas comerciais como Sisulizer . Ele criará um assembly satélite para cada idioma. A única coisa que você deve prestar atenção é não ofuscar os nomes das classes de formulário (se você usar o ofuscador).
fonte
A maioria dos projetos de código aberto usa GetText para essa finalidade. Não sei como e se já foi usado em um projeto .Net antes.
fonte
Usamos um provedor personalizado para suporte a vários idiomas e colocamos todos os textos em uma tabela de banco de dados. Ele funciona bem, exceto que às vezes enfrentamos problemas de cache ao atualizar textos no banco de dados sem atualizar o aplicativo da web.
fonte
Arquivos de recursos padrão são mais fáceis. No entanto, se você tiver dados dependentes de idioma, como tabelas de pesquisa, terá que gerenciar dois conjuntos de recursos.
Não fiz isso, mas em meu próximo projeto implementaria um provedor de recursos de banco de dados. Descobri como fazer isso no MSDN:
http://msdn.microsoft.com/en-us/library/aa905797.aspx
Também encontrei esta implementação:
Provedor DBResource
fonte