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 9cc58dfdc5..ca71fcd589 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/ExportFormat.kt @@ -6,6 +6,7 @@ enum class ExportFormat( val extension: String, val mediaType: String, val defaultFileStructureTemplate: String = ExportFilePathProvider.DEFAULT_TEMPLATE, + val multiLanguage: Boolean = false, ) { JSON("json", "application/json"), JSON_TOLGEE("json", "application/json"), @@ -43,4 +44,10 @@ enum class ExportFormat( CSV("csv", "text/csv"), RESX_ICU("resx", "text/microsoft-resx"), XLSX("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + APPLE_XCSTRINGS( + "xcstrings", + "application/json", + defaultFileStructureTemplate = "Localizable.{extension}", + multiLanguage = true, + ), } 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 6d8a8ac74c..95d1206c54 100644 --- a/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt +++ b/backend/data/src/main/kotlin/io/tolgee/formats/ImportFileProcessorFactory.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.tolgee.dtos.dataImport.ImportFileDto import io.tolgee.exceptions.ImportCannotParseFileException import io.tolgee.formats.apple.`in`.strings.StringsFileProcessor +import io.tolgee.formats.apple.`in`.xcstrings.XcstringsFileProcessor import io.tolgee.formats.csv.`in`.CsvFileProcessor import io.tolgee.formats.flutter.`in`.FlutterArbFileProcessor import io.tolgee.formats.importCommon.ImportFileFormat @@ -66,6 +67,7 @@ class ImportFileProcessorFactory( ImportFileFormat.CSV -> CsvFileProcessor(context) ImportFileFormat.RESX -> ResxProcessor(context) ImportFileFormat.XLSX -> XlsxFileProcessor(context) + ImportFileFormat.XCSTRINGS -> XcstringsFileProcessor(context, objectMapper) } } diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/apple/in/xcstrings/XcstringsFileProcessor.kt b/backend/data/src/main/kotlin/io/tolgee/formats/apple/in/xcstrings/XcstringsFileProcessor.kt new file mode 100644 index 0000000000..196e7ed160 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/apple/in/xcstrings/XcstringsFileProcessor.kt @@ -0,0 +1,130 @@ +package io.tolgee.formats.apple.`in`.xcstrings + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.tolgee.exceptions.ImportCannotParseFileException +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 + +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()) + 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) + } + + context.namespace = guessNamespaceFromPath(context.file.name) + } catch (e: Exception) { + throw ImportCannotParseFileException(context.file.name, e.message) + } + } + + private fun addConvertedTranslation( + key: String, + languageTag: String, + rawValue: String, + forms: Map? = null, + ) { + val converted = + messageConvertor.convert( + rawData = if (forms != null) forms else rawValue, + languageTag = languageTag, + convertPlaceholders = context.importSettings.convertPlaceholdersToIcu, + isProjectIcuEnabled = context.projectIcuPlaceholdersEnabled, + ) + + context.addTranslation( + keyName = key, + languageName = languageTag, + value = converted.message, + convertedBy = importFormat, + rawData = forms ?: rawValue, + pluralArgName = converted.pluralArgName, + ) + } + + private fun processKey( + key: String, + value: JsonNode, + ) { + val localizations = value.get("localizations") ?: return + + value.get("comment")?.asText()?.let { comment -> + context.addKeyDescription(key, comment) + } + + if (!localizations.has(sourceLanguage)) { + addConvertedTranslation(key, sourceLanguage, key) + } + + localizations.fields().forEach { (languageTag, localization) -> + when { + localization.has("stringUnit") -> { + processSingleTranslation(key, languageTag, localization) + } + + localization.has("variations") -> { + processPluralTranslation(key, languageTag, localization) + } + } + } + } + + private fun processSingleTranslation( + key: String, + languageTag: String, + localization: JsonNode, + ) { + val stringUnit = localization.get("stringUnit") + val xcState = stringUnit?.get("state")?.asText() + + val translationValue = stringUnit?.get("value")?.asText() + + if (translationValue != null) { + addConvertedTranslation(key, languageTag, translationValue) + return + } + } + + private fun processPluralTranslation( + key: String, + languageTag: String, + localization: JsonNode, + ) { + val variations = localization.get("variations")?.get("plural") ?: return + val forms = mutableMapOf() + + variations.fields().forEach { (form, content) -> + val stringUnit = content.get("stringUnit") + val value = stringUnit?.get("value")?.asText() + + if (value != null) { + forms[form] = value + } + } + + if (forms.isNotEmpty()) { + addConvertedTranslation(key, languageTag, "", forms) + } + } + + companion object { + private val importFormat = ImportFormat.APPLE_XCSTRINGS + private val messageConvertor = importFormat.messageConvertor + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/formats/apple/out/AppleXcstringsExporter.kt b/backend/data/src/main/kotlin/io/tolgee/formats/apple/out/AppleXcstringsExporter.kt new file mode 100644 index 0000000000..8757df1338 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/formats/apple/out/AppleXcstringsExporter.kt @@ -0,0 +1,157 @@ +package io.tolgee.formats.apple.out + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import io.tolgee.dtos.IExportParams +import io.tolgee.model.enums.TranslationState +import io.tolgee.service.export.ExportFilePathProvider +import io.tolgee.service.export.dataProvider.ExportTranslationView +import io.tolgee.service.export.exporters.FileExporter +import java.io.InputStream + +class AppleXcstringsExporter( + private val translations: List, + private val exportParams: IExportParams, + private val objectMapper: ObjectMapper, + private val isProjectIcuPlaceholdersEnabled: Boolean = true, +) : FileExporter { + private val preparedFiles = mutableMapOf() + + override fun produceFiles(): Map { + translations.forEach { handleTranslation(it) } + + return preparedFiles.mapValues { (_, jsonContent) -> + val root = + objectMapper.createObjectNode().apply { + put("sourceLanguage", exportParams.languages?.firstOrNull() ?: "en") + put("version", "1.0") + set("strings", jsonContent) + } + objectMapper.writeValueAsString(root).byteInputStream() + } + } + + private fun handleTranslation(translation: ExportTranslationView) { + val baseFilePath = getBaseFilePath(translation) + val fileContent = preparedFiles.getOrPut(baseFilePath) { objectMapper.createObjectNode() } + + val keyData = + fileContent.get(translation.key.name)?.let { + it as ObjectNode + } ?: createKeyEntry(translation) + fileContent.set(translation.key.name, keyData) + + val converted = + IcuToAppleMessageConvertor( + message = translation.text ?: "", + translation.key.isPlural, + isProjectIcuPlaceholdersEnabled, + ).convert() + + val localizations = + keyData.get("localizations")?.let { + it as ObjectNode + } ?: objectMapper.createObjectNode() + + if (translation.description != null) { + keyData.put("comment", translation.description) + } + + if (converted.isPlural()) { + handlePluralTranslation(localizations, translation, converted.formsResult) + } else { + handleSingleTranslation(localizations, translation, converted.singleResult) + } + + keyData.set("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, + convertedText: String?, + ) { + if (convertedText == null) return + + localizations.set( + translation.languageTag, + objectMapper.createObjectNode().apply { + set( + "stringUnit", + objectMapper.createObjectNode().apply { + getAppleState(translation.state)?.let { state -> + put("state", state) + } + put("value", convertedText) + }, + ) + }, + ) + } + + private fun handlePluralTranslation( + localizations: ObjectNode, + translation: ExportTranslationView, + forms: Map?, + ) { + if (forms == null) return + + val pluralForms = objectMapper.createObjectNode() + forms.forEach { (form, text) -> + pluralForms.set( + form, + objectMapper.createObjectNode().apply { + set( + "stringUnit", + objectMapper.createObjectNode().apply { + getAppleState(translation.state)?.let { state -> + put("state", state) + } + put("value", text) + }, + ) + }, + ) + } + + localizations.set( + translation.languageTag, + objectMapper.createObjectNode().apply { + set( + "variations", + objectMapper.createObjectNode().apply { + set("plural", pluralForms) + }, + ) + }, + ) + } + + private fun createKeyEntry(translation: ExportTranslationView): ObjectNode { + return objectMapper.createObjectNode().apply { + translation.key.description?.let { + put("extractionState", "manual") + } + } + } + + private fun getBaseFilePath(translation: ExportTranslationView): String { + return ExportFilePathProvider( + exportParams, + "xcstrings", + ).getFilePath( + translation.key.namespace, + null, + replaceExtension = true, + ) + } +} 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 4e59bead4e..756e2b76a4 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 @@ -6,6 +6,7 @@ enum class ImportFileFormat(val extensions: Array) { STRINGS(arrayOf("strings")), STRINGSDICT(arrayOf("stringsdict")), XLIFF(arrayOf("xliff", "xlf")), + XCSTRINGS(arrayOf("xcstrings")), PROPERTIES(arrayOf("properties")), XML(arrayOf("xml")), ARB(arrayOf("arb")), 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 bd10a93594..343c95ad88 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 @@ -125,6 +125,15 @@ enum class ImportFormat( messageConvertorOrNull = appleConvertor, ), + APPLE_XCSTRINGS( + fileFormat = ImportFileFormat.XCSTRINGS, + messageConvertorOrNull = + GenericMapPluralImportRawDataConvertor( + optimizePlurals = true, + canContainIcu = false, + ) { AppleToIcuPlaceholderConvertor() }, + ), + // properties don't store plurals in map, but it doesn't matter. // Since they don't support nesting at all, we cannot have plurals by nesting in them, so the plural extracting // code won't be executed diff --git a/backend/data/src/main/kotlin/io/tolgee/service/export/ExportFilePathProvider.kt b/backend/data/src/main/kotlin/io/tolgee/service/export/ExportFilePathProvider.kt index d06d4ce837..08c0ba93c1 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/export/ExportFilePathProvider.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/export/ExportFilePathProvider.kt @@ -52,19 +52,21 @@ class ExportFilePathProvider( } private fun validateTemplate() { - val containsLanguageTag = - arrayOf( - ExportFilePathPlaceholder.LANGUAGE_TAG, - ExportFilePathPlaceholder.ANDROID_LANGUAGE_TAG, - ExportFilePathPlaceholder.SNAKE_LANGUAGE_TAG, - ).any { getTemplate().contains(it.placeholder) } - - if (!containsLanguageTag) { - throw getMissingPlaceholderException( - ExportFilePathPlaceholder.LANGUAGE_TAG, - ExportFilePathPlaceholder.ANDROID_LANGUAGE_TAG, - ExportFilePathPlaceholder.SNAKE_LANGUAGE_TAG, - ) + if (!params.format.multiLanguage) { + val containsLanguageTag = + arrayOf( + ExportFilePathPlaceholder.LANGUAGE_TAG, + ExportFilePathPlaceholder.ANDROID_LANGUAGE_TAG, + ExportFilePathPlaceholder.SNAKE_LANGUAGE_TAG, + ).any { getTemplate().contains(it.placeholder) } + + if (!containsLanguageTag) { + throw getMissingPlaceholderException( + ExportFilePathPlaceholder.LANGUAGE_TAG, + ExportFilePathPlaceholder.ANDROID_LANGUAGE_TAG, + ExportFilePathPlaceholder.SNAKE_LANGUAGE_TAG, + ) + } } val containsExtension = getTemplate().contains(ExportFilePathPlaceholder.EXTENSION.placeholder) 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 0411786431..a3f435f46f 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 @@ -6,6 +6,7 @@ import io.tolgee.dtos.IExportParams import io.tolgee.dtos.cacheable.LanguageDto import io.tolgee.formats.ExportFormat import io.tolgee.formats.apple.out.AppleStringsStringsdictExporter +import io.tolgee.formats.apple.out.AppleXcstringsExporter import io.tolgee.formats.apple.out.AppleXliffExporter import io.tolgee.formats.csv.out.CsvFileExporter import io.tolgee.formats.flutter.out.FlutterArbFileExporter @@ -98,6 +99,14 @@ class FileExporterFactory( ExportFormat.APPLE_STRINGS_STRINGSDICT -> AppleStringsStringsdictExporter(data, exportParams, projectIcuPlaceholdersSupport) + ExportFormat.APPLE_XCSTRINGS -> + AppleXcstringsExporter( + translations = data, + exportParams = exportParams, + objectMapper = objectMapper, + isProjectIcuPlaceholdersEnabled = projectIcuPlaceholdersSupport, + ) + ExportFormat.FLUTTER_ARB -> FlutterArbFileExporter( data, diff --git a/backend/data/src/test/kotlin/io/tolgee/unit/formats/apple/in/XcstringsFormatProcessorTest.kt b/backend/data/src/test/kotlin/io/tolgee/unit/formats/apple/in/XcstringsFormatProcessorTest.kt new file mode 100644 index 0000000000..c120c6e9be --- /dev/null +++ b/backend/data/src/test/kotlin/io/tolgee/unit/formats/apple/in/XcstringsFormatProcessorTest.kt @@ -0,0 +1,147 @@ +package io.tolgee.unit.formats.apple.`in` + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.tolgee.formats.apple.`in`.xcstrings.XcstringsFileProcessor +import io.tolgee.testing.assert +import io.tolgee.unit.formats.PlaceholderConversionTestHelper +import io.tolgee.util.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class XcstringsFormatProcessorTest { + lateinit var mockUtil: FileProcessorContextMockUtil + + @BeforeEach + fun setup() { + mockUtil = FileProcessorContextMockUtil() + mockUtil.mockIt("example.xcstrings", "src/test/resources/import/apple/example.xcstrings") + } + + @Test + fun `returns correct parsed result`() { + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(2) // en, fr + mockUtil.fileProcessorContext.assertTranslations("en", "hello-world") + .assertSingle { + hasText("Hello, World!") + } + mockUtil.fileProcessorContext.assertTranslations("fr", "hello-world") + .assertSingle { + hasText("Bonjour le monde!") + } + } + + @Test + fun `handles plural translations correctly`() { + processFile() + mockUtil.fileProcessorContext.assertTranslations("en", "messages-count") + .assertSinglePlural { + hasText( + """ + {0, plural, + one {You have # message} + other {You have # messages} + } + """.trimIndent(), + ) + isPluralOptimized() + } + } + + @Test + fun `import with placeholder conversion (disabled ICU)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = false, projectIcuPlaceholdersEnabled = false) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "messages-count") + .assertSinglePlural { + hasText( + """ + {value, plural, + one {You have %lld message} + other {You have %lld messages} + } + """.trimIndent(), + ) + isPluralOptimized() + } + } + + @Test + fun `import with placeholder conversion (with conversion)`() { + mockPlaceholderConversionTestFile(convertPlaceholders = true, projectIcuPlaceholdersEnabled = true) + processFile() + mockUtil.fileProcessorContext.assertLanguagesCount(1) + mockUtil.fileProcessorContext.assertTranslations("en", "messages-count") + .assertSinglePlural { + hasText( + """ + {0, plural, + one {You have # message} + other {You have # messages} + } + """.trimIndent(), + ) + isPluralOptimized() + } + } + + @Test + fun `preserves metadata`() { + processFile() + mockUtil.fileProcessorContext.assertKey("hello-world") { + description.assert.isEqualTo("A greeting message") + } + } + + @Test + fun `placeholder conversion setting application works`() { + PlaceholderConversionTestHelper.testFile( + "example.xcstrings", + "src/test/resources/import/apple/example_params.xcstrings", + assertBeforeSettingsApplication = + listOf( + "{0, plural,\none {You have # message}\nother {You have # messages}\n}", + ), + assertAfterDisablingConversion = + listOf( + "{value, plural,\none {You have %lld message}\nother {You have %lld messages}\n}", + ), + assertAfterReEnablingConversion = + listOf( + "{0, plural,\none {You have # message}\nother {You have # messages}\n}", + ), + ) + } + + @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() + } + + private fun mockPlaceholderConversionTestFile( + convertPlaceholders: Boolean, + projectIcuPlaceholdersEnabled: Boolean, + ) { + mockUtil.mockIt( + "example.xcstrings", + "src/test/resources/import/apple/example_params.xcstrings", + convertPlaceholders, + projectIcuPlaceholdersEnabled, + ) + } +} diff --git a/backend/data/src/test/resources/import/apple/example.xcstrings b/backend/data/src/test/resources/import/apple/example.xcstrings new file mode 100644 index 0000000000..a31bf8c756 --- /dev/null +++ b/backend/data/src/test/resources/import/apple/example.xcstrings @@ -0,0 +1,46 @@ +{ + "sourceLanguage": "en", + "strings": { + "hello-world": { + "extractionState": "manual", + "comment": "A greeting message", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hello, World!" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bonjour le monde!" + } + } + } + }, + "messages-count": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "You have %lld message" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "You have %lld messages" + } + } + } + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/backend/data/src/test/resources/import/apple/example_params.xcstrings b/backend/data/src/test/resources/import/apple/example_params.xcstrings new file mode 100644 index 0000000000..bd44f2290f --- /dev/null +++ b/backend/data/src/test/resources/import/apple/example_params.xcstrings @@ -0,0 +1,28 @@ +{ + "sourceLanguage": "en", + "strings": { + "messages-count": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "You have %lld message" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "You have %lld messages" + } + } + } + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/backend/data/src/test/resources/import/apple/example_params_escaped.xcstrings b/backend/data/src/test/resources/import/apple/example_params_escaped.xcstrings new file mode 100644 index 0000000000..5b394310f0 --- /dev/null +++ b/backend/data/src/test/resources/import/apple/example_params_escaped.xcstrings @@ -0,0 +1,16 @@ +{ + "sourceLanguage": "en", + "strings": { + "welcome-message-escaped": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hello, %@ {meto}" + } + } + } + } + }, + "version": "1.0" +} \ No newline at end of file diff --git a/e2e/cypress/support/dataCyType.d.ts b/e2e/cypress/support/dataCyType.d.ts index d37d6cb7cc..00807c4d1e 100644 --- a/e2e/cypress/support/dataCyType.d.ts +++ b/e2e/cypress/support/dataCyType.d.ts @@ -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" | diff --git a/webapp/src/i18n/cs.json b/webapp/src/i18n/cs.json index 470d838a65..04f796ceb3 100644 --- a/webapp/src/i18n/cs.json +++ b/webapp/src/i18n/cs.json @@ -605,6 +605,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Compose Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", diff --git a/webapp/src/i18n/da.json b/webapp/src/i18n/da.json index d33eb53669..800c5bb6b1 100644 --- a/webapp/src/i18n/da.json +++ b/webapp/src/i18n/da.json @@ -606,6 +606,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Sammensæt Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "Flad JSON", diff --git a/webapp/src/i18n/de.json b/webapp/src/i18n/de.json index 63acdfafff..516e96625b 100644 --- a/webapp/src/i18n/de.json +++ b/webapp/src/i18n/de.json @@ -607,6 +607,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Compose Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", diff --git a/webapp/src/i18n/en.json b/webapp/src/i18n/en.json index a7ff5e3f72..0a799fbab3 100644 --- a/webapp/src/i18n/en.json +++ b/webapp/src/i18n/en.json @@ -608,6 +608,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Compose Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", diff --git a/webapp/src/i18n/es.json b/webapp/src/i18n/es.json index 379f38589f..6816e29a4f 100644 --- a/webapp/src/i18n/es.json +++ b/webapp/src/i18n/es.json @@ -602,6 +602,7 @@ "export-format-apple-strings": "Apple .strings y .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "JSON plano", "export-format-flat-yaml": "YAML plano", diff --git a/webapp/src/i18n/fr.json b/webapp/src/i18n/fr.json index 5a3400cb03..33a601b320 100644 --- a/webapp/src/i18n/fr.json +++ b/webapp/src/i18n/fr.json @@ -604,6 +604,7 @@ "export-format-apple-strings": "Apple .strings et .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Compose Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", diff --git a/webapp/src/i18n/hu.json b/webapp/src/i18n/hu.json index b9dcf99f95..79d3871de5 100644 --- a/webapp/src/i18n/hu.json +++ b/webapp/src/i18n/hu.json @@ -602,6 +602,7 @@ "export-format-apple-strings": "Apple .strings és .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "Sík JSON", "export-format-flat-yaml": "Sík YAML", diff --git a/webapp/src/i18n/it-IT.json b/webapp/src/i18n/it-IT.json index d03a459d37..f16ddb985d 100644 --- a/webapp/src/i18n/it-IT.json +++ b/webapp/src/i18n/it-IT.json @@ -574,6 +574,7 @@ "export-format-apple-strings": "Apple .strings e .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", "export-format-flat-yaml": "Flat YAML", diff --git a/webapp/src/i18n/nl.json b/webapp/src/i18n/nl.json index e7ad85d435..ce1d001aa7 100644 --- a/webapp/src/i18n/nl.json +++ b/webapp/src/i18n/nl.json @@ -604,6 +604,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "Platte JSON", "export-format-flat-yaml": "Platte YAML", diff --git a/webapp/src/i18n/no.json b/webapp/src/i18n/no.json index fbbf997e00..6ef81277a5 100644 --- a/webapp/src/i18n/no.json +++ b/webapp/src/i18n/no.json @@ -602,6 +602,7 @@ "export-format-apple-strings": "Apple .strings og .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "Flat JSON", "export-format-flat-yaml": "Flat YAML", diff --git a/webapp/src/i18n/pt.json b/webapp/src/i18n/pt.json index 4541b153d8..c5c13c1462 100644 --- a/webapp/src/i18n/pt.json +++ b/webapp/src/i18n/pt.json @@ -602,6 +602,7 @@ "export-format-apple-strings": "Apple .strings e .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "JSON plano", "export-format-flat-yaml": "YAML plano", diff --git a/webapp/src/i18n/ro.json b/webapp/src/i18n/ro.json index 9403d2fa2c..fb5dcc4db0 100644 --- a/webapp/src/i18n/ro.json +++ b/webapp/src/i18n/ro.json @@ -606,6 +606,7 @@ "export-format-apple-strings": "Apple .strings & .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-compose-xml": "Compose Multiplatform .xml", "export-format-csv": "CSV", "export-format-flat-json": "JSON plat", diff --git a/webapp/src/i18n/zh.json b/webapp/src/i18n/zh.json index 524f80babf..d7c0f05324 100644 --- a/webapp/src/i18n/zh.json +++ b/webapp/src/i18n/zh.json @@ -602,6 +602,7 @@ "export-format-apple-strings": "Apple .strings 和 .stringsdict", "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-apple-xliff": "Apple .xliff", + "export-format-apple-xcstrings": "Apple .xcstrings", "export-format-csv": "CSV", "export-format-flat-json": "扁平 JSON", "export-format-flat-yaml": "扁平 YAML", diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 2643812ca8..744a3cb138 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -1353,6 +1353,7 @@ export interface components { | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" + | "APPLE_XCSTRINGS" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" @@ -1464,6 +1465,7 @@ export interface components { | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" + | "APPLE_XCSTRINGS" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" @@ -2013,6 +2015,7 @@ export interface components { | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" + | "APPLE_XCSTRINGS" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" @@ -2069,6 +2072,7 @@ export interface components { | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" + | "APPLE_XCSTRINGS" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" @@ -10305,6 +10309,7 @@ export interface operations { | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" + | "APPLE_XCSTRINGS" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" diff --git a/webapp/src/views/projects/export/components/formatGroups.tsx b/webapp/src/views/projects/export/components/formatGroups.tsx index f6004e5a33..087877d9f3 100644 --- a/webapp/src/views/projects/export/components/formatGroups.tsx +++ b/webapp/src/views/projects/export/components/formatGroups.tsx @@ -187,6 +187,12 @@ export const formatGroups: FormatGroup[] = [ name: , format: 'APPLE_XLIFF', }, + { + id: 'apple_xcstrings', + extension: 'xcstrings', + name: , + format: 'APPLE_XCSTRINGS', + }, ], }, { diff --git a/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx b/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx index e398c68acb..e28336c640 100644 --- a/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx +++ b/webapp/src/views/projects/import/component/ImportSupportedFormats.tsx @@ -54,6 +54,7 @@ const FORMATS = [ { name: 'PO Python', logo: }, { name: 'Apple Strings', logo: }, { name: 'Apple Stringsdict', logo: }, + { name: 'Apple Strings Catalog', logo: }, { name: 'Apple XLIFF', logo: }, { name: 'Android XML', logo: }, { name: 'Compose Multiplatform XML', logo: },