Użyj osobnego projektu z zasobami
Mogę to powiedzieć z naszego doświadczenia, mając aktualne rozwiązanie z 12 24 projektami, które obejmują API, MVC, biblioteki projektów (podstawowe funkcje), WPF, UWP i Xamarin. Warto przeczytać ten długi post, bo uważam, że jest to najlepszy sposób. Za pomocą narzędzi VS można łatwo eksportować i importować do biur tłumaczeń lub recenzować przez inne osoby.
EDYCJA 02/2018: Nadal działa , przekonwertowanie go na bibliotekę .NET Standard umożliwia korzystanie z niej nawet w platformach .NET Framework i NET Core. Dodałem dodatkową sekcję do konwersji na JSON, więc na przykład angular może go używać.
EDYTUJ 2019: Idąc dalej z Xamarin, to nadal działa na wszystkich platformach. Np. Xamarin.Forms porady dotyczące używania plików resx, jak również. (Nie stworzyłem jeszcze aplikacji w Xamarin.Forms, ale dokumentacja, czyli szczegółowa, aby dopiero zacząć, obejmuje to: Dokumentacja Xamarin.Forms ). Podobnie jak przy konwersji do formatu JSON, możemy również przekonwertować go na plik .xml dla platformy Xamarin.Android.
EDYCJA 2019 (2): Podczas uaktualniania do platformy UWP z WPF napotkałem, że w UWP wolą używać innego typu pliku .resw
, który jest identyczny pod względem zawartości, ale użycie jest inne. Znalazłem inny sposób na zrobienie tego, który moim zdaniem działa lepiej niż domyślne rozwiązanie .
EDYCJA 2020: Zaktualizowano niektóre sugestie dotyczące większych (modulair) projektów, które mogą wymagać projektów w wielu językach.
A więc przejdźmy do tego.
Pro's
- Mocno napisane prawie wszędzie.
- W WPF nie musisz się zajmować
ResourceDirectories
.
- Obsługiwane dla ASP.NET, bibliotek klas, WPF, Xamarin, .NET Core, .NET Standard, o ile testowałem.
- Nie są potrzebne żadne dodatkowe biblioteki innych firm.
- Obsługuje rezerwę kultury: en-US -> en.
- Nie tylko zaplecze, działa również w XAML dla WPF i Xamarin.Forms, w .cshtml dla MVC.
- Łatwo manipuluj językiem, zmieniając
Thread.CurrentThread.CurrentCulture
- Wyszukiwarki mogą indeksować w różnych językach, a użytkownik może wysyłać lub zapisywać adresy URL specyficzne dla języka.
Cons
- WPF XAML jest czasami błędny, nowo dodane ciągi nie pojawiają się bezpośrednio. Przebuduj to tymczasowa poprawka (vs2015).
- UWP XAML nie wyświetla sugestii Intellisense i nie wyświetla tekstu podczas projektowania.
- Powiedz mi.
Ustawiać
Utwórz projekt językowy w swoim rozwiązaniu, nadaj mu nazwę taką jak MyProject.Language . Dodaj do niego folder o nazwie Resources iw tym folderze utwórz dwa pliki Resources (.resx). Jeden o nazwie Resources.resx, a drugi o nazwie Resources.en.resx (lub .en-GB.resx). W mojej implementacji mam język NL (holenderski) jako język domyślny, więc jest to w moim pierwszym pliku, a angielski w drugim.
Konfiguracja powinna wyglądać następująco:
Właściwości pliku Resources.resx muszą być:
Upewnij się, że przestrzeń nazw narzędzia niestandardowego jest ustawiona na przestrzeń nazw projektu. Przyczyną tego jest to, że w WPF nie można odwoływać się do Resources
wewnątrz XAML.
W pliku zasobów ustaw modyfikator dostępu na Publiczny:
Jeśli masz tak dużą aplikację (powiedzmy różne moduły), możesz rozważyć tworzenie wielu projektów, jak powyżej. W takim przypadku możesz poprzedzić swoje klucze i klasy zasobów konkretnym Module. Użyj najlepszego dostępnego edytora języków dla programu Visual Studio, aby połączyć wszystkie pliki w jeden przegląd.
Wykorzystanie w innym projekcie
Odniesienie do projektu: kliknij prawym przyciskiem myszy References -> Add Reference -> Prjects \ Solutions.
Użyj przestrzeni nazw w pliku: using MyProject.Language;
Użyj tego w taki sposób na zapleczu:
string someText = Resources.orderGeneralError;
jeśli istnieje coś innego, co nazywa się Zasoby, po prostu umieść całą przestrzeń nazw.
Używanie w MVC
W MVC możesz ustawić język w dowolny sposób, ale ja użyłem sparametryzowanych adresów URL, które można skonfigurować w następujący sposób:
RouteConfig.cs
Poniżej innych mapowań
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 (może być konieczne dodanie, jeśli tak, dodaj FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
do Application_start()
metody wGlobal.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 po prostu ustawia informacje o kulturze.
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)
{
}
}
Wykorzystanie w .cshtml
@using MyProject.Language;
<h3>@Resources.w_home_header</h3>
lub jeśli nie chcesz definiować zastosowań, po prostu wypełnij całą przestrzeń nazw LUB możesz zdefiniować przestrzeń nazw w /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>
Ten samouczek dotyczący źródła implementacji MVC: niesamowity blog z samouczkiem
Używanie w bibliotekach klas dla modeli
Korzystanie z zaplecza jest takie samo, ale tylko przykład użycia w atrybutach
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; }
}
}
Jeśli masz przekształcenie, automatycznie sprawdzi, czy podana nazwa zasobu istnieje. Jeśli wolisz bezpieczeństwo typów, możesz użyć szablonów T4 do wygenerowania wyliczenia
Używanie w WPF.
Oczywiście dodaj odniesienie do przestrzeni nazw MyProject.Language , wiemy, jak go używać na zapleczu.
W języku XAML, wewnątrz nagłówka Window lub UserControl, dodaj odwołanie do przestrzeni nazw o nazwie lang
tak:
<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">
Następnie wewnątrz etykiety:
<Label x:Name="lblHeader" Content="{x:Static lang:Resources.w_home_header}" TextBlock.FontSize="20" HorizontalAlignment="Center"/>
Ponieważ jest wpisany silnie, masz pewność, że ciąg zasobów istnieje. Czasami może być konieczne ponowne skompilowanie projektu podczas instalacji, WPF czasami zawiera błędy z nowymi przestrzeniami nazw.
Jeszcze jedno dla WPF, ustaw język wewnątrz App.xaml.cs
. Możesz wykonać własną implementację (wybierz podczas instalacji) lub pozostaw decyzję systemowi.
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;
}
}
}
Używanie w UWP
W przypadku platformy UWP firma Microsoft korzysta z tego rozwiązania , co oznacza, że trzeba będzie utworzyć nowe pliki zasobów. Ponadto nie można ponownie użyć tekstu, ponieważ chcą oni ustawić x:Uid
kontrolę w języku XAML na klucz w zasobach. A w swoich zasobach musisz zrobić, Example.Text
aby wypełnić TextBlock
tekst. W ogóle nie podobało mi się to rozwiązanie, ponieważ chcę ponownie wykorzystać moje pliki zasobów. W końcu wpadłem na następujące rozwiązanie. Właśnie dowiedziałem się o tym dzisiaj (26.09.2019), więc mogę wrócić z czymś innym, jeśli okaże się, że to nie działa zgodnie z oczekiwaniami.
Dodaj to do swojego projektu:
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);
}
}
Dodaj to do App.xaml.cs
w konstruktorze:
CustomXamlResourceLoader.Current = new MyXamlResourceLoader();
Gdziekolwiek chcesz w swojej aplikacji, użyj tego, aby zmienić język:
ApplicationLanguages.PrimaryLanguageOverride = "nl";
Frame.Navigate(this.GetType());
Ostatni wiersz jest potrzebny do odświeżenia interfejsu użytkownika. Podczas gdy nadal pracuję nad tym projektem, zauważyłem, że muszę to zrobić 2 razy. Mogę skończyć z wyborem języka przy pierwszym uruchomieniu użytkownika. Ale ponieważ będzie to dystrybuowane za pośrednictwem Sklepu Windows, język jest zwykle równy językowi systemu.
Następnie użyj w XAML:
<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>
Używanie go w Angular (konwersja do JSON)
Obecnie bardziej powszechne jest posiadanie frameworka takiego jak Angular w połączeniu z komponentami, więc bez cshtml. Tłumaczenia są przechowywane w plikach json, nie zamierzam opisywać, jak to działa, po prostu bardzo polecam ngx-translate zamiast wielojęzycznego tłumaczenia kątowego. Więc jeśli chcesz przekonwertować tłumaczenia na plik JSON, jest to całkiem proste, używam skryptu szablonu T4, który konwertuje plik Resources na plik JSON. Zalecam zainstalowanie edytora T4, aby odczytać składnię i używać jej poprawnie, ponieważ musisz wprowadzić pewne modyfikacje.
Tylko jedna uwaga: nie ma możliwości generowania danych, kopiowania ich, czyszczenia danych i generowania ich dla innego języka. Musisz więc skopiować poniższy kod tyle razy, ile masz języków i zmienić wpis przed „// choose language here”. Obecnie nie ma czasu na naprawianie tego, ale prawdopodobnie zaktualizuję później (jeśli zainteresowany).
Ścieżka: 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);
}
#>
Jeśli masz aplikację modulair i postąpiłeś zgodnie z moją sugestią tworzenia projektów w wielu językach, będziesz musiał utworzyć plik T4 dla każdego z nich. Upewnij się, że pliki json są logicznie zdefiniowane, nie musi en.json
, ale może też być example-en.json
. Aby połączyć wiele plików json do użycia z ngx-translate , postępuj zgodnie z instrukcjami tutaj
Użyj w Xamarin.Android
Jak wyjaśniono powyżej w aktualizacjach, używam tej samej metody, co w przypadku Angular / JSON. Ale Android używa plików XML, więc napisałem plik T4, który generuje te pliki XML.
Ścieżka: 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 działa z values-xx
folderami, więc powyżej jest dla angielskiego w values-en
folderze. Ale musisz też wygenerować domyślne, które trafią do values
folderu. Po prostu skopiuj powyższy szablon T4 i zmień folder w powyższym kodzie.
Proszę bardzo, możesz teraz używać jednego pliku zasobów dla wszystkich swoich projektów. Dzięki temu bardzo łatwo jest wyeksportować wszystko do dokumentu excl i pozwolić komuś przetłumaczyć go i ponownie zaimportować.
Specjalne podziękowania dla tego niesamowitego rozszerzenia VS, które działa niesamowicie z resx
plikami. Rozważ przekazanie mu darowizny za jego niesamowitą pracę (nie mam z tym nic wspólnego, po prostu uwielbiam to rozszerzenie).