Ответ 1
Используйте отдельный проект с ресурсами
Я могу сказать это по собственному опыту, имея текущее решение с 12 24 проектами, которое включает API, MVC, Библиотеки проектов (основные функции), WPF, UWP и Xamarin. Стоит прочитать этот длинный пост, так как я думаю, что это лучший способ сделать это. С помощью инструментов VS их легко экспортировать и импортировать для отправки в бюро переводов или просмотра другими людьми.
ОБНОВЛЕНИЕ 02/2018: По-прежнему сильны, преобразование его в стандартную библиотеку .NET позволяет даже использовать его в .NET Framework и NET Core. Я добавил дополнительный раздел для преобразования его в JSON, чтобы, например, Angular мог его использовать.
ОБНОВЛЕНИЕ 2019: В дальнейшем с Xamarin, это все еще работает на всех платформах. Например. Xamarin.Forms также рекомендует использовать файлы resx. (Я еще не разрабатывал приложение в Xamarin.Forms, но документация, которая является подробным, чтобы только начать, охватывает его: Документация Xamarin.Forms). Точно так же, как преобразование его в JSON, мы также можем преобразовать его в XML файл для Xamarin.Android.
ОБНОВЛЕНИЕ 2019 (2): При обновлении до UWP из WPF я обнаружил, что в UWP они предпочитают использовать другой тип файла .resw
, который с точки зрения содержания идентичен, но использование другое. Я нашел другой способ сделать это, который, на мой взгляд, работает лучше, чем стандартное решение.
Итак, давайте перейдем к этому.
Pro
- Сильно набран почти везде.
- В WPF вам не нужно иметь дело с
ResourceDirectories
. - Насколько я тестировал, поддерживается для ASP.NET, библиотек классов, WPF, Xamarin,.NET Core,.NET Standard.
- Никаких дополнительных сторонних библиотек не требуется.
- Поддерживает отступление культуры: en-US → en.
- Не только бэкэнд, работает также в XAML для WPF и Xamarin.Forms, в .cshtml для MVC.
- Легко манипулируйте языком, изменяя
Thread.CurrentThread.CurrentCulture
- Поисковые системы могут сканировать на разных языках, а пользователь может отправлять или сохранять URL-адреса для конкретных языков.
Con
- WPF XAML иногда глючит, недавно добавленные строки не отображаются напрямую. Перестройка - это временное исправление (vs2015).
- UWP XAML не отображает предложения Intellisense и не отображает текст во время разработки.
- Скажи мне.
Настройка
Создайте языковой проект в своем решении, присвойте ему имя, например MyProject.Language. Добавьте в него папку с именем Resources и в этой папке создайте два файла Resources (.resx). Один называется Resources.resx, а другой - Resources.en.resx (или .en-GB.resx для конкретных целей). В моей реализации я использую язык NL (голландский) в качестве языка по умолчанию, так что это входит в мой первый файл, а английский - во второй файл.
Настройка должна выглядеть следующим образом:
Свойства для Resources.resx должны быть:
Убедитесь, что для пространства имен настраиваемого инструмента задано пространство имен вашего проекта. Причина этого в том, что в WPF нельзя ссылаться на Resources
внутри XAML.
А внутри файла ресурса установите для модификатора доступа значение Public:
Использование в другом проекте
Ссылка на ваш проект: Щелкните правой кнопкой мыши на References → Add Reference → Prjects\Solutions.
Использовать пространство имен в файле: using MyProject.Language;
Используйте это так в back-end:
string someText = Resources.orderGeneralError;
Если есть что-то еще, называемое "Ресурсы", просто поместите все пространство имен.
Использование в MVC
В MVC вы можете делать все, что хотите, чтобы установить язык, но я использовал параметризованные URL, которые можно настроить следующим образом:
RouteConfig.cs Ниже других отображений
routes.MapRoute(
name: "Locolized",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = @"(\w{2})|(\w{2}-\w{2})" }, // en or en-US
defaults: new { controller = "shop", action = "index", id = UrlParameter.Optional }
);
FilterConfig.cs (возможно, потребуется добавить, если так, добавьте FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
к методу Application_start()
в Global.asax
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ErrorHandler.AiHandleErrorAttribute());
//filters.Add(new HandleErrorAttribute());
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 просто устанавливает информацию о культуре.
//fixed number and date format for now, this can be improved.
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;
//later, we will if-else the language here
info.DateTimeFormat.DateSeparator = "/";
info.DateTimeFormat.ShortDatePattern = "dd/MM/yyyy";
Thread.CurrentThread.CurrentUICulture = info;
Thread.CurrentThread.CurrentCulture = info;
}
catch (Exception)
{
}
}
Использование в .cshtml
@using MyProject.Language;
<h3>@Resources.w_home_header</h3>
или если вы не хотите определять использование, просто заполните все пространство имен ИЛИ вы можете определить пространство имен в /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>
Это учебное пособие по реализации mvc: Потрясающий учебный блог
Использование в библиотеках классов для моделей
Внутреннее использование - то же самое, но только пример для использования в атрибутах
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; }
}
}
Если у вас есть Reshaper, он автоматически проверит, существует ли данное имя ресурса. Если вы предпочитаете безопасность типов, вы можете использовать шаблоны T4 для создания перечисления
Использование в WPF.
Конечно, добавьте ссылку на ваше пространство имен MyProject.Language, мы знаем, как ее использовать в бэк-энде.
В XAML, внутри заголовка Window или UserControl, добавьте ссылку на пространство имен с именем lang
следующим образом:
<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">
Затем внутри ярлыка:
<Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>
Поскольку он строго типизирован, вы уверены, что строка ресурса существует. Возможно, вам придется иногда перекомпилировать проект во время установки, WPF иногда глючит с новыми пространствами имен.
Еще одна вещь для WPF, установите язык внутри App.xaml.cs
. Вы можете сделать свою собственную реализацию (выбрать во время установки) или позволить системе принять решение.
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://default english because there can be so many different system language, we rather fallback on english in this case.
MyProject.Language.Resources.Culture = new System.Globalization.CultureInfo("en-GB");
break;
}
}
}
Использование в UWP
В UWP Microsoft использует это решение, что означает, что вам нужно будет создавать новые файлы ресурсов. Кроме того, вы также не можете повторно использовать текст, потому что они хотят, чтобы вы установили x:Uid
вашего элемента управления в XAML как ключ ваших ресурсов. И в ваших ресурсах вы должны сделать Example.Text
, чтобы заполнить текст TextBlock
. Мне совсем не понравилось это решение, потому что я хочу повторно использовать свои файлы ресурсов. В конце концов я пришел к следующему решению. Я только что узнал об этом сегодня (2019-09-26), поэтому я мог бы вернуться с чем-то еще, если окажется, что это не работает должным образом.
Добавьте это в свой проект:
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);
}
}
Добавьте это к App.xaml.cs
в конструкторе:
CustomXamlResourceLoader.Current = new MyXamlResourceLoader();
В любом месте приложения вы можете изменить язык:
ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());
Последняя строка необходима для обновления пользовательского интерфейса. Пока я еще работаю над этим проектом, я заметил, что мне нужно сделать это 2 раза. Я мог бы в конечном итоге выбрать язык в первый раз, когда пользователь запускает. Но так как он будет распространяться через Windows Store, язык обычно равен системному языку.
Затем используйте в XAML:
<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>
Использование его в Angular (преобразовать в JSON)
В наши дни более распространено иметь фреймворк, такой как Angular, в сочетании с компонентами, то есть без cshtml. Переводы хранятся в файлах json, я не собираюсь рассказывать о том, как это работает, но если вы хотите преобразовать это в файл JSON, это довольно просто, я использую шаблонный скрипт T4, который преобразует файл ресурсов в файл json. Я рекомендую установить редактор T4, чтобы прочитать синтаксис и правильно его использовать, потому что вам нужно внести некоторые изменения.
Единственное, на что следует обратить внимание: невозможно сгенерировать данные, скопировать их, очистить данные и сгенерировать их для другого языка. Таким образом, вы должны скопировать приведенный ниже код столько раз, сколько у вас языков, и изменить запись перед тем, как "//выберите язык здесь". В настоящее время нет времени, чтобы исправить это, но, вероятно, будет обновляться позже (если интересно).
Путь: MyProject.Language/T4/CreateWebshopLocalizationEN.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/CreateWebshopLocalizationEN.json";//choose language here
var fileResultPath = Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("")), "MyProject.Language", fileResultName);
//var fileDestinationPath = "../../MyProject.Web/ClientApp/app/i18n/";
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 }; //choose language here
string[] fileNamesDest = new string[] {fileNameDestEn }; //choose language here
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);
}
#>
Используйте в Xamarin.Android
Как объяснялось выше в обновлениях, я использую тот же метод, что и в Angular/JSON. Но Android использует файлы XML, поэтому я написал файл T4, который генерирует эти файлы XML.
Путь: 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;
//if(!name.ToString().Contains("WEBSHOP_") && !name.ToString().Contains("DASHBOARD_"))//only include keys with these prefixes, or the country ones.
//{
// if(name.ToString().Length != 2)
// {
// continue;
// }
//}
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("<<", "");
//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 работает с папками values-xx
, поэтому выше для английского языка в папке values-en
. Но вы также должны сгенерировать значение по умолчанию, которое помещается в папку values
. Просто скопируйте шаблон T4 выше и измените папку в приведенном выше коде.
Теперь вы можете использовать один файл ресурсов для всех ваших проектов. Это позволяет очень легко экспортировать все в отдельный документ и позволить кому-то перевести его и импортировать снова.
Отдельное спасибо этому удивительному расширению VS, которое прекрасно работает с файлами resx
. Подумайте о пожертвовании ему за его потрясающую работу (я не имею к этому никакого отношения, мне просто нравится расширение).