Development Tip

대규모 .NET 프로젝트에서 다국어 / 세계화를 구현하는 가장 좋은 방법

yourdevel 2020. 11. 8. 11:23
반응형

대규모 .NET 프로젝트에서 다국어 / 세계화를 구현하는 가장 좋은 방법


곧 대규모 C # 프로젝트에서 작업 할 예정이며 처음부터 다국어 지원을 구축하고 싶습니다. 나는 놀았고 각 언어에 대해 별도의 리소스 파일을 사용하여 작동하게 한 다음 리소스 관리자를 사용하여 문자열을로드 할 수 있습니다.

내가 살펴볼 수있는 다른 좋은 접근 방법이 있습니까?


리소스와 함께 별도의 프로젝트 사용

API, MVC, 프로젝트 라이브러리 (핵심 기능), WPF, UWP 및 Xamarin을 포함 하는 12 개의 24 개 프로젝트가 포함 된 현재 솔루션이있는 경험을 통해이를 알 수 있습니다 . 내가 그렇게하는 가장 좋은 방법이라고 생각하므로이 긴 게시물을 읽을 가치가 있습니다. VS 도구의 도움으로 쉽게 내보내고 가져올 수 있으며 번역 대행사로 보내거나 다른 사람이 검토 할 수 있습니다.

2018 년 2 월 수정 : 여전히 강력 해지고 .NET Standard 라이브러리로 변환하면 .NET Framework 및 NET Core에서 사용할 수도 있습니다. 예를 들어 angular에서 사용할 수 있도록 JSON으로 변환하기위한 추가 섹션을 추가했습니다.

EDIT 2019 : Xamarin을 통해 앞으로도 모든 플랫폼에서 계속 작동합니다. 예 : resx 파일을 사용하기위한 Xamarin.Forms 조언. (아직 Xamarin.Forms에서 앱을 개발하지 않았지만 시작하기위한 자세한 방법 인 설명서에서 Xamarin.Forms Documentation ). JSON으로 변환하는 것과 마찬가지로 Xamarin.Android 용 .xml 파일로 변환 할 수도 있습니다.

EDIT 2019 (2) : WPF에서 UWP로 업그레이드하는 동안 UWP에서 .resw콘텐츠는 동일하지만 사용법이 다른 다른 파일 형식을 사용하는 것을 선호한다는 것을 발견했습니다 . 내 의견으로 는 기본 솔루션 보다 더 잘 작동하는 다른 방법을 찾았습니다 .

그래서 그것에 도달합시다.

프로

  • 거의 모든 곳에서 강력하게 입력되었습니다.
  • WPF에서는 ResourceDirectories.
  • 테스트 한 한 ASP.NET, 클래스 라이브러리, WPF, Xamarin, .NET Core, .NET Standard에 대해 지원됩니다.
  • 추가 타사 라이브러리가 필요하지 않습니다.
  • 문화 대체 지원 : en-US-> en.
  • 백엔드뿐만 아니라 WPF 용 XAML 및 MVC 용 .cshtml에서도 작동합니다.
  • 변경하여 언어를 쉽게 조작 Thread.CurrentThread.CurrentCulture
  • 검색 엔진은 다른 언어로 크롤링 할 수 있으며 사용자는 언어 별 URL을 보내거나 저장할 수 있습니다.

단점

  • WPF XAML은 때때로 버그가 있으며 새로 추가 된 문자열이 직접 표시되지 않습니다. Rebuild는 임시 수정 (vs2015)입니다.
  • UWP XAML은 인텔리 센스 제안을 표시하지 않으며 디자인하는 동안 텍스트를 표시하지 않습니다.
  • 말해.

설정

솔루션에서 언어 프로젝트를 만들고 MyProject.Language 와 같은 이름을 지정 합니다. Resources라는 폴더를 추가하고 해당 폴더에 두 개의 Resources 파일 (.resx)을 만듭니다. 하나는 Resources.resx 이고 다른 하나는 Resources.en.resx (또는 특정의 경우 .en-GB.resx)입니다. 내 구현에서 기본 언어로 NL (네덜란드어) 언어가 있으므로 첫 번째 파일에, 영어는 두 번째 파일에 들어갑니다.

설정은 다음과 같습니다.

언어 설정 프로젝트

Resources.resx의 속성은 다음과 같아야합니다. 속성

사용자 지정 도구 네임 스페이스가 프로젝트 네임 스페이스로 설정되어 있는지 확인합니다. 그 이유는 WPF에서 ResourcesXAML 내부를 참조 할 수 없기 때문 입니다.

리소스 파일 내에서 액세스 수정자를 Public으로 설정합니다.

액세스 수정 자

다른 프로젝트에서 사용

프로젝트에 대한 참조 : 참조-> 참조 추가-> 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>

또는 usings를 정의하지 않으려면 전체 네임 스페이스를 채우거나 /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 템플릿을 사용하여 열거 형을 생성 할 수 있습니다.

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:UidXAML의 컨트롤을 리소스의 키로 설정하기를 원하기 때문에 텍스트를 다시 사용할 수 없습니다 . 그리고 당신의 자원에서 당신은 의 텍스트 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());

UI를 새로 고치려면 마지막 줄이 필요합니다. 이 프로젝트를 계속 진행하는 동안이 작업을 두 번해야한다는 것을 알았습니다. 사용자가 처음 시작할 때 언어 선택으로 끝날 수 있습니다. 그러나 이것은 Windows Store를 통해 배포되므로 일반적으로 언어는 시스템 언어와 동일합니다.

그런 다음 XAML에서 사용합니다.

<TextBlock Text="{CustomResource ExampleResourceKey}"></TextBlock>

Angular에서 사용 (JSON으로 변환)

요즘에는 Angular와 같은 프레임 워크를 구성 요소와 결합하여 cshtml없이 사용하는 것이 더 일반적입니다. 번역은 json 파일에 저장되며 작동 방식은 다루지 않겠습니다.하지만 이것을 JSON 파일로 변환하려는 경우 매우 쉽습니다. Resources 파일을 json 파일로 변환하는 T4 템플릿 스크립트를 사용합니다. 몇 가지 수정이 필요하므로 구문을 읽고 올바르게 사용하려면 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 파일을 사용하므로 이러한 XML 파일을 생성하는 T4 파일을 작성했습니다.

경로 : 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("&", "&amp;");
                     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 works with values-xx folders, so above is for English for in the values-en folder. But you also have to generate a default which goes into the values folder. Just copy above T4 template and change the folder in the above code.

There you go, you can now use one single resource file for all your projects. This makes it very easy exporting everything to an excl document and let someone translate it and import it again.

Special thanks to this amazing VS extension which works awesome with resx files. Consider donating to him for his awesome work (I have nothing to do with that, I just love the extension).


I've seen projects implemented using a number of different approaches, each have their merits and drawbacks.

  • One did it in the config file (not my favourite)
  • One did it using a database - this worked pretty well, but was a pain in the you know what to maintain.
  • One used resource files the way you're suggesting and I have to say it was my favourite approach.
  • The most basic one did it using an include file full of strings - ugly.

I'd say the resource method you've chosen makes a lot of sense. It would be interesting to see other people's answers too as I often wonder if there's a better way of doing things like this. I've seen numerous resources that all point to the using resources method, including one right here on SO.


I don't think there is a "best way". It really will depend on the technologies and type of application you are building.

Webapps can store the information in the database as other posters have suggested, but I recommend using seperate resource files. That is resource files seperate from your source. Seperate resource files reduces contention for the same files and as your project grows you may find localization will be done seperatly from business logic. (Programmers and Translators).

Microsoft WinForm and WPF gurus recommend using seperate resource assemblies customized to each locale.

WPF's ability to size UI elements to content lowers the layout work required eg: (japanese words are much shorter than english).

If you are considering WPF: I suggest reading this msdn article To be truthful I found the WPF localization tools: msbuild, locbaml, (and maybe an excel spreadsheet) tedious to use, but it does work.

Something only slightly related: A common problem I face is integrating legacy systems that send error messages (usually in english), not error codes. This forces either changes to legacy systems, or mapping backend strings to my own error codes and then to localized strings...yech. Error codes are localizations friend


+1 Database

Forms in your app can even re-translate themselves on the fly if corrections are made to the database.

We used a system where all the controls were mapped in an XML file (one per form) to language resource IDs, but all the IDs were in the database.

Basically, instead of having each control hold the ID (implementing an interface, or using the tag property in VB6), we used the fact that in .NET, the control tree was easily discoverable through reflection. A process when the form loaded would build the XML file if it was missing. The XML file would map the controls to their resource IDs, so this simply needed to be filled in and mapped to the database. This meant that there was no need to change the compiled binary if something was not tagged, or if it needed to be split to another ID (some words in English which might be used as both nouns and verbs might need to translate to two different words in the dictionary and not be re-used, but you might not discover this during initial assignment of IDs). But the fact is that the whole translation process becomes completely independent of your binary (every form has to inherit from a base form which knows how to translate itself and all its controls).

The only ones where the app gets more involved is when a phase with insertion points is used.

The database translation software was your basic CRUD maintenance screen with various workflow options to facilitate going through the missing translations, etc.


I'd go with the multiple resource files. It shouldn't be that hard to configure. In fact I recently answered a similar question on setting a global language based resource files in conjunction with form language resource files.

Localization in Visual Studio 2008

I would consider that the best approach at least for WinForm development.


You can use commercial tools like Sisulizer. It will create satellite assembly for each language. Only thing you should pay attention is not to obfuscate form class names (if you use obfuscator).


I´ve been searching and I´ve found this:

If your using WPF or Silverlight your aproach could be use WPF LocalizationExtension for many reasons.

IT´s Open Source It´s FREE (and will stay free) is in a real stabel state

In a Windows Application you could do someting like this:

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

And I think on a Wep Page the aproach could be the same.

Good Luck!


Most opensource projects use GetText for this purpose. I don't know how and if it's ever been used on a .Net project before.


We use a custom provider for multi language support and put all texts in a database table. It works well except we sometimes face caching problems when updating texts in the database without updating the web application.


표준 리소스 파일이 더 쉽습니다. 그러나 조회 테이블과 같은 언어 종속 데이터가있는 경우 두 개의 리소스 세트를 관리해야합니다.

나는 그것을하지 않았지만 다음 프로젝트에서는 데이터베이스 리소스 공급자를 구현할 것입니다. MSDN에서 수행하는 방법을 찾았습니다.

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

이 구현도 발견했습니다.

DBResource 공급자

참고 URL : https://stackoverflow.com/questions/373388/best-way-to-implement-multi-language-globalization-in-large-net-project

반응형