[iOS Programming Big Nerd Ranch] 6. Localization


Localization을 해보자.


iOS 유저들은 전 세계에 있으며 다양한 언어를 사용한다. internationalization과 localization을 거쳐서 앱이 전세계의 사용자를 위해 준비되었다는 것을 보장할 수 있다.

Internationalization은 문화적인 정보(언어, 날짜 형식, 숫자 형식 등등)이 앱에서 하드 코딩 되어 있지 않은 것을 보장한다. Localization은 사용자의 Language and Region Format 설정에 따라 적절한 데이터를 앱에 제공하는 과정이다.

이 세팅은 iOS 설정 앱에서 확인할 수 있다. General 행을 선택하고 Language & Region 행을 선택해라.

image

애플은 internationalization과 localization을 간단하게 할 수 있게 했다. Localization API를 사용하는 앱은 다른 언어나 지역을 위해 다시 컴파일 될 필요도 없다.

Internationalization

여기서, ConversionViewController를 internationalize하기 위해 NumberFormatterNSNumber 클래스를 사용할 것이다.

Formatters

앞서서, 온도 텍스트를 변환하는데 NumberFormatter를 사용했었다. NumberFormatterlocale 프로퍼티가 있어서, 기기의 현재 지역에 맞출 수 있다. NumberFormatter를 이용해서 숫자를 생성할 때, locale 프로퍼티를 확인해서 포맷을 맞춰준다.

Locale은 심볼, 날짜, 그리고 숫자를 지역에 따라 어떻게 출력해야 하는지 안다. Locale의 인스턴스는 한 지역의 이런 변수들에 대한 세팅을 나타낸다. 만약 Localecurrent 프로퍼티에 접근하면, 사용자의 지역 세닡을 대표하는 Locale의 인스턴스가 반환된다.

let currentLocale = Locale.current
let isMetric = currentLocale.usesMetricSystem
let currencySymbol = currentLocale.currencySymbol

우리는 NumberFormatter를 사용하고 있으므로 온도 라벨은 이미 internationalized가 되어 있지만, 여전히 문제가 있다. 시뮬레이터에서 지역을 바꿔서 한 번 실행해 보겠다. Edit Scheme을 눌러 Option 탭의 앱 지역을 유럽 > 스페인으로 바꿔준다.

image

image

텍스트 필드를 누르면 키보드가 뜨는데, 이전과 달라진 점이 있다면 스페인에서 decimal separator는 . 대신에 ,라는 것이다. 그래서 미국에서 123,456.789는 스페인에서 123.456,789이다.

그래서 앞서 제한했던 여러개의 decimal separator(comma)를 입력하는 것이 지금은 가능해진다.

코드를 아래와 같이 수정한다.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
                   replacementString string: String) -> Bool {
    let currentLocale = Locale.current
    let decimalSeparator = currentLocale.decimalSeparator ?? "."
    let existingTextHasDecimalSeparator = textField.text?.range(of: decimalSeparator)
    let replacementTextHasDecimalSeparator = string.range(of: decimalSeparator)

    if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
        return false
    } else {
        return true
    }
}

이제 문제가 생겼다. 원래 ,을 입력해도 되던게 전환이 되지 않아서 라벨 텍스트가 ??? 로 남아있는 것이다. 이는 Double을 이용해서 숫자를 만들었는데, 이 생성자는 decimal separator로 . 대신에 ,가 들어오면 어떻게 이를 다뤄야 할 지 모르기 때문이다. 추가적으로 코드를 또 수정한다.

@IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
    if let text = textField.text, let number = numberFormatter.number(from: text) {
        fahrenheitValue = Measurement(value: number.doubleValue, unit: .fahrenheit)
    } else {
        fahrenheitValue = nil
    }
}

이 숫자를 바꾸는 부분을 지역에 의존적이게 했다. 이제 ,를 사용해도 숫자가 잘 변환이 된다.

Base internationalization

Internationalization을 할 때, Locale 인스턴스에 질문을 던진다. 하지만 Locale은 적은 수의 지역에 맞는 변수들을 가직 있다. 여기서 다른 지역과 언어 설정에 맞는 앱-명시적인 치환을 생성하는 localization이 중요해진다. Localization은 다른 지역과 언어를 위한 리소스(이미지, 사운드, 인터페이스 파일을)을 여러개 복사하는 것을 포함하거나, 다른 언어로 바꾸는 것을 위해 strings tales을 생성하고 접근하는 것에 관여한다.

리소스를 localizing 하기 전에, iOS 앱이 지역화된 리소스를 어떻게 다루는지 이해해야 한다.

Xcode에서 타겟을 빌드할 때, 앱 번들이 생성된다. Xcode의 target에 추가한 모든 리소스는 스스로 실행가능한 상태로 이 번들에 복사된다. 이 번들은 main bundle로 알려진 Bundle의 인스터느에 의해 런타임에 표현된다. 많은 클래스들이 리소스를 불러오기 위해 Bundle을 이용한다.

리소스를 localizing하는 것은 앱 번들의 리소스들의 또 다른 복사본을 만드는 것이다. 이 리소스들은 lproj 디렉토리로 알려진 언어-명시적인 디렉토리에 들어간다. 지역화된 각각의 것들은 lproj가 끝에 붙어서 이름이 정해진다. 예를 들어, 미국 영어로 지역화된 것은 en_US로, 영국 영어는 en로 명명된다.

번들이 리소스 파일의 경로를 요청받았다면, 먼저 번들의 루트 레벨에 가스 해당 파일 이름이 있는지 본다. 만약 못 찾았다면, 기기의 지역과 언어 세팅을 보고, 적절한 lproj 디렉토리를 찾아서 해당 디렉토리에서 파일을 찾는다. 그래서, 리소스를 지역화하는 것만으로도 앱이 자동으로 정확한 파일을 로드할 수 있는 것이다.

리소스 파일을 지역화 하는 한 옵션은 별개의 스토리 보드 파일을 생성한 다음 각 파일의 각 문자열을 수동으로 수정하는 것이다. 하지만, 여러 나라에 맞는 지역화를 수행할 때 이 방법은 적합하지 않다.

이를 위해 Xcode는 base internationalization이라는 기능이 있다. Base internationalization은 메인 인터페이스 파일을 포함한 Base.lproj 디렉토리를 만든다. 개별 인터페이스 파일을 지역화하는 것은 Localizable.strings 파일을 생성하는 것만으로 할 수 있다. 이제 문자열만을 변경해서 지역화를 수행할 수 없는 경우에도 여전히 전체 인터페이스 파일을 생성하는 것이 가능하다. 하지만, Auto Layout의 도움을 받으면 거의 대부분의 지역화를 위한 문자열 치환을 수행할 수 있다.

Preparing for localization

먼저 스토리보드에서 preview editor를 열었다. 나는 스토리보드에서 이 메뉴를 찾을 수가 없어서 아래처럼 열었다.

image

이제 Conversion View Controller를 선택하면 프리뷰에 아래와 같이 뜬다.

image

이 프리뷰의 하단에 있는 + 버튼은 프리뷰 캔버스에 다른 크기의 화면을 추가할 수 있다. 이거는 인터페이스가 다른 크기의 화면에 따라 어떻게 작용하는지 바로 확인할 수 있게 해준다. 오른쪽에 있는 버튼은 이 인터페이스를 어떤 언어로 미리 볼 지 선택할 수 있게 해준다.

아직 앱을 다른 언어로 localize 하지 않았지만, Xcode는 pseudolanguage를 사용할 수 있게 해준다. Pseudolanguage는 모든 문자열과 에셋에 대해 translation을 하기 전에 앱을 internationalize하는 것을 도와준다. 이미 구현된 Double-Length Pseudolanguage는 텍스트 에 무엇이 있든간에 이를 두 번 반복해서 좀 더 언어가 긴 언어를 흉내낸다. 예를 들어 “is really” 는 “is really is really”가 되는 것이다.

image

여기서 문제를 확인할 수 있다. 라벨이 왼쪽과 오른쪽 화면 끝을 넘어서서 전체 문자열을 읽을 수 없다는 것이다. 그래서 라벨의 line count를 0으로 만들어서, 필요한 경우 라벨이 텍스트가 여러줄일 때 이걸 감싸도록 하게 하는 것이다. 먼저 다시 이전의 언어를 선택한 다음 이 과정을 반복해 보겠다.

이제 라벨에 새로운 제약을 추가할 건데, 라벨의 왼쪽 끝에서 컨트롤을 누른 상태로 superview로 드래그 드랍한다. 그리고 Leading Space ~ 를 선택한다.

image

드래그 하는 방향은 표시될 수 있는 제약들에 영향을 미친다. 수평적으로 드래그하면 수평적인 제약들을 보여주고, 수직으로 드래그 하면 수직 제약을 보여준다. 대각선으로 드래그 하면 수평, 수직 제약을 모두 보여준다. 같은 방법으로 trailing 도 설정해준다.

image

아직도 문제가 있다. 이제 왼쪽 오른쪽 기준은 맞춰졌는데 …으로 생략이 되는 부분이 생기는 것이다. 지금은 leading, trailing edge 사이에 거리가 고정이 되어 있다. 이제 해야 하는 건 라벨과 마진 사이의 거리가 0 이상이게 조정해야 한다. 이를 inequality constraints를 이용해 할 수 있다.

아까 설정한 leading, trailing 의 파란색 실선 바를 선택해서 Relation을 Greater Than or Equal을 선택한다.

image

이 상태에서 실행시켜도 생략되는 부분이 있는 건 마찬가지다. 라벨을 선택해서 Lines를 0으로 조정한다.

image

이제 텍스트가 안 잘리고 잘 나온다.

image

Localization

이제 앱은 internationalized되었다. 인터페이스는 다양한 언어와 지역을 수용할 수 있다. 이제 앱을 지역화할 차례다. 앱의 문자와 리소스를 새로운 언어에 맞게 업데이트해야 한다는 것이다. 이제부터는 interface인 Main.storyboard파일을 지역화할 것이다. 원래의 것에 두개의 lproj 디렉토리를 더해서 English, Spanish localization을 만들 것이다.

먼저 프로젝트 네비게이터에서 Main.storyboard를 클릭하고, 첫 번째 insepctor 메뉴에서 Localization 메뉴를 클릭한다. 영어 박스 부분을 선택하고 드롭다운 메뉴가 Localizable Strings를 표시하도록 한다. 이거는 앱을 localizae하는 문자열 테이블을 만들 것이다.

image

그리고 프로젝트 설정에서 Localizations 부분에 Spanish(es)를 선택한다. 다이얼로그에서, LaunchScreen.storyboard를 체크해제 할 수는 있지만, Main.storyboard 파일을 선택되어 있어야 한다. 참조 언어가 Base이고 파일 타입이 Localizable Strings이어야 한다. 이렇게 하면 es.lproj 폴더를 만들고 base 인터페이스 파일의 모든 문자열을 포함하는 Main.strings 파일을 생성한다.

image

image

이제 프로젝트 네비게이터를 보면 Main.storyboardBase.lproj 디렉토리로 옮기고 Main.strings 파일을 es.lproj 디렉토리에 만든 것을 확인할 수 있다.

image

Spanish 버전의 Main.strings 파일을 아래와 같이 수정했다.


/* Class = "UITextField"; placeholder = "value"; ObjectID = "7CG-2K-bBP"; */
"7CG-2K-bBP.placeholder" = "valor";

/* Class = "UILabel"; text = "100"; ObjectID = "Dp4-Ff-2YL"; */
"Dp4-Ff-2YL.text" = "100";

/* Class = "UILabel"; text = "degrees Celcius"; ObjectID = "JYr-l6-6a0"; */
"JYr-l6-6a0.text" = "grados Celsius";

/* Class = "UILabel"; text = "degrees F"; ObjectID = "JsW-7C-mCi"; */
"JsW-7C-mCi.text" = "grados Fahrenheit";

/* Class = "UITabBarItem"; title = "Map"; ObjectID = "fBe-HG-EVN"; */
"fBe-HG-EVN.title" = "Mapa";

/* Class = "UITabBarItem"; title = "Convert"; ObjectID = "npJ-hr-i1k"; */
"npJ-hr-i1k.title" = "Convertir";

/* Class = "UILabel"; text = "is Really"; ObjectID = "ptB-Rd-Rxr"; */
"ptB-Rd-Rxr.text" = "es realmente";

이제 이 스토리보드 파일을 localize 하긴 했는데, 주의해야 할 점이 있다. 가끔 Xcode는 앱을 빌드할 때 리소스 파일이 변경된 것을 무시한다. 이 변경을 확실히 반영하기 위해 먼저 기기에서 앱을 삭제하고, Xcode를 재시작한다. 그리고 Product 메뉴에서 Clean을 누른다. 마지막으로, Product 메뉴의 Clean Build Folder를 선택할 때 Option 키를 누른 상태로 있는다. 이게 앱이 완전히 재컴파일되고, 다시 번들로 만들어지고, 재설치되는 것을 강제한다.

그리고 scheme에서 언어가 Spanish인지 다시 확인해봐라.

왜 안되지? 다시 확인해서 추가할 예정.