Post

SwiftUI: 번역 기능 (Translation) 사용하기 (iOS 18 이상)

SwiftUI: 번역 기능 (Translation) 사용하기 (iOS 18 이상)

iOS Translation 프레임워크는 iOS 17.4부터 도입된 기능으로, 개발자가 앱 내에 텍스트 번역 기능을 쉽게 통합할 수 있도록 애플이 제공하는 API입니다. 특히 iOS 18에서는 이 기능이 더욱 발전하여, 시스템에서 제공하는 내장 UI를 활용하거나 TranslationSession을 이용해 커스텀된 번역 기능을 구현할 수 있게 되었습니다.

주요 특징

  • 온디바이스 번역: 번역에 필요한 머신러닝 모델이 기기 내에서 실행되므로, 인터넷 연결 없이도 빠르고 안전하게 번역할 수 있습니다.
  • 시스템 통합: 사용자가 번역 모델을 다운로드하도록 권한을 요청하고, 다운로드 진행 상황을 표시하는 등 번역에 필요한 모든 과정을 시스템이 관리합니다.
  • 공유 모델: 한번 다운로드된 언어 모델은 시스템의 모든 앱이 공유하므로, 여러 앱에서 중복해서 다운로드할 필요가 없습니다.
  • 개인정보 보호: 번역이 기기 내에서 처리되기 때문에 개인 정보가 보호됩니다.

사용 방법

Translation 프레임워크는 크게 두 가지 방식으로 사용할 수 있습니다.

1. 내장 UI 활용

가장 간단한 방법으로, .translationPresentation 뷰 수정자를 사용해 시스템이 제공하는 번역 오버레이를 표시할 수 있습니다.

SwiftUI 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import SwiftUI
import Translation // 👈

struct ContentView: View {
  @State private var textToTranslate = "Translationフレームワークは大きく二つの方式で使用できます。"
  @State private var showTranslation = false

  var body: some View {
    VStack {
      Text(textToTranslate)
      Button("번역하기") {
        showTranslation.toggle()
      }
    }
    .translationPresentation(isPresented: $showTranslation, text: textToTranslate) // 👈
  }
}

Swift Translation 1

뒤에 클로저(replacementAction)를 추가하면 번역 창에서 ‘번역으로 대치’라는 메뉴가 생기며 이 메뉴를 눌렀을 때 할 작업을 지정할 수 있습니다.

1
2
3
4
.translationPresentation(isPresented: $showTranslation, text: textToTranslate) { result in
  // replacement action 핸들러
  textToTranslate = result
}

번역으로 대치

2. translationTask를 이용한 커스텀 구현 (上) - 시작시 바로 실행

더 세밀한 제어가 필요하다면 translationTask 를 사용해 직접 번역을 요청하고 결과를 처리할 수 있습니다.

  • source, target 언어가 nil이면 소스 언어를 자동으로 감지하며, 타깃 언어를 현재 OS 에서 사용중인 언어(예: 한국어)를 제공합니다.
  • translationTask의 action 클로저는 뷰가 초기화되면 자동으로 실행됩니다.

    action: A closure that runs when the view first appears, and when source or target change. It provides a TranslationSession instance to perform one or multiple translations. (뷰가 처음 나타날 때, 그리고 소스나 대상이 변경될 때 실행되는 클로저입니다. 하나 이상의 번역을 수행하기 위한 TranslationSession 인스턴스를 제공합니다.)

  • translationTask를 이용한 번역을 이용하려면 기기에 시작, 번역 언어의 데이터가 다운로드 되어있어야 합니다. 다운로드 되어있지 않은 경우 아래처럼 다운로드를 수행하는 시트가 나타납니다. 로컬에 언어 데이터가 다운로드 되어있어야 함

SwiftUI 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import SwiftUI
import Translation

struct CustomTranslationView: View {
  var sourceTexts = [
    "Translationフレームワークは大きく二つの方式で使用できます。",
    "Explore the warehouse and watch our consolidation processes in action, where everything is carefully organized to keep operations running smoothly.",
    "本機能は、プロデュース方針を決める事で、ゲーム内の様々な場面で、効率よくプロデュースを進める事ができるようになる機能です。",
    "And of course, we couldn’t resist a quick stop at the sky bar, where work meets a moment of relaxation.",
    "プロデュース方針は場数ptを使って設定する事ができ、自分のプレイスタイルに合わせたカスタマイズが可能です。"
  ]
  var sourceLanguage: Locale.Language?
  var targetLanguage: Locale.Language?
  
  @State private var translatedText = ""
  
  var body: some View {
    if #available(iOS 18.0, *) {
      Text(translatedText)
        .translationTask(
          source: sourceLanguage,
          target: targetLanguage
        ) { session in
          Task { @MainActor in
            for sourceText in sourceTexts {
              let response = try await session.translate(sourceText)
              translatedText += "\(response.targetText)\n"
            }
          }
        }
    } else {
      Text("Need iOS 18.0 or later")
    }
  }
}

Translation Task 1

3. translationTask를 이용한 커스텀 구현 (下) - configuration 을 trigger하여 실행

translationTaskconfiguration을 설정한 뒤 특정 요건을 만족하면 트리거되면서 번역 작업을 시작합니다.

  • TranslationSession.Configurationnil 상태였다가 초기화합니다.
  • 동일한 source/target 언어에서 invalidate()를 실행합니다.
  • 또는 source/target 언어가 변경된 경우 action 클로저가 트리거됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import SwiftUI
import Translation

@available(iOS 18.0, *)
struct CustomTranslationTriggerStartView: View {
  var sourceTexts = [
    "Translationフレームワークは大きく二つの方式で使用できます。",
    "Explore the warehouse and watch our consolidation processes in action, where everything is carefully organized to keep operations running smoothly.",
    "本機能は、プロデュース方針を決める事で、ゲーム内の様々な場面で、効率よくプロデュースを進める事ができるようになる機能です。",
    "And of course, we couldn’t resist a quick stop at the sky bar, where work meets a moment of relaxation.",
    "プロデュース方針は場数ptを使って設定する事ができ、自分のプレイスタイルに合わせたカスタマイズが可能です。"
  ]

  @State private var translatedTexts = [String](repeating: "", count: 5)
  @State private var isTranslating = false
  @State private var selectedLanguageCode = "ko-KR"
  @State private var lastTranslatedLanguageCode = ""

  // 구성 객체: 버튼으로 할당하면 translationTask에서 세션을 받음
  @State private var configuration: TranslationSession.Configuration?

  var body: some View {
    VStack(spacing: 16) {
      HStack {
        Text("Select target language:")
        Picker("Select target language", selection: $selectedLanguageCode) {
          Text("ko-KR")
            .tag("ko-KR")
          Text("ja-JP")
            .tag("ja-JP")
          Text("en-US")
            .tag("en-US")
        }
      }
      
      List {
        ForEach(sourceTexts.indices, id: \.self) { i in
          VStack(alignment: .leading) {
            Text(sourceTexts[i])
            Text(translatedTexts[i])
              .foregroundStyle(.gray)
          }
        }
      }

      HStack {
          Button {
            let targetLanguage = Locale.Language(identifier: selectedLanguageCode)
            // 여기서 configruation 트리거
            // source, target 언어가 동일하면 트리거가 안됨 => configuration.invalidate()를 사용하여 재트리거
            // 둘 중 하나가 이전과 다르면 재 트리거됨
            configuration = TranslationSession.Configuration(
              source: nil,
              target: targetLanguage
            )
          } label: {
            HStack {
              if isTranslating {
                ProgressView().scaleEffect(0.7)
              }
              let buttonText = selectedLanguageCode == lastTranslatedLanguageCode ? "\(lastTranslatedLanguageCode) 번역 완료" : "번역 시작"
              Text(isTranslating ? "번역 중..." : buttonText)
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 8)
            .background(Color.accentColor.opacity(0.1))
            .cornerRadius(8)
          }
          .disabled(selectedLanguageCode == lastTranslatedLanguageCode)
        }
        .padding()
        // translationTask 수식어: configuration이 설정되면 closure로 session을 받습니다.
        .translationTask(configuration) { session in
          // 세션 제공 시에 배치 번역 실행
          Task {
            await MainActor.run {
              translatedTexts = .init(repeating: "", count: 5)
              isTranslating = true
            }

            for i in sourceTexts.indices {
              let response = try? await session.translate(sourceTexts[i])
              translatedTexts[i] = response?.targetText ?? sourceTexts[i]
            }
            
            // 번역 완료되면 상태 업데이트
            await MainActor.run {
              isTranslating = false
              lastTranslatedLanguageCode = selectedLanguageCode
            }
          }
        }
    }
  }
}

Translation Task 2

지원 범위

  • OS: iOS 17.4 이상, iPadOS 17.4 이상, macOS 14.4 이상 (대부분의 API는 iOS 18, iPadOS 18, macOS 15부터 SwiftUI 환경에서 사용 가능).
  • Xcode: Xcode 16 이상.
This post is licensed under CC BY 4.0 by the author.