web-dev-qa-db-ja.com

大規模な.NETプロジェクトで多言語/グローバリゼーションを実装する最良の方法

私はまもなく大規模なc#プロジェクトに取り組み、最初から多言語サポートを組み込みたいと考えています。いろいろ試してみましたが、言語ごとに個別のリソースファイルを使用して動作させ、リソースマネージャーを使用して文字列を読み込むことができます。

私が検討できる他の良いアプローチはありますか?

68
tjjjohnson

リソースで別のプロジェクトを使用する

私は経験からこれを伝えることができ、現在のソリューションを持っています 12 24API、MVC、プロジェクトライブラリ(コア機能)、WPF、およびXamarinを含むプロジェクト。この長い投稿を読むのは価値があると思います。 VSツールの助けを借りて、簡単にエクスポートおよびインポートして、翻訳会社に送信したり、他の人がレビューしたりできます。

EDIT 02/2018:まだ強力ですが、.NET標準ライブラリに変換すると、.NET FrameworkとNET Coreで使用することも可能になります。たとえば、angularで使用できるように、JSONに変換するための追加セクションを追加しました。

EDIT:2019:Xamarinを使用して前進しているので、これはすべてのプラットフォームで機能します。例えば。 Xamarin.Formsは、resxファイルも使用するようにアドバイスしています。 (Xamarin.Formsでアプリをまだ開発していませんが、開始するための詳細な方法であるドキュメントで説明しています: Xamarin.Forms Documentation )。 JSONに変換するように、Xamarin.Android(現在作業中)の.xmlファイルに変換することもできます。

だから、それに到達することができます。

Pro's

  • ほぼどこでも強力に入力されました。
  • WPFでは、ResourceDirectoriesを扱う必要はありません。
  • 私がテストした限りでは、ASP.NET、クラスライブラリ、WPF、Xamarin、.NET Core、.NET Standardでサポートされています。
  • 追加のサードパーティライブラリは必要ありません。
  • カルチャフォールバックをサポート:en-US-> en。
  • バックエンドだけでなく、WPFのXAMLおよびMVCの.cshtmlのXamarin.Formsでも動作します。
  • _Thread.CurrentThread.CurrentCulture_を変更して言語を簡単に操作します
  • 検索エンジンはさまざまな言語でクロールでき、ユーザーは言語固有のURLを送信または保存できます。

Con's

  • WPF XAMLは時々バグがあり、新しく追加された文字列は直接表示されません。再構築は一時的な修正です(vs2015)。
  • 教えてください。

セットアップ

ソリューションで言語プロジェクトを作成し、MyProject.Languageのような名前を付けます。 Resourcesという名前のフォルダーを追加し、そのフォルダーに2つのResourcesファイル(.resx)を作成します。 1つはResources.resxと呼ばれ、もう1つはResources.en.resx(または特定の場合は.en-GB.resx)。私の実装では、NL(オランダ語)言語をデフォルトの言語として使用しているため、最初のファイルには英語が、2番目のファイルには英語が使用されます。

セットアップは次のようになります。

language setup project

Resources.resxのプロパティは次のとおりである必要があります: properties

カスタムツールの名前空間がプロジェクトの名前空間に設定されていることを確認してください。これは、WPFでは、XAML内でResourcesを参照できないためです。

そして、リソースファイル内で、アクセス修飾子をPublicに設定します。

access modifier

別のプロジェクトで使用する

プロジェクトへの参照:[参照]-> [参照の追加]-> [Prjects\Solutions]を右クリックします。

ファイルで名前空間を使用する:_using MyProject.Language;_

バックエンドで次のように使用します。_string someText = Resources.orderGeneralError;_ Resourcesと呼ばれるものがある場合は、名前空間全体に配置します。

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>
_

または、usingを定義したくない場合は、名前空間全体を入力するだけですOR /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実装ソースチュートリアル: Awesome tutorial blog

モデルのクラスライブラリでの使用

バックエンドの使用方法も同じですが、属性の使用例にすぎません

_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テンプレートを使用してenum を生成できます

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のもう1つのことは、言語を_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;
        }

    }
}
_

Angular(JSONに変換))で使用する

最近では、Angularのようなフレームワークをコンポーネントと組み合わせてcshtmlなしで使用するのがより一般的です。翻訳はjsonファイルに保存されます。これをJSONファイルに変換したい場合は、非常に簡単です。リソースファイルをjsonファイルに変換するT4テンプレートスクリプトを使用します。 T4 editor をインストールして構文を読み取り、いくつかの変更を行う必要があるため、正しく使用してください。

注意すべきことは1つだけです。データを生成、コピー、データをクリーンアップし、別の言語用に生成することはできません。したがって、以下のコードを使用している言語と同じ回数だけコピーし、「// choose language here」の前にエントリを変更する必要があります。現在、これを修正する時間はありませんが、おそらく後で更新されます(興味がある場合)。

パス: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);
}


#>
_

これで、すべてのプロジェクトで1つのリソースファイルを使用できるようになりました。これにより、すべてをexclドキュメントに非常に簡単にエクスポートし、誰かがそれを翻訳して再度インポートできるようになります。

50
CularBytes

多くの異なるアプローチを使用して実装されたプロジェクトを見てきましたが、それぞれに長所と短所があります。

  • 1つは設定ファイルで行いました(私のお気に入りではありません)
  • データベースを使用してそれを行いました-これはかなりうまくいきましたが、あなたが何を維持すべきかを知っているのは苦痛でした。
  • 1つはあなたが提案している方法でリソースファイルを使用し、私はそれが私のお気に入りのアプローチだったと言わざるを得ません。
  • 最も基本的なものは、文字列でいっぱいのインクルードファイルを使用して実行しました-いです。

あなたが選んだリソースの方法は非常に理にかなっていると思います。このようなことをするより良い方法があるのではないかとよく思うので、他の人の答えも見るのは面白いでしょう。 SOの1つ など、リソースの使用方法を指す多くのリソースを見てきました。

20
BenAlabaster

「最善の方法」があるとは思わない。それは本当にあなたが構築しているアプリケーションの技術とタイプに依存します。

他のポスターが示唆しているように、Webアプリはデータベースに情報を保存できますが、別のリソースファイルを使用することをお勧めします。つまり、リソースファイルはソースとは別です。リソースファイルを分離すると、同じファイルの競合が減り、プロジェクトが成長するにつれて、ローカライズがビジネスロジックとは別に行われることがあります。 (プログラマーおよび翻訳者)。

Microsoft WinFormとWPFの達人は、各ロケールにカスタマイズされた個別のリソースアセンブリの使用を推奨しています。

UI要素をコンテンツに合わせてサイズ調整するWPFの機能により、必要なレイアウト作業が低下します(例:日本語の単語は英語よりもはるかに短い)。

WPFを検討している場合: このmsdnの記事を読むことをお勧めします 正直に言うと、WPFのローカライズツールであるmsbuild、locbaml(およびExcelスプレッドシート)を使用するのは面倒ですが、機能します。

少しだけ関連するもの:私が直面する一般的な問題は、エラーコードではなく、エラーメッセージ(通常は英語)を送信するレガシーシステムを統合することです。これにより、レガシーシステムへの変更、またはバックエンド文字列を自分のエラーコードにマッピングしてからローカライズされた文字列へのマッピングのいずれかが強制されます。 エラーコードはローカライズの友達です

5
MW_dev

+1データベース

データベースの修正が行われた場合、アプリのフォームはその場で再翻訳することもできます。

すべてのコントロールがXMLファイル(フォームごとに1つ)で言語リソースIDにマップされるシステムを使用しましたが、すべてのIDはデータベースにありました。

基本的に、各コントロールにIDを保持させる代わりに(インターフェイスの実装、またはVB6のタグプロパティの使用)、. NETでは、コントロールツリーはリフレクションを通じて簡単に発見できるという事実を使用しました。読み込まれたフォームが存在しない場合、XMLファイルを構築するプロセス。 XMLファイルはコントロールをリソースIDにマップするため、これを入力してデータベースにマップするだけで済みました。これは、タグ付けされていない場合、または別のIDに分割する必要がある場合、コンパイルされたバイナリを変更する必要がないことを意味しますディクショナリ内で再利用されませんが、IDの初期割り当て時にこれを発見できない場合があります)。しかし、実際には、翻訳プロセス全体がバイナリから完全に独立します(すべてのフォームは、それ自体とそのすべてのコントロールを翻訳する方法を知っている基本フォームから継承する必要があります)。

アプリがより複雑になるのは、挿入ポイントを持つフェーズが使用される場合のみです。

データベース翻訳ソフトウェアは、欠落している翻訳などの処理を容易にするさまざまなワークフローオプションを備えた基本的なCRUDメンテナンス画面でした。

4
Cade Roux

Sisulizer のような市販のツールを使用できます。各言語のサテライトアセンブリが作成されます。注意が必要なのは、フォームクラス名を難読化しないことだけです(難読化ツールを使用する場合)。

2
Davorin

私は探していましたが、これを見つけました:

WPFまたはSilverlightを使用している場合、多くの理由で WPF LocalizationExtension を使用できます。

ITのオープンソース無料(無料)は真のスターベル状態です

Windowsアプリケーションでは、次のようなことができます。

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);  
    }  
} 

そして、私はウェップページでアプローチが同じかもしれないと思います。

がんばろう!

2
JxXx

複数のリソースファイルを使用します。設定するのはそれほど難しくないはずです。実際、最近、フォーム言語リソースファイルと組み合わせてグローバル言語ベースのリソースファイルを設定することに関する同様の質問に答えました。

Visual Studio 2008のローカリゼーション

少なくともWinForm開発には最善のアプローチだと思います。

2
KMessenger

多言語サポートにカスタムプロバイダーを使用し、すべてのテキストをデータベーステーブルに入れます。 Webアプリケーションを更新せずにデータベース内のテキストを更新するときにキャッシュの問題に直面する場合があることを除いて、うまく機能します。

0
Cossintan

ほとんどのオープンソースプロジェクトでは、この目的で GetText を使用しています。以前に.Netプロジェクトでどのように使用されたか、またそれが使用されたかどうかはわかりません。

0
Vasil

標準リソースファイルの方が簡単です。ただし、ルックアップテーブルなどの言語依存データがある場合は、2つのリソースセットを管理する必要があります。

まだやっていませんが、次のプロジェクトではデータベースリソースプロバイダーを実装します。私はMSDNでそれを行う方法を見つけました:

http://msdn.Microsoft.com/en-us/library/aa905797.aspx

私もこの実装を見つけました:

DBResourceプロバイダー

0
Ben Dempsey