Back to Blog
📚 Translation - Part 2

[Translation] iOS 앱에서 네트워크 상의 텍스트 다국어 지원하기 2️⃣ / 커스텀 환경에서의 번역 준비하기

2025년 01월 19일
18분
Swift
Translation, Framework, iOS, SwiftUI, 글또 10기
[Translation] iOS 앱에서 네트워크 상의 텍스트 다국어 지원하기 2️⃣ / 커스텀 환경에서의 번역 준비하기

지난 시간에는 iOS 앱에서 네트워크 상의 텍스트를 번역하는 방법 중 하나인 Translation의 개요와 시스템 내장 UI 환경에서 번역을 제공하는 방법을 알아봤습니다.
Translation은 시스템 내장 UI뿐만 아니라 커스텀 환경에서 번역을 지원하는 방법 또한 제공하고 있는데, 이번 시간부터는 커스텀 환경에서 번역을 제공하는 방법을 알아보겠습니다.

특히, 이번 2부에서는 커스텀 환경에서 번역을 제공하기 위한 방법인 TranslationSession의 개요와 커스텀 환경에서 번역을 제공하기 위한 준비, 그리고 Translation에서 지원하는 언어 및 상태에 대해 알아보겠습니다.

TranslationSession

iOS 기본 앱 중에는 iMessage, Safari 등 이미 시스템 UI를 사용하여 번역을 제공하는 좋은 예시들이 있습니다.
이렇게 시스템 UI를 사용해도 좋은 앱 경험을 만들 수 있지만, 더 자연스럽고 효과적인 번역 환경 및 앱 경험을 제공하기 위해 Translation은 시스템 UI 환경이 아닌 커스텀 번역 환경 또한 제공합니다.

앱 내에서 커스텀 번역 환경을 제공하기 위해서는 TranslationSession 객체와 관련 번역 기능을 사용해야 합니다.

TranslationSession은 번역하려는 콘텐츠의 언어와 번역될 대상 언어로 이루어진 한 쌍의 언어 간 번역을 수행하는 클래스입니다.

이 클래스는 개발자가 직접 인스턴스화해서 사용할 수 없습니다. 그 대신 translationTask(\_:action:) 또는 translationTask(source:target:action:) 모디파이어의 클로저가 제공하는 TranslationSession 인스턴스를 사용할 수 있습니다.

이 모디파이어들을 사용하면 모디파이어가 적용된 뷰가 화면에 표시되면 실행되는 액션 클로저에 TranslationSession의 인스턴스를 전달하며, 이 인스턴스를 받은 후 번역 함수 중 하나를 사용하여 하나 이상의 텍스트 문자열을 번역할 수 있습니다.

TranslationSession 인스턴스를 제공하는 모디파이어

위에서 설명했듯 TranslationSession 인스턴스를 제공하는 모디파이어에는 translationTask(source:target:action:)translationTask(\_:action:)이 있습니다.

그럼 이제부터 이 두 가지 모디파이어에 대해 알아보겠습니다.

1. translationTask(source:target:action:)

nonisolated func translationTask(source: Locale.Language? = nil, target: Locale.Language? = nil, action: @escaping (TranslationSession) async -> Void) -> some View

먼저, translationTask(source:target:action:) 모디파이어는 연결된 뷰가 표시되기 전, 혹은 source 파라미터나 target 파라미터가 변경될 때 action 클로저의 동작을 실행합니다.
action 클로저는 하나 이상의 번역을 수행할 수 있는 TranslationSession 인스턴스를 제공하는데, 만약 연결된 뷰가 사라진 후에 인스턴스를 사용하거나, source 혹은 target 파라미터를 변경한 후에 사용하면 클로저가 새 TranslationSession 인스턴스를 제공하게 되므로 시스템에서 fatalError를 발생시킵니다.

아래의 코드는 translationTask(source:target:action:) 모디파이어를 사용하여 번역하는 예시 코드입니다.

struct CommentView: View {
    var userName: String
    var originalText: String
    var date: String
    var sourceLanguage: Locale.Language?
    var targetLanguage: Locale.Language?
 
    @State private var translatedText: String?
 
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: "person.crop.circle")
                Text(userName)
                Spacer()
                Text(date)
                    .font(.footnote)
            }
            Divider()
            Text(originalText)
                .translationTask(source: sourceLanguage, target: targetLanguage) { session in
                    Task { @MainActor in
                        do {
                            // 번역 코드
                        } catch {
                            // 오류 처리 코드
                        }
                    }
                }
 
            if translatedText != nil {
                Divider()
                Text("번역된 텍스트")
                    .font(.footnote)
                Text(translatedText!)
            }
        }
        .padding()
        .background {
            RoundedRectangle(cornerRadius: 16)
                .foregroundStyle(Color(uiColor: .systemGray6))
        }
    }
}

translationTask(source:target:action:) 모디파이어는 간단한 번역 작업을 수행 하기에는 좋지만, 버튼을 이용해 번역을 트리거한다거나, 혹은 번역할 콘텐츠의 언어는 그대로인데 콘텐츠의 내용만 변경된 경우 등의 상황에는 대처하기 어렵습니다.

이렇게 버튼을 이용해 번역을 트리거하거나, 동일한 sourcetarget으로 새 번역을 다시 수행해야하는 경우 아래의 translationTask(\_:action:)을 사용한 후, TranslationSession.Configurationinvalidate()를 호출하는 것이 더 좋습니다.

2. translationTask(_:action:)

nonisolated func translationTask(\_ configuration: TranslationSession.Configuration?, action: @escaping (TranslationSession) async -> Void) -> some View

translationTask(\_:action:) 모디파이어는 연결된 뷰가 표시되기 전, 혹은 configuration 파라미터가 변경될 때 action 클로저의 동작을 실행합니다.
action 클로저는 translation(source:target:action:)과 마찬가지로 하나 이상의 번역을 수행할 수 있는 TranslationSession 인스턴스를 제공합니다.
마찬가지로, 만약 연결된 뷰가 사라진 후 인스턴스를 사용하거나, configuration 파라미터를 변경한 후에 사용하면 클로저가 새 TranslationSession 인스턴스를 제공하게 되므로 시스템에서 fatalError를 발생시킵니다.

아래의 코드는 translationTask(\_:action:) 모디파이어를 사용하여 번역하는 예시 코드입니다.

struct CommentView: View {
    var userName: String
    var date: String
    var sourceLanguage: Locale.Language?
    var targetLanguage: Locale.Language?
 
    @State private var originalText: String = ""
    @State private var translatedText: String?
    @State private var configuration: TranslationSession.Configuration?
 
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Image(systemName: "person.crop.circle")
                Text(userName)
                Spacer()
                Text(date)
                    .font(.footnote)
                Button {
                    guard configuration == nil else {
                        configuration?.invalidate()
                        return
                    }
 
                    configuration = TranslationSession.Configuration(source: sourceLanguage, target: targetLanguage)
                } label: {
                    Image(systemName: "translate")
                }
                .translationTask(configuration) { session in
                    Task { @MainActor in
                        do {
                            // 번역 코드
                        } catch {
                            // 오류 처리 코드
                        }
                    }
                }
            }
            Divider()
            Text(originalText)
 
            if translatedText != nil {
                Divider()
                Text("번역된 텍스트")
                    .font(.footnote)
                Text(translatedText!)
            }
        }
        .padding()
        .background {
            RoundedRectangle(cornerRadius: 16)
                .foregroundStyle(Color(uiColor: .systemGray6))
        }
    }
}

translationTask(\_:action:)을 사용하면 동일한 언어 구성이어도, 콘텐츠가 변경된다면 다시 번역을 수행할 수 있으며, 버튼을 이용해서 새 번역을 표시할 수도 있습니다.

TranslationSession.Configuration

위의 translationTask(\_:action:) 모디파이어를 보면서 configuration 파라미터의 타입으로 TranslationSession.Configuration이 사용된 것을 확인할 수 있습니다.

그렇다면 이 TranslationSession.Configuration 타입은 무엇일까요?

struct Configuration

translationTask(\_:action:) 모디파이어에서는 translationTask(source:target:action:) 모디파이어에서 사용된 source, target 파라미터를 대신해서 TranslationSession.Configuration 타입의 configuration 파라미터를 사용하여 TranslationSession에 사용할 sourcetarget 언어를 지정할 수 있습니다.

TranslationSession.Configuration 타입은 init(source:target:) 생성자를 이용하여 생성할 수 있습니다.

생성자

init(source: Locale.Language? = nil, target: Locale.Language? = nil)

source 파라미터는 원본 콘텐츠의 언어로, 개발자가 직접 원본 콘텐츠의 언어를 지정할 수도 있지만, nil로 설정하면 세션이 스스로 언어를 식별하려고 시도합니다.
만약, 언어가 명확하지 않다면 사용자에게 원본 언어를 선택하라는 메시지를 표시합니다.
이 때, 하나의 세션으로 번역된 모든 텍스트는 동일한 원본 언어로 작성되어야 합니다.
즉, 일본어 콘텐츠와 영어 콘텐츠를 하나의 세션으로 번역할 수는 없다는 의미로, 여러 언어를 번역하려면 여러 세션을 사용해야 합니다.

target 파라미터는 콘텐츠를 번역할 언어로, 만약 nil이라면 세션은 사용자 디바이스의 Locale.preferredLanguagessource에 따라 대상 언어를 선택하려고 시도합니다.

참고로, TranslationSession.Configuration을 만들 때는 LanguageAvailability.supportedLanguages에서 반환되는 Locale.Language 값을 사용하는 것이 가장 좋습니다. 만약 다른 Locale.Language 값을 전달하면 프레임워크는 지원되는 언어 중 하나에 일치시키려고 합니다.

새 번역 실행하기

translationTask(\_:action:) 모디파이어 코드 예시에서 configurationnil이 아닐 때, invalidate()라는 메서드를 사용했습니다.

mutating func invalidate()

invalidate() 메서드는 현재의 TranslationSession을 무효화하여 새 콘텐츠로 세션을 다시 실행하는 메서드로, 동일한 sourcetarget을 사용하여 새 콘텐츠를 번역하기 위해서는 이 메서드를 사용해야만 합니다.
이렇게 하면 translationTask(\_:action:) 메서드가 해당 액션 클로저를 호출하고 콘텐츠를 다시 번역하게 됩니다.

Translation에서 사용 가능한 언어

Translation은 다양한 언어를 지원하지만, 모든 언어를 지원하지는 않습니다. 이러한 언어 지원 및 상태를 확인하기 위해 LanguageAvailability를 사용할 수 있습니다.

class LanguageAvailability

LangugageAvailabilityTranslation 프레임워크가 지원하는 언어, 또는 언어 쌍을 지원하는지 확인할 수 있는 클래스로 사용자의 디바이스가 번역을 지원하는지 확인하기 위해서 아래와 같은 코드를 사용할 수 있습니다.

func checkTranslationSupportLanguage(source: Locale.Language, target: Locale.Language) async {
    let availability = LanguageAvailability()
    let status = await availability.status(from: source, to: target)
 
    switch status {
        case .installed:
            print("언어가 디바이스에 설치되어 있음")
        case .supported:
            print("프레임워크에서 지원하는 언어임")
        case .unsupported:
            print("프레임워크에서 지원하지 않는 언어임")
    }
}

프레임워크에서 지원하는 언어 알아보기

var supportedLanguages: [Locale.Language] { get async }

supportedLanguagesTranslation 프레임워크에서 지원하는 언어 배열을 반환하는 프로퍼티로, 이 배열을 참고하여 TranslationSession.Configuration을 구성하는 것이 가장 좋습니다.
만약 이 배열에 포함되지 않는 언어를 사용하여 구성한다면, Configuration에 사용된 언어와 가장 비슷한 언어를 해당 배열에서 찾아서 번역을 시도합니다.

번역 언어 상태 확인하기

번역할 언어의 상태를 확인하는 데에는 status(from:to:)status(for:to:)의 두 가지 메서드를 사용할 수 있습니다.
이 두 가지 메서드는 번역에 필요한 언어가 사용자의 디바이스에 다운로드되어 있는지 확인할 수 있습니다.

1. status(from:to:)

func status(from source: Locale.Language, to target: Locale.Language?) async -> LanguageAvailability.Status

먼저, status(from:to:) 메서드는 특정 언어 쌍이 설치되어 번역할 준비가 되었는지를 확인하는 메서드입니다.
source 파라미터는 원본 콘텐츠의 언어를 의미하며, target은 번역할 대상 언어입니다.
targetnil이라면 시스템이 사용자가 선호하는 언어에 따라 적절한 언어를 선택하여 해당 언어의 상태를 반환합니다.

2. status(for:to:)

func status(for text: String, to target: Locale.Language?) async throws -> LanguageAvailability.Status

status(for:to:) 메서드는 text 파라미터에 사용된 샘플 텍스트 문자열을 기반으로 target 파라미터의 언어와 언어 쌍이 지원되는 지 확인하는 메서드로, target 파라미터가 nil이라면 status(from:to:)와 마찬가지로 시스템이 사용자가 선호하는 언어에 따라 적절한 언어를 선택하여 해당 언어의 상태를 반환합니다.

이 메서드는 source 언어를 모르고 프레임워크가 사용자의 샘플 텍스트를 기반으로 번역을 시도하려할 때 사용하면 좋습니다.
시스템은 text에 전달된 텍스트의 언어를 감지하여 번역을 시도하고, 만약 언어를 감지하지 못했다면 TranslationError를 발생시킵니다.
이 때, 자동 언어 감지에서 최상의 결과를 얻으려면 최소 20자 이상의 샘플 문자열을 전달하는 것이 좋습니다.

번역 언어의 상태

enum Status {
    case installed
    case supported
    case unsupported
}

Status 열거형은 언어 또는 언어 쌍의 사용 가능 상태를 의미합니다.

installed는 프레임워크에서 해당 언어, 혹은 언어 쌍을 지원하며, 디바이스에 해당 언어가 설치되어 있어 번역에 사용할 수 있음을 의미합니다.
supported는 프레임워크에서 해당 언어를 지원하지만, 디바이스에는 설치되어 있지 않아 사용할 수 없음을 의미합니다. 해당 언어를 번역에 사용하려면 먼저 디바이스에 언어를 설치해야 합니다.
unsupported는 프레임워크에서 해당 언어를 지원하지 않음을 의미하며, 번역에 해당 언어를 사용할 수 없습니다.

참고로, Translation 프레임워크는 영국 영어(en-GB)에서 미국 영어(en-US)로의 번역 등 동일한 언어 간의 번역은 지원하지 않습니다.

번역 준비하기

class TranslationSession {
    func prepareTranslation() async throws
}

TranslationSessionprepareTranslation() 메서드는 번역을 직접 수행하지는 않지만, 후에 번역을 수행하기 위해 번역 언어를 다운로드할 수 있는 권한을 사용자에게 요청합니다.

사용자가 번역해야하는 언어를 미리 알고 있는 경우, 이 메서드를 호출하여 해당 언어를 미리 다운로드하라는 메시지를 표시할 수 있습니다.
이 메서드를 호출하면 TranslationSession.Configuration에서 사용된 sourceLanguagetargetLanguage를 다운로드할 수 있는 권한을 요청합니다. 언어가 미리 설치되었거나 다운로드 중이면 사용자에게 메시지를 표시하지 않고 함수가 반환됩니다.
sourceLanguagenil일 때 이 함수를 호출하면 번역에 사용할 source 언어를 식별할 샘플 텍스트가 없으므로 unableToIdentifyLanguage 에러가 발생합니다.

아래의 코드는 언어 쌍을 다운로드하기 위해 사용자에게 권한을 묻는 시트를 표시하는 예제 코드입니다.

struct ExampleView: View {
    @State private var configuration = TrnaslationSession.Configuration(source: Locale.Language(identifier: "ja_JP"), target: Locale.Language(identifier: "ko_KR"))
    ...
 
    var body: some View {
        ...
        Text("")
            .translationTask(configuration) { session in
                do {
                    try await session.prepareTranslation()
                } catch {
                    // 오류 처리
                }
            }
        ...
    }
}

마무리

이번 시간에는 TranslationSession의 개요와 TranslationSession.Configuration, LanguageAvailability 등 본격적인 커스텀 환경에서의 번역에 앞서, 사용가능한 언어를 확인하고 번역을 준비하는 방법을 알아보았습니다.
다음 시간에는 커스텀 환경에서 실제로 번역을 하는 방법에 대해 알아보겠습니다.

참고자료

댓글 및 반응

GitHub 계정으로 댓글을 남기거나 반응을 남길 수 있습니다