From aec644daf353d4706699c346c6c5784f6272c469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Tue, 7 Jan 2025 17:08:50 +0100 Subject: [PATCH 1/6] feat: xlsx import and export --- backend/data/build.gradle | 6 ++ .../kotlin/io/tolgee/formats/ExportFormat.kt | 1 + .../formats/ImportFileProcessorFactory.kt | 2 + .../io/tolgee/formats/csv/in/CsvFileParser.kt | 48 +++---------- .../tolgee/formats/csv/in/CsvFileProcessor.kt | 41 ++---------- .../tolgee/formats/csv/out/CsvFileExporter.kt | 65 ++---------------- .../tolgee/formats/csv/out/CsvFileWriter.kt | 4 +- .../TableModel.kt} | 4 +- .../formats/genericTable/in/TableParser.kt | 49 ++++++++++++++ .../formats/genericTable/in/TableProcessor.kt | 43 ++++++++++++ .../formats/genericTable/out/TableExporter.kt | 67 +++++++++++++++++++ .../formats/importCommon/ImportFileFormat.kt | 1 + .../formats/importCommon/ImportFormat.kt | 21 ++++++ .../tolgee/formats/xlsx/in/XlsxFileParser.kt | 24 +++++++ .../formats/xlsx/in/XlsxFileProcessor.kt | 30 +++++++++ .../xlsx/in/XlsxImportFormatDetector.kt | 46 +++++++++++++ .../formats/xlsx/out/XlsxFileExporter.kt | 23 +++++++ .../tolgee/formats/xlsx/out/XlsxFileWriter.kt | 43 ++++++++++++ .../service/export/FileExporterFactory.kt | 4 ++ e2e/cypress/common/export.ts | 10 +++ webapp/src/svgs/logos/xlsx.svg | 3 + .../export/components/formatGroups.tsx | 13 ++++ .../component/ImportSupportedFormats.tsx | 2 + 23 files changed, 412 insertions(+), 138 deletions(-) rename backend/data/src/main/kotlin/io/tolgee/formats/{csv/CsvModel.kt => genericTable/TableModel.kt} (52%) create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableProcessor.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/genericTable/out/TableExporter.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileParser.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileProcessor.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxImportFormatDetector.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt create mode 100644 webapp/src/svgs/logos/xlsx.svg diff --git a/backend/data/build.gradle b/backend/data/build.gradle index 770dfe03a1..8933dee28b 100644 --- a/backend/data/build.gradle +++ b/backend/data/build.gradle @@ -168,7 +168,13 @@ dependencies { implementation libs.jacksonKotlin implementation("org.apache.commons:commons-configuration2:2.10.1") implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion" + + /** + * Table formats + */ implementation("com.opencsv:opencsv:5.9") + implementation 'org.apache.poi:poi:5.3.0' + implementation 'org.apache.poi:poi-ooxml:5.3.0' /** * Google translation API diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt b/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt index f707cb5444..9cc58dfdc5 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt @@ -42,4 +42,5 @@ enum class ExportFormat( JSON_I18NEXT("json", "application/json"), CSV("csv", "text/csv"), RESX_ICU("resx", "text/microsoft-resx"), + XLSX("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), } diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt b/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt index cb2b75b3bd..6d8a8ac74c 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt @@ -13,6 +13,7 @@ import io.tolgee.formats.po.`in`.PoFileProcessor import io.tolgee.formats.properties.`in`.PropertiesFileProcessor import io.tolgee.formats.resx.`in`.ResxProcessor import io.tolgee.formats.xliff.`in`.XliffFileProcessor +import io.tolgee.formats.xlsx.`in`.XlsxFileProcessor import io.tolgee.formats.xmlResources.`in`.XmlResourcesProcessor import io.tolgee.formats.yaml.`in`.YamlFileProcessor import io.tolgee.service.dataImport.processors.FileProcessorContext @@ -64,6 +65,7 @@ class ImportFileProcessorFactory( ImportFileFormat.YAML -> YamlFileProcessor(context, yamlObjectMapper) ImportFileFormat.CSV -> CsvFileProcessor(context) ImportFileFormat.RESX -> ResxProcessor(context) + ImportFileFormat.XLSX -> XlsxFileProcessor(context) } } diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileParser.kt b/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileParser.kt index 1a5de42bd2..c3ea360880 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileParser.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileParser.kt @@ -2,7 +2,8 @@ package io.tolgee.formats.csv.`in` import com.opencsv.CSVParserBuilder import com.opencsv.CSVReaderBuilder -import io.tolgee.formats.csv.CsvEntry +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.`in`.TableParser import java.io.InputStream class CsvFileParser( @@ -10,52 +11,19 @@ class CsvFileParser( private val delimiter: Char, private val languageFallback: String, ) { - val rawData: List> by lazy { + val rawData: List> by lazy { val inputReader = inputStream.reader() val parser = CSVParserBuilder().withSeparator(delimiter).build() val reader = CSVReaderBuilder(inputReader).withCSVParser(parser).build() - return@lazy reader.readAll() + return@lazy reader.readAll().map { it.toList() } } - val headers: Array? by lazy { - rawData.firstOrNull() + val tableParser: TableParser by lazy { + TableParser(rawData, languageFallback) } - val languages: List by lazy { - headers?.takeIf { it.size > 1 }?.drop(1) ?: emptyList() - } - - val languagesWithFallback: Sequence - get() = languages.asSequence().plus(generateSequence { languageFallback }) - - val rows: List> by lazy { - rawData.takeIf { it.size > 1 }?.drop(1) ?: emptyList() - } - - fun Array.rowToCsvEntries(): Sequence { - if (isEmpty()) { - return emptySequence() - } - val keyName = getOrNull(0) ?: "" - if (size == 1) { - return sequenceOf(CsvEntry(keyName, languageFallback, null)) - } - val translations = drop(1).asSequence() - return translations - .zip(languagesWithFallback) - .map { (translation, languageTag) -> - CsvEntry( - keyName, - languageTag, - translation, - ) - } - } - - fun parse(): List { - return rows.flatMap { - it.rowToCsvEntries() - } + fun parse(): List { + return tableParser.parse() } } diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt b/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt index 3f251a556f..b8c41a1fe3 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt @@ -1,46 +1,15 @@ package io.tolgee.formats.csv.`in` import io.tolgee.exceptions.ImportCannotParseFileException -import io.tolgee.formats.ImportFileProcessor -import io.tolgee.formats.csv.CsvEntry +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.`in`.TableProcessor import io.tolgee.formats.importCommon.ImportFormat import io.tolgee.service.dataImport.processors.FileProcessorContext class CsvFileProcessor( override val context: FileProcessorContext, -) : ImportFileProcessor() { - override fun process() { - val (data, format) = parse() - data.importAll(format) - } - - fun Iterable.importAll(format: ImportFormat) { - forEachIndexed { idx, it -> it.import(idx, format) } - } - - fun CsvEntry.import( - index: Int, - format: ImportFormat, - ) { - val converted = - format.messageConvertor.convert( - value, - language, - convertPlaceholders = context.importSettings.convertPlaceholdersToIcu, - isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled, - ) - context.addTranslation( - key, - language, - converted.message, - index, - pluralArgName = converted.pluralArgName, - rawData = value, - convertedBy = format, - ) - } - - private fun parse(): Pair, ImportFormat> { +) : TableProcessor(context) { + override fun parse(): Pair, ImportFormat> { try { val detector = CsvDelimiterDetector(context.file.data.inputStream()) val parser = @@ -50,7 +19,7 @@ class CsvFileProcessor( languageFallback = firstLanguageTagGuessOrUnknown, ) val data = parser.parse() - val format = getFormat(parser.rows) + val format = getFormat(parser.tableParser.rows) return data to format } catch (e: Exception) { throw ImportCannotParseFileException(context.file.name, e.message ?: "", e) diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileExporter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileExporter.kt index 86bbd2deb9..cde1944631 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileExporter.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileExporter.kt @@ -1,68 +1,17 @@ package io.tolgee.formats.csv.out import io.tolgee.dtos.IExportParams -import io.tolgee.formats.ExportMessageFormat -import io.tolgee.formats.csv.CsvEntry -import io.tolgee.formats.generic.IcuToGenericFormatMessageConvertor -import io.tolgee.service.export.ExportFilePathProvider +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.out.TableExporter import io.tolgee.service.export.dataProvider.ExportTranslationView -import io.tolgee.service.export.exporters.FileExporter import java.io.InputStream class CsvFileExporter( - val translations: List, - val exportParams: IExportParams, - private val isProjectIcuPlaceholdersEnabled: Boolean = true, -) : FileExporter { - private val pathProvider by lazy { - ExportFilePathProvider( - exportParams, - "csv", - ) - } - - private val messageFormat - get() = exportParams.messageFormat ?: ExportMessageFormat.ICU - - private val placeholderConvertorFactory - get() = messageFormat.paramConvertorFactory - - val entries = - translations.map { - val converted = convertMessage(it.text, it.key.isPlural) - val path = - pathProvider.getFilePath(it.key.namespace) - val entry = - CsvEntry( - key = it.key.name, - language = it.languageTag, - value = converted, - ) - path to entry - }.groupBy({ it.first }, { it.second }) - - private fun convertMessage( - text: String?, - isPlural: Boolean, - ): String? { - return getMessageConvertor(text, isPlural).convert() - } - - private fun getMessageConvertor( - text: String?, - isPlural: Boolean, - ) = IcuToGenericFormatMessageConvertor( - text, - isPlural, - isProjectIcuPlaceholdersEnabled = isProjectIcuPlaceholdersEnabled, - paramConvertorFactory = placeholderConvertorFactory, - ) - - override fun produceFiles(): Map { - return entries.mapValues { (_, entry) -> entry.toCsv() } - } - - private fun List.toCsv(): InputStream { + translations: List, + exportParams: IExportParams, + isProjectIcuPlaceholdersEnabled: Boolean = true, +) : TableExporter(translations, exportParams, "csv", isProjectIcuPlaceholdersEnabled) { + override fun List.toFileContents(): InputStream { val languageTags = exportParams.languages?.sorted()?.toTypedArray() ?: this.map { it.language }.distinct().sorted().toTypedArray() diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileWriter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileWriter.kt index 01fce234fd..3b453f08f7 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileWriter.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/csv/out/CsvFileWriter.kt @@ -1,13 +1,13 @@ package io.tolgee.formats.csv.out import com.opencsv.CSVWriterBuilder -import io.tolgee.formats.csv.CsvEntry +import io.tolgee.formats.genericTable.TableEntry import java.io.InputStream import java.io.StringWriter class CsvFileWriter( private val languageTags: Array, - private val data: List, + private val data: List, private val delimiter: Char, ) { val translations: Map> by lazy { diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/csv/CsvModel.kt b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/TableModel.kt similarity index 52% rename from backend/data/src/main/kotlin/io/tolgee/formats/csv/CsvModel.kt rename to backend/data/src/main/kotlin/io/tolgee/formats/genericTable/TableModel.kt index 5231a56d82..b85b088948 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/csv/CsvModel.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/TableModel.kt @@ -1,6 +1,6 @@ -package io.tolgee.formats.csv +package io.tolgee.formats.genericTable -data class CsvEntry( +data class TableEntry( val key: String, val language: String, val value: String?, diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt new file mode 100644 index 0000000000..ff5c23bb82 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt @@ -0,0 +1,49 @@ +package io.tolgee.formats.genericTable.`in` + +import io.tolgee.formats.genericTable.TableEntry + +class TableParser( + private val rawData: List>, + private val languageFallback: String, +) { + val headers: List? by lazy { + rawData.firstOrNull() + } + + val languages: List by lazy { + headers?.takeIf { it.size > 1 }?.drop(1) ?: emptyList() + } + + val languagesWithFallback: Sequence + get() = languages.asSequence().plus(generateSequence { languageFallback }) + + val rows: List> by lazy { + rawData.takeIf { it.size > 1 }?.drop(1) ?: emptyList() + } + + fun List.rowToTableEntries(): Sequence { + if (isEmpty()) { + return emptySequence() + } + val keyName = getOrNull(0) ?: "" + if (size == 1) { + return sequenceOf(TableEntry(keyName, languageFallback, null)) + } + val translations = drop(1).asSequence() + return translations + .zip(languagesWithFallback) + .map { (translation, languageTag) -> + TableEntry( + keyName, + languageTag, + translation, + ) + } + } + + fun parse(): List { + return rows.flatMap { + it.rowToTableEntries() + } + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableProcessor.kt b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableProcessor.kt new file mode 100644 index 0000000000..bb707928db --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableProcessor.kt @@ -0,0 +1,43 @@ +package io.tolgee.formats.genericTable.`in` + +import io.tolgee.formats.ImportFileProcessor +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.importCommon.ImportFormat +import io.tolgee.service.dataImport.processors.FileProcessorContext + +abstract class TableProcessor( + override val context: FileProcessorContext, +) : ImportFileProcessor() { + override fun process() { + val (data, format) = parse() + data.importAll(format) + } + + fun Iterable.importAll(format: ImportFormat) { + forEachIndexed { idx, it -> it.import(idx, format) } + } + + fun TableEntry.import( + index: Int, + format: ImportFormat, + ) { + val converted = + format.messageConvertor.convert( + value, + language, + convertPlaceholders = context.importSettings.convertPlaceholdersToIcu, + isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled, + ) + context.addTranslation( + key, + language, + converted.message, + index, + pluralArgName = converted.pluralArgName, + rawData = value, + convertedBy = format, + ) + } + + protected abstract fun parse(): Pair, ImportFormat> +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/out/TableExporter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/out/TableExporter.kt new file mode 100644 index 0000000000..f91b668fba --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/genericTable/out/TableExporter.kt @@ -0,0 +1,67 @@ +package io.tolgee.formats.genericTable.out + +import io.tolgee.dtos.IExportParams +import io.tolgee.formats.ExportMessageFormat +import io.tolgee.formats.generic.IcuToGenericFormatMessageConvertor +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.service.export.ExportFilePathProvider +import io.tolgee.service.export.dataProvider.ExportTranslationView +import io.tolgee.service.export.exporters.FileExporter +import java.io.InputStream + +abstract class TableExporter( + val translations: List, + val exportParams: IExportParams, + val fileExtension: String, + val isProjectIcuPlaceholdersEnabled: Boolean = true, +) : FileExporter { + val pathProvider by lazy { + ExportFilePathProvider( + exportParams, + fileExtension, + ) + } + + val messageFormat + get() = exportParams.messageFormat ?: ExportMessageFormat.ICU + + val placeholderConvertorFactory + get() = messageFormat.paramConvertorFactory + + val entries = + translations.map { + val converted = convertMessage(it.text, it.key.isPlural) + val path = + pathProvider.getFilePath(it.key.namespace) + val entry = + TableEntry( + key = it.key.name, + language = it.languageTag, + value = converted, + ) + path to entry + }.groupBy({ it.first }, { it.second }) + + fun convertMessage( + text: String?, + isPlural: Boolean, + ): String? { + return getMessageConvertor(text, isPlural).convert() + } + + fun getMessageConvertor( + text: String?, + isPlural: Boolean, + ) = IcuToGenericFormatMessageConvertor( + text, + isPlural, + isProjectIcuPlaceholdersEnabled = isProjectIcuPlaceholdersEnabled, + paramConvertorFactory = placeholderConvertorFactory, + ) + + override fun produceFiles(): Map { + return entries.mapValues { (_, entry) -> entry.toFileContents() } + } + + abstract fun List.toFileContents(): InputStream +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFileFormat.kt b/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFileFormat.kt index f7922b305e..4e59bead4e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFileFormat.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFileFormat.kt @@ -12,6 +12,7 @@ enum class ImportFileFormat(val extensions: Array) { YAML(arrayOf("yaml", "yml")), CSV(arrayOf("csv")), RESX(arrayOf("resx")), + XLSX(arrayOf("xls", "xlsx")), ; companion object { diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFormat.kt b/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFormat.kt index 9ff5fb5ff5..cf3d13d8ed 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFormat.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/importCommon/ImportFormat.kt @@ -225,6 +225,27 @@ enum class ImportFormat( ), ), + XLSX_ICU( + ImportFileFormat.XLSX, + messageConvertorOrNull = + GenericMapPluralImportRawDataConvertor( + canContainIcu = true, + toIcuPlaceholderConvertorFactory = null, + ), + ), + XLSX_JAVA( + ImportFileFormat.XLSX, + messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { JavaToIcuPlaceholderConvertor() }, + ), + XLSX_PHP( + ImportFileFormat.XLSX, + messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { PhpToIcuPlaceholderConvertor() }, + ), + XLSX_RUBY( + ImportFileFormat.XLSX, + messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { RubyToIcuPlaceholderConvertor() }, + ), + ; val messageConvertor: ImportMessageConvertor diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileParser.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileParser.kt new file mode 100644 index 0000000000..ddc260a9fc --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileParser.kt @@ -0,0 +1,24 @@ +package io.tolgee.formats.xlsx.`in` + +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.`in`.TableParser +import org.apache.poi.ss.usermodel.WorkbookFactory +import java.io.InputStream + +class XlsxFileParser( + private val inputStream: InputStream, + private val languageFallback: String, +) { + val rawData: List>> by lazy { + val workbook = WorkbookFactory.create(inputStream) + return@lazy workbook.sheetIterator().asSequence().map { + it.map { it.map { it.stringCellValue } } + }.toList() + } + + fun parse(): List { + return rawData.flatMap { + TableParser(it, languageFallback).parse() + } + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileProcessor.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileProcessor.kt new file mode 100644 index 0000000000..c91d2a7d5e --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxFileProcessor.kt @@ -0,0 +1,30 @@ +package io.tolgee.formats.xlsx.`in` + +import io.tolgee.exceptions.ImportCannotParseFileException +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.`in`.TableProcessor +import io.tolgee.formats.importCommon.ImportFormat +import io.tolgee.service.dataImport.processors.FileProcessorContext + +class XlsxFileProcessor( + override val context: FileProcessorContext, +) : TableProcessor(context) { + override fun parse(): Pair, ImportFormat> { + try { + val parser = + XlsxFileParser( + inputStream = context.file.data.inputStream(), + languageFallback = firstLanguageTagGuessOrUnknown, + ) + val data = parser.parse() + val format = getFormat(parser.rawData) + return data to format + } catch (e: Exception) { + throw ImportCannotParseFileException(context.file.name, e.message ?: "", e) + } + } + + private fun getFormat(data: Any?): ImportFormat { + return context.mapping?.format ?: XlsxImportFormatDetector().detectFormat(data) + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxImportFormatDetector.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxImportFormatDetector.kt new file mode 100644 index 0000000000..5c48cd395f --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/in/XlsxImportFormatDetector.kt @@ -0,0 +1,46 @@ +package io.tolgee.formats.xlsx.`in` + +import io.tolgee.formats.genericStructuredFile.`in`.FormatDetectionUtil +import io.tolgee.formats.genericStructuredFile.`in`.FormatDetectionUtil.ICU_DETECTION_REGEX +import io.tolgee.formats.genericStructuredFile.`in`.FormatDetectionUtil.detectFromPossibleFormats +import io.tolgee.formats.importCommon.ImportFormat +import io.tolgee.formats.paramConvertors.`in`.JavaToIcuPlaceholderConvertor +import io.tolgee.formats.paramConvertors.`in`.PhpToIcuPlaceholderConvertor +import io.tolgee.formats.paramConvertors.`in`.RubyToIcuPlaceholderConvertor + +class XlsxImportFormatDetector { + companion object { + private val possibleFormats = + mapOf( + ImportFormat.XLSX_ICU to + arrayOf( + FormatDetectionUtil.regexFactor( + ICU_DETECTION_REGEX, + ), + ), + ImportFormat.XLSX_PHP to + arrayOf( + FormatDetectionUtil.regexFactor( + PhpToIcuPlaceholderConvertor.PHP_DETECTION_REGEX, + ), + ), + ImportFormat.XLSX_JAVA to + arrayOf( + FormatDetectionUtil.regexFactor( + JavaToIcuPlaceholderConvertor.JAVA_DETECTION_REGEX, + ), + ), + ImportFormat.XLSX_RUBY to + arrayOf( + FormatDetectionUtil.regexFactor( + RubyToIcuPlaceholderConvertor.RUBY_DETECTION_REGEX, + 0.95, + ), + ), + ) + } + + fun detectFormat(data: Any?): ImportFormat { + return detectFromPossibleFormats(possibleFormats, data) ?: ImportFormat.XLSX_ICU + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt new file mode 100644 index 0000000000..ee97fd708b --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt @@ -0,0 +1,23 @@ +package io.tolgee.formats.xlsx.out + +import io.tolgee.dtos.IExportParams +import io.tolgee.formats.genericTable.TableEntry +import io.tolgee.formats.genericTable.out.TableExporter +import io.tolgee.service.export.dataProvider.ExportTranslationView +import java.io.InputStream + +class XlsxFileExporter( + translations: List, + exportParams: IExportParams, + isProjectIcuPlaceholdersEnabled: Boolean = true, +) : TableExporter(translations, exportParams, "xlsx", isProjectIcuPlaceholdersEnabled) { + override fun List.toFileContents(): InputStream { + val languageTags = + exportParams.languages?.sorted()?.toTypedArray() + ?: this.map { it.language }.distinct().sorted().toTypedArray() + return XlsxFileWriter( + languageTags = languageTags, + data = this, + ).produceFiles() + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt new file mode 100644 index 0000000000..cb059009f1 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt @@ -0,0 +1,43 @@ +package io.tolgee.formats.xlsx.out + +import io.tolgee.formats.genericTable.TableEntry +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class XlsxFileWriter( + private val languageTags: Array, + private val data: List, +) { + val translations: Map> by lazy { + data.groupBy { it.key }.mapValues { (_, values) -> + values.associate { it.language to it.value } + } + } + + fun produceFiles(): InputStream { + val output = ByteArrayOutputStream() + val workbook = XSSFWorkbook() + val sheet = workbook.createSheet() + + val header = sheet.createRow(0) + (listOf("key") + languageTags).forEachIndexed { i, v -> + header.createCell(i).setCellValue(v) + } + + translations.entries.forEachIndexed { i, it -> + val row = sheet.createRow(i + 1) + ( + listOf(it.key) + + languageTags.map { languageTag -> + it.value.getOrDefault(languageTag, null) ?: "" + } + ).forEachIndexed { i, v -> + row.createCell(i).setCellValue(v) + } + } + + workbook.write(output) + return output.toByteArray().inputStream() + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt b/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt index 1ee64b3b2c..9b4be478fb 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt @@ -14,6 +14,7 @@ import io.tolgee.formats.po.out.PoFileExporter import io.tolgee.formats.properties.out.PropertiesFileExporter import io.tolgee.formats.resx.out.ResxExporter import io.tolgee.formats.xliff.out.XliffFileExporter +import io.tolgee.formats.xlsx.out.XlsxFileExporter import io.tolgee.formats.xmlResources.out.XmlResourcesExporter import io.tolgee.formats.yaml.out.YamlFileExporter import io.tolgee.service.export.dataProvider.ExportTranslationView @@ -109,6 +110,9 @@ class FileExporterFactory( ExportFormat.RESX_ICU -> ResxExporter(data, exportParams, projectIcuPlaceholdersSupport) + + ExportFormat.XLSX -> + XlsxFileExporter(data, exportParams, projectIcuPlaceholdersSupport) } } } diff --git a/e2e/cypress/common/export.ts b/e2e/cypress/common/export.ts index 8365099bb3..22f9755cf4 100644 --- a/e2e/cypress/common/export.ts +++ b/e2e/cypress/common/export.ts @@ -241,6 +241,16 @@ export const testExportFormats = ( }, } ); + + testFormatWithMessageFormats( + ['ICU', 'PHP Sprintf', 'C Sprintf', 'Ruby Sprintf', 'Java String.format'], + { + format: 'XLSX', + expectedParams: { + format: 'XLSX', + }, + } + ); }; const testFormat = ( diff --git a/webapp/src/svgs/logos/xlsx.svg b/webapp/src/svgs/logos/xlsx.svg new file mode 100644 index 0000000000..64559c916b --- /dev/null +++ b/webapp/src/svgs/logos/xlsx.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/src/views/projects/export/components/formatGroups.tsx b/webapp/src/views/projects/export/components/formatGroups.tsx index 9cc271f854..a5dbe5699e 100644 --- a/webapp/src/views/projects/export/components/formatGroups.tsx +++ b/webapp/src/views/projects/export/components/formatGroups.tsx @@ -156,6 +156,19 @@ export const formatGroups: FormatGroup[] = [ 'RUBY_SPRINTF', ], }, + { + id: 'generic_xlsx', + extension: 'xlsx', + name: , + format: 'XLSX', + supportedMessageFormats: [ + 'ICU', + 'JAVA_STRING_FORMAT', + 'PHP_SPRINTF', + 'C_SPRINTF', + 'RUBY_SPRINTF', + ], + }, ], }, { diff --git a/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx b/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx index 8aa0c9fcd2..e398c68acb 100644 --- a/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx +++ b/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx @@ -14,6 +14,7 @@ import RailsLogo from 'tg.svgs/logos/rails.svg?react'; import I18nextLogo from 'tg.svgs/logos/i18next.svg?react'; import CsvLogo from 'tg.svgs/logos/csv.svg?react'; import DotnetLogo from 'tg.svgs/logos/dotnet.svg?react'; +import XlsxLogo from 'tg.svgs/logos/xlsx.svg?react'; const TechLogo = ({ svg, @@ -61,6 +62,7 @@ const FORMATS = [ { name: 'i18next', logo: }, { name: 'CSV', logo: }, { name: '.NET RESX', logo: }, + { name: 'XLSX', logo: }, ]; export const ImportSupportedFormats = () => { From 168e6abd837d57d2b270bc8063b1ef4b756258ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Fri, 10 Jan 2025 17:23:24 +0100 Subject: [PATCH 2/6] fix: tests + fixes --- .../formats/xlsx/out/XlsxFileExporter.kt | 3 + .../tolgee/formats/xlsx/out/XlsxFileWriter.kt | 4 + .../service/export/FileExporterFactory.kt | 9 +- .../csv/in/CsvImportFormatDetectorTest.kt | 2 +- .../io/tolgee/unit/util/exportAssertUtil.kt | 20 + .../unit/xlsx/in/XlsxFormatProcessorTest.kt | 432 ++++++++++++++++++ .../xlsx/in/XlsxImportFormatDetectorTest.kt | 57 +++ .../unit/xlsx/out/XlsxFileExporterTest.kt | 251 ++++++++++ .../test/resources/import/xlsx/example.xlsx | Bin 0 -> 10728 bytes .../resources/import/xlsx/example_params.xlsx | Bin 0 -> 10321 bytes .../import/xlsx/example_semicolon.xlsx | Bin 0 -> 9701 bytes .../resources/import/xlsx/example_tab.xlsx | Bin 0 -> 9707 bytes .../src/test/resources/import/xlsx/icu.xlsx | Bin 0 -> 10160 bytes .../src/test/resources/import/xlsx/java.xlsx | Bin 0 -> 10200 bytes .../src/test/resources/import/xlsx/php.xlsx | Bin 0 -> 10165 bytes .../import/xlsx/placeholder_conversion.xlsx | Bin 0 -> 10274 bytes .../test/resources/import/xlsx/simple.xlsx | Bin 0 -> 10226 bytes .../test/resources/import/xlsx/unknown.xlsx | Bin 0 -> 10177 bytes 18 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxFormatProcessorTest.kt create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxImportFormatDetectorTest.kt create mode 100644 backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt create mode 100644 backend/data/src/test/resources/import/xlsx/example.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/example_params.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/example_semicolon.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/example_tab.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/icu.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/java.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/php.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/placeholder_conversion.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/simple.xlsx create mode 100644 backend/data/src/test/resources/import/xlsx/unknown.xlsx diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt index ee97fd708b..0ad1231e9c 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileExporter.kt @@ -5,8 +5,10 @@ import io.tolgee.formats.genericTable.TableEntry import io.tolgee.formats.genericTable.out.TableExporter import io.tolgee.service.export.dataProvider.ExportTranslationView import java.io.InputStream +import java.util.Date class XlsxFileExporter( + val currentDate: Date, translations: List, exportParams: IExportParams, isProjectIcuPlaceholdersEnabled: Boolean = true, @@ -16,6 +18,7 @@ class XlsxFileExporter( exportParams.languages?.sorted()?.toTypedArray() ?: this.map { it.language }.distinct().sorted().toTypedArray() return XlsxFileWriter( + createdDate = currentDate, languageTags = languageTags, data = this, ).produceFiles() diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt index cb059009f1..44ee26cd88 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/xlsx/out/XlsxFileWriter.kt @@ -4,8 +4,11 @@ import io.tolgee.formats.genericTable.TableEntry import org.apache.poi.xssf.usermodel.XSSFWorkbook import java.io.ByteArrayOutputStream import java.io.InputStream +import java.util.Date +import java.util.Optional class XlsxFileWriter( + private val createdDate: Date, private val languageTags: Array, private val data: List, ) { @@ -18,6 +21,7 @@ class XlsxFileWriter( fun produceFiles(): InputStream { val output = ByteArrayOutputStream() val workbook = XSSFWorkbook() + workbook.properties.coreProperties.setCreated(Optional.of(createdDate)) val sheet = workbook.createSheet() val header = sheet.createRow(0) diff --git a/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt b/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt index 9b4be478fb..0411786431 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/export/FileExporterFactory.kt @@ -1,6 +1,7 @@ package io.tolgee.service.export import com.fasterxml.jackson.databind.ObjectMapper +import io.tolgee.component.CurrentDateProvider import io.tolgee.dtos.IExportParams import io.tolgee.dtos.cacheable.LanguageDto import io.tolgee.formats.ExportFormat @@ -28,6 +29,7 @@ class FileExporterFactory( @Qualifier("yamlObjectMapper") private val yamlObjectMapper: ObjectMapper, private val customPrettyPrinter: CustomPrettyPrinter, + private val currentDateProvider: CurrentDateProvider, ) { fun create( data: List, @@ -112,7 +114,12 @@ class FileExporterFactory( ResxExporter(data, exportParams, projectIcuPlaceholdersSupport) ExportFormat.XLSX -> - XlsxFileExporter(data, exportParams, projectIcuPlaceholdersSupport) + XlsxFileExporter( + currentDateProvider.date, + data, + exportParams, + projectIcuPlaceholdersSupport, + ) } } } diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/formats/csv/in/CsvImportFormatDetectorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/formats/csv/in/CsvImportFormatDetectorTest.kt index b83cda08e7..c31ae386a2 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/formats/csv/in/CsvImportFormatDetectorTest.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/formats/csv/in/CsvImportFormatDetectorTest.kt @@ -49,7 +49,7 @@ class CsvImportFormatDetectorTest { delimiter = ',', languageFallback = "unknown", ) - return parser.rows + return parser.tableParser.rows } private fun String.assertDetected(format: ImportFormat) { diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt b/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt index 136bcf8984..503c9950d8 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt @@ -2,6 +2,7 @@ package io.tolgee.unit.util import io.tolgee.service.export.exporters.FileExporter import io.tolgee.testing.assert +import java.util.zip.ZipInputStream fun Map.assertFile( file: String, @@ -15,3 +16,22 @@ fun getExported(exporter: FileExporter): Map { val data = files.map { it.key to it.value.bufferedReader().readText() }.toMap() return data } + +fun getExportedCompressed(exporter: FileExporter): Map { + val files = exporter.produceFiles() + val data = files.map { + it.key to buildString { + val stream = ZipInputStream(it.value) + var entry = stream.nextEntry + while (entry != null) { + appendLine("====================") + appendLine(entry.name) + appendLine("--------------------") + append(stream.bufferedReader().readText()) + appendLine() + entry = stream.nextEntry + } + } + }.toMap() + return data +} diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxFormatProcessorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxFormatProcessorTest.kt new file mode 100644 index 0000000000..fae6b60b02 --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxFormatProcessorTest.kt @@ -0,0 +1,432 @@ +package io.tolgee.unit.xlsx.`in` + +import io.tolgee.formats.xlsx.`in`.XlsxFileProcessor +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 XlsxFormatProcessorTest { + lateinit var mockUtil: FileProcessorContextMockUtil + + @BeforeEach + fun setup() { + mockUtil = FileProcessorContextMockUtil() + } + + // This is how to generate the test: + // 1. run the test in debug mode + // 2. copy the result of calling: + // io.tolgee.unit.util.generateTestsForImportResult(mockUtil.fileProcessorContext) + // from the debug window + @Test + fun `returns correct parsed result`() { + mockUtil.mockIt("example.xlsx", "src/test/resources/import/xlsx/example.xlsx") + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyDeep.inner") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyDeep.inner") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolate") + .assertSingle { + hasText("replace this {value}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolate") + .assertSingle { + hasText("nahradit toto {value}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolateWithFormatting") + .assertSingle { + hasText("replace this {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolateWithFormatting") + .assertSingle { + hasText("nahradit toto {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { the singular} + other { the plural {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { jednotné číslo} + other { množné číslo {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("en", "escapedCharacters") + .assertSingle { + hasText("this is a \"quote\"") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "escapedCharacters") + .assertSingle { + hasText("toto je \"citace\"") + } + mockUtil.fileProcessorContext.assertTranslations("en", "escapedCharacters2") + .assertSingle { + hasText("this is a\nnew line") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "escapedCharacters2") + .assertSingle { + hasText("toto je\nnový řádek") + } + mockUtil.fileProcessorContext.assertTranslations("en", "escapedCharacters3") + .assertSingle { + hasText("this is a \\ backslash") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "escapedCharacters3") + .assertSingle { + hasText("toto je zpětné \\ lomítko") + } + mockUtil.fileProcessorContext.assertTranslations("en", "escapedCharacters4") + .assertSingle { + hasText("this is a , comma") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "escapedCharacters4") + .assertSingle { + hasText("toto je , čárka") + } + mockUtil.fileProcessorContext.assertKey("keyPluralSimple") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `returns correct parsed result (semicolon delimiter)`() { + mockUtil.mockIt("example.xlsx", "src/test/resources/import/xlsx/example_semicolon.xlsx") + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyDeep.inner") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyDeep.inner") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolate") + .assertSingle { + hasText("replace this {value}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolate") + .assertSingle { + hasText("nahradit toto {value}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolateWithFormatting") + .assertSingle { + hasText("replace this {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolateWithFormatting") + .assertSingle { + hasText("nahradit toto {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { the singular} + other { the plural {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { jednotné číslo} + other { množné číslo {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertKey("keyPluralSimple") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `returns correct parsed result (tab delimiter)`() { + mockUtil.mockIt("example.xlsx", "src/test/resources/import/xlsx/example_tab.xlsx") + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyDeep.inner") + .assertSingle { + hasText("value") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyDeep.inner") + .assertSingle { + hasText("hodnota") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolate") + .assertSingle { + hasText("replace this {value}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolate") + .assertSingle { + hasText("nahradit toto {value}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyInterpolateWithFormatting") + .assertSingle { + hasText("replace this {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyInterpolateWithFormatting") + .assertSingle { + hasText("nahradit toto {value, number}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { the singular} + other { the plural {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "keyPluralSimple") + .assertSinglePlural { + hasText( + """ + {value, plural, + one { jednotné číslo} + other { množné číslo {value}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertKey("keyPluralSimple") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `import with placeholder conversion (disabled ICU)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = false, projectIcuPlaceholdersEnabled = false) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("Hello {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("Ahoj {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Hello one '#' '{'icuParam'}'} + other {Hello other '{'icuParam'}'} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Ahoj jedno '#' '{'icuParam'}'} + other {Ahoj jiné '{'icuParam'}'} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertKey("plural") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `import with placeholder conversion (no conversion)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = false, projectIcuPlaceholdersEnabled = true) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("Hello {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("Ahoj {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Hello one # {icuParam}} + other {Hello other {icuParam}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Ahoj jedno # {icuParam}} + other {Ahoj jiné {icuParam}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertKey("plural") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `import with placeholder conversion (with conversion)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = true, projectIcuPlaceholdersEnabled = true) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) + mockUtil.fileProcessorContext.assertTranslations("en", "key") + .assertSingle { + hasText("Hello {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("cs", "key") + .assertSingle { + hasText("Ahoj {icuPara}") + } + mockUtil.fileProcessorContext.assertTranslations("en", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Hello one # {icuParam}} + other {Hello other {icuParam}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertTranslations("cs", "plural") + .assertSinglePlural { + hasText( + """ + {icuPara, plural, + one {Ahoj jedno # {icuParam}} + other {Ahoj jiné {icuParam}} + } + """.trimIndent(), + ) + isPluralOptimized() + } + mockUtil.fileProcessorContext.assertKey("plural") { + custom.assert.isNull() + description.assert.isNull() + } + } + + @Test + fun `placeholder conversion setting application works`() { + PlaceholderConversionTestHelper.testFile( + "import.xlsx", + "src/test/resources/import/xlsx/placeholder_conversion.xlsx", + assertBeforeSettingsApplication = + listOf( + "this is xlsx {0, number}", + "this is xlsx", + "toto je xlsx {0, number}", + "toto je xlsx", + ), + assertAfterDisablingConversion = + listOf( + "this is xlsx %d", + "toto je xlsx %d", + ), + assertAfterReEnablingConversion = + listOf( + "this is xlsx {0, number}", + "toto je xlsx {0, number}", + ), + ) + } + + private fun mockPlaceholderConversionTestFile( + convertPlaceholders: Boolean, + projectIcuPlaceholdersEnabled: Boolean, + ) { + mockUtil.mockIt( + "import.xlsx", + "src/test/resources/import/xlsx/example_params.xlsx", + convertPlaceholders, + projectIcuPlaceholdersEnabled, + ) + } + + private fun processFile() { + XlsxFileProcessor(mockUtil.fileProcessorContext).process() + } +} diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxImportFormatDetectorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxImportFormatDetectorTest.kt new file mode 100644 index 0000000000..5765e76c10 --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/in/XlsxImportFormatDetectorTest.kt @@ -0,0 +1,57 @@ +package io.tolgee.unit.xlsx.`in` + +import io.tolgee.formats.importCommon.ImportFormat +import io.tolgee.formats.xlsx.`in`.XlsxFileParser +import io.tolgee.formats.xlsx.`in`.XlsxImportFormatDetector +import io.tolgee.testing.assert +import io.tolgee.util.FileProcessorContextMockUtil +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.File + +class XlsxImportFormatDetectorTest { + lateinit var mockUtil: FileProcessorContextMockUtil + + @BeforeEach + fun setup() { + mockUtil = FileProcessorContextMockUtil() + } + + @Test + fun `detected i18next`() { + "src/test/resources/import/xlsx/example.xlsx".assertDetected(ImportFormat.XLSX_ICU) + } + + @Test + fun `detected icu`() { + "src/test/resources/import/xlsx/icu.xlsx".assertDetected(ImportFormat.XLSX_ICU) + } + + @Test + fun `detected java`() { + "src/test/resources/import/xlsx/java.xlsx".assertDetected(ImportFormat.XLSX_JAVA) + } + + @Test + fun `detected php`() { + "src/test/resources/import/xlsx/php.xlsx".assertDetected(ImportFormat.XLSX_PHP) + } + + @Test + fun `fallbacks to icu`() { + "src/test/resources/import/xlsx/unknown.xlsx".assertDetected(ImportFormat.XLSX_ICU) + } + + private fun parseFile(path: String): Any? { + val parser = + XlsxFileParser( + inputStream = File(path).inputStream(), + languageFallback = "unknown", + ) + return parser.rawData + } + + private fun String.assertDetected(format: ImportFormat) { + XlsxImportFormatDetector().detectFormat(parseFile(this)).assert.isEqualTo(format) + } +} diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt new file mode 100644 index 0000000000..fedc641b80 --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt @@ -0,0 +1,251 @@ +package io.tolgee.unit.xlsx.out + +import io.tolgee.component.CurrentDateProvider +import io.tolgee.dtos.request.export.ExportParams +import io.tolgee.formats.xlsx.out.XlsxFileExporter +import io.tolgee.service.export.dataProvider.ExportTranslationView +import io.tolgee.unit.util.assertFile +import io.tolgee.unit.util.getExportedCompressed +import io.tolgee.util.buildExportTranslationList +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.util.Calendar +import java.util.Date + +class XlsxFileExporterTest { + + private val currentDateProvider = Mockito.mock(CurrentDateProvider::class.java) + + @BeforeEach + fun setup() { + val now = Date(2025, Calendar.JANUARY, 10) + Mockito.`when`(currentDateProvider.date).thenReturn(now) + } + + @AfterEach + fun teardown() { + Mockito.reset(currentDateProvider) + } + + @Test + fun `exports with placeholders (ICU placeholders disabled)`() { + val exporter = getIcuPlaceholdersDisabledExporter() + val data = getExportedCompressed(exporter) + data.assertFile("all.xlsx", """ + |==================== + |[Content_Types].xml + |-------------------- + | + |==================== + |_rels/.rels + |-------------------- + | + |==================== + |docProps/app.xml + |-------------------- + | + |Apache POI + |==================== + |docProps/core.xml + |-------------------- + |3925-01-09T23:00:00ZApache POI + |==================== + |xl/sharedStrings.xml + |-------------------- + | + |keycskey3{count, plural, one {# den {icuParam}} few {# dny} other {# dní}}itemI will be first {icuParam, number}Text with multiple lines + |and , commas and "quotes" + |==================== + |xl/styles.xml + |-------------------- + | + | + |==================== + |xl/workbook.xml + |-------------------- + | + | + |==================== + |xl/_rels/workbook.xml.rels + |-------------------- + | + |==================== + |xl/worksheets/sheet1.xml + |-------------------- + | + |01234506 + | + """.trimMargin()) + } + + private fun getIcuPlaceholdersDisabledExporter(): XlsxFileExporter { + 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 = "item", + text = "I will be first {icuParam, number}", + ) + add( + languageTag = "cs", + keyName = "key", + text = "Text with multiple lines\nand , commas and \"quotes\" ", + ) + } + return getExporter(built.translations, false) + } + + @Test + fun `exports with placeholders (ICU placeholders enabled)`() { + val exporter = getIcuPlaceholdersEnabledExporter() + val data = getExportedCompressed(exporter) + data.assertFile("all.xlsx", """ + |==================== + |[Content_Types].xml + |-------------------- + | + |==================== + |_rels/.rels + |-------------------- + | + |==================== + |docProps/app.xml + |-------------------- + | + |Apache POI + |==================== + |docProps/core.xml + |-------------------- + |3925-01-09T23:00:00ZApache POI + |==================== + |xl/sharedStrings.xml + |-------------------- + | + |keycskey3{count, plural, one {# den {icuParam, number}} few {# dny} other {# dní}}itemI will be first '{'icuParam'}' {hello, number} + |==================== + |xl/styles.xml + |-------------------- + | + | + |==================== + |xl/workbook.xml + |-------------------- + | + | + |==================== + |xl/_rels/workbook.xml.rels + |-------------------- + | + |==================== + |xl/worksheets/sheet1.xml + |-------------------- + | + |012345 + | + """.trimMargin()) + } + + @Test + fun `correct exports translation with colon`() { + val exporter = getExporter(getTranslationWithColon()) + val data = getExportedCompressed(exporter) + data.assertFile("all.xlsx", """ + |==================== + |[Content_Types].xml + |-------------------- + | + |==================== + |_rels/.rels + |-------------------- + | + |==================== + |docProps/app.xml + |-------------------- + | + |Apache POI + |==================== + |docProps/core.xml + |-------------------- + |3925-01-09T23:00:00ZApache POI + |==================== + |xl/sharedStrings.xml + |-------------------- + | + |keycsitemname : {name} + |==================== + |xl/styles.xml + |-------------------- + | + | + |==================== + |xl/workbook.xml + |-------------------- + | + | + |==================== + |xl/_rels/workbook.xml.rels + |-------------------- + | + |==================== + |xl/worksheets/sheet1.xml + |-------------------- + | + |0123 + | + """.trimMargin()) + } + + private fun getTranslationWithColon(): MutableList { + val built = + buildExportTranslationList { + add( + languageTag = "cs", + keyName = "item", + text = "name : {name}", + ) + } + return built.translations + } + + private fun getIcuPlaceholdersEnabledExporter(): XlsxFileExporter { + val built = + buildExportTranslationList { + add( + languageTag = "cs", + keyName = "key3", + text = "{count, plural, one {# den {icuParam, number}} few {# dny} other {# dní}}", + ) { + key.isPlural = true + } + add( + languageTag = "cs", + keyName = "item", + text = "I will be first '{'icuParam'}' {hello, number}", + ) + } + return getExporter(built.translations, true) + } + + private fun getExporter( + translations: List, + isProjectIcuPlaceholdersEnabled: Boolean = true, + exportParams: ExportParams = ExportParams(), + ): XlsxFileExporter { + return XlsxFileExporter( + currentDateProvider.date, + translations = translations, + exportParams = exportParams, + isProjectIcuPlaceholdersEnabled = isProjectIcuPlaceholdersEnabled, + ) + } +} + diff --git a/backend/data/src/test/resources/import/xlsx/example.xlsx b/backend/data/src/test/resources/import/xlsx/example.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..71b6b1ca03ce8984b55170fd3a4cd03e7cfab0e3 GIT binary patch literal 10728 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 literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/example_params.xlsx b/backend/data/src/test/resources/import/xlsx/example_params.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4b1cd479dd43897a5c81fe6f2ab927b55441c591 GIT binary patch literal 10321 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 Jn}I(+{XY+mIU@i7 literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/example_semicolon.xlsx b/backend/data/src/test/resources/import/xlsx/example_semicolon.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c84ceaf57a1aa5028f7d2ab9939e9f00394d4eb6 GIT binary patch literal 9701 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 B-}3+f literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/icu.xlsx b/backend/data/src/test/resources/import/xlsx/icu.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2501ccdd4a7b4e03df87827df3c646ee5d58e3af GIT binary patch literal 10160 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 DF^=LV literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/java.xlsx b/backend/data/src/test/resources/import/xlsx/java.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ebe4227d7ed611e82c8accbca86d6fd47f63585b GIT binary patch literal 10200 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 literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/php.xlsx b/backend/data/src/test/resources/import/xlsx/php.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3f807f2430fff942ec156b0ca7371628c91251e5 GIT binary patch literal 10165 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 literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/placeholder_conversion.xlsx b/backend/data/src/test/resources/import/xlsx/placeholder_conversion.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4624b351672abeb2376b9a5c686eccda7dcba96f GIT binary patch literal 10274 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 literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/simple.xlsx b/backend/data/src/test/resources/import/xlsx/simple.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5184105537a00b9b02eeb42bd716d54e6cc81e10 GIT binary patch literal 10226 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 XwNz0Ny9)q3M0|n~KKm)-Z&&{hzHbID literal 0 HcmV?d00001 diff --git a/backend/data/src/test/resources/import/xlsx/unknown.xlsx b/backend/data/src/test/resources/import/xlsx/unknown.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..27146f379b118e74889b789f882acbb58c70b397 GIT binary patch literal 10177 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 MVL_Xj?&sP60|#8_pa1{> literal 0 HcmV?d00001 From 0717730e3178730cc2270d32590e72874483a56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Fri, 10 Jan 2025 17:25:13 +0100 Subject: [PATCH 3/6] fix: lint --- .../io/tolgee/unit/util/exportAssertUtil.kt | 30 ++++++++++--------- .../unit/xlsx/out/XlsxFileExporterTest.kt | 23 +++++++++----- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt b/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt index 503c9950d8..6476a3fecd 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/util/exportAssertUtil.kt @@ -19,19 +19,21 @@ fun getExported(exporter: FileExporter): Map { fun getExportedCompressed(exporter: FileExporter): Map { val files = exporter.produceFiles() - val data = files.map { - it.key to buildString { - val stream = ZipInputStream(it.value) - var entry = stream.nextEntry - while (entry != null) { - appendLine("====================") - appendLine(entry.name) - appendLine("--------------------") - append(stream.bufferedReader().readText()) - appendLine() - entry = stream.nextEntry - } - } - }.toMap() + val data = + files.map { + it.key to + buildString { + val stream = ZipInputStream(it.value) + var entry = stream.nextEntry + while (entry != null) { + appendLine("====================") + appendLine(entry.name) + appendLine("--------------------") + append(stream.bufferedReader().readText()) + appendLine() + entry = stream.nextEntry + } + } + }.toMap() return data } diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt index fedc641b80..1f8ef130c6 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt @@ -15,7 +15,6 @@ import java.util.Calendar import java.util.Date class XlsxFileExporterTest { - private val currentDateProvider = Mockito.mock(CurrentDateProvider::class.java) @BeforeEach @@ -33,7 +32,9 @@ class XlsxFileExporterTest { fun `exports with placeholders (ICU placeholders disabled)`() { val exporter = getIcuPlaceholdersDisabledExporter() val data = getExportedCompressed(exporter) - data.assertFile("all.xlsx", """ + data.assertFile( + "all.xlsx", + """ |==================== |[Content_Types].xml |-------------------- @@ -77,7 +78,8 @@ class XlsxFileExporterTest { | |01234506 | - """.trimMargin()) + """.trimMargin(), + ) } private fun getIcuPlaceholdersDisabledExporter(): XlsxFileExporter { @@ -108,7 +110,9 @@ class XlsxFileExporterTest { fun `exports with placeholders (ICU placeholders enabled)`() { val exporter = getIcuPlaceholdersEnabledExporter() val data = getExportedCompressed(exporter) - data.assertFile("all.xlsx", """ + data.assertFile( + "all.xlsx", + """ |==================== |[Content_Types].xml |-------------------- @@ -151,14 +155,17 @@ class XlsxFileExporterTest { | |012345 | - """.trimMargin()) + """.trimMargin(), + ) } @Test fun `correct exports translation with colon`() { val exporter = getExporter(getTranslationWithColon()) val data = getExportedCompressed(exporter) - data.assertFile("all.xlsx", """ + data.assertFile( + "all.xlsx", + """ |==================== |[Content_Types].xml |-------------------- @@ -201,7 +208,8 @@ class XlsxFileExporterTest { | |0123 | - """.trimMargin()) + """.trimMargin(), + ) } private fun getTranslationWithColon(): MutableList { @@ -248,4 +256,3 @@ class XlsxFileExporterTest { ) } } - From 6a2fd8b7423b6a1d6795193d17ee6dd026d9a8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Mon, 13 Jan 2025 23:40:48 +0100 Subject: [PATCH 4/6] fix: make xlsx tests timezone independent --- .../io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt index 1f8ef130c6..6ac5744bb5 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt @@ -19,7 +19,7 @@ class XlsxFileExporterTest { @BeforeEach fun setup() { - val now = Date(2025, Calendar.JANUARY, 10) + val now = Date(Date.UTC(2025-1900, Calendar.JANUARY, 10, 0, 0, 0)) Mockito.`when`(currentDateProvider.date).thenReturn(now) } @@ -51,7 +51,7 @@ class XlsxFileExporterTest { |==================== |docProps/core.xml |-------------------- - |3925-01-09T23:00:00ZApache POI + |2025-01-10T00:00:00ZApache POI |==================== |xl/sharedStrings.xml |-------------------- @@ -129,7 +129,7 @@ class XlsxFileExporterTest { |==================== |docProps/core.xml |-------------------- - |3925-01-09T23:00:00ZApache POI + |2025-01-10T00:00:00ZApache POI |==================== |xl/sharedStrings.xml |-------------------- @@ -182,7 +182,7 @@ class XlsxFileExporterTest { |==================== |docProps/core.xml |-------------------- - |3925-01-09T23:00:00ZApache POI + |2025-01-10T00:00:00ZApache POI |==================== |xl/sharedStrings.xml |-------------------- From ce0211a1762a3361e7a018bc41a756b26a2c8949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Mon, 13 Jan 2025 23:43:30 +0100 Subject: [PATCH 5/6] fix: lint --- .../test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt index 6ac5744bb5..c5df14dd5e 100644 --- a/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt +++ b/backend/data/src/test/kotlin/io/tolgee/unit/xlsx/out/XlsxFileExporterTest.kt @@ -19,7 +19,7 @@ class XlsxFileExporterTest { @BeforeEach fun setup() { - val now = Date(Date.UTC(2025-1900, Calendar.JANUARY, 10, 0, 0, 0)) + val now = Date(Date.UTC(2025 - 1900, Calendar.JANUARY, 10, 0, 0, 0)) Mockito.`when`(currentDateProvider.date).thenReturn(now) } From 1736e9c282af0d94540e9031d66c5cc22e3391ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Wed, 15 Jan 2025 10:11:43 +0100 Subject: [PATCH 6/6] fix: regenerate api schema --- webapp/src/service/apiSchema.generated.ts | 708 ++++++++++++++++++---- 1 file changed, 596 insertions(+), 112 deletions(-) diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 3e80d71cbc..92dd27fe93 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -343,6 +343,12 @@ export interface paths { /** Pairs user account with slack account. */ post: operations["userLogin"]; }; + "/v2/public/translator/translate": { + post: operations["translate"]; + }; + "/v2/public/telemetry/report": { + post: operations["report"]; + }; "/v2/public/slack": { post: operations["slackCommand"]; }; @@ -358,8 +364,26 @@ export interface paths { */ post: operations["fetchBotEvent"]; }; + "/v2/public/licensing/subscription": { + post: operations["getMySubscription"]; + }; + "/v2/public/licensing/set-key": { + post: operations["onLicenceSetKey"]; + }; + "/v2/public/licensing/report-usage": { + post: operations["reportUsage"]; + }; + "/v2/public/licensing/report-error": { + post: operations["reportError"]; + }; + "/v2/public/licensing/release-key": { + post: operations["releaseKey"]; + }; + "/v2/public/licensing/prepare-set-key": { + post: operations["prepareSetLicenseKey"]; + }; "/v2/public/business-events/report": { - post: operations["report"]; + post: operations["report_1"]; }; "/v2/public/business-events/identify": { post: operations["identify"]; @@ -438,7 +462,7 @@ export interface paths { }; "/v2/projects/{projectId}/start-batch-job/pre-translate-by-tm": { /** Pre-translate provided keys to provided languages by TM. */ - post: operations["translate"]; + post: operations["translate_1"]; }; "/v2/projects/{projectId}/start-batch-job/machine-translate": { /** Translate provided keys to provided languages through primary MT provider. */ @@ -524,7 +548,7 @@ export interface paths { }; "/v2/ee-license/prepare-set-license-key": { /** Get info about the upcoming EE subscription. This will show, how much the subscription will cost when key is applied. */ - post: operations["prepareSetLicenseKey"]; + post: operations["prepareSetLicenseKey_1"]; }; "/v2/api-keys": { get: operations["allByUser"]; @@ -1206,6 +1230,14 @@ export interface components { | "SERVER_ADMIN"; /** @description The user's permission type. This field is null if uses granular permissions */ type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @deprecated + * @description Deprecated (use translateLanguageIds). + * + * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. + * @example 200001,200004 + */ + permittedLanguageIds?: number[]; /** * @description List of languages user can translate to. If null, all languages editing is permitted. * @example 200001,200004 @@ -1216,14 +1248,6 @@ export interface components { * @example 200001,200004 */ stateChangeLanguageIds?: number[]; - /** - * @deprecated - * @description Deprecated (use translateLanguageIds). - * - * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. - * @example 200001,200004 - */ - permittedLanguageIds?: number[]; /** * @description List of languages user can view. If null, all languages view is permitted. * @example 200001,200004 @@ -1994,7 +2018,8 @@ export interface components { | "YAML" | "JSON_I18NEXT" | "CSV" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX"; /** * @description Delimiter to structure file content. * @@ -2097,7 +2122,8 @@ export interface components { | "YAML" | "JSON_I18NEXT" | "CSV" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX"; /** * @description Delimiter to structure file content. * @@ -2209,10 +2235,10 @@ export interface components { createNewKeys: boolean; }; ImportSettingsModel: { - /** @description If true, placeholders from other formats will be converted to ICU when possible */ - convertPlaceholdersToIcu: boolean; /** @description If true, key descriptions will be overridden by the import */ overrideKeyDescriptions: boolean; + /** @description If true, placeholders from other formats will be converted to ICU when possible */ + convertPlaceholdersToIcu: boolean; /** @description If false, only updates keys, skipping the creation of new keys */ createNewKeys: boolean; }; @@ -2377,11 +2403,11 @@ export interface components { expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; + description: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; - description: string; }; SetOrganizationRoleDto: { roleType: "MEMBER" | "OWNER"; @@ -2542,16 +2568,16 @@ export interface components { /** Format: int64 */ id: number; userFullName?: string; + projectName: string; + scopes: string[]; + /** Format: int64 */ + projectId: number; /** Format: int64 */ expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; - scopes: string[]; - /** Format: int64 */ - projectId: number; - username?: string; description: string; - projectName: string; + username?: string; }; SuperTokenRequest: { /** @description Has to be provided when TOTP enabled */ @@ -2563,6 +2589,49 @@ export interface components { name: string; oldSlug?: string; }; + ExampleItem: { + source: string; + target: string; + key: string; + keyNamespace?: string; + }; + Metadata: { + examples: components["schemas"]["ExampleItem"][]; + closeItems: components["schemas"]["ExampleItem"][]; + keyDescription?: string; + projectDescription?: string; + languageDescription?: string; + }; + TolgeeTranslateParams: { + text: string; + keyName?: string; + sourceTag: string; + targetTag: string; + metadata?: components["schemas"]["Metadata"]; + formality?: "FORMAL" | "INFORMAL" | "DEFAULT"; + isBatch: boolean; + pluralForms?: { [key: string]: string }; + pluralFormExamples?: { [key: string]: string }; + }; + MtResult: { + translated?: string; + /** Format: int32 */ + price: number; + contextDescription?: string; + }; + TelemetryReportRequest: { + instanceId: string; + /** Format: int64 */ + projectsCount: number; + /** Format: int64 */ + translationsCount: number; + /** Format: int64 */ + languagesCount: number; + /** Format: int64 */ + distinctLanguagesCount: number; + /** Format: int64 */ + usersCount: number; + }; SlackCommandDto: { token?: string; team_id: string; @@ -2575,6 +2644,130 @@ export interface components { trigger_id?: string; team_domain: string; }; + GetMySubscriptionDto: { + licenseKey: string; + instanceId: string; + }; + PlanIncludedUsageModel: { + /** Format: int64 */ + seats: number; + /** Format: int64 */ + translationSlots: number; + /** Format: int64 */ + translations: number; + /** Format: int64 */ + mtCredits: number; + }; + PlanPricesModel: { + perSeat: number; + perThousandTranslations?: number; + perThousandMtCredits?: number; + subscriptionMonthly: number; + subscriptionYearly: number; + }; + SelfHostedEePlanModel: { + /** Format: int64 */ + id: number; + name: string; + public: boolean; + enabledFeatures: ( + | "GRANULAR_PERMISSIONS" + | "PRIORITIZED_FEATURE_REQUESTS" + | "PREMIUM_SUPPORT" + | "DEDICATED_SLACK_CHANNEL" + | "ASSISTED_UPDATES" + | "DEPLOYMENT_ASSISTANCE" + | "BACKUP_CONFIGURATION" + | "TEAM_TRAINING" + | "ACCOUNT_MANAGER" + | "STANDARD_SUPPORT" + | "PROJECT_LEVEL_CONTENT_STORAGES" + | "WEBHOOKS" + | "MULTIPLE_CONTENT_DELIVERY_CONFIGS" + | "AI_PROMPT_CUSTOMIZATION" + | "SLACK_INTEGRATION" + | "TASKS" + | "SSO" + | "ORDER_TRANSLATION" + )[]; + prices: components["schemas"]["PlanPricesModel"]; + includedUsage: components["schemas"]["PlanIncludedUsageModel"]; + hasYearlyPrice: boolean; + free: boolean; + nonCommercial: boolean; + }; + SelfHostedEeSubscriptionModel: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + currentPeriodStart?: number; + /** Format: int64 */ + currentPeriodEnd?: number; + currentBillingPeriod: "MONTHLY" | "YEARLY"; + /** Format: int64 */ + createdAt: number; + plan: components["schemas"]["SelfHostedEePlanModel"]; + status: + | "ACTIVE" + | "CANCELED" + | "PAST_DUE" + | "UNPAID" + | "ERROR" + | "KEY_USED_BY_ANOTHER_INSTANCE"; + licenseKey?: string; + estimatedCosts?: number; + }; + SetLicenseKeyLicensingDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + instanceId: string; + }; + ReportUsageDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + }; + ReportErrorDto: { + stackTrace: string; + licenseKey: string; + }; + ReleaseKeyDto: { + licenseKey: string; + }; + PrepareSetLicenseKeyDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + }; + AverageProportionalUsageItemModel: { + total: number; + unusedQuantity: number; + usedQuantity: number; + usedQuantityOverPlan: number; + }; + PrepareSetEeLicenceKeyModel: { + plan: components["schemas"]["SelfHostedEePlanModel"]; + usage: components["schemas"]["UsageModel"]; + }; + SumUsageItemModel: { + total: number; + /** Format: int64 */ + unusedQuantity: number; + /** Format: int64 */ + usedQuantity: number; + /** Format: int64 */ + usedQuantityOverPlan: number; + }; + UsageModel: { + subscriptionPrice?: number; + /** @description Relevant for invoices only. When there are applied stripe credits, we need to reduce the total price by this amount. */ + appliedStripeCredits?: number; + seats: components["schemas"]["AverageProportionalUsageItemModel"]; + translations: components["schemas"]["AverageProportionalUsageItemModel"]; + credits?: components["schemas"]["SumUsageItemModel"]; + total: number; + }; BusinessEventReportRequest: { eventName: string; anonymousUserId?: string; @@ -3143,7 +3336,11 @@ export interface components { | "XLIFF_JAVA" | "XLIFF_PHP" | "XLIFF_RUBY" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX_ICU" + | "XLSX_JAVA" + | "XLSX_PHP" + | "XLSX_RUBY"; /** * @description The existing language tag in the Tolgee platform to which the imported language should be mapped. * @@ -3280,7 +3477,8 @@ export interface components { | "YAML" | "JSON_I18NEXT" | "CSV" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX"; /** * @description Delimiter to structure file content. * @@ -3452,82 +3650,6 @@ export interface components { createdAt: string; location?: string; }; - AverageProportionalUsageItemModel: { - total: number; - unusedQuantity: number; - usedQuantity: number; - usedQuantityOverPlan: number; - }; - PlanIncludedUsageModel: { - /** Format: int64 */ - seats: number; - /** Format: int64 */ - translationSlots: number; - /** Format: int64 */ - translations: number; - /** Format: int64 */ - mtCredits: number; - }; - PlanPricesModel: { - perSeat: number; - perThousandTranslations?: number; - perThousandMtCredits?: number; - subscriptionMonthly: number; - subscriptionYearly: number; - }; - PrepareSetEeLicenceKeyModel: { - plan: components["schemas"]["SelfHostedEePlanModel"]; - usage: components["schemas"]["UsageModel"]; - }; - SelfHostedEePlanModel: { - /** Format: int64 */ - id: number; - name: string; - public: boolean; - enabledFeatures: ( - | "GRANULAR_PERMISSIONS" - | "PRIORITIZED_FEATURE_REQUESTS" - | "PREMIUM_SUPPORT" - | "DEDICATED_SLACK_CHANNEL" - | "ASSISTED_UPDATES" - | "DEPLOYMENT_ASSISTANCE" - | "BACKUP_CONFIGURATION" - | "TEAM_TRAINING" - | "ACCOUNT_MANAGER" - | "STANDARD_SUPPORT" - | "PROJECT_LEVEL_CONTENT_STORAGES" - | "WEBHOOKS" - | "MULTIPLE_CONTENT_DELIVERY_CONFIGS" - | "AI_PROMPT_CUSTOMIZATION" - | "SLACK_INTEGRATION" - | "TASKS" - | "SSO" - | "ORDER_TRANSLATION" - )[]; - prices: components["schemas"]["PlanPricesModel"]; - includedUsage: components["schemas"]["PlanIncludedUsageModel"]; - hasYearlyPrice: boolean; - free: boolean; - nonCommercial: boolean; - }; - SumUsageItemModel: { - total: number; - /** Format: int64 */ - unusedQuantity: number; - /** Format: int64 */ - usedQuantity: number; - /** Format: int64 */ - usedQuantityOverPlan: number; - }; - UsageModel: { - subscriptionPrice?: number; - /** @description Relevant for invoices only. When there are applied stripe credits, we need to reduce the total price by this amount. */ - appliedStripeCredits?: number; - seats: components["schemas"]["AverageProportionalUsageItemModel"]; - translations: components["schemas"]["AverageProportionalUsageItemModel"]; - credits?: components["schemas"]["SumUsageItemModel"]; - total: number; - }; CreateApiKeyDto: { /** Format: int64 */ projectId: number; @@ -3759,13 +3881,13 @@ export interface components { name: string; /** Format: int64 */ id: number; + basePermissions: components["schemas"]["PermissionModel"]; /** * @description The role of currently authorized user. * * Can be null when user has direct access to one of the projects owned by the organization. */ currentUserRole?: "MEMBER" | "OWNER"; - basePermissions: components["schemas"]["PermissionModel"]; /** @example btforg */ slug: string; avatar?: components["schemas"]["Avatar"]; @@ -3841,7 +3963,8 @@ export interface components { | "YAML" | "JSON_I18NEXT" | "CSV" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX"; extension: string; mediaType: string; defaultFileStructureTemplate: string; @@ -4565,11 +4688,11 @@ export interface components { expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; + description: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; - description: string; }; PagedModelOrganizationModel: { _embedded?: { @@ -4689,16 +4812,16 @@ export interface components { /** Format: int64 */ id: number; userFullName?: string; + projectName: string; + scopes: string[]; /** Format: int64 */ - expiresAt?: number; + projectId: number; /** Format: int64 */ - lastUsedAt?: number; - scopes: string[]; + expiresAt?: number; /** Format: int64 */ - projectId: number; - username?: string; + lastUsedAt?: number; description: string; - projectName: string; + username?: string; }; PagedModelUserAccountModel: { _embedded?: { @@ -10508,6 +10631,96 @@ export interface operations { }; }; }; + translate: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["MtResult"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TolgeeTranslateParams"]; + }; + }; + }; + report: { + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TelemetryReportRequest"]; + }; + }; + }; slackCommand: { parameters: { header: { @@ -10672,7 +10885,277 @@ export interface operations { }; }; }; - report: { + getMySubscription: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GetMySubscriptionDto"]; + }; + }; + }; + onLicenceSetKey: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetLicenseKeyLicensingDto"]; + }; + }; + }; + reportUsage: { + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReportUsageDto"]; + }; + }; + }; + reportError: { + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReportErrorDto"]; + }; + }; + }; + releaseKey: { + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ReleaseKeyDto"]; + }; + }; + }; + prepareSetLicenseKey: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["PrepareSetEeLicenceKeyModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PrepareSetLicenseKeyDto"]; + }; + }; + }; + report_1: { responses: { /** OK */ 200: unknown; @@ -12173,7 +12656,7 @@ export interface operations { }; }; /** Pre-translate provided keys to provided languages by TM. */ - translate: { + translate_1: { parameters: { path: { projectId: number; @@ -12616,7 +13099,8 @@ export interface operations { | "YAML" | "JSON_I18NEXT" | "CSV" - | "RESX_ICU"; + | "RESX_ICU" + | "XLSX"; /** * Delimiter to structure file content. * @@ -13693,7 +14177,7 @@ export interface operations { }; }; /** Get info about the upcoming EE subscription. This will show, how much the subscription will cost when key is applied. */ - prepareSetLicenseKey: { + prepareSetLicenseKey_1: { responses: { /** OK */ 200: {