Skip to content

Commit

Permalink
feat: Add Ruby YAML support (#2183)
Browse files Browse the repository at this point in the history
Tasks before ready:
- [x] When importing plural, the parameter should be converted to
REPLACE_NUMBER (#)
- [x] Make XLIFF, JSON, and properties generic and accepting different
placeholder types
- [x] Make import formats aligned with supported message formats for
export, so it imports what's exported
- [x] Add Flat YAML format
  • Loading branch information
JanCizmar committed Mar 26, 2024
1 parent 35a6fe9 commit 118ecf7
Show file tree
Hide file tree
Showing 184 changed files with 5,471 additions and 2,152 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,7 @@ backend/app/src/main/resources/application-js-e2e.properties

/backend/*/build
/backend/*/out
/gradle-user-home
/profile-out
/.stash*

Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,20 @@ import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.dtos.dataImport.ImportAddFilesParams
import io.tolgee.dtos.dataImport.ImportFileDto
import io.tolgee.dtos.dataImport.SetFileNamespaceRequest
import io.tolgee.exceptions.BadRequestException
import io.tolgee.exceptions.ErrorException
import io.tolgee.exceptions.ErrorResponseBody
import io.tolgee.exceptions.NotFoundException
import io.tolgee.hateoas.dataImport.ImportAddFilesResultModel
import io.tolgee.hateoas.dataImport.ImportFileIssueModel
import io.tolgee.hateoas.dataImport.ImportFileIssueModelAssembler
import io.tolgee.hateoas.dataImport.ImportLanguageModel
import io.tolgee.hateoas.dataImport.ImportLanguageModelAssembler
import io.tolgee.hateoas.dataImport.ImportNamespaceModel
import io.tolgee.hateoas.dataImport.ImportTranslationModel
import io.tolgee.hateoas.dataImport.ImportTranslationModelAssembler
import io.tolgee.model.Language
import io.tolgee.model.dataImport.ImportFile
import io.tolgee.model.dataImport.ImportLanguage
import io.tolgee.model.enums.Scope
import io.tolgee.model.views.ImportFileIssueView
import io.tolgee.model.views.ImportLanguageView
import io.tolgee.model.views.ImportTranslationView
import io.tolgee.security.ProjectHolder
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.security.authorization.RequiresProjectPermissions
import io.tolgee.service.LanguageService
import io.tolgee.service.dataImport.ForceMode
import io.tolgee.service.dataImport.ImportService
import io.tolgee.service.dataImport.status.ImportApplicationStatus
Expand All @@ -50,7 +39,6 @@ import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.data.web.SortDefault
import org.springframework.hateoas.CollectionModel
import org.springframework.hateoas.PagedModel
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
Expand All @@ -59,10 +47,8 @@ import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestPart
Expand All @@ -82,17 +68,10 @@ class V2ImportController(
private val importService: ImportService,
private val authenticationFacade: AuthenticationFacade,
private val importLanguageModelAssembler: ImportLanguageModelAssembler,
private val importTranslationModelAssembler: ImportTranslationModelAssembler,
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
private val pagedLanguagesResourcesAssembler: PagedResourcesAssembler<ImportLanguageView>,
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
private val pagedTranslationsResourcesAssembler: PagedResourcesAssembler<ImportTranslationView>,
@Suppress("SpringJavaInjectionPointsAutowiringInspection")
private val pagedImportFileIssueResourcesAssembler: PagedResourcesAssembler<ImportFileIssueView>,
private val projectHolder: ProjectHolder,
private val languageService: LanguageService,
private val namespaceService: NamespaceService,
private val importFileIssueModelAssembler: ImportFileIssueModelAssembler,
private val streamingResponseBodyProvider: StreamingResponseBodyProvider,
) : Logging {
@PostMapping("", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
Expand Down Expand Up @@ -200,58 +179,6 @@ class V2ImportController(
return pagedLanguagesResourcesAssembler.toModel(languages, importLanguageModelAssembler)
}

@GetMapping("/result/languages/{languageId}")
@Operation(description = "Returns language prepared to import.", summary = "Get import language")
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun getImportLanguage(
@PathVariable("languageId") languageId: Long,
): ImportLanguageModel {
checkImportLanguageInProject(languageId)
val language = importService.findLanguageView(languageId) ?: throw NotFoundException()
return importLanguageModelAssembler.toModel(language)
}

@GetMapping("/result/languages/{languageId}/translations")
@Operation(description = "Returns translations prepared to import.", summary = "Get translations")
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun getImportTranslations(
@PathVariable("projectId") projectId: Long,
@PathVariable("languageId") languageId: Long,
@Parameter(
description =
"Whether only translations, which are in conflict " +
"with existing translations should be returned",
)
@RequestParam("onlyConflicts", defaultValue = "false")
onlyConflicts: Boolean = false,
@Parameter(
description =
"Whether only translations with unresolved conflicts" +
"with existing translations should be returned",
)
@RequestParam("onlyUnresolved", defaultValue = "false")
onlyUnresolved: Boolean = false,
@Parameter(description = "String to search in translation text or key")
@RequestParam("search")
search: String? = null,
@ParameterObject
@SortDefault("keyName")
pageable: Pageable,
): PagedModel<ImportTranslationModel> {
checkImportLanguageInProject(languageId)
val translations =
importService.getTranslationsView(
languageId,
pageable,
onlyConflicts,
onlyUnresolved,
search,
)
return pagedTranslationsResourcesAssembler.toModel(translations, importTranslationModelAssembler)
}

@DeleteMapping("")
@Operation(description = "Deletes prepared import data.", summary = "Delete")
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
Expand All @@ -260,134 +187,6 @@ class V2ImportController(
this.importService.deleteImport(projectHolder.project.id, authenticationFacade.authenticatedUser.id)
}

@DeleteMapping("/result/languages/{languageId}")
@Operation(description = "Deletes language prepared to import.", summary = "Delete language")
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun deleteLanguage(
@PathVariable("languageId") languageId: Long,
) {
val language = checkImportLanguageInProject(languageId)
this.importService.deleteLanguage(language)
}

@PutMapping("/result/languages/{languageId}/translations/{translationId}/resolve/set-override")
@Operation(
description = "Resolves translation conflict. The old translation will be overridden.",
summary = "Resolve conflict (override)",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun resolveTranslationSetOverride(
@PathVariable("languageId") languageId: Long,
@PathVariable("translationId") translationId: Long,
) {
resolveTranslation(languageId, translationId, true)
}

@PutMapping("/result/languages/{languageId}/translations/{translationId}/resolve/set-keep-existing")
@Operation(
description = "Resolves translation conflict. The old translation will be kept.",
summary = "Resolve conflict (keep existing)",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun resolveTranslationSetKeepExisting(
@PathVariable("languageId") languageId: Long,
@PathVariable("translationId") translationId: Long,
) {
resolveTranslation(languageId, translationId, false)
}

@PutMapping("/result/languages/{languageId}/resolve-all/set-override")
@Operation(
description = "Resolves all translation conflicts for provided language. The old translations will be overridden.",
summary = "Resolve all translation conflicts (override)",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun resolveTranslationSetOverride(
@PathVariable("languageId") languageId: Long,
) {
resolveAllOfLanguage(languageId, true)
}

@PutMapping("/result/languages/{languageId}/resolve-all/set-keep-existing")
@Operation(
description = "Resolves all translation conflicts for provided language. The old translations will be kept.",
summary = "Resolve all translation conflicts (keep existing)",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun resolveTranslationSetKeepExisting(
@PathVariable("languageId") languageId: Long,
) {
resolveAllOfLanguage(languageId, false)
}

@PutMapping("/result/files/{fileId}/select-namespace")
@Operation(
description = "Sets namespace for file to import.",
summary = "Select namespace",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun selectNamespace(
@PathVariable fileId: Long,
@RequestBody req: SetFileNamespaceRequest,
) {
val file = checkFileFromProject(fileId)
this.importService.selectNamespace(file, req.namespace)
}

@PutMapping("/result/languages/{importLanguageId}/select-existing/{existingLanguageId}")
@Operation(
description =
"Sets existing language to pair with language to import. " +
"Data will be imported to selected existing language when applied.",
summary = "Pair existing language",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun selectExistingLanguage(
@PathVariable("importLanguageId") importLanguageId: Long,
@PathVariable("existingLanguageId") existingLanguageId: Long,
) {
val existingLanguage = checkLanguageFromProject(existingLanguageId)
val importLanguage = checkImportLanguageInProject(importLanguageId)
this.importService.selectExistingLanguage(importLanguage, existingLanguage)
}

@PutMapping("/result/languages/{importLanguageId}/reset-existing")
@Operation(
description = "Resets existing language paired with language to import.",
summary = "Reset existing language pairing",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun resetExistingLanguage(
@PathVariable("importLanguageId") importLanguageId: Long,
) {
val importLanguage = checkImportLanguageInProject(importLanguageId)
this.importService.selectExistingLanguage(importLanguage, null)
}

@GetMapping("/result/files/{importFileId}/issues")
@Operation(
description = "Returns issues for uploaded file.",
summary = "Get file issues",
)
@RequiresProjectPermissions([Scope.TRANSLATIONS_VIEW])
@AllowApiAccess
fun getImportFileIssues(
@PathVariable("importFileId") importFileId: Long,
@ParameterObject pageable: Pageable,
): PagedModel<ImportFileIssueModel> {
checkFileFromProject(importFileId)
val page = importService.getFileIssues(importFileId, pageable)
return pagedImportFileIssueResourcesAssembler.toModel(page, importFileIssueModelAssembler)
}

@GetMapping("/all-namespaces")
@Operation(
description = "Returns all existing and imported namespaces",
Expand All @@ -404,9 +203,7 @@ class V2ImportController(
val importNamespaces = importService.getAllNamespaces(import.id)
val existingNamespaces = namespaceService.getAllInProject(projectId = projectHolder.project.id)
val result =
existingNamespaces
.map { it.name to ImportNamespaceModel(it.id, it.name) }
.toMap(mutableMapOf())
existingNamespaces.associateTo(mutableMapOf()) { it.name to ImportNamespaceModel(it.id, it.name) }
importNamespaces.filterNotNull().forEach { importNamespace ->
result.computeIfAbsent(importNamespace) {
ImportNamespaceModel(id = null, name = importNamespace)
Expand All @@ -429,46 +226,4 @@ class V2ImportController(

return assembler.toCollectionModel(result.values.sortedBy { it.name })
}

private fun resolveAllOfLanguage(
languageId: Long,
override: Boolean,
) {
val language = checkImportLanguageInProject(languageId)
importService.resolveAllOfLanguage(language, override)
}

private fun resolveTranslation(
languageId: Long,
translationId: Long,
override: Boolean,
) {
checkImportLanguageInProject(languageId)
return importService.resolveTranslationConflict(translationId, languageId, override)
}

private fun checkFileFromProject(fileId: Long): ImportFile {
val file = importService.findFile(fileId) ?: throw NotFoundException()
if (file.import.project.id != projectHolder.project.id) {
throw BadRequestException(io.tolgee.constants.Message.IMPORT_LANGUAGE_NOT_FROM_PROJECT)
}
return file
}

private fun checkLanguageFromProject(languageId: Long): Language {
val existingLanguage = languageService.getEntity(languageId)
if (existingLanguage.project.id != projectHolder.project.id) {
throw BadRequestException(io.tolgee.constants.Message.IMPORT_LANGUAGE_NOT_FROM_PROJECT)
}
return existingLanguage
}

private fun checkImportLanguageInProject(languageId: Long): ImportLanguage {
val language = importService.findLanguage(languageId) ?: throw NotFoundException()
val languageProjectId = language.file.import.project.id
if (languageProjectId != projectHolder.project.id) {
throw BadRequestException(io.tolgee.constants.Message.IMPORT_LANGUAGE_NOT_FROM_PROJECT)
}
return language
}
}

0 comments on commit 118ecf7

Please sign in to comment.