Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow import CSV #2475

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ dependencies {
implementation libs.jacksonKotlin
implementation("org.apache.commons:commons-configuration2:2.10.1")
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion"
implementation("com.opencsv:opencsv")

/**
* Google translation API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.dtos.dataImport.ImportFileDto
import io.tolgee.exceptions.ImportCannotParseFileException
import io.tolgee.formats.android.`in`.AndroidStringsXmlProcessor
import io.tolgee.formats.apple.`in`.strings.StringsFileProcessor
import io.tolgee.formats.csv.`in`.CsvFileProcessor
import io.tolgee.formats.flutter.`in`.FlutterArbFileProcessor
import io.tolgee.formats.importCommon.ImportFileFormat
import io.tolgee.formats.json.`in`.JsonFileProcessor
Expand Down Expand Up @@ -60,6 +61,7 @@ class ImportFileProcessorFactory(
ImportFileFormat.XML -> AndroidStringsXmlProcessor(context)
ImportFileFormat.ARB -> FlutterArbFileProcessor(context, objectMapper)
ImportFileFormat.YAML -> YamlFileProcessor(context, yamlObjectMapper)
ImportFileFormat.CSV -> CsvFileProcessor(context)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.tolgee.formats.csv.`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 CSVImportFormatDetector {
companion object {
private val possibleFormats =
mapOf(
ImportFormat.CSV_ICU to
arrayOf(
FormatDetectionUtil.regexFactor(
ICU_DETECTION_REGEX,
),
),
ImportFormat.CSV_PHP to
arrayOf(
FormatDetectionUtil.regexFactor(
PhpToIcuPlaceholderConvertor.PHP_DETECTION_REGEX,
),
),
ImportFormat.CSV_JAVA to
arrayOf(
FormatDetectionUtil.regexFactor(
JavaToIcuPlaceholderConvertor.JAVA_DETECTION_REGEX,
),
),
ImportFormat.CSV_RUBY to
arrayOf(
FormatDetectionUtil.regexFactor(
RubyToIcuPlaceholderConvertor.RUBY_DETECTION_REGEX,
0.95,
),
),
)
}

fun detectFormat(data: List<Array<String>>): ImportFormat {
return detectFromPossibleFormats(possibleFormats, data) ?: ImportFormat.CSV_ICU
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.tolgee.formats.csv.`in`

import com.opencsv.CSVParserBuilder
import com.opencsv.CSVReaderBuilder
import io.tolgee.exceptions.ImportCannotParseFileException
import io.tolgee.formats.ImportFileProcessor
import io.tolgee.formats.importCommon.ImportFormat
import io.tolgee.service.dataImport.processors.FileProcessorContext

class CsvFileProcessor(
override val context: FileProcessorContext,
) : ImportFileProcessor() {

override fun process() {
val inputStream = context.file.data.inputStream()
val reader = inputStream.reader()
val data: List<Array<String>>

CSVReaderBuilder(reader)
.withCSVParser(
CSVParserBuilder()
.withSeparator(';') // TODO make delimiter parametrizable
.build()
).build().use { csvReader -> data = csvReader.readAll() }


val format = getFormat(data)

// Read the first line to extract headers (language keys) - "key_name", ...languages
val headers = data.firstOrNull()
val languages = headers?.drop(1) ?: emptyList()

// Parse body - key_name, ...translations
for ((idx, row) in data.drop(1).withIndex()) {
val keyName = row.getOrNull(0) ?: throw ImportCannotParseFileException(context.file.name, "empty row $idx")

for (i in 1 until row.size) {
val languageTag = languages.getOrNull(i - 1) ?: throw ImportCannotParseFileException(
context.file.name, "more translations than defined languages in row $idx"
)
val translation = row.getOrNull(i) ?: throw ImportCannotParseFileException(
context.file.name, "missing translation in row $idx"
)

val converted = format.messageConvertor.convert(
translation, languageTag,
convertPlaceholders = context.importSettings.convertPlaceholdersToIcu,
isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled
)
context.addTranslation(
keyName,
languageTag,
converted.message,
idx,
pluralArgName = converted.pluralArgName,
rawData = row[i],
convertedBy = format,
)
}
}
}

private fun getFormat(data: List<Array<String>>): ImportFormat {
return context.mapping?.format ?: CSVImportFormatDetector().detectFormat(data)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ object FormatDetectionUtil {
when (data) {
is Map<*, *> -> data.forEach { (_, value) -> processMapRecursive(value, regex, hits, total) }
is List<*> -> data.forEach { item -> processMapRecursive(item, regex, hits, total) }
is Array<*> -> data.forEach { item -> processMapRecursive(item, regex, hits, total) }
else -> {
if (data is String) {
val count = regex.findAll(data).count()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class ImportFileFormat(val extensions: Array<String>) {
XML(arrayOf("xml")),
ARB(arrayOf("arb")),
YAML(arrayOf("yaml", "yml")),
CSV(arrayOf("csv")),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ enum class ImportFormat(
val messageConvertorOrNull: ImportMessageConvertor? = null,
val rootKeyIsLanguageTag: Boolean = false,
) {
CSV_ICU(
ImportFileFormat.CSV,
messageConvertorOrNull =
GenericMapPluralImportRawDataConvertor(
canContainIcu = true,
toIcuPlaceholderConvertorFactory = null
),
),
CSV_JAVA(
ImportFileFormat.CSV,
messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { JavaToIcuPlaceholderConvertor() },
),
CSV_PHP(
ImportFileFormat.CSV,
messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { PhpToIcuPlaceholderConvertor() },
),
CSV_RUBY(
ImportFileFormat.CSV,
messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { RubyToIcuPlaceholderConvertor() },
),

JSON_ICU(
ImportFileFormat.JSON,
messageConvertorOrNull =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const FORMATS = [
{ name: 'Android XML', logo: <AndroidLogo /> },
{ name: 'Flutter ARB', logo: <FluttrerLogo /> },
{ name: 'Ruby YAML', logo: <RailsLogo /> },
{
name: 'CSV',
logo: <TolgeeLogo />,
logoHeight: '24px',
logoWidth: '24px',
},
];

export const ImportSupportedFormats = () => {
Expand Down
Loading