When I released the first version of LyricLens, it only supported translating to English. Someone mentioned they would like the translation to be in German, so I decided to explore how to achieve that.

That is how I stumbled upon the LanguageAvailability class. The idea was to provide users with a picker containing all the supported languages they can translate. Then, check the status to determine if that translation is even possible.

Fetching Available Languages

To get started, I fetch the list of available languages supported by the Translation Framework. The LanguageAvailability class provides a convenient asynchronous property called supportedLanguages that returns an array of Locale.Language objects representing the supported languages.

@available(iOS 18.0, macOS 15.0, *)
public var supportedLanguages: [Locale.Language] { get async }

Here is how to fetch the available languages:

@Published var availableLanguages: [Locale.Language] = []

func fetchAvailableLanguages() async {
  self.availableLanguages = await LanguageAvailability().supportedLanguages
}

This is what is printed for the available languages at the time of writing this post:

[Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(vi), script: nil, region: Optional(VN))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(pt), script: nil, region: Optional(BR))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(uk), script: nil, region: Optional(UA))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(it), script: nil, region: Optional(IT))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(zh), script: nil, region: Optional(TW))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(ko), script: nil, region: Optional(KR))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(en), script: nil, region: Optional(GB))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(de), script: nil, region: Optional(DE))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(zh), script: nil, region: Optional(CN))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(ja), script: nil, region: Optional(JP))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(id), script: nil, region: Optional(ID))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(nl), script: nil, region: Optional(NL))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(fr), script: nil, region: Optional(FR))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(th), script: nil, region: Optional(TH))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(es), script: nil, region: Optional(ES))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(tr), script: nil, region: Optional(TR))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(pl), script: nil, region: Optional(PL))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(ar), script: nil, region: Optional(AE))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(ru), script: nil, region: Optional(RU))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(en), script: nil, region: Optional(US))), Foundation.Locale.Language(components: Foundation.Locale.Language.Components(languageCode: Optional(hi), script: nil, region: Optional(IN)))]

Displaying Language Options

Once I have the list of available languages, I can display them to the user in a dropdown menu using a Picker view. By default, I have English as the target language. Here is an example to set up the Picker:

@Published var selectedLanguage = Locale.Language(components: Locale.Language.Components(languageCode: Locale.LanguageCode("en"), script: nil, region: Locale.Region("US")))")

Picker("Target Language", selection: $selectedLanguage) {
  ForEach(availableLanguages, id: \.self) { language in
     Text(language.localizedName + " " + language.regionIdentifier)
    .tag(language)
  }
}
.pickerStyle(.menu)
.padding(.horizontal)

To make the code more readable and maintainable, I defined two extensions Locale.Language to provide convenient computed properties for accessing a language's localized name and region identifier.

extension Locale.Language {
  var localizedName: String {
    Locale.current.localizedString(forLanguageCode: languageCode?.identifier ?? "") ?? ""
  }
}

extension Locale.Language {
  var regionIdentifier: String {
    self.region?.identifier ?? ""
  }
}

The first extension localizedName returns the localized name of the language using Locale.current.localizedString(forLanguageCode:). The region identifier is important when displaying the available languages because there might be multiple languages with the same name but different regions. For example, consider the following list of languages:

["Vietnamese", "Portuguese", "Ukrainian", "Italian", "Chinese", "Korean", "English", "German", "Chinese", "Japanese", "Indonesian", "Dutch", "French", "Thai", "Spanish", "Turkish", "Polish", "Arabic", "Russian", "English", "Hindi"]

The second extension, regionIdentifier, returns the region identifier of the language:

["VN", "BR", "UA", "IT", "TW", "KR", "GB", "DE", "CN", "JP", "ID", "NL", "FR", "TH", "ES", "TR", "PL", "AE", "RU", "US", "IN"]

This clarifies that there are two variants of Chinese (Simplified Chinese and Traditional Chinese) and two variants of English (American English and British English).

By combining the localized name and region identifier, users can select the language variant they want to use for translation.

Checking Language Support

Before translating text, I must check if the framework supports the selected language pairing. The LanguageAvailability class provides a status(from:to:) function that returns the availability status of a specific language pairing.

@available(iOS 18.0, macOS 15.0, *)
func status(from source: Locale.Language, to target: Locale.Language?) async -> LanguageAvailability.Status

It returns an LanguageAvailability.Status enum indicating the support status of the language pairing, which has three cases:

.installed: The framework supports the language or language pairing, and the necessary language assets have been downloaded and installed on the device. I can confidently use the language pairing for translation because all the required resources are available locally.

.supported: The framework supports the language or language pairing, but the necessary language assets have not yet been downloaded or installed. The required language resources must be downloaded before the translation can be performed. When I attempt to use this language pairing for translation, the framework automatically handles the download and installation process.

.unsupported: This case indicates that the framework does not support the language or pairing. Regardless of whether the language assets are installed, it cannot translate between the specified languages. For example, Translation does not support translating from and to the same language, like translating from English (US) to English (UK), Chinese Simplified to Chinese Traditional, or vice versa.

Here is an example of how to check language support:

func checkLanguageSupport(from source: Locale.Language, to target: Locale.Language) async {
  let availability = LanguageAvailability()
  let status = await availability.status(from: source, to: target)

  switch status {
    case .installed:
      print("Language pairing is installed and ready for translation.")
    case .supported:
      print("Language pairing is supported but needs to be downloaded.")
    case .unsupported:
      print("Language pairing is not supported by the framework.")
    @unknown default:
      print("Unknown status")
  }
}

Using Lyrics for Language Detection

In my app, I do not know the source language of the song. The above method requires the source language, even though it does not require the target language and when set to nil, the system picks an appropriate target based on the user's preferred languages.

In this case, the framework provides a convenient way to detect the language based on a text snippet.

@available(iOS 18.0, macOS 15.0, *)
func status(for text: String, to target: Locale.Language?) async throws -> LanguageAvailability.Status

Here is an example of how to use the status(for:to:) function to detect the language and check its support:

func checkLanguageSupport(for lyrics: String, to targetLanguage: Locale.Language) async {
  do {
      let textSnippet = lyrics.split(separator: "\n").joined(separator: " ")
      let status = try await LanguageAvailability().status(for: textSnippet, to: targetLanguage)

      switch status {
      case .installed, .supported:
          print("Translation supported for the detected language.")
          isTranslationSupported = true
      case .unsupported:
          print("Translation not supported for the detected language.")
          isTranslationSupported = false
      @unknown default:
          print("Unknown status")
      }
  } catch {
      print("Error detecting language: \(error)")
  }
}

Based on the support check's result, I updated the view to display an alert to the user when the translation is not supported.

Conclusion

The LanguageAvailability the class helps us to check translation capabilities before attempting a translation, especially when we do not know the source language beforehand.

If you have any questions or suggestions or want to chat about the Translation Framework or app development, contact me at https://x.com/rudrankriyam!

Stay tuned for more posts exploring other aspects of the Translation Framework and how you can add them to your app. Let's create apps that bring people closer, one translation at a time!

Runway sponsorship.

Death by a thousand branch cuts

Or use Runway for your mobile release management instead.

Tagged in: