쉽게 하는 방법
지난 포스트는 쉽게 하는 방법에 대해 쓴 것이다.
단!
요구사항이 프로그램 실행중에 언어를 변경해도 런타임시에 변경되지 않아야 한다는 조건에서다.
왜냐하면 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<string, string> _resourceDictionary;
public Dictionary<string, string> ResourceDictionary
{
set
{
_resourceDictionary = value;
if (_resourceDictionary != null && PropertyChanged != null)
{
// Set property name "Binding.IndexerName" for PropertyChanged event
PropertyChanged(this, new 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<string, string>));
byte[] fileByte = Encoding.UTF8.GetBytes(fileStream);
MemoryStream ms = new MemoryStream(fileByte);
if (ResourceDictionary != null)
{
ResourceDictionary.Clear();
ResourceDictionary = null;
}
ResourceDictionary = dcjs.ReadObject(ms) as Dictionary<string, string>;
}
catch
{
Debug.Assert(false);
}
}
5. Support ViewModel binding
사실 언어 리소스는 xaml에 정적 바인딩 해 두고 indexer에서 PropertyChanged로 변경 가능하게 하는게 가장 정석이지만, MVVM으로 만들어 둔 ViewModel의 Property에 PropertyChanged가 될 때 각 Property의 get에서 뭔가 string format 처리라던지 분기 처리를 통한 적절한 리소스를 보여주고 싶을 때가 있을 때도 지원 가능하도록 처리를 해 두었다.
#region NotifyPropertyChangedDictoionary
private Dictionary<INotifyPropertyChanged, string[]> _NotifyPropertyChangedDictoionary = null;
private Dictionary<INotifyPropertyChanged, string[]> NotifyPropertyChangedDictoionary
{
get
{
if (_NotifyPropertyChangedDictoionary == null)
{
_NotifyPropertyChangedDictoionary = new Dictionary<INotifyPropertyChanged, string[]>();
}
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 파일이 나오고 이걸 사용할 프로젝트에 출력 경로로 세팅해 주는 것이다> |
다국어를 사용할 프로젝트에 이 라이브러리 프로젝트를 참조시킨다.
![]() |
<역시 당연한 얘기지만 참조를 추가해 주는 건 기본> |
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가 걸려서 언어에 맞는 리소스로 변경되서 보여진다.
![]() |
<디자인타임에도 리소스가 보이는 걸 볼 수 있다.> |
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.