Skip to content

Commit

Permalink
Add untranslatableStringsRegex to mark strings as untranslatable (#75)
Browse files Browse the repository at this point in the history
* Add untranslatableStringsRegex to mark strings as untranslatable

* Apply suggestions from code review

* Fix regex usage
  • Loading branch information
adriangl authored Dec 21, 2023
1 parent 888a694 commit 7825fda
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 44 deletions.
32 changes: 30 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security
- No security issues fixed!

## [4.2.0] - 2023-12-21
### Added
- Add new `untranslatableStringsRegex` to define a regex to mark matching PoEditor string keys as untranslatable.
<details open><summary>Groovy</summary>

```groovy
poEditor {
apiToken = "your_api_token"
projectId = 12345
defaultLang = "en"
untranslatableStringsRegex = "(.*)"
}
```

</details>

<details><summary>Kotlin</summary>

```kotlin
poEditor {
apiToken = "your_api_token"
projectId = 12345
defaultLang = "en"
untranslatableStringsRegex = "(.*)"
}
```

## [4.1.2] - 2023-12-11
### Fixed
- Fix default resource file name constant value.
Expand Down Expand Up @@ -496,8 +523,9 @@ res_dir_path -> resDirPath
### Added
- Initial release.

[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.2...HEAD
[4.1.2]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.0...4.1.2
[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.2.0...HEAD
[4.2.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.2...4.2.0
[4.1.2]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.1...4.1.2
[4.1.1]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.0...4.1.1
[4.1.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.0.0...4.1.0
[4.0.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.2...4.0.0
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Attribute | Description
```order``` | (Since 3.1.0) (Optional) Defines how to order the export. Accepted values are defined by the POEditor API.
```unquoted``` | (Since 3.2.0) (Optional) Defines if the strings should be unquoted, overriding default PoEditor configuration. Defaults to `false`.
```unescapeHtmlTags``` | (Since 3.4.0) (Optional) Whether or not to unescape HTML entitites from strings. Defaults to true.
```untranslatableStringsRegex``` | (Since 4.2.0) (Optional) Pattern to use to mark strings as translatable=false in the strings file. Defaults to null.

After the configuration is done, just run the new ```importPoEditorStrings``` task via Android Studio or command line:

Expand Down Expand Up @@ -601,6 +602,41 @@ tasks.register("importCustomPoEditorStrings", ImportPoEditorStringsTask::class.j

</details>

### Mark strings as untranslatable
> Requires version 4.2.0 of the plug-in
You can use the `untranslatableStringsRegex` property to define a regex to mark matching PoEditor string keys as
untranslatable.
These strings will be marked as `translatable="false"` in the final strings file.

<details open><summary>Groovy</summary>

```groovy
poEditor {
apiToken = "your_api_token"
projectId = 12345
defaultLang = "en"
untranslatableStringsRegex = "(.*)"
}
```

</details>

<details><summary>Kotlin</summary>

```kotlin
poEditor {
apiToken = "your_api_token"
projectId = 12345
defaultLang = "en"
untranslatableStringsRegex = "(.*)"
}
```

Keep in mind that the regex must match the whole string name and not just a part, as it relies on
[`CharSequence.matches(Regex)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/matches.html) from the
Kotlin API.

## iOS alternative
If you want a similar solution for your iOS projects, check this out: [poeditor-parser-swift](https://github.com/hyperdevs-team/poeditor-parser-swift)

Expand Down
36 changes: 19 additions & 17 deletions src/main/kotlin/com/hyperdevs/poeditor/gradle/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,29 @@ fun main() {
val resDirPath = dotenv.get("RES_DIR_PATH", "")
val defaultLanguage = dotenv.get("DEFAULT_LANGUAGE", "")
val filters = dotenv.get("FILTERS", "")
.takeIf { it.isNotBlank() }
?.split(",")
?.map { it.trim() }
?.map { FilterType.from(it) }
?: emptyList()
.takeIf { it.isNotBlank() }
?.split(",")
?.map { it.trim() }
?.map { FilterType.from(it) }
?: emptyList()
val order = OrderType.from(dotenv.get("ORDER", OrderType.NONE.name))
val tags = dotenv.get("TAGS", "")
.takeIf { it.isNotBlank() }
?.split(",")
?.map { it.trim() }
?: emptyList()
.takeIf { it.isNotBlank() }
?.split(",")
?.map { it.trim() }
?: emptyList()
val languageValuesOverridePathMap = dotenv.get("LANGUAGE_VALUES_OVERRIDE_PATH_MAP", "")
.takeIf { it.isNotBlank() }
?.split(",")
?.associate {
val (key, value) = it.split(":")
key to value
}
?: emptyMap()
.takeIf { it.isNotBlank() }
?.split(",")
?.associate {
val (key, value) = it.split(":")
key to value
}
?: emptyMap()
val minimumTranslationPercentage = dotenv.get("MINIMUM_TRANSLATION_PERCENTAGE", "85").toInt()
val unquoted = dotenv.get("UNQUOTED", "false").toBoolean()
val unescapeHtmlTags = dotenv.get("UNESCAPE_HTML_TAGS", "true").toBoolean()
val untranslatableStringsRegex = dotenv.get("UNTRANSLATABLE_STRINGS_REGEX", null)

PoEditorStringsImporter.importPoEditorStrings(
apiToken,
Expand All @@ -72,6 +73,7 @@ fun main() {
minimumTranslationPercentage,
resFileName,
unquoted,
unescapeHtmlTags
unescapeHtmlTags,
untranslatableStringsRegex
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ open class PoEditorPluginExtension @Inject constructor(objects: ObjectFactory, p
@get:Input
val unescapeHtmlTags: Property<Boolean> = objects.property(Boolean::class.java)

/**
* Pattern to use to mark strings as translatable=false in the strings file.
*
* Defaults to null.
*/
@get:Optional
@get:Input
val untranslatableStringsRegex: Property<String?> = objects.property(String::class.java)

/**
* Sets the configuration as enabled or not.
*
Expand Down Expand Up @@ -280,4 +289,14 @@ open class PoEditorPluginExtension @Inject constructor(objects: ObjectFactory, p
* Gradle Kotlin DSL users must use `unescapeHtmlTags.set(value)`.
*/
fun setUnescapeHtmlTags(value: Boolean) = unescapeHtmlTags.set(value)

/**
* Sets the pattern to use to mark strings as translatable=false in the strings file.
*
* NOTE: added for Gradle Groovy DSL compatibility. Check the note on
* https://docs.gradle.org/current/userguide/lazy_configuration.html#lazy_properties for more details.
*
* Gradle Kotlin DSL users must use `setUntranslatableStringsRegex.set(value)`.
*/
fun setUntranslatableStringsRegex(value: String) = untranslatableStringsRegex.set(value)
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ object PoEditorStringsImporter {
minimumTranslationPercentage: Int,
resFileName: String,
unquoted: Boolean,
unescapeHtmlTags: Boolean) {
unescapeHtmlTags: Boolean,
untranslatableStringsRegex: String?) {
try {
val poEditorApiController = PoEditorApiControllerImpl(apiToken, moshi, poEditorApi)

Expand Down Expand Up @@ -145,7 +146,8 @@ object PoEditorStringsImporter {
val postProcessedXmlDocumentMap = xmlPostProcessor.postProcessTranslationXml(
translationFile,
listOf(TABLET_REGEX_STRING),
unescapeHtmlTags
unescapeHtmlTags,
untranslatableStringsRegex
)

xmlWriter.saveXml(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ abstract class ImportPoEditorStringsTask @Inject constructor() : DefaultTask() {
@get:Input
abstract val unescapeHtmlTags: Property<Boolean>

/**
* Pattern to use to mark strings as translatable=false in the strings file.
*
* Defaults to null.
*/
@get:Optional
@get:Input
abstract val untranslatableStringsRegex: Property<String?>

/**
* Main task entrypoint.
*/
Expand Down Expand Up @@ -184,7 +193,8 @@ abstract class ImportPoEditorStringsTask @Inject constructor() : DefaultTask() {
minimumTranslationPercentage.getOrElse(DefaultValues.MINIMUM_TRANSLATION_PERCENTAGE),
resFileName.getOrElse(DefaultValues.RES_FILE_NAME),
unquoted.getOrElse(DefaultValues.UNQUOTED),
unescapeHtmlTags.getOrElse(DefaultValues.UNESCAPE_HTML_TAGS)
unescapeHtmlTags.getOrElse(DefaultValues.UNESCAPE_HTML_TAGS),
untranslatableStringsRegex.orNull
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class XmlPostProcessor {
private const val TAG_ITEM = "item"

private const val ATTR_NAME = "name"
private const val ATTR_TRANSLATABLE = "translatable"
}

/**
Expand All @@ -49,20 +50,27 @@ class XmlPostProcessor {
*/
fun postProcessTranslationXml(translationFileXmlString: String,
fileSplitRegexStringList: List<String>,
unescapeHtmlTags: Boolean): Map<String, Document> =
splitTranslationXml(formatTranslationXml(translationFileXmlString, unescapeHtmlTags), fileSplitRegexStringList)
unescapeHtmlTags: Boolean,
untranslatableStringsRegex: String?): Map<String, Document> =
splitTranslationXml(
formatTranslationXml(translationFileXmlString, unescapeHtmlTags, untranslatableStringsRegex),
fileSplitRegexStringList
)

/**
* Formats a given translations XML string to conform to Android strings.xml format.
*/
fun formatTranslationXml(translationFileXmlString: String, unescapeHtmlTags: Boolean): String {
fun formatTranslationXml(translationFileXmlString: String,
unescapeHtmlTags: Boolean,
untranslatableStringsRegex: String?): String {
// Parse line by line by traversing the original file using DOM
val translationFileXmlDocument = translationFileXmlString.toStringsXmlDocument()

formatTranslationXmlDocument(
translationFileXmlDocument,
translationFileXmlDocument.childNodes,
null
null,
untranslatableStringsRegex?.toRegex()
)

return translationFileXmlDocument.toAndroidXmlString(unescapeHtmlTags)
Expand Down Expand Up @@ -135,26 +143,37 @@ class XmlPostProcessor {

private fun formatTranslationXmlDocument(document: Document,
nodeList: NodeList,
rootNode: Node? = null) {
rootNode: Node? = null,
untranslatableStringsRegex: Regex?) {
for (i in 0 until nodeList.length) {
if (nodeList.item(i).nodeType == Node.ELEMENT_NODE) {
val nodeElement = nodeList.item(i) as Element
when (nodeElement.tagName) {
TAG_RESOURCES -> {
// Main node, traverse its children
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
formatTranslationXmlDocument(
document,
nodeElement.childNodes,
nodeElement,
untranslatableStringsRegex
)
}
TAG_PLURALS -> {
// Plurals node, process its children
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
formatTranslationXmlDocument(
document,
nodeElement.childNodes,
nodeElement,
untranslatableStringsRegex
)
}
TAG_STRING -> {
// String node, apply transformation to the content
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
processTextAndReplaceNodeContent(document, nodeElement, rootNode, untranslatableStringsRegex)
}
TAG_ITEM -> {
// Plurals item node, apply transformation to the content
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
processTextAndReplaceNodeContent(document, nodeElement, rootNode, untranslatableStringsRegex)
}
}
}
Expand All @@ -163,7 +182,8 @@ class XmlPostProcessor {

private fun processTextAndReplaceNodeContent(document: Document,
nodeElement: Element,
rootNode: Node?) {
rootNode: Node?,
untranslatableStringsRegex: Regex?) {
// First check if we have a CDATA node as the child of the element. If we have it, we have to
// preserve the CDATA node but process the text. Else, we handle the node as a usual text node
val copiedNodeElement: Element
Expand All @@ -185,6 +205,16 @@ class XmlPostProcessor {
}
}

// Add the translatable = false node if the string name matches the untranslatable pattern
untranslatableStringsRegex?.let {
val nodeName = copiedNodeElement.getAttribute(ATTR_NAME)

if (nodeName.matches(untranslatableStringsRegex)) {
// Add translatable attribute
copiedNodeElement.setAttribute(ATTR_TRANSLATABLE, "false")
}
}

document.adoptNode(copiedNodeElement)
rootNode?.replaceChild(copiedNodeElement, nodeElement)
}
Expand Down
Loading

0 comments on commit 7825fda

Please sign in to comment.