Showing posts with label multilanguage. Show all posts
Showing posts with label multilanguage. Show all posts

Monday, July 21, 2014

WPF globalization language, using singleton class indexer property (WPF 다국어 작업, 동적으로 해보자)

지난 포스트에 이어...

쉽게 하는 방법

지난 포스트는 쉽게 하는 방법에 대해 쓴 것이다.
단!
요구사항이 프로그램 실행중에 언어를 변경해도 런타임시에 변경되지 않아야 한다는 조건에서다.
왜냐하면 CurrentUICulture는 처음 실행될 때 세팅해 줄 수 있고, 그 이후에는 그 언어에 맞는 리소스 dll 파일을 가져와 바인딩 하기 때문에 런타임시에 변경이 불가능하기 때문이다.

그리고 이러한 요구사항은 아래와 같은 이유로 종종 타협이 되서 런타임시 언어 변경 기능은 빼는 경우가 있다.

1. 규모가 큰 프로그램의 경우 리소스를 사용하는 갯수가 수백 수천개이기 때문에, 일일이 리소스 별로 변경되는 걸 Notify 해 주기가 어렵다. (즉, 바인딩 된 언어 리소스의 Notify가 쉽지 않기 때문에) 라고 실드치는 경우. 이유는 명백하다, 처음부터 다국어를 고려하지 않고 만들었기 때문이다.

2. 크롬이나 기타 윈도우 프로그램 (심지어 윈도우 OS)의 경우도, 언어 변경 후 프로그램을 재 시작하라는 메시지와 함께 재시작 하는걸 당연하게 생각하는 사용자가 있기에 우리 프로그램도 런타임 시에 언어 변경이 고객의 필수(?) 요구사항이 아닌 이상 굳이 그렇게 까지 해야 하나? 라고 반문하는 경우.

그래서 첫 포스팅의 경우 처럼 개발자도 작업하기 쉽고, 언어 변경 후 프로그램 다시 재시작 하는게 크게 어려운 일이 아니기에 쉬운 방법으로 많이들 사용한다.

그럼에도 불구하고!
런타임시에 언어 변경이 아주 불가능한건 아니기 때문에 이번에 한번 소개해 보고자 한다.
이건 c#의 indexer를 PropertyChanged 이벤트를 줄 수 있기 때문에 착안한 것이며, 이걸 Singleton 패턴으로 instance를 관리하여 작성하였다.

프로젝트 환경은 다음과 같다.
OS: Windows 8.1
Framework: .NET Framework 4.5
Development Tool: Visual Studio Ultimate 2013
Language: C#
Project: WPF Class LIbrary


* Source description

1. Single instance (Singleton)
한 언어별 리소스들을 일괄로 관리하기 위해서 Singleton 패턴을 사용하여 하나의 인스턴스로 관리한다. 나중에 일괄로 PropertyChanged할 때 용이하다.


private static volatile LanguageResources instance;
private static object syncRoot = new Object();


public static LanguageResources Instance
{
    get
    {
        if (instance == null)
        {
            lock (syncRoot)
            {
                if (instance == null)
                {
                    instance = new LanguageResources();
                }
            }
        }
        return instance;
    }
}


2. Managed dictionary collection
내부적으로 key-value로 관리할 dictionary 클래스를 사용한다. 리소스 자체가 key-value pair이기 때문에 이만한 collection이 없다. 그리고 dictionary가 변경될 때 마다 PropertyChanged를 걸어준다. "item[]"(Binding.Indexer)을 PropertyName으로 주면 indexer에 바인딩이 걸린 모든 Binding path에 notify가 간다. 5번에서도 설명될 것이지만 각 ViewModel의 PropertyChagned도 여기서 모두 걸어준다.


private Dictionary<stringstring> _resourceDictionary;
public Dictionary<stringstring> ResourceDictionary
{
    set
    {
        _resourceDictionary = value;
        if (_resourceDictionary != null && PropertyChanged != null)
        {
            // Set property name "Binding.IndexerName" for PropertyChanged event
            PropertyChanged(thisnew PropertyChangedEventArgs("Item[]"));
            // call PropertyChanged in registered viewmodels implement INotifyPropertyChanged interface
            foreach (var item in NotifyPropertyChangedDictoionary)
            {
                if (item.Key != null && item.Value != null)
                {
                    foreach (string propertyname in item.Value)
                    {
                        PropertyChanged(item.Key, new PropertyChangedEventArgs(propertyname));
                    }
                }
            }
        }
    }
    get
    {
        return _resourceDictionary;
    }
}

3. Using indexer
인덱서의 경우는 c# 기본적인 것이니 자세한 설명은 생략하고, Singleton 객체에 indexer property를 노출시킨다. 직관적으로 key값을 주면 내부 dictionary에서 검색하여 value를 리턴하는 식으로 만든다. code behind에서 동적으로 세팅해 주는게 편할 수도 있지만, xaml 코드에서 정적으로 binding 하는게 더 강력하다. 왜냐하면 이 indexer에 PropertyChanged가 걸리기 때문이다.


public string this[string key]
{
    get
    {
        string value = key == null ? "" : key;
        if (ResourceDictionary != null && ResourceDictionary.ContainsKey(key) == true)
        {
            value = ResourceDictionary[key];
        }
        else
        {
            value = string.Format(Settings.RESOURCE_NOT_FOUND_MESSAGE, key);
        }
        return value;
    }
}

4. Load resource file (DataContractJsonSerializer)
JSON 포맷으로 되어있는 텍스트 파일을 읽어와서, 최종적으로 dictionary의 key, value collection 형태로 만든다. 단, Serialize 가능하게 만드는 것이 file read, write 할때도 직관적이면서 쉽다. 예제의 경우에는 JSON 포맷으로 텍스트 파일을 만들었으며, DataContractJsonSerializer 클래스를 사용하여 serialize 했다.


private void LoadResource()
{
    string fileStream = "";
    try
    {
        string filepath = string.Format(Settings.LANGUAGE_FILE_PATH, CultureName);
        if (DesignerProperties.GetIsInDesignMode(new DependencyObject()) == false)
        {
            fileStream = File.ReadAllText(filepath);
        }
        else
        {
            fileStream = File.ReadAllText(Settings.TARGET_PROJECT_NAME + Settings.OUTPUT_PATH + filepath);
        }
        DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(Dictionary<stringstring>));
        byte[] fileByte = Encoding.UTF8.GetBytes(fileStream);
        MemoryStream ms = new MemoryStream(fileByte);
        if (ResourceDictionary != null)
        {
            ResourceDictionary.Clear();
            ResourceDictionary = null;
        }
        ResourceDictionary = dcjs.ReadObject(ms) as Dictionary<stringstring>;
    }
    catch
    {
        Debug.Assert(false);
    }
}

5. Support ViewModel binding
사실 언어 리소스는 xaml에 정적 바인딩 해 두고 indexer에서 PropertyChanged로 변경 가능하게 하는게 가장 정석이지만, MVVM으로 만들어 둔 ViewModel의 Property에 PropertyChanged가 될 때 각 Property의 get에서 뭔가 string format 처리라던지 분기 처리를 통한 적절한 리소스를 보여주고 싶을 때가 있을 때도 지원 가능하도록 처리를 해 두었다.


#region NotifyPropertyChangedDictoionary
private Dictionary<INotifyPropertyChangedstring[]> _NotifyPropertyChangedDictoionary = null;
private Dictionary<INotifyPropertyChangedstring[]> NotifyPropertyChangedDictoionary
{
    get
    {
        if (_NotifyPropertyChangedDictoionary == null)
        {
            _NotifyPropertyChangedDictoionary = new Dictionary<INotifyPropertyChangedstring[]>();
        }
        return _NotifyPropertyChangedDictoionary;
    }
    set
    {
        _NotifyPropertyChangedDictoionary = value;
    }
}
#endregion
 
#region SetRegisterNotifyPropertyChanged
public void SetRegisterNotifyPropertyChanged(INotifyPropertyChanged sender, params string[] propertynames)
{
    if (NotifyPropertyChangedDictoionary != null)
    {
        NotifyPropertyChangedDictoionary.Add(sender, propertynames);
    }
}

#endregion


어쨌든 이런 식으로 리소스를 관리하면 런타임시에 언어 변경이 된다.
다만 언어별 텍스트 read, write시에 JSON notation error나
중복된 key 입력 정도만 조심하면 된다.

JSON notation error는 웹 상의 json viewer 같은 걸로 체크해 주면 되고
Dictionary class를 deseriazlize 하기 때문에 언어 파일에 중복키가 들어가게 되면 예외가 발생하게 된다. 이 부분을 조심하자.

* Benefit

1. Language resources changed on run time.

손쉽게 런타임 시에 언어 리소스 변경이 가능하다. 애초에 이 목적으로 만들어졌기 때문에 당연히 장점이 된다.

2. Can view design time.

디자인 타임에 리소스를 확인해 볼 수 있는 기능은 어찌 보면 별거 아니지만
실제 사용해 보면 상당히 편하고 좋은 기능임을 알 수 있다.

3. One time called all PropertyChanged event by indexer

Singleton instance에 CultureName이 변경되면 즉시 설정된 리소스 파일을 로딩하게 되고 그 과정에서 PropertyChanged 이벤트가 걸리게 되기 때문에 indexer를 사용하는 모든 binding된 property는 변경이 (저절로 되는 것처럼) 된다. 자세한 내용은 파일 다운로드해서 보면 알 수 있다.


* How to use LocalizationResource project library

1. Set output directory

Project properties -> Build -> Output
여기에 리소스를 사용할 프로젝트의 output 경로를 적는다.



<당연한 얘기지만 리소스를 빌드하면 dll 파일이 나오고 이걸 사용할 프로젝트에 출력 경로로 세팅해 주는 것이다>
 
2. Add reference library to target project.

다국어를 사용할 프로젝트에 이 라이브러리 프로젝트를 참조시킨다.

 
<역시 당연한 얘기지만 참조를 추가해 주는 건 기본>

3. Edit language resource files.

ko-KR, en-US가 기본으로 있으며 테스트용으로 key-value 값이 몇 개 들어 있다.
사용하고 싶은 key-value 값을 두 파일에 동일하게 추가해서 리소스 파일을 만들어 간다.


<Resources.en-US.txt>
<Resources.ko-KR.txt>
4. Use resource to xaml. You can see resource values design time.

디자인 타임에 실시간으로 리소스의 키를 입력했을 때 값이 확인되는 건 더할 나위 없이 좋은 기능임을 강조하고 싶다. 실제 쓸 때도 static binding으로 세팅만 해 두면 나중에 언어가 변경되더라도 PropertyChanged가 걸려서 언어에 맞는 리소스로 변경되서 보여진다.

<디자인타임에도 리소스가 보이는 걸 볼 수 있다.>
5. To see other property binding, download sample zip file.

Static binding 뿐 아니라 MVVM의 ViewModel binidng 까지 예제가 있으니 다운로드 해서 확인해 보면 된다.

LocalizationResources sample project file

<한국어>

파일 다운로드 방법
- 링크를 눌러 새 창이 뜨면 Ctrl+S 를 누른다.
- 아니면 메뉴에 파일 > 다운로드를 클릭한다.

<English>

How to download zip file.
- Link click, new tab opened -> press key 'Ctrl+s'
- or File > Download menu click.

Friday, July 18, 2014

WPF globalization language, using resx file (WPF 다국어 작업, 쉬운 방법)

WPF로 다국어 작업을 해야 하는데 이것저것 검색해 보면 다음과 같은 것들을 접할 수 있다.

Resgen.exe
Al.exe
LocBaml.exe
msbuild.exe
uid

이것저것 검색해 봐도 쉽고 직관적으로 할 수 있는 방법은 없는 것 같고
뭔가 command line 명령으로 파일 생성하고 수정하고 해서 로딩해 오는 방법을 써야 한다.

결국 이런 방법들의 공통점은 아래와 같다.
1. resource 가 있는 파일이 있고
2. 내가 원하는 언어를 선택하여 언어에 맞게 resource를 로딩해 오면
3. 그걸 사용하는 것이다.
4. 그것도 정적으로!

이게 무엇인고 하니 시작하기 전에 System.Threading.Thread.CurrentUICulture를 세팅해서 쓰는 건 뭔 짓을 해도 공통이라는 것이다.

런타임시에 동적으로 언어 변경하는 건 다음 포스팅에서 설명하기로 하고
우선 정적으로라도 하는 방법, 그것도 쉽게 하는 방법을 써보자.

이것 만큼 쉽게 하는 방법은 없다고 본다.
(만약 있다면 제보 부탁!)

WPF이니 Window일 것이고 내 개발 환경은 다음과 같다.
OS: Windows 8.1
Framework: .NET Framework 4.5
Development Tool: Visual Studio Ultimate 2013
Language: C#
Project: WPF Application

<그냥 WPF 프로젝트 만들면 볼 수 있는 친숙한 화면>

이름을 ResourcesSample로 생성하면 기본적으로 위와 같은 화면이 나올 것이다.
여기서 부터 순서대로 해보면,

1. Add resource value in Resources.resx
이 파일을 열면 기본 리소스를 관리할 수 있는 엑셀 파일 같은게 나오는데 두 개를 추가해 본다.
String1=야옹, String2=멍멍

<Resource.resx 파일을 열면 리소스 관리가 가능하다>


2. Static binding text in MainWindow.xaml
리소스를 사용해 본다.
xmlns:res="clr-namespace:ResourcesSample.Properties"를 추가해 주고
TextBlock에 Text="{x:Static res:Resources.String1}"을
쓰면 디자인 타임에도 볼 수 있듯이 "야옹"이라는 글자가 잘 나온다.

<Design time에도 방금 추가한 리소스가 잘 바인딩이 되는 걸 볼 수 있다>

3. Change access modifier
그런데 실제 빌드해 보고 실행하려 하면 오류가 난다. 왜냐하면 String1은 internal 한정자라 public으로 바꿔줘야 하는데 아래 처럼 엑세스 한정자를 public으로 바꿔준다. String1뿐 아니라 Resource내에 모든 리소스가 public으로 변경 된다. 일단 실행은 잘 된다.

<엑세스 한정자를 public으로 변경한다. 그래야 String1의 키 값이 public으로 변경되고 접근할 수 있게 된다.>

<진짜 별거 없지만, 바인딩 된 "야옹" 텍스트가 잘 나오는게 확인 된다>


4. Add other Resource file
지금은 한국어용 리소스를 넣고 테스트 한 것이고, 영어 리소스를 넣어야 한다.
여기서 주의해야 할 것은 리소스 파일 이름을 CultureInfo 클래스의 Name대로 이름을 명명해야 한다는 것이다. 관련된 건 구글 좀 찾아보면 나오니 자세한 설명은 생략하겠다. 기존에 Resources.resx 파일과 같은 namespace 상에 있어야 하기 때문에 Resources.resx 파일을 Ctrl+C, Ctrl+V해서 추가한 뒤에 이름을 Resources.en-US.resx로 바꿔준다. 그리고 같은 String1, String2라는 이름을 가진 값을 "Yaong", "MeongMeong"으로 추가해 본다. 

<추가한 "Resources - 복사본.resx" 파일을 "Resources.en-US.resx"로 변경한다.>

5. Confirm output folder after build.
그리고 빌드를 해서 output 폴더(프로젝트 폴더의 bin/Debug/)를 보면 "en-US"라는 폴더가 생기고 그 안에 ResourcesSample.resources.dll 파일이 있는 걸 볼 수 있다.

<드디어 영어 리소스 파일이 생긴 것이다>
6. Set CultureInfo
그럼 마지막으로 App에 아래와 같은 코드를 추가한다.
이건 프로그램이 시작하기 전에 CultureInfo를 세팅하는 것인데 "en-US" 리소스를 추가했기 때문에 "en-US"로 생성한다.
그리고 CurrentUICulture에 세팅하면 모든 리소스가 우리가 추가한 언어의 리소스로 변경이 된다.
System.Threading.Thread.CurrentThread.CurrentUICulutre = new CultureInfo("en-US");

<CurrentUICulture에 "en-US"의 CultureInfo 객체를 세팅하면 끝이다>

<확인해 보면 별거 없지만, CurrentUICulture에 세팅한 대로 "Yaong" 텍스트가 나온다>

WPF 한 사람 치고 이 정도 했는데 다국어 못하겠다고 한 사람은 없을 것이다.
이후 작업은 이름-값에 해당하는 리소스들을 계속 추가해 주고 쓰는 것 뿐이다. 시간 싸움이라는 뜻이기도 하다.

그런데 다들 이런 식 -> 이런 식이라 함은 프로그램 시작시 리소스를 로딩해 와서 바인딩이 되는 식이라는 뜻이다. 런타임 시에 동적으로 되는 예제가 몇 있긴 한데 그것도 역시 리소스 파일을 가져와서 바꿔치기 하는 식이고 리소스 key-value를 메모리에 올려서 쓰겠다는 뜻이기도 하다.

다음 포스트에는 런타임시에 다국어 적용에 대해 써볼까 한다.


<한국어>
파일 다운로드 방법
- 링크를 눌러 새 창이 뜨면 ctrl+s를 누른다.
- 아니면 메뉴에 파일->다운로드를 클릭한다.

<English>
How to download to zip file.
- Link click, new tab opened -> press key 'ctrl+s'
- or File -> Download menu click.