From abe6d79eda65a8ab617f365be50778d636e01bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Thu, 19 Dec 2024 16:17:38 +0100 Subject: [PATCH] fix: compose multiplatform tests - part 2 --- .../in/ComposeXmlFormatProcessorTest.kt | 223 ++++++++++++ .../compose/out/ComposeXmlFileExporterTest.kt | 327 ++++++++++++++++++ .../out/TextToComposeXmlConvertorTest.kt | 155 +++++++++ .../import/composeMultiplatform/strings.xml | 2 +- .../strings_params_everywhere.xml | 2 +- 5 files changed, 707 insertions(+), 2 deletions(-) create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/in/ComposeXmlFormatProcessorTest.kt create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/ComposeXmlFileExporterTest.kt create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/TextToComposeXmlConvertorTest.kt diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/in/ComposeXmlFormatProcessorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/in/ComposeXmlFormatProcessorTest.kt new file mode 100644 index 0000000000..0d22ccb2d6 --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/in/ComposeXmlFormatProcessorTest.kt @@ -0,0 +1,223 @@ +package io.tolgee.unit.formats.compose.`in` + +import io.tolgee.formats.xmlResources.`in`.XmlResourcesProcessor +import io.tolgee.testing.assert +import io.tolgee.unit.formats.PlaceholderConversionTestHelper +import io.tolgee.util.FileProcessorContextMockUtil +import io.tolgee.util.assertKey +import io.tolgee.util.assertLanguagesCount +import io.tolgee.util.assertSingle +import io.tolgee.util.assertSinglePlural +import io.tolgee.util.assertTranslations +import io.tolgee.util.custom +import io.tolgee.util.description +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ComposeXmlFormatProcessorTest { + lateinit var mockUtil: FileProcessorContextMockUtil + + @BeforeEach + fun setup() { + mockUtil = FileProcessorContextMockUtil() + mockUtil.mockIt("values-en/strings.xml", "src/test/resources/import/composeMultiplatform/strings.xml") + } + + @Test + fun `returns correct parsed result`() { + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "app_name") + .assertSingle { + hasText("Tolgee test") + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "dogs_count") + .assertSinglePlural { + hasText( + """ + {0, plural, + one {# dog} + other {# dogs} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[0]") + .assertSingle { + hasText("First item") + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[1]") + .assertSingle { + hasText("Second item") + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "with_spaces") + .assertSingle { + hasText("Hello!") + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "with_html") + .assertSingle { + hasText("Hello!") + } + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "with_params") + .assertSingle { + hasText("{0, number} {3} {2, number, .00} {3, number, scientific} %5${'$'}+d") + } + mockUtil.fileProcessorContext.assertKey("app_name") { + custom.assert.isNull() + description.assert.isEqualTo("This is a comment") + } + mockUtil.fileProcessorContext.assertKey("dogs_count") { + custom.assert.isNull() + description.assert.isEqualTo("This is a comment above a plural") + } + mockUtil.fileProcessorContext.assertKey("string_array") { + custom.assert.isNull() + description.assert.isEqualTo("This is a comment above an array item #2") + } + mockUtil.fileProcessorContext.assertKey("with_spaces") { + custom.assert.isNull() + description.assert.isEqualTo("and only the last one will be kept") + } + } + + @Test + fun `import with placeholder conversion (disabled ICU)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = false, projectIcuPlaceholdersEnabled = false) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "dogs_count") + .assertSinglePlural { + hasText( + """ + {value, plural, + one {%1${'$'}d dog %2${'$'}s '{'escape'}'} + other {%1${'$'}d dogs %2${'$'}s} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[0]") + .assertSingle { + hasText("First item %1${'$'}d {escape}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "with_params") + .assertSingle { + hasText("%1${'$'}d %4${'$'}s %3${'$'}.2f %4${'$'}e %5${'$'}+d {escape}") + } + } + + @Test + fun `import with placeholder conversion (no conversion)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = false, projectIcuPlaceholdersEnabled = true) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "dogs_count") + .assertSinglePlural { + hasText( + """ + {value, plural, + one {%1${'$'}d dog %2${'$'}s '{'escape'}'} + other {%1${'$'}d dogs %2${'$'}s} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[0]") + .assertSingle { + hasText("First item %1${'$'}d '{'escape'}'") + } + mockUtil.fileProcessorContext.assertTranslations("en", "with_params") + .assertSingle { + hasText("%1${'$'}d %4${'$'}s %3${'$'}.2f %4${'$'}e %5${'$'}+d '{'escape'}'") + } + } + + @Test + fun `import with placeholder conversion (with conversion)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = true, projectIcuPlaceholdersEnabled = true) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "dogs_count") + .assertSinglePlural { + hasText( + """ + {0, plural, + one {# dog {1} '{'escape'}'} + other {# dogs {1}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[0]") + .assertSingle { + hasText("First item {0, number} '{'escape'}'") + } + mockUtil.fileProcessorContext.assertTranslations("en", "string_array[1]") + .assertSingle { + hasText("Second item {0, number}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "with_params") + .assertSingle { + hasText("{0, number} {3} {2, number, .00} {3, number, scientific} %5${'$'}+d '{'escape'}'") + } + mockUtil.fileProcessorContext.assertKey("dogs_count") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `placeholder conversion setting application works`() { + PlaceholderConversionTestHelper.testFile( + "values-en/strings.xml", + "src/test/resources/import/android/strings_params_everywhere.xml", + assertBeforeSettingsApplication = + listOf( + "{0, plural,\none {# dog {1} '{'escape'}'}\nother {# dogs {1}}\n}", + "First item {0, number} '{'escape'}'", + "Second item {0, number}", + "{0, number} {3} {2, number, .00} {3, number, scientific} %+d '{'escape'}'", + ), + assertAfterDisablingConversion = + listOf( + "{value, plural,\none {%d dog %s '{'escape'}'}\nother {%d dogs %s}\n}", + "First item %d '{'escape'}'", + "Second item %d", + "%d %4\$s %.2f %e %+d '{'escape'}'", + ), + assertAfterReEnablingConversion = + listOf( + "{0, plural,\none {# dog {1} '{'escape'}'}\nother {# dogs {1}}\n}", + "First item {0, number} '{'escape'}'", + "Second item {0, number}", + "{0, number} {3} {2, number, .00} {3, number, scientific} %+d '{'escape'}'", + ), + ) + } + + private fun mockPlaceholderConversionTestFile( + convertPlaceholders: Boolean, + projectIcuPlaceholdersEnabled: Boolean, + ) { + mockUtil.mockIt( + "values-en/strings.xml", + "src/test/resources/import/composeMultiplatform/strings_params_everywhere.xml", + convertPlaceholders, + projectIcuPlaceholdersEnabled, + ) + } + + private fun processFile() { + XmlResourcesProcessor(mockUtil.fileProcessorContext).process() + } +} diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/ComposeXmlFileExporterTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/ComposeXmlFileExporterTest.kt new file mode 100644 index 0000000000..32b630442b --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/ComposeXmlFileExporterTest.kt @@ -0,0 +1,327 @@ +package io.tolgee.unit.formats.compose.out + +import io.tolgee.dtos.request.export.ExportParams +import io.tolgee.formats.ExportFormat +import io.tolgee.formats.xmlResources.XML_RESOURCES_CDATA_CUSTOM_KEY +import io.tolgee.formats.xmlResources.out.XmlResourcesExporter +import io.tolgee.service.export.dataProvider.ExportTranslationView +import io.tolgee.testing.assert +import io.tolgee.util.buildExportTranslationList +import org.junit.jupiter.api.Test + +class ComposeXmlFileExporterTest { + @Test + fun exports() { + val exporter = getExporter() + val data = getExported(exporter) + // generate this with: + // data.map { "data.assertFile(\"${it.key}\", \"\"\"\n |${it.value.replace("\$", "\${'$'}").replace("\n", "\n |")}\n \"\"\".trimMargin())" }.joinToString("\n") + data.assertFile( + "values-cs/strings.xml", + """ + | + | + | Ahoj! I%d, %s, %e, %f + | I am just a percent \% sign! + | + | I am not just a percent %s %% sign! + | I am not just a percent %s]]> %% sign! + | Hey! sign!]]> + | + | + | + | + | + | + | + | + | %d den + | %d dny + | %d dní + | %d dní + | + | {count, plural, one {# den} few {# dny} other {# dní}} + | OK! + | I have exact key name + | + | + | I will be first + | I will be second + | + | + | + """.trimMargin(), + ) + data.assertFile( + "values-en/strings.xml", + """ + | + | + | This is english! + | + | %1${'$'}s dog + | %1${'$'}s dogs + | + | + | + """.trimMargin(), + ) + } + + @Test + fun `honors the provided fileStructureTemplate`() { + val exporter = + getExporter( + params = + getExportParams().also { + it.fileStructureTemplate = "{languageTag}/hello/{namespace}.{extension}" + }, + ) + + val files = exporter.produceFiles() + + files["cs/hello.xml"].assert.isNotNull() + } + + @Test + fun `exports with placeholders (ICU placeholders enabled)`() { + val exporter = getIcuPlaceholdersEnabledExporter() + val data = getExported(exporter) + data.assertFile( + "values-cs/strings.xml", + """ + | + | + | + | %d den %s + | %d dny + | %d dní + | %d dní + | + | + | I will be first {icuParam} + | + | + | + """.trimMargin(), + ) + } + + @Test + fun `exports with placeholders (ICU placeholders disabled)`() { + val exporter = getIcuPlaceholdersDisabledExporter() + val data = getExported(exporter) + data.assertFile( + "values-cs/strings.xml", + """ + | + | + | + | # den {icuParam} \' + | # dny + | # dní + | # dní + | + | + | I will be first {icuParam} \'{hey}\' + | + | + | + """.trimMargin(), + ) + } + + private fun getExported(exporter: XmlResourcesExporter): Map { + val files = exporter.produceFiles() + val data = files.map { it.key to it.value.bufferedReader().readText() }.toMap() + return data + } + + private fun Map.assertFile( + file: String, + content: String, + ) { + this[file]!!.assert.isEqualTo(content) + } + + private fun getExporter(params: ExportParams = getExportParams()): XmlResourcesExporter { + val built = + buildExportTranslationList { + add( + languageTag = "cs", + keyName = "key1", + text = + "Ahoj! I" + + "{number, number}, {name}, {number, number, scientific}, " + + "{number, number, 0.000000}", + ) + add( + languageTag = "cs", + keyName = "percent no placeholders", + text = + "I am just a percent % sign!", + ) + add( + languageTag = "cs", + keyName = "percent and placeholders", + text = "I am not just a percent {name} % sign!", + description = "This is a description", + ) + add( + languageTag = "cs", + keyName = "percent and placeholders and tags", + text = + "I am not just a percent {name} % sign!", + ) + add( + languageTag = "cs", + keyName = "forced CDATA", + text = + "Forced CDATA Hey! sign!", + fn = { + key.custom = mapOf(XML_RESOURCES_CDATA_CUSTOM_KEY to true) + }, + ) + add( + languageTag = "cs", + keyName = "Empty plural", + text = null, + ) { + key.isPlural = true + } + + add( + languageTag = "cs", + keyName = "key3", + text = "{count, plural, one {# den} few {# dny} other {# dní}}", + description = "This is a description above plural", + ) { + key.isPlural = true + } + add( + languageTag = "cs", + keyName = "forced_not plural", + text = "{count, plural, one {# den} few {# dny} other {# dní}}", + ) { + key.isPlural = false + } + + add( + languageTag = "cs", + keyName = "key!with_unsupported!characters", + text = "OK!", + ) + + add( + languageTag = "cs", + // this key will be replaced by key, which has exact key name + keyName = "unsupported_key_will_be!replaced", + text = "I will be missing!", + ) + + add( + languageTag = "cs", + keyName = "unsupported_key_will_be_replaced", + text = "I have exact key name", + ) + + add( + languageTag = "cs", + keyName = "unsupported_key_will_be~replaced", + text = "I will be missing too replace it!", + ) + + add( + languageTag = "cs", + keyName = "i_am_array_item[20]", + text = "I will be first", + description = "This is a description above array item", + ) + + add( + languageTag = "cs", + keyName = "i_am_array_item[100]", + text = "I will be second", + ) + + add( + languageTag = "cs", + keyName = "i_am_array!item[106]", + text = "I won't be added", + ) + + add( + languageTag = "cs", + keyName = "i_am_array~item[106]", + text = "I won't be added", + ) + add( + languageTag = "en", + keyName = "i_am_array_english", + text = "This is english!", + ) + add( + languageTag = "en", + keyName = "plural with placeholders", + text = "{count, plural, one {{0} dog} other {{0} dogs}}", + ) { + key.isPlural = true + } + } + return getExporter(built.translations, params = params) + } + + private fun getIcuPlaceholdersEnabledExporter(): XmlResourcesExporter { + val built = + buildExportTranslationList { + add( + languageTag = "cs", + keyName = "key3", + text = "{count, plural, one {# den {icuParam}} few {# dny} other {# dní}}", + ) { + key.isPlural = true + } + add( + languageTag = "cs", + keyName = "i_am_array_item[20]", + text = "I will be first '{'icuParam'}'", + ) + } + return getExporter(built.translations, true) + } + + private fun getIcuPlaceholdersDisabledExporter(): XmlResourcesExporter { + val built = + buildExportTranslationList { + add( + languageTag = "cs", + keyName = "key3", + text = "{count, plural, one {'#' den '{'icuParam'}' ''} few {'#' dny} other {'#' dní}}", + ) { + key.isPlural = true + } + add( + languageTag = "cs", + keyName = "i_am_array_item[20]", + text = "I will be first {icuParam} '{hey}'", + ) + } + return getExporter(built.translations, false) + } + + private fun getExporter( + translations: List, + isProjectIcuPlaceholdersEnabled: Boolean = true, + params: ExportParams = getExportParams(), + ): XmlResourcesExporter { + return XmlResourcesExporter( + translations = translations, + exportParams = params, + isProjectIcuPlaceholdersEnabled = isProjectIcuPlaceholdersEnabled, + ) + } + + private fun getExportParams(): ExportParams { + return ExportParams().also { it.format = ExportFormat.COMPOSE_XML } + } +} diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/TextToComposeXmlConvertorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/TextToComposeXmlConvertorTest.kt new file mode 100644 index 0000000000..5f41281a92 --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/formats/compose/out/TextToComposeXmlConvertorTest.kt @@ -0,0 +1,155 @@ +package io.tolgee.unit.formats.compose.out + +import io.tolgee.formats.ExportFormat +import io.tolgee.formats.xmlResources.XmlResourcesStringValue +import io.tolgee.formats.xmlResources.out.TextToXmlResourcesConvertor +import io.tolgee.testing.assert +import org.assertj.core.api.AbstractStringAssert +import org.junit.jupiter.api.Test +import org.w3c.dom.Document +import org.w3c.dom.Node +import javax.xml.parsers.DocumentBuilderFactory + +class TextToComposeXmlConvertorTest { + @Test + fun `xml and placeholders is converted to CDATA`() { + "%s".assertSingleCdataNodeText().isEqualTo("%s") + } + + @Test + fun `escaped newline is not double escaped text node`() { + "\\n \\n".assertSingleTextNode().isEqualTo("\\n \\n") + } + + @Test + fun `it's possible to escape escape char before newline`() { + "\\\\n \\\\n".assertSingleTextNode().isEqualTo("\\\\n \\\\n") + } + + @Test + fun `trailing spaces are handled`() { + "%s ".assertSingleTextNode().isEqualTo("%s ") + } + + @Test + fun `trailing percents are handled`() { + "%s %%".assertSingleTextNode().isEqualTo("%s %%") + } + + @Test + fun `unsupported tags are converted to CDATA nodes`() { + var nodes = + "What a link ' %% \" ." + .convertedNodes().toList() + nodes[0].assertTextContent("What a ") + nodes[1].nodeAssertCdataNodeText( + "link \' \\% \" " + + "", + ) + nodes[2].assertTextContent(".") + + nodes = + ( + "What a link ' %% %s \" " + + "." + ).convertedNodes().toList() + nodes[0].assertTextContent("What a ") + nodes[1].nodeAssertCdataNodeText( + "link \' %% %s \" " + + "", + ) + nodes[2].assertTextContent(".") + } + + @Test + fun `all possible spaces are preserved`() { + "a\n\t \u0020 \u2008 \u2003a".assertSingleTextNode("a\n\t \u0020 \u2008 \u2003a") + } + + @Test + fun `it doesn't re-escape UTF symbols`() { + "\\u0020\\u2008\\u2003".assertSingleTextNode("\\u0020\\u2008\\u2003") + } + + @Test + fun `converts capital U to lower`() { + "\\U0020".assertSingleTextNode("\\u0020") + } + + @Test + fun `percent signs are escaped`() { + "I am just a %% sign".assertSingleTextNode("I am just a \\% sign") + } + + @Test + fun `escapes in text nodes`() { + val nodes = "'\" \n\n \u0020\u2008\u2003".convertedNodes().toList() + nodes[0].textContent.assert.isEqualTo("'\" ") + nodes[2].textContent.assert.isEqualTo("\n\n \u0020\u2008\u2003") + } + + @Test + fun `wrapping with CDATA works for invalid XML`() { + val nodes = " a ".convertedNodes(isWrappedWithCdata = true) + nodes.getSingleNode().assertSingleCdataNodeText().isEqualTo(" a ") + } + + @Test + fun `new lines wrapped with text kept unchanged`() { + "a\n\na".assertSingleTextNode("a\n\na") + } + + private fun Node.assertTextContent(text: String) { + this.nodeType.assert.isEqualTo(Node.TEXT_NODE) + this.textContent.assert.isEqualTo(text) + } + + private fun String.assertSingleTextNode(text: String) { + this.assertSingleTextNode().isEqualTo(text) + } + + private fun String.assertSingleTextNode(): AbstractStringAssert<*> { + val nodes = this.convertedNodes() + return nodes.getSingleNode().textContent.assert + } + + private fun Collection.getSingleNode(): Node { + this.assert.hasSize(1) + return this.single() + } + + private fun Node.nodeAssertCdataNodeText(text: String) { + this.nodeType.assert.isEqualTo(Node.CDATA_SECTION_NODE) + this.textContent.assert.isEqualTo(text) + } + + private fun String.assertSingleCdataNodeText(): AbstractStringAssert<*> { + val node = this.convertedNodes().single() + return node.assertSingleCdataNodeText() + } + + private fun Node.assertSingleCdataNodeText(): AbstractStringAssert<*> { + this.nodeType.assert.isEqualTo(Node.CDATA_SECTION_NODE) + return this.textContent.assert + } + + private fun String.getConverted(isWrappedWithCdata: Boolean = false) = + TextToXmlResourcesConvertor( + document, + XmlResourcesStringValue(this, isWrappedWithCdata), + ExportFormat.COMPOSE_XML, + ).convert() + + private fun String.convertedNodes(isWrappedWithCdata: Boolean = false): Collection { + val result = this.getConverted(isWrappedWithCdata) + result.text.assert.isNull() + result.children.assert.isNotNull + return result.children!! + } + + private val document: Document by lazy { + val factory = DocumentBuilderFactory.newInstance() + val builder = factory.newDocumentBuilder() + builder.newDocument() + } +} diff --git a/backend/data/src/test/resources/import/composeMultiplatform/strings.xml b/backend/data/src/test/resources/import/composeMultiplatform/strings.xml index 6407734dcd..54ab1e2f22 100644 --- a/backend/data/src/test/resources/import/composeMultiplatform/strings.xml +++ b/backend/data/src/test/resources/import/composeMultiplatform/strings.xml @@ -29,6 +29,6 @@ Dont'translate this - %1$d %4$s %2$.2f %3$e %5$+d + %1$d %4$s %3$.2f %4$e %5$+d diff --git a/backend/data/src/test/resources/import/composeMultiplatform/strings_params_everywhere.xml b/backend/data/src/test/resources/import/composeMultiplatform/strings_params_everywhere.xml index 6b7c9f9ec5..c865ed01eb 100644 --- a/backend/data/src/test/resources/import/composeMultiplatform/strings_params_everywhere.xml +++ b/backend/data/src/test/resources/import/composeMultiplatform/strings_params_everywhere.xml @@ -8,6 +8,6 @@ Second item %1$d - %1$d %4$s %2$.2f %3$e %5$+d {escape} + %1$d %4$s %3$.2f %4$e %5$+d {escape}