はじめに
本来はDeepLクライアントをiOS+SwiftUIで作っていたのですが、
翻訳先の言語選択を行うUIを考えていたときに「設定アプリのクローン」を作ったら面白いんじゃないかと思い立ってしまいました。
しかしながら、選択肢の言語リストを表示するだけで引っかかってしまったので共有したいと思います。
技術検討
設定アプリの言語選択画面を観察してみます。
各言語のリストは以下のように上下2行で構成されています。
指針
Locale
を使用すれば、知らない言語や文字であってもほぼ対応できます。
Localizable.strings
で頑張らなくても大丈夫です。
1行目
目的の言語のLocale識別子
("JA"など)でLocale
を作成し、・・・(1)
同じ識別子でlocalizedString
を取得します。・・・(2)
let identifier = "JA"
let lang = Locale(identifier: identifier) // (1)
.localizedString(forIdentifier: identifier) // (2)
print(lang) // "日本語"
2行目
環境変数から現在のLocale
を取得します。・・・(1)
そのLocale
に対して、目的の言語の Local識別子
でlocalizedString
を取得します。・・・(2)
@Environment(\.locale) var locale // (1) 日本語設定と仮定
let identifier = "ES" // スペイン語
let lang = locale.localizedString(forIdentifier: identifier) // (2)
print(lang) // "スペイン語"
作ってみた
自作アプリ | 設定アプリ |
---|---|
![]() |
![]() |
うまくいったように見えますが、iOSの設定アプリと比較してみると、
ところどころ「おや?」と思うところがあります。
1つ1つ見てみましょう。
その1 頭文字が小文字の場合がある
頭文字が大文字だったり小文字だったりで統一感がありません。
対応策
localizedString(forIdentifier:)
で取得した文字列に.capitalized
を付けることで頭文字が大文字になります。(英語以外の文字でも!)
@Environment(\.locale) var locale // es_ES と仮定
let identifier = "ES" // スペイン語
let lang = locale.localizedString(forIdentifier: identifier)
.capitalized // 頭文字を大文字にする
print(lang) // "Español"
その2 言語の並び順が異なっている
設定アプリは、端末の「言語_地域」がリストの先頭になります。
(ja_US=日本語が先頭、en_GB=English(UK)が先頭)
残りは"地域"毎にApple社のマーケティングに基づいた順番になっているようです。
("地域"を変更すると言語の順番も変わる)
予想通りなら、めったに使わない地味な機能なのにすごい努力ですね。
対応策
"地域"を変更すると言語の順番も変わりますので単純なソートでは対応できません。
"地域"毎の言語の順番はApple社しか知りませんので、enum
の定義をそれっぽい順序にしておくしかなさそうです。
現在のLocaleの言語を先頭にするためのコードを追加しました。
import SwiftUI
enum TargetLang: String, CaseIterable {
/// English (British) 英語 (イギリス)
case EN_GB = "EN-GB"
/// English (American) 英語 (アメリカ)
case EN_US = "EN-US"
/// Chinese (simplified) 中国語 (簡体字)
case ZH
/// Japanese 日本語
case JA
/// Spanish スペイン語
case ES
/// French フランス語
case FR
}
@Environment(\.locale) var locale
/// 現在のLocaleの言語が先頭になるように並べ替える.
func getSortedLanguages() -> [TargetLang] {
var languages: [TargetLang] = []
let currentLocale = locale as NSLocale
let currentLanguageCode = currentLocale.languageCode
let currentCountryCode = currentLocale.countryCode
for lang in TargetLang.allCases {
let aLocale = NSLocale(localeIdentifier: lang.rawValue)
let aLanguageCode = aLocale.languageCode
let aCountryCode = aLocale.countryCode
if aLanguageCode != currentLanguageCode {
languages.append(lang)
continue
}
if let aCountryCode, aCountryCode != currentCountryCode {
languages.append(lang)
} else {
languages.insert(lang, at: 0)
}
}
return languages
}
その3 中国語の文言が異なっている
自作アプリ | 設定アプリ |
---|---|
![]() |
![]() |
1行目:中文 2行目:中国語 |
1行目:簡体中文 2行目:簡体中国語 |
対応策
指定しているLocal識別子
がZH
なのが原因でした。
簡体を明示するためにはZH-HANS
を指定する必要がありました。
入力されたLocal識別子
がZH
だったらZH-HANS
に置換する方法を採用しました。
その4 English の (国名)が省略形ではない
自作アプリ | 設定アプリ |
---|---|
![]() |
![]() |
(United Kingdom) (United States) |
(UK) (US) |
Localeが"en"の時:
localizedString(forIdentifier: "en_GB")
は「English (United Kingdom)」を、
localizedString(forIdentifier: "en_US")
は「English (United States)」を返します。
設定アプリはそれぞれ「English (UK)」「English (US)」になっています。
対応策
NSLocale
のcountryCode
が使えると思ったのですが、
イギリスは"Great Britain"なので、countryCode
は"GB"なのです。
イギリスのLocale情報から"UK"を取得する方法がわかりませんでした。
また、英語で国名が省略形なのはアメリカとイギリスだけで、オーストラリアなどはフルネームなので単純にcountryCode
を使えば良いというわけでもありません。
不本意ながら、特定のLocal識別子
が入力されたら特定の文字列を出力する方法を採用しました。
入力 | 出力 |
---|---|
EN-GB | "English (UK)" |
EN-US | "English (US)" |
その5 "アメリカ"が"アメリカ合衆国"で表示されている
Localeが"ja"の時localizedString(forIdentifier: "en_US")
は
「英語 (アメリカ合衆国)」を返します。
設定アプリは「英語 (アメリカ)」になっています。
対応策
アメリカのLocale情報から"アメリカ"を取得する方法がわかりませんでした。
不本意ながら、入力されたLocal識別子
がEN-US
だったら"アメリカ合衆国"を"アメリカ"に置換する方法を採用しました。
その6 ブルガリア語が"伝統式フォント"で表示されている
自作アプリ | 設定アプリ |
---|---|
![]() |
![]() |
伝統式フォント | 現代式フォント |
ブルガリア語の表記には伝統式フォント
と現代式フォント
の2種類があります。1
設定アプリは現代式フォント
になっています。
対応策
ブルガリアの人、すみません。
フォント設定で解決するんでしょうか??
フォントが違うとどれ位のインパクトがあるのか? すらわからず。。
(日本語の ゐ・ゑ・ヰ・ヱ みたいなもの?)
不本意ながら、このままにしておくことにしました。
対策してみた
自作アプリ (ja_JP) | 自作アプリ (en_US) | 設定アプリ |
---|---|---|
![]() |
![]() |
![]() |
力業の対策ばかりなのがアレですが、かなり近くなった!
検索窓は気が向いたら考える。。
もっといい方法があったら教えてください。
Playgorundでlocaleを検討する方法
Playgroundで検討する際には以下の記事を参考にしました。
↑をSwift 5にアレンジしたコードを載せます。
import Foundation
extension NSLocale {
@objc
class var swizzled_currentLocale: NSLocale {
NSLocale(localeIdentifier: "fr") // ここは適当に変えましょう
}
}
let method = class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.current))
let swizzledMethod = class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.swizzled_currentLocale))
method_exchangeImplementations(method!, swizzledMethod!)
print(NSLocale.current.identifier) // -> "fr"
開発環境
Xcode 14.2
iOS 16.2
GitHubにソースコードを上げたら更新します。