Skip to content

Commit 7825fda

Browse files
authored
Add untranslatableStringsRegex to mark strings as untranslatable (#75)
* Add untranslatableStringsRegex to mark strings as untranslatable * Apply suggestions from code review * Fix regex usage
1 parent 888a694 commit 7825fda

File tree

8 files changed

+199
-44
lines changed

8 files changed

+199
-44
lines changed

CHANGELOG.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
### Security
3535
- No security issues fixed!
3636

37+
## [4.2.0] - 2023-12-21
38+
### Added
39+
- Add new `untranslatableStringsRegex` to define a regex to mark matching PoEditor string keys as untranslatable.
40+
<details open><summary>Groovy</summary>
41+
42+
```groovy
43+
poEditor {
44+
apiToken = "your_api_token"
45+
projectId = 12345
46+
defaultLang = "en"
47+
untranslatableStringsRegex = "(.*)"
48+
}
49+
```
50+
51+
</details>
52+
53+
<details><summary>Kotlin</summary>
54+
55+
```kotlin
56+
poEditor {
57+
apiToken = "your_api_token"
58+
projectId = 12345
59+
defaultLang = "en"
60+
untranslatableStringsRegex = "(.*)"
61+
}
62+
```
63+
3764
## [4.1.2] - 2023-12-11
3865
### Fixed
3966
- Fix default resource file name constant value.
@@ -496,8 +523,9 @@ res_dir_path -> resDirPath
496523
### Added
497524
- Initial release.
498525

499-
[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.2...HEAD
500-
[4.1.2]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.0...4.1.2
526+
[Unreleased]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.2.0...HEAD
527+
[4.2.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.2...4.2.0
528+
[4.1.2]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.1...4.1.2
501529
[4.1.1]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.1.0...4.1.1
502530
[4.1.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/4.0.0...4.1.0
503531
[4.0.0]: https://github.com/hyperdevs-team/poeditor-android-gradle-plugin/compare/3.4.2...4.0.0

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ Attribute | Description
124124
```order``` | (Since 3.1.0) (Optional) Defines how to order the export. Accepted values are defined by the POEditor API.
125125
```unquoted``` | (Since 3.2.0) (Optional) Defines if the strings should be unquoted, overriding default PoEditor configuration. Defaults to `false`.
126126
```unescapeHtmlTags``` | (Since 3.4.0) (Optional) Whether or not to unescape HTML entitites from strings. Defaults to true.
127+
```untranslatableStringsRegex``` | (Since 4.2.0) (Optional) Pattern to use to mark strings as translatable=false in the strings file. Defaults to null.
127128

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

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

602603
</details>
603604

605+
### Mark strings as untranslatable
606+
> Requires version 4.2.0 of the plug-in
607+
608+
You can use the `untranslatableStringsRegex` property to define a regex to mark matching PoEditor string keys as
609+
untranslatable.
610+
These strings will be marked as `translatable="false"` in the final strings file.
611+
612+
<details open><summary>Groovy</summary>
613+
614+
```groovy
615+
poEditor {
616+
apiToken = "your_api_token"
617+
projectId = 12345
618+
defaultLang = "en"
619+
untranslatableStringsRegex = "(.*)"
620+
}
621+
```
622+
623+
</details>
624+
625+
<details><summary>Kotlin</summary>
626+
627+
```kotlin
628+
poEditor {
629+
apiToken = "your_api_token"
630+
projectId = 12345
631+
defaultLang = "en"
632+
untranslatableStringsRegex = "(.*)"
633+
}
634+
```
635+
636+
Keep in mind that the regex must match the whole string name and not just a part, as it relies on
637+
[`CharSequence.matches(Regex)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/matches.html) from the
638+
Kotlin API.
639+
604640
## iOS alternative
605641
If you want a similar solution for your iOS projects, check this out: [poeditor-parser-swift](https://github.com/hyperdevs-team/poeditor-parser-swift)
606642

src/main/kotlin/com/hyperdevs/poeditor/gradle/Main.kt

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,29 @@ fun main() {
3737
val resDirPath = dotenv.get("RES_DIR_PATH", "")
3838
val defaultLanguage = dotenv.get("DEFAULT_LANGUAGE", "")
3939
val filters = dotenv.get("FILTERS", "")
40-
.takeIf { it.isNotBlank() }
41-
?.split(",")
42-
?.map { it.trim() }
43-
?.map { FilterType.from(it) }
44-
?: emptyList()
40+
.takeIf { it.isNotBlank() }
41+
?.split(",")
42+
?.map { it.trim() }
43+
?.map { FilterType.from(it) }
44+
?: emptyList()
4545
val order = OrderType.from(dotenv.get("ORDER", OrderType.NONE.name))
4646
val tags = dotenv.get("TAGS", "")
47-
.takeIf { it.isNotBlank() }
48-
?.split(",")
49-
?.map { it.trim() }
50-
?: emptyList()
47+
.takeIf { it.isNotBlank() }
48+
?.split(",")
49+
?.map { it.trim() }
50+
?: emptyList()
5151
val languageValuesOverridePathMap = dotenv.get("LANGUAGE_VALUES_OVERRIDE_PATH_MAP", "")
52-
.takeIf { it.isNotBlank() }
53-
?.split(",")
54-
?.associate {
55-
val (key, value) = it.split(":")
56-
key to value
57-
}
58-
?: emptyMap()
52+
.takeIf { it.isNotBlank() }
53+
?.split(",")
54+
?.associate {
55+
val (key, value) = it.split(":")
56+
key to value
57+
}
58+
?: emptyMap()
5959
val minimumTranslationPercentage = dotenv.get("MINIMUM_TRANSLATION_PERCENTAGE", "85").toInt()
6060
val unquoted = dotenv.get("UNQUOTED", "false").toBoolean()
6161
val unescapeHtmlTags = dotenv.get("UNESCAPE_HTML_TAGS", "true").toBoolean()
62+
val untranslatableStringsRegex = dotenv.get("UNTRANSLATABLE_STRINGS_REGEX", null)
6263

6364
PoEditorStringsImporter.importPoEditorStrings(
6465
apiToken,
@@ -72,6 +73,7 @@ fun main() {
7273
minimumTranslationPercentage,
7374
resFileName,
7475
unquoted,
75-
unescapeHtmlTags
76+
unescapeHtmlTags,
77+
untranslatableStringsRegex
7678
)
7779
}

src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorPluginExtension.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,15 @@ open class PoEditorPluginExtension @Inject constructor(objects: ObjectFactory, p
151151
@get:Input
152152
val unescapeHtmlTags: Property<Boolean> = objects.property(Boolean::class.java)
153153

154+
/**
155+
* Pattern to use to mark strings as translatable=false in the strings file.
156+
*
157+
* Defaults to null.
158+
*/
159+
@get:Optional
160+
@get:Input
161+
val untranslatableStringsRegex: Property<String?> = objects.property(String::class.java)
162+
154163
/**
155164
* Sets the configuration as enabled or not.
156165
*
@@ -280,4 +289,14 @@ open class PoEditorPluginExtension @Inject constructor(objects: ObjectFactory, p
280289
* Gradle Kotlin DSL users must use `unescapeHtmlTags.set(value)`.
281290
*/
282291
fun setUnescapeHtmlTags(value: Boolean) = unescapeHtmlTags.set(value)
292+
293+
/**
294+
* Sets the pattern to use to mark strings as translatable=false in the strings file.
295+
*
296+
* NOTE: added for Gradle Groovy DSL compatibility. Check the note on
297+
* https://docs.gradle.org/current/userguide/lazy_configuration.html#lazy_properties for more details.
298+
*
299+
* Gradle Kotlin DSL users must use `setUntranslatableStringsRegex.set(value)`.
300+
*/
301+
fun setUntranslatableStringsRegex(value: String) = untranslatableStringsRegex.set(value)
283302
}

src/main/kotlin/com/hyperdevs/poeditor/gradle/PoEditorStringsImporter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ object PoEditorStringsImporter {
9494
minimumTranslationPercentage: Int,
9595
resFileName: String,
9696
unquoted: Boolean,
97-
unescapeHtmlTags: Boolean) {
97+
unescapeHtmlTags: Boolean,
98+
untranslatableStringsRegex: String?) {
9899
try {
99100
val poEditorApiController = PoEditorApiControllerImpl(apiToken, moshi, poEditorApi)
100101

@@ -145,7 +146,8 @@ object PoEditorStringsImporter {
145146
val postProcessedXmlDocumentMap = xmlPostProcessor.postProcessTranslationXml(
146147
translationFile,
147148
listOf(TABLET_REGEX_STRING),
148-
unescapeHtmlTags
149+
unescapeHtmlTags,
150+
untranslatableStringsRegex
149151
)
150152

151153
xmlWriter.saveXml(

src/main/kotlin/com/hyperdevs/poeditor/gradle/tasks/ImportPoEditorStringsTask.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ abstract class ImportPoEditorStringsTask @Inject constructor() : DefaultTask() {
150150
@get:Input
151151
abstract val unescapeHtmlTags: Property<Boolean>
152152

153+
/**
154+
* Pattern to use to mark strings as translatable=false in the strings file.
155+
*
156+
* Defaults to null.
157+
*/
158+
@get:Optional
159+
@get:Input
160+
abstract val untranslatableStringsRegex: Property<String?>
161+
153162
/**
154163
* Main task entrypoint.
155164
*/
@@ -184,7 +193,8 @@ abstract class ImportPoEditorStringsTask @Inject constructor() : DefaultTask() {
184193
minimumTranslationPercentage.getOrElse(DefaultValues.MINIMUM_TRANSLATION_PERCENTAGE),
185194
resFileName.getOrElse(DefaultValues.RES_FILE_NAME),
186195
unquoted.getOrElse(DefaultValues.UNQUOTED),
187-
unescapeHtmlTags.getOrElse(DefaultValues.UNESCAPE_HTML_TAGS)
196+
unescapeHtmlTags.getOrElse(DefaultValues.UNESCAPE_HTML_TAGS),
197+
untranslatableStringsRegex.orNull
188198
)
189199
}
190200

src/main/kotlin/com/hyperdevs/poeditor/gradle/xml/XmlPostProcessor.kt

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class XmlPostProcessor {
3838
private const val TAG_ITEM = "item"
3939

4040
private const val ATTR_NAME = "name"
41+
private const val ATTR_TRANSLATABLE = "translatable"
4142
}
4243

4344
/**
@@ -49,20 +50,27 @@ class XmlPostProcessor {
4950
*/
5051
fun postProcessTranslationXml(translationFileXmlString: String,
5152
fileSplitRegexStringList: List<String>,
52-
unescapeHtmlTags: Boolean): Map<String, Document> =
53-
splitTranslationXml(formatTranslationXml(translationFileXmlString, unescapeHtmlTags), fileSplitRegexStringList)
53+
unescapeHtmlTags: Boolean,
54+
untranslatableStringsRegex: String?): Map<String, Document> =
55+
splitTranslationXml(
56+
formatTranslationXml(translationFileXmlString, unescapeHtmlTags, untranslatableStringsRegex),
57+
fileSplitRegexStringList
58+
)
5459

5560
/**
5661
* Formats a given translations XML string to conform to Android strings.xml format.
5762
*/
58-
fun formatTranslationXml(translationFileXmlString: String, unescapeHtmlTags: Boolean): String {
63+
fun formatTranslationXml(translationFileXmlString: String,
64+
unescapeHtmlTags: Boolean,
65+
untranslatableStringsRegex: String?): String {
5966
// Parse line by line by traversing the original file using DOM
6067
val translationFileXmlDocument = translationFileXmlString.toStringsXmlDocument()
6168

6269
formatTranslationXmlDocument(
6370
translationFileXmlDocument,
6471
translationFileXmlDocument.childNodes,
65-
null
72+
null,
73+
untranslatableStringsRegex?.toRegex()
6674
)
6775

6876
return translationFileXmlDocument.toAndroidXmlString(unescapeHtmlTags)
@@ -135,26 +143,37 @@ class XmlPostProcessor {
135143

136144
private fun formatTranslationXmlDocument(document: Document,
137145
nodeList: NodeList,
138-
rootNode: Node? = null) {
146+
rootNode: Node? = null,
147+
untranslatableStringsRegex: Regex?) {
139148
for (i in 0 until nodeList.length) {
140149
if (nodeList.item(i).nodeType == Node.ELEMENT_NODE) {
141150
val nodeElement = nodeList.item(i) as Element
142151
when (nodeElement.tagName) {
143152
TAG_RESOURCES -> {
144153
// Main node, traverse its children
145-
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
154+
formatTranslationXmlDocument(
155+
document,
156+
nodeElement.childNodes,
157+
nodeElement,
158+
untranslatableStringsRegex
159+
)
146160
}
147161
TAG_PLURALS -> {
148162
// Plurals node, process its children
149-
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
163+
formatTranslationXmlDocument(
164+
document,
165+
nodeElement.childNodes,
166+
nodeElement,
167+
untranslatableStringsRegex
168+
)
150169
}
151170
TAG_STRING -> {
152171
// String node, apply transformation to the content
153-
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
172+
processTextAndReplaceNodeContent(document, nodeElement, rootNode, untranslatableStringsRegex)
154173
}
155174
TAG_ITEM -> {
156175
// Plurals item node, apply transformation to the content
157-
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
176+
processTextAndReplaceNodeContent(document, nodeElement, rootNode, untranslatableStringsRegex)
158177
}
159178
}
160179
}
@@ -163,7 +182,8 @@ class XmlPostProcessor {
163182

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

208+
// Add the translatable = false node if the string name matches the untranslatable pattern
209+
untranslatableStringsRegex?.let {
210+
val nodeName = copiedNodeElement.getAttribute(ATTR_NAME)
211+
212+
if (nodeName.matches(untranslatableStringsRegex)) {
213+
// Add translatable attribute
214+
copiedNodeElement.setAttribute(ATTR_TRANSLATABLE, "false")
215+
}
216+
}
217+
188218
document.adoptNode(copiedNodeElement)
189219
rootNode?.replaceChild(copiedNodeElement, nodeElement)
190220
}

0 commit comments

Comments
 (0)