From ecba8732088d27fac84c9d1c84c6082e0638009f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 2 Aug 2022 12:44:21 -0700 Subject: [PATCH] Multilang: handle localized plurals (#25337) * Add localized plural handling * Do another location with plurals * fix test --- selfdrive/ui/qt/util.cc | 6 +- selfdrive/ui/qt/widgets/input.cc | 2 +- selfdrive/ui/tests/test_translations.py | 2 +- selfdrive/ui/translations/languages.json | 2 +- selfdrive/ui/translations/main_en.ts | 38 +++++ selfdrive/ui/translations/main_ja.ts | 200 +++++++++++++---------- selfdrive/ui/translations/main_ko.ts | 40 ++++- selfdrive/ui/translations/main_zh-CHS.ts | 40 ++++- selfdrive/ui/translations/main_zh-CHT.ts | 40 ++++- selfdrive/ui/update_translations.py | 10 +- 10 files changed, 259 insertions(+), 121 deletions(-) create mode 100644 selfdrive/ui/translations/main_en.ts diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index a7d5438ae4..381c065901 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -79,13 +79,13 @@ QString timeAgo(const QDateTime &date) { s = "now"; } else if (diff < 60 * 60) { int minutes = diff / 60; - s = QObject::tr("%1 minute%2 ago").arg(minutes).arg(minutes > 1 ? "s" : ""); + s = QObject::tr("%n minute(s) ago", "", minutes); } else if (diff < 60 * 60 * 24) { int hours = diff / (60 * 60); - s = QObject::tr("%1 hour%2 ago").arg(hours).arg(hours > 1 ? "s" : ""); + s = QObject::tr("%n hour(s) ago", "", hours); } else if (diff < 3600 * 24 * 7) { int days = diff / (60 * 60 * 24); - s = QObject::tr("%1 day%2 ago").arg(days).arg(days > 1 ? "s" : ""); + s = QObject::tr("%n day(s) ago", "", days); } else { s = date.date().toString(); } diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index dc54a3621c..d703825885 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -165,7 +165,7 @@ void InputDialog::handleEnter() { done(QDialog::Accepted); emitText(line->text()); } else { - setMessage(tr("Need at least %1 characters!").arg(minLength), false); + setMessage(tr("Need at least %n character(s)!", "", minLength), false); } } diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index b44ab11ede..ae91dd6f83 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -39,7 +39,7 @@ class TestTranslations(unittest.TestCase): f"{name} has no XML translation file, run selfdrive/ui/update_translations.py") def test_translations_updated(self): - update_translations(translations_dir=TMP_TRANSLATIONS_DIR) + update_translations(plural_only=["main_en"], translations_dir=TMP_TRANSLATIONS_DIR) for name, file in self.translation_files.items(): with self.subTest(name=name, file=file): diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index 48f0948674..cc3995738e 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -1,5 +1,5 @@ { - "English": "", + "English": "main_en", "中文(繁體)": "main_zh-CHT", "中文(简体)": "main_zh-CHS", "한국어": "main_ko" diff --git a/selfdrive/ui/translations/main_en.ts b/selfdrive/ui/translations/main_en.ts new file mode 100644 index 0000000000..3f9692e5fa --- /dev/null +++ b/selfdrive/ui/translations/main_en.ts @@ -0,0 +1,38 @@ + + + + + InputDialog + + Need at least %n character(s)! + + Need at least %n character! + Need at least %n characters! + + + + + QObject + + %n minute(s) ago + + %n minute ago + %n minutes ago + + + + %n hour(s) ago + + %n hour ago + %n hours ago + + + + %n day(s) ago + + %n day ago + %n days ago + + + + diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index c3b2b850ee..67f48341b7 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID ドングル番号 (Dongle ID) - + N/A N/A - + Serial シリアル番号 - + Driver Camera 車内カメラ - + PREVIEW 見る - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 車内カメラをプレビューして、ドライバー監視システムの視界を確認ができます。(車両の電源を切る必要があります) - + Reset Calibration キャリブレーションをリセット - + RESET リセット - + Are you sure you want to reset calibration? キャリブレーションをリセットしてもよろしいですか? - + Review Training Guide 入門書を見る - + REVIEW 見る - + Review the rules, features, and limitations of openpilot openpilot の特徴を見る - + Are you sure you want to review the training guide? 入門書を見てもよろしいですか? - + Regulatory 認証情報 - + VIEW 見る - + Change Language 言語を変更 - + CHANGE 変更 - + Select a language 言語を選択 - + Reboot 再起動 - + Power Off 電源を切る - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot は、左または右の4°以内、上の5°または下の8°以内にデバイスを取付ける必要があります。キャリブレーションを引き続きます、リセットはほとんど必要ありません。 - + Your device is pointed %1° %2 and %3° %4. このデバイスは%2の%1°、%4の%3°に向けます。 - + down - + up - + left - + right - + Are you sure you want to reboot? 再起動してもよろしいですか? - + Disengage to Reboot openpilot をキャンセルして再起動ができます - + Are you sure you want to power off? シャットダウンしてもよろしいですか? - + Disengage to Power Off openpilot をキャンセルしてシャットダウンができます @@ -294,7 +294,7 @@ DriverViewScene - + camera starting カメラを起動しています @@ -306,10 +306,16 @@ Cancel キャンセル - + + Need at least %n character(s)! + + パスワードは%%n文字以上で入力してください! + + + Need at least %1 characters! - パスワードは%%1文字以上で入力してください! + パスワードは%%1文字以上で入力してください! @@ -488,30 +494,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX 最高速度 - - + + SPEED 速度 - - + + LIMIT 制限速度 @@ -635,20 +641,38 @@ location set openpilot openpilot - + + %n minute(s) ago + + %n 分前 + + + + + %n hour(s) ago + + %n 時間前 + + + + + %n day(s) ago + + %n 日前 + + + %1 minute%2 ago - %1 分%2 前 + %1 分%2 前 - %1 hour%2 ago - %1 時間%2 前 + %1 時間%2 前 - %1 day%2 ago - %1 日%2 前 + %1 日%2 前 @@ -710,33 +734,33 @@ location set SettingsWindow - + × × - + Device デバイス - - + + Network ネットワーク - + Toggles 切り替え - + Software ソフトウェア - + Navigation ナビゲーション @@ -975,89 +999,89 @@ location set SoftwarePanel - + Git Branch Git ブランチ - + Git Commit Git コミット - + OS Version OS バージョン - + Version バージョン - + Last Update Check 最終更新確認 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. openpilotが最後にアップデートの確認に成功してからの時間です。アップデート処理は、車の電源が切れているときのみ実行されます。 - + Check for Update 更新プログラムをチェック - + CHECKING 確認中 - + Switch Branch - + ENTER - - + + The new branch will be pulled the next time the updater runs. - + Enter branch name - + UNINSTALL アンインストール - + Uninstall %1 %1をアンインストール - + Are you sure you want to uninstall? アンインストールしてもよろしいですか? - + failed to fetch update 更新のダウンロードにエラーが発生しました - - + + CHECK 確認 @@ -1165,72 +1189,70 @@ location set 時速31マイル(50km)を超えるスピードで走行中、方向指示器を作動させずに検出された車線ライン上に車両が触れた場合、車線に戻るアラートを受信します。 - Enable Right-Hand Drive - 右ハンドルを有効化 + 右ハンドルを有効化 - Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. - openpilot が左側通行規則を遵守し、右ハンドルでドライバー監視を行うことを可能にします。 + openpilot が左側通行規則を遵守し、右ハンドルでドライバー監視を行うことを可能にします。 - + Use Metric System メートル法を有効化 - + Display speed in km/h instead of mph. 速度は mph ではなく km/h で表示されます。 - + Record and Upload Driver Camera 車内カメラの録画とアップロード - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - + Disengage On Accelerator Pedal アクセル踏むと openpilot をキャンセル - + When enabled, pressing the accelerator pedal will disengage openpilot. 有効な場合は、アクセルを踏むと openpilot をキャンセルします。 - + Show ETA in 24h format 24時間表示 - + Use 24h format instead of am/pm AM/PM の代わりに24時間形式を使用します - + Show Map on Left Side of UI ディスプレイの左側にマップを表示 - + Show map on left side when in split screen view. 分割画面表示の場合、ディスプレイの左側にマップを表示します。 - + openpilot Longitudinal Control openpilot 縦方向制御 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot は、車のレーダーを無効化し、アクセルとブレーキの制御を引き継ぎます。注意:AEB を無効化にします! diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index fe10008635..bc93fb7bda 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -306,10 +306,16 @@ Cancel 취소 - + + Need at least %n character(s)! + + + + + Need at least %1 characters! - 최소 %1 자가 필요합니다! + 최소 %1 자가 필요합니다! @@ -635,20 +641,38 @@ location set openpilot openpilot - + + %n minute(s) ago + + + + + + + %n hour(s) ago + + + + + + + %n day(s) ago + + + + + %1 minute%2 ago - %1 분전 + %1 분전 - %1 hour%2 ago - %1 시간전 + %1 시간전 - %1 day%2 ago - %1 일전 + %1 일전 diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 5c949b0d3b..b1c8928cac 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -306,10 +306,16 @@ Cancel 取消 - + + Need at least %n character(s)! + + 至少需要 %n 个字符! + + + Need at least %1 characters! - 至少需要 %1 个字符! + 至少需要 %1 个字符! @@ -633,20 +639,38 @@ location set openpilot openpilot - + + %n minute(s) ago + + %n 分钟前 + + + + + %n hour(s) ago + + %n 小时前 + + + + + %n day(s) ago + + %n 天前 + + + %1 minute%2 ago - %1 分钟%2 前 + %1 分钟%2 前 - %1 hour%2 ago - %1 小时%2 前 + %1 小时%2 前 - %1 day%2 ago - %1 天%2 前 + %1 天%2 前 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index d1b1566a5a..b79e275116 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -306,10 +306,16 @@ Cancel 取消 - + + Need at least %n character(s)! + + 需要至少 %n 個字元! + + + Need at least %1 characters! - 需要至少 %1 個字元! + 需要至少 %1 個字元! @@ -635,23 +641,41 @@ location set openpilot openpilot - + + %n minute(s) ago + + %n 分鐘前 + + + + + %n hour(s) ago + + %n 小時前 + + + + + %n day(s) ago + + %n 天前 + + + %1 minute%2 ago we don't need %2 - %1 分鐘前 + %1 分鐘前 - %1 hour%2 ago we don't need %2 - %1 小時前 + %1 小時前 - %1 day%2 ago we don't need %2 - %1 天前 + %1 天前 diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index 9731160752..78c973c86b 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -10,7 +10,10 @@ TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations") LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") -def update_translations(vanish=False, translations_dir=TRANSLATIONS_DIR): +def update_translations(vanish=False, plural_only=None, translations_dir=TRANSLATIONS_DIR): + if plural_only is None: + plural_only = [] + with open(LANGUAGES_FILE, "r") as f: translation_files = json.load(f) @@ -23,6 +26,8 @@ def update_translations(vanish=False, translations_dir=TRANSLATIONS_DIR): args = f"lupdate -recursive {UI_DIR} -ts {tr_file}" if vanish: args += " -no-obsolete" + if file in plural_only: + args += " -pluralonly" ret = os.system(args) assert ret == 0 @@ -31,6 +36,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="Update translation files for UI", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found") + parser.add_argument("--plural-only", type=str, nargs="*", default=["main_en"], help="Translation codes to only create plural translations for (ie. the base language)") args = parser.parse_args() - update_translations(args.vanish) + update_translations(args.vanish, args.plural_only)