Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ dependencies {
testImplementation(libs.kotlinx.serialization.json)

debugImplementation(project(":internal:lintchecks"))

// Directly, not via :internal:lintchecks. That module declares lintChecks too, but
// lintChecks only applies to the module declaring it, and a debugImplementation
// dependency does not carry lint checks to the consumer — so the custom rules were never
// running here.
lintChecks(project(":internal:lint"))
}

kotlin {
Expand Down
3 changes: 2 additions & 1 deletion auth/src/main/res/values-da/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
<string name="fui_title_recover_password_activity">Gendan adgangskode</string>
<string name="fui_title_confirm_recover_password">Tjek din mail.</string>
<string name="fui_password_recovery_body">Få en vejledning sendt til denne mail om, hvordan du nulstiller din adgangskode.</string>
<string name="fui_button_text_send">Send</string>
<!-- Identical to English: this is the imperative of "sende". -->
<string name="fui_button_text_send" tools:ignore="UntranslatedResource">Send</string>
<string name="fui_confirm_recovery_body">Følg vejledningen, der blev sendt til %1$s, for at gendanne din adgangskode.</string>
<string name="fui_progress_dialog_sending">Sender…</string>
<string name="fui_error_email_does_not_exist">Mailadressen matcher ikke en eksisterende konto</string>
Expand Down
6 changes: 3 additions & 3 deletions auth/src/main/res/values-fil/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
<string name="fui_tos_and_pp_footer">%1$s \u00A0 \u00A0 %2$s</string>
<string name="fui_no_internet" translation_descripition="User message shown when sign-in is attempted while offline.">Error sa network, tingnan ang koneksyon mo sa internet.</string>
<string name="fui_idp_name_google">Google</string>
<string name="fui_idp_name_facebook">Fecebook</string>
<string name="fui_idp_name_facebook">Facebook</string>
<string name="fui_idp_name_twitter">Twitter</string>
<string name="fui_idp_name_github">GitHub</string>
<string name="fui_idp_name_phone">Telepono</string>
<string name="fui_idp_name_email">Email</string>
<string name="fui_idp_name_email" tools:ignore="UntranslatedResource">Email</string>
<string name="fui_sign_in_with_google">Mag-sign in sa Google</string>
<string name="fui_continue_with_google">Mag-sign in sa Google</string>
<string name="fui_sign_in_with_facebook">Mag-sign in sa Facebook</string>
Expand All @@ -36,7 +36,7 @@
<string name="fui_country_hint">Bansa</string>
<string name="fui_country_selector_title" translation_description="Country selector modal title">Pumili ng bansa</string>
<string name="fui_search_country_field_hint" translation_description="Hint for search for country input field">Maghanap ng bansa hal. +1, "US"</string>
<string name="fui_password_hint">Password</string>
<string name="fui_password_hint" tools:ignore="UntranslatedResource">Password</string>
<string name="fui_new_password_hint">Bagong password</string>
<string name="fui_required_field">Hindi mo ito maaaring iwanan na walang laman.</string>
<string name="fui_invalid_email_address">Mali ang email address na iyon</string>
Expand Down
4 changes: 2 additions & 2 deletions auth/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<string name="fui_idp_name_twitter">Twitter</string>
<string name="fui_idp_name_github">GitHub</string>
<string name="fui_idp_name_phone">Telefono</string>
<string name="fui_idp_name_email">Email</string>
<string name="fui_idp_name_email" tools:ignore="UntranslatedResource">Email</string>
<string name="fui_sign_in_with_google">Accedi con Google</string>
<string name="fui_continue_with_google">Accedi con Google</string>
<string name="fui_sign_in_with_facebook">Accedi con Facebook</string>
Expand All @@ -36,7 +36,7 @@
<string name="fui_country_hint">Paese</string>
<string name="fui_country_selector_title" translation_description="Country selector modal title">Seleziona un paese</string>
<string name="fui_search_country_field_hint" translation_description="Hint for search for country input field">Cerca paese ad es. +1, "US"</string>
<string name="fui_password_hint">Password</string>
<string name="fui_password_hint" tools:ignore="UntranslatedResource">Password</string>
<string name="fui_new_password_hint">Nuova password</string>
<string name="fui_required_field">Questo campo non può restare vuoto.</string>
<string name="fui_invalid_email_address">L\'indirizzo email non è corretto</string>
Expand Down
3 changes: 2 additions & 1 deletion auth/src/main/res/values-nb/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
<string name="fui_title_recover_password_activity">Gjenopprett passordet</string>
<string name="fui_title_confirm_recover_password">Sjekk e-posten din</string>
<string name="fui_password_recovery_body">Få instruksjoner sendt til denne e-postadressen for hvordan du tilbakestiller passordet ditt.</string>
<string name="fui_button_text_send">Send</string>
<!-- Identical to English: this is the imperative of "sende". -->
<string name="fui_button_text_send" tools:ignore="UntranslatedResource">Send</string>
<string name="fui_confirm_recovery_body">Følg veiledningen som er sendt til %1$s, for å gjenopprette passordet ditt.</string>
<string name="fui_progress_dialog_sending">Sender…</string>
<string name="fui_error_email_does_not_exist">Denne e-postadressen samsvarer ikke med en eksisterende konto</string>
Expand Down
3 changes: 2 additions & 1 deletion auth/src/main/res/values-no/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
<string name="fui_title_recover_password_activity">Gjenopprett passordet</string>
<string name="fui_title_confirm_recover_password">Sjekk e-posten din</string>
<string name="fui_password_recovery_body">Få instruksjoner sendt til denne e-postadressen for hvordan du tilbakestiller passordet ditt.</string>
<string name="fui_button_text_send">Send</string>
<!-- Identical to English: this is the imperative of "sende". -->
<string name="fui_button_text_send" tools:ignore="UntranslatedResource">Send</string>
<string name="fui_confirm_recovery_body">Følg veiledningen som er sendt til %1$s, for å gjenopprette passordet ditt.</string>
<string name="fui_progress_dialog_sending">Sender…</string>
<string name="fui_error_email_does_not_exist">Denne e-postadressen samsvarer ikke med en eksisterende konto</string>
Expand Down
4 changes: 2 additions & 2 deletions auth/src/main/res/values-pt-rPT/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<string name="fui_idp_name_twitter">Twitter</string>
<string name="fui_idp_name_github">GitHub</string>
<string name="fui_idp_name_phone">Telemóvel</string>
<string name="fui_idp_name_email">Email</string>
<string name="fui_idp_name_email" tools:ignore="UntranslatedResource">Email</string>
<string name="fui_sign_in_with_google">Iniciar sessão com o Google</string>
<string name="fui_continue_with_google">Iniciar sessão com o Google</string>
<string name="fui_sign_in_with_facebook">Iniciar sessão com o Facebook</string>
Expand All @@ -31,7 +31,7 @@
<string name="fui_sign_in_with_yahoo">Iniciar sessão com o Yahoo</string>
<string name="fui_continue_with_yahoo">Iniciar sessão com o Yahoo</string>
<string name="fui_next_default">Seguinte</string>
<string name="fui_email_hint">Email</string>
<string name="fui_email_hint" tools:ignore="UntranslatedResource">Email</string>
<string name="fui_phone_hint">Número de telefone</string>
<string name="fui_country_hint">País</string>
<string name="fui_country_selector_title" translation_description="Country selector modal title">Selecione um país</string>
Expand Down
6 changes: 3 additions & 3 deletions auth/src/main/res/values-tl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
<string name="fui_tos_and_pp_footer">%1$s \u00A0 \u00A0 %2$s</string>
<string name="fui_no_internet" translation_descripition="User message shown when sign-in is attempted while offline.">Error sa network, tingnan ang koneksyon mo sa internet.</string>
<string name="fui_idp_name_google">Google</string>
<string name="fui_idp_name_facebook">Fecebook</string>
<string name="fui_idp_name_facebook">Facebook</string>
<string name="fui_idp_name_twitter">Twitter</string>
<string name="fui_idp_name_github">GitHub</string>
<string name="fui_idp_name_phone">Telepono</string>
<string name="fui_idp_name_email">Email</string>
<string name="fui_idp_name_email" tools:ignore="UntranslatedResource">Email</string>
<string name="fui_sign_in_with_google">Mag-sign in sa Google</string>
<string name="fui_continue_with_google">Mag-sign in sa Google</string>
<string name="fui_sign_in_with_facebook">Mag-sign in sa Facebook</string>
Expand All @@ -36,7 +36,7 @@
<string name="fui_country_hint">Bansa</string>
<string name="fui_country_selector_title" translation_description="Country selector modal title">Pumili ng bansa</string>
<string name="fui_search_country_field_hint" translation_description="Hint for search for country input field">Maghanap ng bansa hal. +1, "US"</string>
<string name="fui_password_hint">Password</string>
<string name="fui_password_hint" tools:ignore="UntranslatedResource">Password</string>
<string name="fui_new_password_hint">Bagong password</string>
<string name="fui_required_field">Hindi mo ito maaaring iwanan na walang laman.</string>
<string name="fui_invalid_email_address">Mali ang email address na iyon</string>
Expand Down
1 change: 1 addition & 0 deletions internal/lint/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dependencies {
compileOnly(libs.lint.api)
compileOnly(libs.kotlin.stdlib)

testImplementation(libs.junit)
testImplementation(libs.lint.api)
testImplementation(libs.lint.tests)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class LintIssueRegistry : IssueRegistry() {
get() = com.android.tools.lint.detector.api.CURRENT_API

override val issues = listOf(
NonGlobalIdDetector.NON_GLOBAL_ID
NonGlobalIdDetector.NON_GLOBAL_ID,
UntranslatedResourceDetector.UNTRANSLATED_RESOURCE
)

override val vendor = Vendor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.firebaseui.lint.internal

import com.android.SdkConstants.ATTR_NAME
import com.android.SdkConstants.ATTR_TRANSLATABLE
import com.android.SdkConstants.TAG_STRING
import com.android.SdkConstants.VALUE_FALSE
import com.android.ide.common.resources.configuration.FolderConfiguration
import com.android.resources.ResourceFolderType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Context
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.ResourceXmlDetector
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.XmlContext
import org.w3c.dom.Element

/**
* Flags a string in a locale folder whose value is byte-identical to the base English one.
*
* `MissingTranslation` only fires when a string is *absent* from a locale, so a resource that
* was copied over untranslated is invisible to it: it is present, it just holds English. A
* whole class of shipped-in-English strings therefore never reaches a lint report; #2509
* removed 269 such values, found by scanning rather than by any gate.
*
* Only `<string>` elements are checked. An English copy of a `<plurals>` or `<string-array>`
* item is still invisible to this check as well as to `MissingTranslation`.
*
* English regional folders (`values-en-rGB` and friends) are skipped, as are strings marked
* `translatable="false"` and the [ALLOWED] names below. Anything else that is legitimately the
* same word in another language is suppressed at the site with `tools:ignore`, so the decision
* sits next to the string.
*/
class UntranslatedResourceDetector : ResourceXmlDetector() {

/** Base `values/strings.xml` text, keyed by resource name. */
private val baseStrings = mutableMapOf<String, String>()

/** Localized strings to judge once every resource file has been read. */
private val localized = mutableListOf<LocalizedString>()

private data class LocalizedString(
val name: String,
val folder: String,
val text: String,
val handle: Location.Handle
)

override fun appliesTo(folderType: ResourceFolderType): Boolean =
folderType == ResourceFolderType.VALUES

override fun getApplicableElements(): List<String> = listOf(TAG_STRING)

override fun visitElement(context: XmlContext, element: Element) {
val name = element.getAttribute(ATTR_NAME)
if (name.isEmpty()) return
if (element.getAttribute(ATTR_TRANSLATABLE) == VALUE_FALSE) return

// Compare rendered text rather than markup: a value that differs from the base only in
// its xliff placeholders is still untranslated copy.
val text = element.textContent.trim()
if (text.isEmpty()) return
if (!hasTranslatableWords(text)) return

val folderName = context.file.parentFile?.name ?: return

if (folderName == BASE_VALUES_FOLDER) {
// putIfAbsent rather than assignment: if a second source set ever contributes its
// own values/ folder, first-wins keeps the map from depending on traversal order.
baseStrings.putIfAbsent(name, text)
return
}

// Only the base folder above seeds the comparison. Any other unqualified folder
// (values-v26, values-sw360dp, a future values-night) is a configuration variant of the
// English copy, not a translation, so it is neither a source nor a candidate.
val locale = FolderConfiguration.getConfigForFolder(folderName)?.localeQualifier ?: return

if (locale.language == LANGUAGE_ENGLISH) return
if (name in ALLOWED) return

localized += LocalizedString(
name = name,
folder = folderName,
text = text,
handle = context.createLocationHandle(element)
)
}

override fun afterCheckRootProject(context: Context) {
// Nothing to compare against when lint runs over a single file, which is the IDE's
// incremental mode. Reporting there would flag every locale string in the file.
if (baseStrings.isEmpty()) return

for (string in localized) {
if (baseStrings[string.name] != string.text) continue
context.report(
UNTRANSLATED_RESOURCE,
string.handle.resolve(),
"\"${string.name}\" is the base English string in ${string.folder}, so it " +
"ships untranslated. Translate it, or mark it `tools:ignore=" +
"\"$ISSUE_ID\"` if the translation is genuinely identical."
)
}

baseStrings.clear()
localized.clear()
}

/**
* Whether [text] contains anything a translator could change.
*
* `fui_tos_and_pp_footer` is two format specifiers separated by non-breaking spaces, so it
* is necessarily identical in all 84 locale folders that define it. Strings made only of
* placeholders, punctuation and whitespace have no words to translate.
*
* Escape sequences are stripped first because they are spelled with letters. A non-breaking
* space is written in these files as a backslash followed by u00A0, and the u and the A in
* that sequence would otherwise read as translatable content.
*/
private fun hasTranslatableWords(text: String): Boolean =
text.replace(UNICODE_ESCAPE, "")
.replace(FORMAT_SPECIFIER, "")
.any(Char::isLetter)

companion object {
private const val ISSUE_ID = "UntranslatedResource"
private const val LANGUAGE_ENGLISH = "en"
private const val BASE_VALUES_FOLDER = "values"

/** `%s`, `%d`, `%1$s` and friends. */
private val FORMAT_SPECIFIER = Regex("""%(\d+\$)?[-#+ 0,(]*\d*(\.\d+)?[a-zA-Z%]""")

/** Matches the escape sequence as written in the file, not the character it denotes. */
private val UNICODE_ESCAPE = Regex("""\\u[0-9a-fA-F]{4}""")

/**
* Names exempted wholesale, because suppressing them per folder would mean roughly 375
* `tools:ignore` attributes.
*
* The four `fui_idp_name_*` entries are brand names, which are not translated. Note this
* does make a regression invisible: if a locale ever replaced one with a mistranslation,
* nothing here would report it. That is not hypothetical. `values-fil` and `values-tl`
* spelled `fui_idp_name_facebook` as `Fecebook` from the original translation import
* (#771) until this change corrected it, and an exemption is exactly why no gate would
* have caught it.
*
* `fui_mfa_method_sms` is a **provisional** exemption and not the same kind of entry.
* "SMS" is genuinely translated in several locales (`رسالة نصية` in `ar`, `短信` in `zh`,
* `СМС` in `sr`), so the 70 folders that carry the bare English acronym are real hits
* this silences. It is exempted only because the string is unreferenced dead copy that
* CPRN-445 is expected to delete; when that lands, drop this entry rather than keeping
* it.
*/
private val ALLOWED = setOf(
"fui_idp_name_facebook",
"fui_idp_name_github",
"fui_idp_name_google",
"fui_idp_name_twitter",
"fui_mfa_method_sms"
)

val UNTRANSLATED_RESOURCE = Issue.create(
ISSUE_ID,
"Localized string still holds the base English text",
"A string that is present in a locale folder but identical to the base English " +
"value ships as English to users of that locale. `MissingTranslation` cannot " +
"catch this, because it only reports strings that are absent.",
Category.MESSAGES,
6,
Severity.ERROR,
Implementation(UntranslatedResourceDetector::class.java, Scope.ALL_RESOURCES_SCOPE)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.firebaseui.lint.internal

import com.android.tools.lint.checks.infrastructure.TestLintTask
import java.io.File
import java.util.Properties

/**
* Points [TestLintTask] at the local Android SDK.
*
* Lint's test harness refuses to run without one, and Gradle does not put `ANDROID_HOME` into
* the test JVM's environment. The previous approach scanned `java.library.path` for a segment
* containing "SDK", splitting on `;`, which only ever resolved on Windows: on macOS and Linux
* it silently found nothing, the SDK went unconfigured, and every test in this module failed
* with "This test requires an Android SDK".
*/
internal fun TestLintTask.withLocalSdk(): TestLintTask {
val sdk = androidSdkHome() ?: error(
"No Android SDK found. Set ANDROID_HOME, or add sdk.dir to local.properties at the " +
"repository root."
)
return sdkHome(sdk)
}

private fun androidSdkHome(): File? {
val fromEnv = sequenceOf("ANDROID_HOME", "ANDROID_SDK_ROOT")
.mapNotNull { System.getenv(it) }
.map(::File)
.firstOrNull(File::isDirectory)
if (fromEnv != null) return fromEnv

// Tests run with the module directory as the working directory, so walk up to the root.
var dir: File? = File(".").absoluteFile
while (dir != null) {
val properties = File(dir, "local.properties")
if (properties.isFile) {
val sdkDir = properties.inputStream().use { Properties().apply { load(it) } }
.getProperty("sdk.dir")
?.let(::File)
if (sdkDir?.isDirectory == true) return sdkDir
}
dir = dir.parentFile
}
return null
}
Loading
Loading