diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 697f79b1..c0c1d93a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -30,6 +30,7 @@ jobs: apt-get install -qq git python$PYVER_MINOR-dev python$PYVER_MINOR-venv git config --global --add safe.directory "$GITHUB_WORKSPACE" ln -s `which python$PYVER_MINOR` /usr/local/bin/python + ln -s `which python$PYVER_MINOR` /usr/local/bin/python3 PYVER_TEMP=`/usr/local/bin/python --version` export PYVERINST=${PYVER_TEMP#* } echo "PYVERINST=$PYVERINST" >> $GITHUB_ENV diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78526768..99d8197c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,12 @@ repos: language: script entry: arb_reformatter.py require_serial: true + - id: update-android-strings + name: update-android-strings + files: \.arb$ + language: script + entry: update_android_strings.py + require_serial: true # Python - repo: local diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt index fd063b14..f7f0c207 100755 --- a/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/AppPreferences.kt @@ -33,6 +33,9 @@ class AppPreferences(context: Context) { const val PREF_CLIP_KBD_LAYOUT = "flutter.prefClipKbdLayout" const val DEFAULT_CLIP_KBD_LAYOUT = "US" + + const val PREF_ENABLE_COMMUNITY_TRANSLATIONS = + "flutter.APP_STATE_ENABLE_COMMUNITY_TRANSLATIONS" } private val logger = LoggerFactory.getLogger(AppPreferences::class.java) @@ -66,6 +69,9 @@ class AppPreferences(context: Context) { val openAppOnUsb: Boolean get() = prefs.getBoolean(PREF_USB_OPEN_APP, false) + val communityTranslationsEnabled: Boolean + get() = prefs.getBoolean(PREF_ENABLE_COMMUNITY_TRANSLATIONS, false) + fun registerListener(listener: OnSharedPreferenceChangeListener) { logger.debug("registering change listener") prefs.registerOnSharedPreferenceChangeListener(listener) diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt index 662150f6..14c67342 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/NdefActivity.kt @@ -24,13 +24,12 @@ import android.nfc.Tag import android.os.Build import android.os.Bundle import android.widget.Toast - import com.yubico.authenticator.ndef.KeyboardLayout import com.yubico.yubikit.core.util.NdefUtils - import org.slf4j.LoggerFactory - import java.nio.charset.StandardCharsets +import java.util.Locale + typealias ResourceId = Int @@ -66,8 +65,8 @@ class NdefActivity : Activity() { compatUtil.until(Build.VERSION_CODES.TIRAMISU) { showToast( when (otpSlotContent.type) { - OtpType.Otp -> R.string.otp_success_set_otp_to_clipboard - OtpType.Password -> R.string.otp_success_set_password_to_clipboard + OtpType.Otp -> R.string.p_ndef_set_otp + OtpType.Password -> R.string.p_ndef_set_password }, Toast.LENGTH_SHORT ) } @@ -77,16 +76,19 @@ class NdefActivity : Activity() { illegalArgumentException.message ?: "Failure when handling YubiKey OTP", illegalArgumentException ) - showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG) + showToast(R.string.p_ndef_parse_failure, Toast.LENGTH_LONG) } catch (_: UnsupportedOperationException) { - showToast(R.string.otp_set_clip_failure, Toast.LENGTH_LONG) + showToast(R.string.p_ndef_set_clip_failure, Toast.LENGTH_LONG) } } if (appPreferences.openAppOnNfcTap) { val mainAppIntent = Intent(this, MainActivity::class.java).apply { // Pass the NFC Tag to the main Activity. - putExtra(NfcAdapter.EXTRA_TAG, intent.parcelableExtra(NfcAdapter.EXTRA_TAG)) + putExtra( + NfcAdapter.EXTRA_TAG, + intent.parcelableExtra(NfcAdapter.EXTRA_TAG) + ) } startActivity(mainAppIntent) } @@ -96,7 +98,15 @@ class NdefActivity : Activity() { } private fun showToast(value: ResourceId, length: Int) { - Toast.makeText(this, value, length).show() + val context = if (appPreferences.communityTranslationsEnabled) + this + else { + // always use 'us' locale + val configuration = resources.configuration + configuration.setLocale(Locale.US) + createConfigurationContext(configuration) + } + Toast.makeText(context, value, length).show() } private fun parseOtpFromIntent(): OtpSlotValue { diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..69937a22 --- /dev/null +++ b/android/app/src/main/res/values-de/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..69937a22 --- /dev/null +++ b/android/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..69937a22 --- /dev/null +++ b/android/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml new file mode 100644 index 00000000..9bb0debc --- /dev/null +++ b/android/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,7 @@ + + + OTP zostało skopiowane do schowka. + Hasło statyczne zostało skopiowane do schowka. + Błąd czytania OTP z YubiKey. + Błąd kopiowania OTP do schowka. + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 77fd72dd..aaba2899 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,8 +1,8 @@ - + - Yubico Authenticator - Successfully copied OTP code from YubiKey to clipboard. - Successfully copied password from YubiKey to clipboard. - Failed to parse OTP code from YubiKey. - Failed to access clipboard when trying to copy OTP code from YubiKey. + Yubico Authenticator + Successfully copied OTP code from YubiKey to clipboard. + Successfully copied password from YubiKey to clipboard. + Failed to parse OTP code from YubiKey. + Failed to access clipboard when trying to copy OTP code from YubiKey. \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 486d1ed0..b1ad2c1d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -626,5 +626,11 @@ "s_nfc_dialog_oath_failure": null, "s_nfc_dialog_oath_add_multiple_accounts": null, + "@_ndef": {}, + "p_ndef_set_otp": null, + "p_ndef_set_password": null, + "p_ndef_parse_failure": null, + "p_ndef_set_clip_failure": null, + "@_eof": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d3f462eb..8c0acadd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -626,5 +626,11 @@ "s_nfc_dialog_oath_failure": "OATH operation failed", "s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts", + "@_ndef": {}, + "p_ndef_set_otp": "Successfully copied OTP code from YubiKey to clipboard.", + "p_ndef_set_password": "Successfully copied password from YubiKey to clipboard.", + "p_ndef_parse_failure": "Failed to parse OTP code from YubiKey.", + "p_ndef_set_clip_failure": "Failed to access clipboard when trying to copy OTP code from YubiKey.", + "@_eof": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index fbe734e3..73203e3a 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -626,5 +626,11 @@ "s_nfc_dialog_oath_failure": "Échec de l'opération OATH", "s_nfc_dialog_oath_add_multiple_accounts": "Action: ajouter plusieurs comptes", + "@_ndef": {}, + "p_ndef_set_otp": null, + "p_ndef_set_password": null, + "p_ndef_parse_failure": null, + "p_ndef_set_clip_failure": null, + "@_eof": {} } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 9feef971..8875fd1c 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -626,5 +626,11 @@ "s_nfc_dialog_oath_failure": "OATH操作は失敗しました", "s_nfc_dialog_oath_add_multiple_accounts": "操作:複数アカウントの追加", + "@_ndef": {}, + "p_ndef_set_otp": null, + "p_ndef_set_password": null, + "p_ndef_parse_failure": null, + "p_ndef_set_clip_failure": null, + "@_eof": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 166881ea..6503d990 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -626,5 +626,11 @@ "s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się", "s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont", + "@_ndef": {}, + "p_ndef_set_otp": "OTP zostało skopiowane do schowka.", + "p_ndef_set_password": "Hasło statyczne zostało skopiowane do schowka.", + "p_ndef_parse_failure": "Błąd czytania OTP z YubiKey.", + "p_ndef_set_clip_failure": "Błąd kopiowania OTP do schowka.", + "@_eof": {} } diff --git a/update_android_strings.py b/update_android_strings.py new file mode 100755 index 00000000..5d35247b --- /dev/null +++ b/update_android_strings.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 Yubico. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Rebuild Android String resources from ARB files.""" + +import json +import os +import xml.etree.ElementTree as ET +from os import path as p + + +def read_arb_file(file_path): + """Load translations from flutter ARB file.""" + with open(file_path, "r", encoding="utf-8") as file: + return json.load(file) + + +def get_lang_file_dir(lang): + """Return path of Android resource directory for lang.""" + return ( + f"android/app/src/main/res/values-{lang}" + if lang != "en" + else "android/app/src/main/res/values" + ) + + +def get_lang_file(lang): + """Return path of Android string resource file for lang.""" + return p.join(get_lang_file_dir(lang), "strings.xml") + + +def process_android_res(lang, arb, keys_to_translate): + """Generate or update Android string resource for lang. + + Parameters + ---------- + lang : str + language code + arb : dict + content of flutter ARB file + keys_to_translate : list + string resources which will be generated or updated + """ + res_dir = get_lang_file_dir(lang) + if not p.exists(res_dir): + os.makedirs(res_dir) + + res_path = get_lang_file(lang) + + res = ( + ET.parse(res_path).getroot() if p.exists(res_path) else ET.Element("resources") + ) + for key in keys_to_translate: + # only add the string if translation exists in arb + if key in arb.keys() and arb[key] is not None: + existing = res.find(f"./string[@name='{key}']") + if existing is not None: + existing.text = arb[key] + else: + ET.SubElement(res, "string", name=f"{key}").text = arb[key] + tree = ET.ElementTree(res) + ET.indent(tree, " ") + tree.write(res_path, encoding="utf-8", xml_declaration=True) + return True + + +def get_english_strings(): + """Extract translatable strings from English Android string resource.""" + strings_en = "android/app/src/main/res/values/strings.xml" + resources_en = ET.parse(strings_en).getroot() + + return [ + key.attrib.get("name") + for key in resources_en + if key.attrib.get("translatable") in [None, True] + ] + + +if __name__ == "__main__": + arb_files = "lib/l10n" + english_strings = get_english_strings() + + for arb_file in os.listdir(arb_files): + if arb_file.startswith("app_") and arb_file.endswith(".arb"): + lang = arb_file.split("_")[1].split(".")[0] + arb_path = p.join(arb_files, arb_file) + arb = read_arb_file(arb_path) + if process_android_res(lang, arb, english_strings): + print(f"Processed: {get_lang_file(lang)}")