Skip to content

Commit

Permalink
feat: import and export in dotnet resx format (#2803)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anty0 authored Jan 8, 2025
1 parent a414a7f commit c414b29
Show file tree
Hide file tree
Showing 18 changed files with 1,810 additions and 297 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ enum class ExportFormat(
YAML("yaml", "application/x-yaml"),
JSON_I18NEXT("json", "application/json"),
CSV("csv", "text/csv"),
RESX_ICU("resx", "text/microsoft-resx"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.tolgee.formats.importCommon.ImportFileFormat
import io.tolgee.formats.json.`in`.JsonFileProcessor
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.xmlResources.`in`.XmlResourcesProcessor
import io.tolgee.formats.yaml.`in`.YamlFileProcessor
Expand Down Expand Up @@ -62,6 +63,7 @@ class ImportFileProcessorFactory(
ImportFileFormat.ARB -> FlutterArbFileProcessor(context, objectMapper)
ImportFileFormat.YAML -> YamlFileProcessor(context, yamlObjectMapper)
ImportFileFormat.CSV -> CsvFileProcessor(context)
ImportFileFormat.RESX -> ResxProcessor(context)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum class ImportFileFormat(val extensions: Array<String>) {
ARB(arrayOf("arb")),
YAML(arrayOf("yaml", "yml")),
CSV(arrayOf("csv")),
RESX(arrayOf("resx")),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ enum class ImportFormat(
messageConvertorOrNull = GenericMapPluralImportRawDataConvertor { RubyToIcuPlaceholderConvertor() },
),

RESX_ICU(
ImportFileFormat.RESX,
messageConvertorOrNull =
GenericMapPluralImportRawDataConvertor(
canContainIcu = true,
toIcuPlaceholderConvertorFactory = null,
),
),

;

val messageConvertor: ImportMessageConvertor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.tolgee.formats.resx

data class ResxEntry(
val key: String,
val data: String? = null,
val comment: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.tolgee.formats.resx.`in`

import io.tolgee.formats.resx.ResxEntry
import javax.xml.namespace.QName
import javax.xml.stream.XMLEventReader
import javax.xml.stream.events.Characters
import javax.xml.stream.events.EndElement
import javax.xml.stream.events.StartElement
import javax.xml.stream.events.XMLEvent

class ResxParser(
private val xmlEventReader: XMLEventReader,
) {
var curState: State? = null
var currentEntry: ResxEntry? = null
var currentText: String = ""

fun parse(): Sequence<ResxEntry> =
sequence {
while (xmlEventReader.hasNext()) {
val event = xmlEventReader.nextEvent()
when {
event.isStartElement -> handleStartElement(event.asStartElement())
event.isEndElement -> handleEndElement(event.asEndElement())
}
handleText(event)
}
}

fun handleStartElement(element: StartElement) {
val name = element.name.localPart.lowercase()
if (isAnyToContentSaveOpen()) {
throw IllegalStateException("Unexpected start of xml element: $name")
}
when (name) {
"data" -> {
element.getKeyName()?.let {
currentEntry = ResxEntry(it)
}
}

"value" -> {
currentText = ""
curState = State.READING_VALUE
}

"comment" -> {
currentText = ""
curState = State.READING_COMMENT
}
}
}

suspend fun SequenceScope<ResxEntry>.handleEndElement(element: EndElement) {
val name = element.name.localPart.lowercase()
when (name) {
"data" -> {
currentEntry?.let { yield(it) }
currentEntry = null
currentText = ""
curState = null
}

"value" -> {
if (curState == State.READING_VALUE) {
currentEntry = currentEntry?.copy(data = currentText)
}
curState = null
}

"comment" -> {
if (curState == State.READING_COMMENT) {
currentEntry = currentEntry?.copy(comment = currentText)
}
curState = null
}
}
}

fun handleText(event: XMLEvent) {
if (!isAnyToContentSaveOpen() || event !is Characters) {
return
}
currentText += event.asCharacters().data
}

fun isAnyToContentSaveOpen(): Boolean = curState != null

private fun StartElement.getKeyName() = getAttributeByName(QName(null, "name"))?.value

enum class State {
READING_VALUE,
READING_COMMENT,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.tolgee.formats.resx.`in`

import io.tolgee.exceptions.ImportCannotParseFileException
import io.tolgee.formats.ImportFileProcessor
import io.tolgee.formats.importCommon.ImportFormat
import io.tolgee.formats.resx.ResxEntry
import io.tolgee.service.dataImport.processors.FileProcessorContext
import javax.xml.stream.XMLEventReader
import javax.xml.stream.XMLInputFactory

class ResxProcessor(override val context: FileProcessorContext) : ImportFileProcessor() {
override fun process() {
try {
val parser = ResxParser(xmlEventReader)
val data = parser.parse()
data.importAll()
} catch (e: Exception) {
throw ImportCannotParseFileException(context.file.name, e.message ?: "", e)
}
}

fun Sequence<ResxEntry>.importAll() {
forEachIndexed { idx, it -> it.import(idx) }
}

fun ResxEntry.import(index: Int) {
val converted =
messageConvertor.convert(
data,
firstLanguageTagGuessOrUnknown,
convertPlaceholders = context.importSettings.convertPlaceholdersToIcu,
isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled,
)
context.addKeyDescription(key, comment)
context.addTranslation(
key,
firstLanguageTagGuessOrUnknown,
converted.message,
index,
pluralArgName = converted.pluralArgName,
rawData = data,
convertedBy = importFormat,
)
}

private val xmlEventReader: XMLEventReader by lazy {
val inputFactory: XMLInputFactory = XMLInputFactory.newInstance()
inputFactory.createXMLEventReader(context.file.data.inputStream())
}

companion object {
private val importFormat = ImportFormat.RESX_ICU

private val messageConvertor = importFormat.messageConvertor
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.tolgee.formats.resx.out

import io.tolgee.dtos.IExportParams
import io.tolgee.formats.IcuToIcuPlaceholderConvertor
import io.tolgee.formats.generic.IcuToGenericFormatMessageConvertor
import io.tolgee.formats.resx.ResxEntry
import io.tolgee.service.export.ExportFilePathProvider
import io.tolgee.service.export.dataProvider.ExportTranslationView
import io.tolgee.service.export.exporters.FileExporter
import java.io.InputStream
import kotlin.collections.forEach

class ResxExporter(
val translations: List<ExportTranslationView>,
val exportParams: IExportParams,
private val isProjectIcuPlaceholdersEnabled: Boolean = true,
) : FileExporter {
private val fileUnits = mutableMapOf<String, MutableList<ResxEntry>>()

private fun getModels(): Map<String, List<ResxEntry>> {
prepare()
return fileUnits
}

private fun prepare() {
translations.forEach { translation ->
val converted = getConvertedMessage(translation)
val entry =
ResxEntry(
key = translation.key.name,
data = converted,
comment = translation.description,
)

val units = getFileUnits(translation)
units.add(entry)
}
}

private fun getFileUnits(translation: ExportTranslationView): MutableList<ResxEntry> {
val filePath =
pathProvider.getFilePath(
languageTag = translation.languageTag,
namespace = translation.key.namespace,
)
return fileUnits.computeIfAbsent(filePath) { mutableListOf() }
}

private val pathProvider by lazy {
ExportFilePathProvider(
exportParams,
"resx",
)
}

private fun getConvertedMessage(
translation: ExportTranslationView,
isPlural: Boolean = translation.key.isPlural,
): String? {
return IcuToGenericFormatMessageConvertor(
translation.text,
isPlural,
isProjectIcuPlaceholdersEnabled,
) {
IcuToIcuPlaceholderConvertor()
}.convert()
}

override fun produceFiles(): Map<String, InputStream> {
return getModels().map { (path, model) ->
path to ResxWriter(model).produceFiles()
}.toMap()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.tolgee.formats.resx.out

import io.tolgee.formats.resx.ResxEntry
import io.tolgee.util.attr
import io.tolgee.util.buildDom
import io.tolgee.util.element
import org.w3c.dom.Element
import java.io.InputStream

class ResxWriter(private val model: List<ResxEntry>) {
fun produceFiles(): InputStream {
return buildDom {
element("root") {
addHeaders()
model.forEach { addToElement(it) }
}
}.write().toByteArray().inputStream()
}

private fun Element.addHeaders() {
element("resheader") {
attr("name", "resmimetype")
element("value") {
textContent = "text/microsoft-resx"
}
}
element("resheader") {
attr("name", "version")
element("value") {
textContent = "2.0"
}
}
element("resheader") {
attr("name", "reader")
element("value") {
textContent = "System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral"
}
}
element("resheader") {
attr("name", "writer")
element("value") {
textContent = "System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral"
}
}
}

private fun Element.addToElement(entry: ResxEntry) {
element("data") {
attr("name", entry.key)
attr("xml:space", "preserve")
entry.data?.let {
element("value") {
textContent = it
}
}
entry.comment?.let {
element("comment") {
textContent = it
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.tolgee.formats.genericStructuredFile.out.CustomPrettyPrinter
import io.tolgee.formats.json.out.JsonFileExporter
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.xmlResources.out.XmlResourcesExporter
import io.tolgee.formats.yaml.out.YamlFileExporter
Expand Down Expand Up @@ -105,6 +106,9 @@ class FileExporterFactory(

ExportFormat.PROPERTIES ->
PropertiesFileExporter(data, exportParams, projectIcuPlaceholdersSupport)

ExportFormat.RESX_ICU ->
ResxExporter(data, exportParams, projectIcuPlaceholdersSupport)
}
}
}
Loading

0 comments on commit c414b29

Please sign in to comment.