From 4ffdf01f6af28020a00949c0509594d01df43181 Mon Sep 17 00:00:00 2001
From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com>
Date: Mon, 21 Sep 2026 11:04:40 +0100
Subject: [PATCH 1/3] fix(lint): make the internal lint test suite runnable
outside Windows
---
internal/lint/build.gradle.kts | 1 +
.../firebaseui/lint/internal/LintTestSdk.kt | 44 +++++++++++++++++++
.../lint/internal/NonGlobalIdDetectorTest.kt | 13 +-----
3 files changed, 46 insertions(+), 12 deletions(-)
create mode 100644 internal/lint/src/test/java/com/firebaseui/lint/internal/LintTestSdk.kt
diff --git a/internal/lint/build.gradle.kts b/internal/lint/build.gradle.kts
index 352a272573..f385f37dbf 100644
--- a/internal/lint/build.gradle.kts
+++ b/internal/lint/build.gradle.kts
@@ -6,6 +6,7 @@ dependencies {
compileOnly(libs.lint.api)
compileOnly(libs.kotlin.stdlib)
+ testImplementation(libs.junit)
testImplementation(libs.lint.api)
testImplementation(libs.lint.tests)
}
diff --git a/internal/lint/src/test/java/com/firebaseui/lint/internal/LintTestSdk.kt b/internal/lint/src/test/java/com/firebaseui/lint/internal/LintTestSdk.kt
new file mode 100644
index 0000000000..88d4825b02
--- /dev/null
+++ b/internal/lint/src/test/java/com/firebaseui/lint/internal/LintTestSdk.kt
@@ -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
+}
diff --git a/internal/lint/src/test/java/com/firebaseui/lint/internal/NonGlobalIdDetectorTest.kt b/internal/lint/src/test/java/com/firebaseui/lint/internal/NonGlobalIdDetectorTest.kt
index 5bdf360c3b..793efedf3f 100644
--- a/internal/lint/src/test/java/com/firebaseui/lint/internal/NonGlobalIdDetectorTest.kt
+++ b/internal/lint/src/test/java/com/firebaseui/lint/internal/NonGlobalIdDetectorTest.kt
@@ -4,21 +4,10 @@ import com.android.tools.lint.checks.infrastructure.TestFiles.xml
import com.android.tools.lint.checks.infrastructure.TestLintTask
import com.firebaseui.lint.internal.NonGlobalIdDetector.Companion.NON_GLOBAL_ID
import org.junit.Test
-import java.io.File
class NonGlobalIdDetectorTest {
- // Nasty hack to make lint tests pass on Windows. For some reason, lint doesn't
- // automatically find the Android SDK in its standard path on Windows. This hack looks
- // through the system properties to find the path defined in `local.properties` and then
- // sets lint's SDK home to that path if it's found.
- private val sdkPath = System.getProperty("java.library.path").split(';').find {
- it.contains("SDK", true)
- }
-
- fun configuredLint(): TestLintTask = TestLintTask.lint().apply {
- sdkHome(File(sdkPath ?: return@apply))
- }
+ fun configuredLint(): TestLintTask = TestLintTask.lint().withLocalSdk()
@Test
fun `Passes on valid view id`() {
From 2447342122715a9e6dc751da748839ce210c1e12 Mon Sep 17 00:00:00 2001
From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com>
Date: Mon, 21 Sep 2026 11:04:40 +0100
Subject: [PATCH 2/3] feat(lint): flag localized strings that still hold the
base English text
---
auth/build.gradle.kts | 6 +
auth/src/main/res/values-da/strings.xml | 3 +-
auth/src/main/res/values-fil/strings.xml | 4 +-
auth/src/main/res/values-it/strings.xml | 4 +-
auth/src/main/res/values-nb/strings.xml | 3 +-
auth/src/main/res/values-no/strings.xml | 3 +-
auth/src/main/res/values-pt-rPT/strings.xml | 4 +-
auth/src/main/res/values-tl/strings.xml | 4 +-
.../lint/internal/LintIssueRegistry.kt | 3 +-
.../internal/UntranslatedResourceDetector.kt | 175 +++++++++++++++++
.../UntranslatedResourceDetectorTest.kt | 184 ++++++++++++++++++
11 files changed, 381 insertions(+), 12 deletions(-)
create mode 100644 internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
create mode 100644 internal/lint/src/test/java/com/firebaseui/lint/internal/UntranslatedResourceDetectorTest.kt
diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts
index 0bc26ceb51..60390d512f 100644
--- a/auth/build.gradle.kts
+++ b/auth/build.gradle.kts
@@ -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 {
diff --git a/auth/src/main/res/values-da/strings.xml b/auth/src/main/res/values-da/strings.xml
index 7ae951c406..1940569d05 100755
--- a/auth/src/main/res/values-da/strings.xml
+++ b/auth/src/main/res/values-da/strings.xml
@@ -62,7 +62,8 @@
Gendan adgangskode
Tjek din mail.
Få en vejledning sendt til denne mail om, hvordan du nulstiller din adgangskode.
- Send
+
+ Send
Følg vejledningen, der blev sendt til %1$s, for at gendanne din adgangskode.
Sender…
Mailadressen matcher ikke en eksisterende konto
diff --git a/auth/src/main/res/values-fil/strings.xml b/auth/src/main/res/values-fil/strings.xml
index 3dda0a5b3e..0c582986c8 100755
--- a/auth/src/main/res/values-fil/strings.xml
+++ b/auth/src/main/res/values-fil/strings.xml
@@ -10,7 +10,7 @@
Twitter
GitHub
Telepono
- Email
+ Email
Mag-sign in sa Google
Mag-sign in sa Google
Mag-sign in sa Facebook
@@ -36,7 +36,7 @@
Bansa
Pumili ng bansa
Maghanap ng bansa hal. +1, "US"
- Password
+ Password
Bagong password
Hindi mo ito maaaring iwanan na walang laman.
Mali ang email address na iyon
diff --git a/auth/src/main/res/values-it/strings.xml b/auth/src/main/res/values-it/strings.xml
index 7641358e5b..6f322d844f 100755
--- a/auth/src/main/res/values-it/strings.xml
+++ b/auth/src/main/res/values-it/strings.xml
@@ -10,7 +10,7 @@
Twitter
GitHub
Telefono
- Email
+ Email
Accedi con Google
Accedi con Google
Accedi con Facebook
@@ -36,7 +36,7 @@
Paese
Seleziona un paese
Cerca paese ad es. +1, "US"
- Password
+ Password
Nuova password
Questo campo non può restare vuoto.
L\'indirizzo email non è corretto
diff --git a/auth/src/main/res/values-nb/strings.xml b/auth/src/main/res/values-nb/strings.xml
index f422080c02..ede91bf8cf 100755
--- a/auth/src/main/res/values-nb/strings.xml
+++ b/auth/src/main/res/values-nb/strings.xml
@@ -62,7 +62,8 @@
Gjenopprett passordet
Sjekk e-posten din
Få instruksjoner sendt til denne e-postadressen for hvordan du tilbakestiller passordet ditt.
- Send
+
+ Send
Følg veiledningen som er sendt til %1$s, for å gjenopprette passordet ditt.
Sender…
Denne e-postadressen samsvarer ikke med en eksisterende konto
diff --git a/auth/src/main/res/values-no/strings.xml b/auth/src/main/res/values-no/strings.xml
index 9af534059a..5ffff3ba74 100755
--- a/auth/src/main/res/values-no/strings.xml
+++ b/auth/src/main/res/values-no/strings.xml
@@ -62,7 +62,8 @@
Gjenopprett passordet
Sjekk e-posten din
Få instruksjoner sendt til denne e-postadressen for hvordan du tilbakestiller passordet ditt.
- Send
+
+ Send
Følg veiledningen som er sendt til %1$s, for å gjenopprette passordet ditt.
Sender…
Denne e-postadressen samsvarer ikke med en eksisterende konto
diff --git a/auth/src/main/res/values-pt-rPT/strings.xml b/auth/src/main/res/values-pt-rPT/strings.xml
index 6296fb7da7..e1def2148b 100755
--- a/auth/src/main/res/values-pt-rPT/strings.xml
+++ b/auth/src/main/res/values-pt-rPT/strings.xml
@@ -10,7 +10,7 @@
Twitter
GitHub
Telemóvel
- Email
+ Email
Iniciar sessão com o Google
Iniciar sessão com o Google
Iniciar sessão com o Facebook
@@ -31,7 +31,7 @@
Iniciar sessão com o Yahoo
Iniciar sessão com o Yahoo
Seguinte
- Email
+ Email
Número de telefone
País
Selecione um país
diff --git a/auth/src/main/res/values-tl/strings.xml b/auth/src/main/res/values-tl/strings.xml
index 7b6bd865a9..53040b7199 100755
--- a/auth/src/main/res/values-tl/strings.xml
+++ b/auth/src/main/res/values-tl/strings.xml
@@ -10,7 +10,7 @@
Twitter
GitHub
Telepono
- Email
+ Email
Mag-sign in sa Google
Mag-sign in sa Google
Mag-sign in sa Facebook
@@ -36,7 +36,7 @@
Bansa
Pumili ng bansa
Maghanap ng bansa hal. +1, "US"
- Password
+ Password
Bagong password
Hindi mo ito maaaring iwanan na walang laman.
Mali ang email address na iyon
diff --git a/internal/lint/src/main/java/com/firebaseui/lint/internal/LintIssueRegistry.kt b/internal/lint/src/main/java/com/firebaseui/lint/internal/LintIssueRegistry.kt
index 7f01ec34fc..6a9788ef94 100644
--- a/internal/lint/src/main/java/com/firebaseui/lint/internal/LintIssueRegistry.kt
+++ b/internal/lint/src/main/java/com/firebaseui/lint/internal/LintIssueRegistry.kt
@@ -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(
diff --git a/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt b/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
new file mode 100644
index 0000000000..120f5db832
--- /dev/null
+++ b/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
@@ -0,0 +1,175 @@
+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 `` elements are checked. An English copy of a `` or ``
+ * 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()
+
+ /** Localized strings to judge once every resource file has been read. */
+ private val localized = mutableListOf()
+
+ 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 = 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. `values-fil` and `values-tl` already carry `Fecebook`
+ * for `fui_idp_name_facebook`, a pre-existing typo this exemption would hide.
+ *
+ * `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)
+ )
+ }
+}
diff --git a/internal/lint/src/test/java/com/firebaseui/lint/internal/UntranslatedResourceDetectorTest.kt b/internal/lint/src/test/java/com/firebaseui/lint/internal/UntranslatedResourceDetectorTest.kt
new file mode 100644
index 0000000000..471ca53ab9
--- /dev/null
+++ b/internal/lint/src/test/java/com/firebaseui/lint/internal/UntranslatedResourceDetectorTest.kt
@@ -0,0 +1,184 @@
+package com.firebaseui.lint.internal
+
+import com.android.tools.lint.checks.infrastructure.TestFiles.xml
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.firebaseui.lint.internal.UntranslatedResourceDetector.Companion.UNTRANSLATED_RESOURCE
+import org.junit.Test
+
+class UntranslatedResourceDetectorTest {
+
+ private fun configuredLint(): TestLintTask = TestLintTask.lint().withLocalSdk()
+
+ private fun base(vararg entries: String) = xml(
+ "res/values/strings.xml",
+ """
+ |
+ |${entries.joinToString("\n") { " $it" }}
+ |""".trimMargin()
+ )
+
+ private fun locale(folder: String, vararg entries: String) = xml(
+ "res/values-$folder/strings.xml",
+ """
+ |
+ |${entries.joinToString("\n") { " $it" }}
+ |""".trimMargin()
+ )
+
+ @Test
+ fun `Passes on a translated string`() {
+ configuredLint()
+ .files(
+ base("""Sign in"""),
+ locale("fr", """Se connecter""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Fails on a string left as base English`() {
+ configuredLint()
+ .files(
+ base("""Authentication Error"""),
+ locale(
+ "zh-rTW",
+ """Authentication Error"""
+ )
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectErrorCount(1)
+ }
+
+ @Test
+ fun `Passes on English regional folders`() {
+ configuredLint()
+ .files(
+ base("""Sign in"""),
+ locale("en-rGB", """Sign in""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Passes on allowlisted brand names`() {
+ configuredLint()
+ .files(
+ base("""Google"""),
+ locale("fr", """Google""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Passes on non-translatable strings`() {
+ configuredLint()
+ .files(
+ base(
+ """Sign in""",
+ """Internal"""
+ ),
+ locale(
+ "fr",
+ """Se connecter""",
+ """Internal"""
+ )
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Passes on a BCP47 locale folder with a real translation`() {
+ configuredLint()
+ .files(
+ base("""Sign in"""),
+ locale("b+es+419", """Iniciar sesión""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Fails on a BCP47 locale folder left as base English`() {
+ configuredLint()
+ .files(
+ base("""Sign in"""),
+ locale("b+es+419", """Sign in""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectErrorCount(1)
+ }
+
+ @Test
+ fun `Passes on a placeholder-only string`() {
+ configuredLint()
+ .files(
+ base(
+ """Sign in""",
+ """%1${'$'}s \u00A0 %2${'$'}s"""
+ ),
+ locale(
+ "fr",
+ """Se connecter""",
+ """%1${'$'}s \u00A0 %2${'$'}s"""
+ )
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun `Passes when the string carries a tools ignore`() {
+ configuredLint()
+ .files(
+ base("""Password"""),
+ xml(
+ "res/values-it/strings.xml",
+ """
+ |
+ | Password
+ |""".trimMargin()
+ )
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+
+ /**
+ * Only `values/` seeds the comparison. A string defined solely in a configuration variant is
+ * not a base string, so a locale copying it is not reported.
+ *
+ * The fixture deliberately keeps `fui_sign_in` out of `values/`: if the detector treated any
+ * unqualified folder as the base, `values-v26` would seed it and `values-fr` would report.
+ */
+ @Test
+ fun `Does not treat a non-locale configuration folder as the base`() {
+ configuredLint()
+ .files(
+ base("""Unrelated"""),
+ xml(
+ "res/values-v26/strings.xml",
+ """
+ |
+ | Sign in
+ |""".trimMargin()
+ ),
+ locale("fr", """Sign in""")
+ )
+ .issues(UNTRANSLATED_RESOURCE)
+ .run()
+ .expectClean()
+ }
+}
From a7dbbe04b906d3002184015ab7da011c09cb3c89 Mon Sep 17 00:00:00 2001
From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com>
Date: Mon, 21 Sep 2026 18:55:17 +0100
Subject: [PATCH 3/3] fix(auth): spell the Facebook provider name correctly in
Filipino and Tagalog
---
auth/src/main/res/values-fil/strings.xml | 2 +-
auth/src/main/res/values-tl/strings.xml | 2 +-
.../lint/internal/UntranslatedResourceDetector.kt | 6 ++++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/auth/src/main/res/values-fil/strings.xml b/auth/src/main/res/values-fil/strings.xml
index 0c582986c8..791f496ab0 100755
--- a/auth/src/main/res/values-fil/strings.xml
+++ b/auth/src/main/res/values-fil/strings.xml
@@ -6,7 +6,7 @@
%1$s \u00A0 \u00A0 %2$s
Error sa network, tingnan ang koneksyon mo sa internet.
Google
- Fecebook
+ Facebook
Twitter
GitHub
Telepono
diff --git a/auth/src/main/res/values-tl/strings.xml b/auth/src/main/res/values-tl/strings.xml
index 53040b7199..ac1f690d53 100755
--- a/auth/src/main/res/values-tl/strings.xml
+++ b/auth/src/main/res/values-tl/strings.xml
@@ -6,7 +6,7 @@
%1$s \u00A0 \u00A0 %2$s
Error sa network, tingnan ang koneksyon mo sa internet.
Google
- Fecebook
+ Facebook
Twitter
GitHub
Telepono
diff --git a/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt b/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
index 120f5db832..03cfa28d6d 100644
--- a/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
+++ b/internal/lint/src/main/java/com/firebaseui/lint/internal/UntranslatedResourceDetector.kt
@@ -142,8 +142,10 @@ class UntranslatedResourceDetector : ResourceXmlDetector() {
*
* 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. `values-fil` and `values-tl` already carry `Fecebook`
- * for `fui_idp_name_facebook`, a pre-existing typo this exemption would hide.
+ * 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`,