Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import io.tolgee.formats.ImportFileProcessor
import io.tolgee.formats.apple.`in`.guessNamespaceFromPath
import io.tolgee.formats.importCommon.ImportFormat
import io.tolgee.service.dataImport.processors.FileProcessorContext
import io.tolgee.model.enums.TranslationState

class XcstringsFileProcessor(
override val context: FileProcessorContext,
private val objectMapper: ObjectMapper,
) : ImportFileProcessor() {
private lateinit var sourceLanguage: String

override fun process() {
try {
val root = objectMapper.readTree(context.file.data.inputStream())
val strings =
root.get("strings") ?: throw ImportCannotParseFileException(
context.file.name,
"Missing 'strings' object in xcstrings file",
)
sourceLanguage = root.get("sourceLanguage")?.asText()
?: throw ImportCannotParseFileException(context.file.name, "Missing sourceLanguage in xcstrings file")

val strings = root.get("strings")
?: throw ImportCannotParseFileException(context.file.name, "Missing 'strings' object in xcstrings file")

strings.fields().forEach { (key, value) ->
processKey(key, value)
Expand All @@ -41,33 +44,77 @@ class XcstringsFileProcessor(
context.addKeyDescription(key, comment)
}

if (!localizations.has(sourceLanguage)) {
val converted = messageConvertor.convert(
rawData = key,
languageTag = sourceLanguage,
convertPlaceholders = context.importSettings.convertPlaceholdersToIcu,
isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled
)

context.addTranslation(
keyName = key,
languageName = sourceLanguage,
value = converted.message,
convertedBy = importFormat,
state = TranslationState.TRANSLATED,
rawData = key,
pluralArgName = converted.pluralArgName
)
}

localizations.fields().forEach { (languageTag, localization) ->
when {
localization.has("stringUnit") -> {
processSingleTranslation(key, languageTag, localization)
}

localization.has("variations") -> {
processPluralTranslation(key, languageTag, localization)
}
}
}
}


private fun convertXCtoTolgeeTranslationState(xcState: String?): TranslationState {
return when (xcState) {
"needs_review" -> TranslationState.TRANSLATED
"translated" -> TranslationState.REVIEWED
null -> TranslationState.UNTRANSLATED
else -> TranslationState.UNTRANSLATED
}
}

private fun processSingleTranslation(
key: String,
languageTag: String,
localization: JsonNode,
) {
val stringUnit = localization.get("stringUnit")
val xcState = stringUnit?.get("state")?.asText()
var translationState = convertXCtoTolgeeTranslationState(xcState)

val translationValue = stringUnit?.get("value")?.asText()

if (translationValue != null) {
val converted = messageConvertor.convert(
rawData = translationValue,
languageTag = languageTag,
convertPlaceholders = context.importSettings.convertPlaceholdersToIcu,
isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled
)

context.addTranslation(
keyName = key,
languageName = languageTag,
value = translationValue,
value = converted.message,
convertedBy = importFormat,
state = translationState,
rawData = translationValue,
pluralArgName = converted.pluralArgName
)
return
}
}

Expand All @@ -78,9 +125,16 @@ class XcstringsFileProcessor(
) {
val variations = localization.get("variations")?.get("plural") ?: return
val forms = mutableMapOf<String, String>()
var translationState = TranslationState.UNTRANSLATED

variations.fields().forEach { (form, content) ->
val value = content.get("stringUnit")?.get("value")?.asText()
val stringUnit = content.get("stringUnit")
val value = stringUnit?.get("value")?.asText()
val currentState = convertXCtoTolgeeTranslationState(stringUnit?.get("state")?.asText())

if (form == "other" || form == "zero" || translationState == TranslationState.UNTRANSLATED) {
translationState = currentState
}
if (value != null) {
forms[form] = value
}
Expand All @@ -102,6 +156,7 @@ class XcstringsFileProcessor(
pluralArgName = converted.pluralArgName,
rawData = forms,
convertedBy = importFormat,
state = translationState
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.dtos.IExportParams
import io.tolgee.service.export.ExportFilePathProvider
import io.tolgee.service.export.dataProvider.ExportTranslationView
import io.tolgee.service.export.exporters.FileExporter
import io.tolgee.model.enums.TranslationState
import java.io.InputStream

class AppleXcstringsExporter(
Expand Down Expand Up @@ -52,6 +53,10 @@ class AppleXcstringsExporter(
it as ObjectNode
} ?: objectMapper.createObjectNode()

if (translation.description != null) {
keyData.put("comment", translation.description)
}

if (converted.isPlural()) {
handlePluralTranslation(localizations, translation, converted.formsResult)
} else {
Expand All @@ -61,6 +66,15 @@ class AppleXcstringsExporter(
keyData.set<ObjectNode>("localizations", localizations)
}

private fun getAppleState(state: TranslationState): String? {
return when (state) {
TranslationState.TRANSLATED -> "needs_review"
TranslationState.REVIEWED -> "translated"
TranslationState.UNTRANSLATED -> null
TranslationState.DISABLED -> null
}
}

private fun handleSingleTranslation(
localizations: ObjectNode,
translation: ExportTranslationView,
Expand All @@ -74,7 +88,9 @@ class AppleXcstringsExporter(
set<ObjectNode>(
"stringUnit",
objectMapper.createObjectNode().apply {
put("state", "translated")
getAppleState(translation.state)?.let { state ->
put("state", state)
}
put("value", convertedText)
},
)
Expand All @@ -97,7 +113,9 @@ class AppleXcstringsExporter(
set<ObjectNode>(
"stringUnit",
objectMapper.createObjectNode().apply {
put("state", "translated")
getAppleState(translation.state)?.let { state ->
put("state", state)
}
put("value", text)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.hypersistence.utils.hibernate.type.json.JsonBinaryType
import io.tolgee.formats.importCommon.ImportFormat
import io.tolgee.model.StandardAuditModel
import io.tolgee.model.translation.Translation
import io.tolgee.model.enums.TranslationState
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
Expand Down Expand Up @@ -85,6 +86,19 @@ class ImportTranslation(
@Enumerated(EnumType.STRING)
var convertor: ImportFormat? = null

/**
* Transient field to store desired state during import process
*/
@Transient
var state: TranslationState = TranslationState.TRANSLATED
get() = field ?: when {
text.isNullOrEmpty() -> TranslationState.UNTRANSLATED
else -> TranslationState.TRANSLATED
}
set(value) {
field = value
}

private fun String?.computeMurmur(): String? {
if (this == null) {
return "__null_value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ class StoredDataImporter(
this.language = language
}
translation.key = existingKey
translation.state = this.state
if (language == language.project.baseLanguage && translation.text != this.text) {
outdatedFlagKeys.add(translation.key.id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.tolgee.model.dataImport.ImportFile
import io.tolgee.model.dataImport.ImportKey
import io.tolgee.model.dataImport.ImportLanguage
import io.tolgee.model.dataImport.ImportTranslation
import io.tolgee.model.enums.TranslationState
import io.tolgee.model.dataImport.issues.issueTypes.FileIssueType
import io.tolgee.model.dataImport.issues.paramTypes.FileIssueParamType
import io.tolgee.model.key.KeyMeta
Expand Down Expand Up @@ -55,6 +56,7 @@ data class FileProcessorContext(
replaceNonPlurals: Boolean = false,
rawData: Any? = null,
convertedBy: ImportFormat? = null,
state: TranslationState = TranslationState.TRANSLATED
) {
val stringValue = value as? String

Expand All @@ -81,6 +83,7 @@ data class FileProcessorContext(
it.isPlural = isPlural
it.rawData = rawData.wrapIfRequired()
it.convertor = convertedBy
it.state = state
}
if (isPlural && replaceNonPlurals) {
_translations[keyName]!!.removeIf { it.language == language && !it.isPlural }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ class XcstringsFormatProcessorTest {
)
}

@Test
fun `import with ICU escaping (disabled ICU)`() {
mockUtil.mockIt(
"example.xcstrings",
"src/test/resources/import/apple/example_params_escaped.xcstrings",
convertPlaceholders = false,
projectIcuPlaceholdersEnabled = false
)
processFile()
mockUtil.fileProcessorContext.assertTranslations("en", "welcome-message-escaped")
.assertSingle {
hasText("Hello, %@ {meto}")
}
}

private fun processFile() {
XcstringsFileProcessor(mockUtil.fileProcessorContext, jacksonObjectMapper()).process()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"sourceLanguage": "en",
"strings": {
"welcome-message-escaped": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Hello, %@ {meto}"
}
}
}
}
},
"version": "1.0"
}
1 change: 0 additions & 1 deletion e2e/cypress/support/dataCyType.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
declare namespace DataCy {
export type Value =
"accept-auth-provider-change-accept" |
"accept-auth-provider-change-decline" |
"accept-auth-provider-change-info-text" |
"accept-invitation-accept" |
"accept-invitation-decline" |
Expand Down
Loading