iOS에서 Xcode로 다국어 지원하기(4)

0

iOS 다국어 처리

목록 보기
4/4
post-thumbnail

서론

https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기2
https://velog.io/@sustainable-git/iOS에서-Xcode로-다국어-지원하기3
이전 글에서 String catalog를 활용해 다국어를 처리하는 방법을 소개하였습니다.
오늘은 이전 글에 이어서 String Catalog를 활용해서 겪을수 있는, 제가 실제로 겪었던 이슈를 소개해보겠습니다.


%lld만 덩그러니

String Catalog를 쓰다 보면 이렇게 덩그렇게 %lld만 있는 상황이 벌어집니다.
이런 이유는 String Catalog가 Text(_:) View에 넣어놓은 모든 String을 LocalizedStringResource로 판단하기 때문입니다.
그래서 이렇게 번역이 되지 않아야 할 문자까지 번역으로 넣어놓습니다.
이를 해결하려면 Text(_:)가 아닌 Text(verbatim:)을 사용해야 합니다.

verbatim은 직역하면 "말 그대로" 라는 뜻입니다.
이렇게 하면 String catalog가 자동으로 String을 가져오지 않아 번역이 되지 않는 Text View를 만들 수 있습니다.


String(format:,_:) 값을 번역하는 법

    if hours > 0 {
        return String(format: "%02d시 %02d분 %02d초", hours, minutes, seconds)
    } else {
        return String(format: "%02d분 %02d초", minutes, seconds)
    }

개발을 하다 보면 시 분 초를 두 자리씩 보여줘야 하는 경우를 마주할 수 있습니다.
하지만, String(format:,_:) 함수는 string catalog에서 자동으로 등록해주지 않습니다.
그렇기에 이를 자동으로 처리할 수 있는 String(localized:) 함수를 사용하는게 좋습니다.


동일한 Key가 여러개일 때(동음이의)

String catalog에서 Key는 동일한 값이 존재할 수 없고, 반드시 Hashable해야 합니다.
따라서 위의 예시의 방망이박쥐의 사례처럼 각각의 Key에 값을 다르게 지정해 주어야 합니다.
저의 경우 개발 가독성 향상을 위해 영어 Key를 한국어 Key로 변경하는 작업을 했기 때문에 의 사례와 같은 경우가 발생하였습니다.
Key를 한국어로 바꾸어야하는 상황이었는데, 6개 언어(한,간체,번체,일,영,태국)로 번역되어야 하는 것이 큰 문제였습니다.
각 언어마다 3500자, 6개 언어인 21000자가 String Catalog로 Migration되어 있습니다.

다행인 점은, 기존의 영어 Key도 Hashable해야하기 때문에(사실은 겹치는게 있어서 고생했지만)
Filter에 기존의 영어 Key를 넣어 주면 쉽게 찾을 수 있었습니다.
String catalog에서 새로운 Key를 만들고(자동으로 만들어진 Key가 있으면 사용하면 됩니다)
만약 의 사례처럼 중복되는 것이 있으면 배1 처럼 Key를 만들고 Korean 번역을 로 해주면 됩니다.
단, 해당 Key에 각 언어마다 번역된 값을 일일이 넣어줘야 합니다.


Merge Conflict

String catalogs는 merge conflict에 매우 매우 취약한 형태입니다.
매 빌드마다 String catalogs가 조금씩 바뀔 수 있는데, 크지 않은 변경 또는 변경이 없는데에도 conflict가 발생할 수 있습니다.
저장되는 형태의 특성상 컴퓨터가 변경된 위치를 알아차리기 힘들기 때문에 두 명 이상의 수정에 엄청난 수의 conflict가 발생하곤 합니다.
게다가 소스코드와 달리 디버깅이 매우 어려운 형태로 발생하며, 조금의 문제라도 있으면 String catalog가 열리지 않습니다.
또한, merge conflict의 패턴이 일관적이지 않아 간단한 스크립트로도 처리하기 어렵습니다.
때문에 String catalog를 사용할 때에는 merge conflict에 유의하며 개발하는 것을 추천드립니다.

혹시 tuist처럼 변환해주는 프로그램을 만드신다면 공유 부탁드립니다...


개행이 있는 문자가 번역이 안되는 현상

우선, String catalog에서 Key가 아닌 value에서 개행을 처리하려면 \n이 아닌 control + enter를 사용하는 것에 유의해야 합니다.
그러나, 이런 처리를 제대로 하였음에도 번역이 안되는 경우가 있습니다.
그럴 때에는 String catalog를 source code로 열어 보아야 합니다.

개행이 \\n으로 되어 있거나 \r이 들어가 있는 경우가 있습니다.
이는 특히 기존의 legacy strings 파일을 Migration한 경우 더 많이 발생할 수 있습니다.


국가마다 숫자를 표기하는 방법이 다릅니다

국가마다 천의 자리를 표기하는 방법이 다릅니다.
인도의 경우 특정 지역에서는 불규칙적으로 띄워쓰기를 하는 경우도 있습니다.
여기서 주의해야할 점은 바로 숫자를 \(Int)의 형태로 써야한다는 것입니다.

많은 개발자들이 숫자를 보여준다에 집중하기 때문에 이를 String의 형태로 전달하곤 합니다.
하지만, 그렇게 되면 국가마다 다른 숫자 단위 표기를 제대로 지원하기 어렵습니다.
때문에 정수 값을 다룰 때에는 \(Int)의 형태로 쓰기 바랍니다.


언어 제거하기

String Catalog는 각 하나의 Key를 제거하는 방법은 존재하지만, 하나의 언어를 통째로 지우기는 불가능합니다.
프로젝트 언어에서 특정 언어를 제거하고 빌드해도 사라지지 않습니다. (이걸 지우려고 한 세월 날렸습니다)
결국 제가 선택한 방법은 python 스크립트를 이용해 string catalog 파일에서 특정 언어를 지우고, 다시 갈아 끼우는 것이었습니다.

import json

def remove_localizations(input_filepath, output_filepath, languages_to_remove):

    with open(input_filepath, 'r', encoding='utf-8') as file:      # Load the JSON data from the file
        data = json.load(file)

    for string_key in list(data["strings"].keys()):                 # Remove specified languages from the 'strings' section
        string_data = data["strings"][string_key]
        if "localizations" in string_data:                          # Check if 'localizations' exists before trying to access it
            for lang in languages_to_remove:
                if lang in string_data["localizations"]:
                    del string_data["localizations"][lang]

    with open(output_filepath, 'w', encoding='utf-8') as file:     # Write the updated JSON data back to a file
        json.dump(data, file, indent=2, ensure_ascii=False)


input_filepath      = 'local_in.txt'        # Name of file with all languages       (ie: 'local_in.txt' file in the same directory, so use the relative path)
output_filepath     = 'local_out.txt'       # Name of file with the the new changes (ie: 'local_out.txt' the original or you can overwrite the file if you want)
languages_to_remove = ['en', 'ja', 'th-TH', 'zh-Hans', 'zh-Hant']          # Languages to remove                   (in my case, I only wanted to keep the root ('en') language and 'jp')

# Call the function to remove the languages
remove_localizations(input_filepath, output_filepath, languages_to_remove)

한 문장에 여러 개의 색상과 폰트가 필요한 경우

여기 제가 개발한 방법을 소개하지만, 여러 가지 방법으로 개발할 수 있습니다.
언어마다 각 속성이 어디서 어떻게 동작하는지가 바뀔 수 있습니다.
그 전에 속성이 동작 해야하는지, 하지 않아야 하는지도 국가마다 다를 수 있습니다.

  • 예) 중국어는 빨갛게, 한국어는 볼드로 해주세요. 근데 영어는 해당 표현이 없으니 표현을 빼고 해주세요.

그럼에도 불구하고 가능한 최선의 경우라고 생각하는 코드를 만들었으니 도움이 되길 바랍니다.
html과 유사한 느낌으로 개발해 보았습니다.

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack(spacing: 5) {
            LocalizedAttributedText("사과는 6000원 입니다.", attributes: ["<color>" : .init([.foregroundColor : UIColor.red])])
        }
    }
}

struct LocalizedAttributedText: View {
    let attributedString: AttributedString

    init(_ localizedStringResource: LocalizedStringResource, attributes: [String: AttributeContainer]) {
        var attributedString = AttributedString(String(localized: consume localizedStringResource))
        attributes.forEach { (key, value) in
            if let regex = try? Regex(#"<(?<tagName>[a-zA-Z0-9_]+)>"#),
               let match = key.firstMatch(of: consume regex),
               let tagName = (consume match).output["tagName"]?.value,
               let startRange = attributedString.range(of: "<\(tagName)>"),
               let endRange = attributedString.range(of: "</\(tagName)>") {
                attributedString[startRange.upperBound..<endRange.lowerBound].setAttributes(value)
                attributedString.removeSubrange(consume endRange)
                attributedString.removeSubrange(consume startRange)
            }
        }
        self.attributedString = consume attributedString
    }

    var body: some View {
        Text(attributedString)
    }
}

#Preview {
    ContentView()
}

마무리

Xcode에서 String catalog를 활용해 다국어 처리를 하는 방법과 발생할 수 있는 다양한 이슈, 해결방법을 알아보았습니다.
위 내용이 SwiftUI 위주로 구성되어 있지만, UIKit에서도 충분히 사용이 가능합니다.
또한, LocalizedStringResource 활용이 가능한 iOS16 환경에서의 예시를 보여드렸지만,
사실 저는 iOS14를 지원해야 했어서 개발에 더 많은 고생을 하였습니다.

iOS14의 경우 NSLocalizedString을 사용해야 합니다.
또한 crash의 위험이 존재하기 때문에 Unit Test를 적극 활용해 사고를 미연에 방지할 필요가 있습니다.
하지만, 지금까지 알려준 내용에 크게 벗어나지 않기 때문에 이를 잘 활용 하면 문제 없이 다국어 처리를 할 수 있을거라 생각합니다.
아래에는 iOS14 이하의 경우에 Unit Test를 하는 방법에 관한 간단한 코드를 남겨두고 글을 이만 마치겠습니다.

extension String {
    func localized(bundle: Bundle, _ arguments: CVarArg...) -> Self {
        return String(format: NSLocalizedString(self, bundle: bundle, comment: ""), arguments: arguments)
    }
}

final class Tests: XCTestCase {
    enum LocalizationTestError: Error {
        case invalidKoreanBundle
        case invalidEnglishBundle
    }
    
    private var ko: Bundle!
    private var en: Bundle!
    
    override func setUpWithError() throws {
        guard let koreanPath = Bundle.main.path(forResource: "ko", ofType: "lproj"),
              let koreanBundle = Bundle(path: koreanPath) else {
            throw LocalizationTestError.invalidKoreanBundle
        }
        ko = koreanBundle
        
        guard let englishPath = Bundle.main.path(forResource: "en", ofType: "lproj"),
              let englishBundle = Bundle(path: englishPath) else {
            throw LocalizationTestError.invalidEnglishBundle
        }
        en = englishBundle
    }

    override func tearDown() {
        ko = nil
        en = nil
    }
}
profile
https://github.com/sustainable-git

0개의 댓글

관련 채용 정보