Skip to content

Commit

Permalink
improvement(lint): 'TranslationTypo' - link to Crowdin
Browse files Browse the repository at this point in the history
Aims to save time when maintainers are performing
translation syncs

Sample link: https://crowdin.com/editor/ankidroid/7290/en-af#q=create_subdeck
  • Loading branch information
david-allison committed Sep 30, 2024
1 parent 9c6d86c commit f7d4a33
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.android.tools.lint.detector.api.XmlContext
import com.android.tools.lint.detector.api.XmlScanner
import com.google.common.annotations.VisibleForTesting
import com.ichi2.anki.lint.utils.Constants
import com.ichi2.anki.lint.utils.CrowdinContext.Companion.toCrowdinContext
import com.ichi2.anki.lint.utils.ext.isRightToLeftLanguage
import org.w3c.dom.Element

Expand Down Expand Up @@ -94,13 +95,17 @@ class TranslationTypo : ResourceXmlDetector(), XmlScanner {
return
}

val crowdinContext = context.toCrowdinContext()

/** Helper function to report 'TranslationTypo' issues */
fun Element.reportIssue(message: String) {
val elementToReport = this
val crowdinEditUrl = crowdinContext?.getEditUrl(elementToReport)
?.let { url -> "; $url" } ?: ""
context.report(
issue = ISSUE,
location = context.getElementLocation(elementToReport),
message = message
message = message + crowdinEditUrl
)
}

Expand Down
131 changes: 131 additions & 0 deletions lint-rules/src/main/java/com/ichi2/anki/lint/utils/Crowdin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2024 David Allison <davidallisongithub@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki.lint.utils

import com.android.tools.lint.detector.api.ResourceContext
import org.w3c.dom.Element
import java.io.File
import java.util.Locale

/**
* Identifier of an XML file in a Crowdin URL.
* 8236 -> `20-search-preference.xml` in the URL https://crowdin.com/editor/ankidroid/8236/en-yu
*/
@JvmInline
value class CrowdinFileIdentifier(private val value: Long) {
override fun toString(): String = value.toString()

companion object {
private val fileNameToIdentifier = mapOf(
"01-core" to 7290,
"02-strings" to 7291,
"03-dialogs" to 7303,
"04-network" to 8167,
"05-feedback" to 8168,
"06-statistics" to 8169,
"07-cardbrowser" to 8170,
"08-widget" to 8171,
"09-backup" to 8172,
"10-preferences" to 8173,
"11-arrays" to 8174,
"16-multimedia-editor" to 8229,
"17-model-manager" to 8230,
"19-standard-models" to 8232,
"20-search-preference" to 8236
).mapValues { CrowdinFileIdentifier(it.value.toLong()) }

fun fromFile(file: File): CrowdinFileIdentifier? =
fileNameToIdentifier[file.nameWithoutExtension]
}
}

/**
* The language key which Crowdin uses to represent the language: `yu`, NOT `yue`
*/
@JvmInline
value class CrowdinLanguageTag(private val tag: String) {
override fun toString() = tag

companion object {
private val customMappings = mapOf(
/* from tools/localization/src/update.ts */
"yue" to "yu",
"heb" to "he",
"iw" to "he",
"ind" to "id",
"tgl" to "tl",

/* Other weirdness */

/* Malayalam */
"ml" to "mlin",
/* Punjabi */
"pa" to "pain",
// Norwegian Nynorsk
"nn" to "nnno",

/* Tatar (Russia) */
"tt" to "ttru",

/* Urdu (Pakistan) */
"ur" to "urpk",

// Crowdin does not handle 'Spanish (Spain)' as 'eses', it needs 'es'
"eses" to "es",
"ptpt" to "pt"
)

fun fromFolderName(folderName: String): CrowdinLanguageTag {
val language = folderName.substring("values-".length)
.replace("-r", "") // es-rAR -> esAR
.lowercase(Locale.ROOT) // esAR -> esar

val crowdinLanguage = customMappings[language] ?: language

return CrowdinLanguageTag(crowdinLanguage)
}

fun fromFolder(folder: File): CrowdinLanguageTag = fromFolderName(folder.name)
}
}

/**
* How Crowdin represents the Android path of `values-yue/01-core.xml`
* @param languageTag How 'values-zh-rCN' is represented. See [CrowdinLanguageTag]
* @param fileIdentifier How `01-core` is represented. See [CrowdinFileIdentifier]
*/
data class CrowdinContext(val languageTag: CrowdinLanguageTag, val fileIdentifier: CrowdinFileIdentifier) {
private fun getStringName(element: Element): String? =
if (element.hasAttribute("name")) element.getAttribute("name") else null

fun getEditUrl(element: Element): String? {
val stringName = getStringName(element) ?: return null
return getEditUrl(stringName)
}

/** Example: [https://crowdin.com/editor/ankidroid/7290/en-af#q=create_subdeck](https://crowdin.com/editor/ankidroid/7290/en-af#q=create_subdeck) */
fun getEditUrl(string: String): String =
"https://crowdin.com/editor/ankidroid/$fileIdentifier/en-$languageTag#q=$string"

companion object {
fun ResourceContext.toCrowdinContext(): CrowdinContext? {
val androidLanguage = CrowdinLanguageTag.fromFolder(file.parentFile)
val identifier = CrowdinFileIdentifier.fromFile(file) ?: return null
return CrowdinContext(androidLanguage, identifier)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,67 @@ class TranslationTypoTest {

TranslationTypo.ISSUE.assertXmlStringsNoIssues(stringRemoved)
}

/** A link to the string on Crowdin should be provided */
@Test
fun crowdinEditLinkIsProvided() {
// Use links in the form: https://crowdin.com/editor/ankidroid/7290/en-af#q=create_subdeck
// where 7290 is 01-core.xml, `en-af` is Afrikaans, and `create_subdeck` is the key

// The actual link is https://crowdin.com/editor/ankidroid/7290/en-af#6534818, but
// we don't have context to map from `create_subdeck` to `6534818`

// We do not use '...', as this is not checked for RTL languages
val error = """<resources>
<string name="create_subdeck">javascript</string>
</resources>"""

// 'standard' test
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7290/en-af#q=create_subdeck",
fileName = "01-core",
androidLanguageFolder = "af"
)

// 02-strings -> 7291
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7291/en-af#q=create_subdeck",
fileName = "02-strings",
androidLanguageFolder = "af"
)

// custom mapping: yue -> yu
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7290/en-yu#q=create_subdeck",
fileName = "01-core",
androidLanguageFolder = "yue"
)

// Used region specifier: Chinese
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7290/en-zhcn#q=create_subdeck",
fileName = "01-core",
androidLanguageFolder = "zh-rCN"
)

// no -> nnno
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7290/en-nnno#q=create_subdeck",
fileName = "01-core",
androidLanguageFolder = "nn"
)

// ur -> urpa
TranslationTypo.ISSUE.assertXmlStringsHasError(
error,
expectedError = "https://crowdin.com/editor/ankidroid/7290/en-urpk#q=create_subdeck",
fileName = "01-core",
androidLanguageFolder = "ur"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,29 @@ fun Issue.assertXmlStringsHasErrorCount(@Language("XML") xmlFile: String, expect
.expectErrorCount(expectedErrorCount)
}

fun Issue.assertXmlStringsHasError(@Language("XML") xmlFile: String, expectedError: String) {
/**
* @param androidLanguageFolder the code used in the Android `values-XX` folder.
* Cantonese: `yue`, not `yu`
* @param fileName The name of the xml file without extension: `01-core` etc...
*/
fun Issue.assertXmlStringsHasError(
@Language("XML") xmlFile: String,
expectedError: String,
androidLanguageFolder: String? = null,
fileName: String? = null
) {
val languageQualifier = if (androidLanguageFolder != null) "-$androidLanguageFolder" else ""
val resourceFileName = fileName ?: "constants"
TestLintTask.lint()
.allowMissingSdk()
.allowCompilationErrors()
.files(TestFiles.xml("res/values/constants.xml", xmlFile))
.files(TestFiles.xml("res/values$languageQualifier/$resourceFileName.xml", xmlFile))
.issues(this)
.run()
.expectErrorCount(1)
.check({ output: String ->
assertTrue(
"check should fail wth '$expectedError', but was '$output'",
"check should fail with '$expectedError', but was '$output'",
output.contains(expectedError)
)
})
Expand Down
Loading

0 comments on commit f7d4a33

Please sign in to comment.