Skip to content

feat: Glossaries #3003

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

Draft
wants to merge 59 commits into
base: glossaries
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
2635fa7
feat: wip: initial work on glossaries - db schema, ui prototyping, re…
Anty0 Mar 21, 2025
93fbad1
revert some of the refractoring
Anty0 Mar 25, 2025
25287f6
chore: revert some more of insignificant changes
Anty0 Mar 25, 2025
210749e
chore: lint
Anty0 Mar 25, 2025
65c3f59
feat: implemented glossary create form
Anty0 Mar 27, 2025
43728e8
fix: lint
Anty0 Mar 27, 2025
ae644e8
fix: implemented glossary creation form validation and submit
Anty0 Mar 27, 2025
7b4fd39
feat: implement glossaries list view
Anty0 Mar 28, 2025
b17843f
feat: create term dialog, wip - glossary view
Anty0 Apr 1, 2025
0e59b59
fix: regenerate schema migrations
Anty0 Apr 1, 2025
d7f034f
feat: implement glossary term api and infinite list
Anty0 Apr 2, 2025
dab60a8
feat: wip - glossary view;
Anty0 Apr 3, 2025
9db5a46
feat: load selected translations as part of glossary terms
Anty0 Apr 4, 2025
c0af898
feat: implement glossary view
Anty0 Apr 10, 2025
054deda
feat: glossary and glossary term editing + other ui improvements
Anty0 Apr 11, 2025
183528a
feat: glossary empty view + close menu on click
Anty0 Apr 14, 2025
9df02f8
fix: merge header bar component from BaseView and BaseSettingsView - …
Anty0 Apr 14, 2025
26807bd
fix: missing feature checks
Anty0 Apr 14, 2025
542079e
fix: broken dialog + better missing feature banner
Anty0 Apr 14, 2025
7495335
fix: remove unused error translate
Anty0 Apr 14, 2025
dad15b9
fix: api and validation fixes
Anty0 Apr 15, 2025
00e91db
feat: highlight glossary terms in translation view and show tooltip o…
Anty0 Apr 22, 2025
c48e1f9
fix: properly center content of BaseSettingsView
Anty0 Apr 22, 2025
b82f473
feat: allow editing assigned projects
Anty0 Apr 22, 2025
865c400
fix: forward ref element type
Anty0 Apr 22, 2025
91fc656
fix: try fix forward ref component type in ee
Anty0 Apr 22, 2025
91c91bd
fix: use tooltip element in tooltip title instead of as tooltip compo…
Anty0 Apr 22, 2025
0727198
feat: properly handle term flags
Anty0 Apr 22, 2025
cd1e92f
fix: show highlights for base language only
Anty0 Apr 22, 2025
a8b5403
feat: suggest translation in glossary tooltip
Anty0 Apr 22, 2025
8d2c709
feat: confirmation dialog when changing term to non-translatable
Anty0 Apr 22, 2025
06af730
feat: allow delete glossary term from edit dialog
Anty0 Apr 22, 2025
cfc2fc3
fix: use default error handling for term delete
Anty0 Apr 23, 2025
b970f92
feat: batch delete of selected glossary terms
Anty0 Apr 23, 2025
ce9017b
fix: ui fixes - part 1
Anty0 Apr 23, 2025
6f230e8
feat: show glossary terms in right panel + ui fixes
Anty0 Apr 24, 2025
9cc0393
fix: ui fixes and improvements
Anty0 Apr 25, 2025
42e6267
fix: use small version of button in glossary panel
Anty0 Apr 25, 2025
a7b7dc3
fix: glossaryview pass search from query params properly
Anty0 Apr 28, 2025
24e172b
feat: tooltip for non-translatable terms
Anty0 Apr 29, 2025
258fc3c
fix: stuff broken by rebase
Anty0 Apr 29, 2025
0cca5ca
fix: display all languages for glossary by default
Anty0 Apr 29, 2025
df425f5
fix: glossary term preview spacing
Anty0 Apr 29, 2025
6b2d14e
fix: color and shadow of glossary empty view
Anty0 Apr 29, 2025
4607d80
fix: glossary term preview spacing fixes
Anty0 Apr 29, 2025
7425fa4
fix: save on enter
Anty0 Apr 29, 2025
528f292
feat: permissions handling for glossaries + refactoring and todo cleanup
Anty0 Apr 30, 2025
0540b8f
feat: glossary support for machine translation
Anty0 May 6, 2025
3b16d7c
fix: machine translation tests
Anty0 May 6, 2025
5331c07
fix: better handling of overlapping highlights
Anty0 May 6, 2025
15eb495
fix: code cleanup and fixes - part 1
Anty0 May 6, 2025
5d2d993
fix: lint and backend build
Anty0 May 6, 2025
d24bdee
chore: ignore autowiring issues related to EnabledFeaturesProvider an…
Anty0 May 6, 2025
47bf74a
fix: code cleanup and fixes - part 2
Anty0 May 7, 2025
dddbf9d
fix: code cleanup and refactoring - part 3
Anty0 May 12, 2025
bcd39a6
fix: rebase leftovers
Anty0 May 12, 2025
198c1ae
fix: typo
Anty0 May 12, 2025
f1192ed
feat: use context provider to pass organization and glossary on fe side
Anty0 May 12, 2025
4501441
feat: SimpleGlossaryModel for fetching list of glossaries without org…
Anty0 May 12, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@RestController
@RequestMapping("/v2/user")
@Tag(name = "User", description = "Manipulates currently authenticated user")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ package io.tolgee.api.v2.controllers.organization

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.dtos.cacheable.OrganizationLanguageDto
import io.tolgee.exceptions.NotFoundException
import io.tolgee.facade.ProjectWithStatsFacade
import io.tolgee.hateoas.language.OrganizationLanguageModel
import io.tolgee.hateoas.language.OrganizationLanguageModelAssembler
import io.tolgee.hateoas.project.ProjectModel
import io.tolgee.hateoas.project.ProjectModelAssembler
import io.tolgee.hateoas.project.ProjectWithStatsModel
import io.tolgee.model.views.ProjectWithLanguagesView
import io.tolgee.security.authorization.UseDefaultPermissions
import io.tolgee.service.language.LanguageService
import io.tolgee.service.organization.OrganizationService
import io.tolgee.service.project.ProjectService
import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.data.web.SortDefault
import org.springframework.hateoas.PagedModel
Expand All @@ -38,6 +43,9 @@ class OrganizationProjectController(
private val projectService: ProjectService,
private val projectModelAssembler: ProjectModelAssembler,
private val projectWithStatsFacade: ProjectWithStatsFacade,
private val languageService: LanguageService,
private val organizationLanguageModelAssembler: OrganizationLanguageModelAssembler,
private val pagedOrganizationLanguageAssembler: PagedResourcesAssembler<OrganizationLanguageDto>,
) {
@GetMapping("/{id:[0-9]+}/projects")
@Operation(
Expand Down Expand Up @@ -110,4 +118,22 @@ class OrganizationProjectController(
return getAllWithStatistics(pageable, search, organization.id)
} ?: throw NotFoundException()
}

@Operation(
summary = "Get all languages in use by projects owned by specified organization",
description = "Returns all languages in use by projects owned by specified organization",
)
@GetMapping("/{organizationId:[0-9]+}/languages")
@UseDefaultPermissions
fun getAllLanguagesInUse(
@ParameterObject
@SortDefault("base", direction = Sort.Direction.DESC)
@SortDefault("tag", direction = Sort.Direction.ASC)
pageable: Pageable,
@RequestParam("search") search: String?,
@PathVariable organizationId: Long,
): PagedModel<OrganizationLanguageModel> {
val languages = languageService.getPagedByOrganization(organizationId, pageable, search)
return pagedOrganizationLanguageAssembler.toModel(languages, organizationLanguageModelAssembler)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

@Suppress(names = ["MVCPathVariableInspection", "SpringJavaInjectionPointsAutowiringInspection"])
@Suppress("MVCPathVariableInspection", "SpringJavaInjectionPointsAutowiringInspection")
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.tolgee.service.organization.OrganizationService
import io.tolgee.service.security.UserPreferencesService
import org.springframework.stereotype.Component

@Suppress("SpringJavaInjectionPointsAutowiringInspection")
@Component
class PreferredOrganizationFacade(
private val authenticationFacade: AuthenticationFacade,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ class WebhookConfigModel(
description = """Date of the last webhook request.""",
)
var lastExecuted: Long?,
) : RepresentationModel<io.tolgee.hateoas.ee.webhooks.WebhookConfigModel>(), Serializable
) : RepresentationModel<WebhookConfigModel>(), Serializable
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.tolgee.hateoas.language

import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation

@Suppress("unused")
@Relation(collectionRelation = "languages", itemRelation = "language")
open class OrganizationLanguageModel(
@Schema(example = "Czech", description = "Language name in english")
val name: String,
@Schema(example = "cs-CZ", description = "Language tag according to BCP 47 definition")
var tag: String,
@Schema(example = "čeština", description = "Language name in this language")
var originalName: String? = null,
@Schema(example = "\uD83C\uDDE8\uD83C\uDDFF", description = "Language flag emoji as UTF-8 emoji")
var flagEmoji: String? = null,
@Schema(example = "false", description = "Whether is base language of any project")
var base: Boolean,
) : RepresentationModel<OrganizationLanguageModel>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.tolgee.hateoas.language

import io.tolgee.api.v2.controllers.organization.OrganizationProjectController
import io.tolgee.dtos.cacheable.OrganizationLanguageDto
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
import org.springframework.stereotype.Component

@Component
class OrganizationLanguageModelAssembler :
RepresentationModelAssemblerSupport<OrganizationLanguageDto, OrganizationLanguageModel>(
OrganizationProjectController::class.java,
OrganizationLanguageModel::class.java,
) {
override fun toModel(languageDto: OrganizationLanguageDto): OrganizationLanguageModel {
return OrganizationLanguageModel(
name = languageDto.name,
originalName = languageDto.originalName,
tag = languageDto.tag,
flagEmoji = languageDto.flagEmoji,
base = languageDto.base,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.tolgee.openApiDocs.OpenApiCloudExtension
import io.tolgee.openApiDocs.OpenApiEeExtension
import io.tolgee.openApiDocs.OpenApiOrderExtension
import io.tolgee.openApiDocs.OpenApiSelfHostedExtension
import io.tolgee.openApiDocs.OpenApiUnstableOperationExtension
import io.tolgee.security.authentication.AllowApiAccess
import org.springdoc.core.models.GroupedOpenApi
import org.springframework.web.method.HandlerMethod
Expand Down Expand Up @@ -74,9 +75,10 @@ class OpenApiGroupBuilder(
private fun addMethodExtensions() {
customizeOperations { operation, handlerMethod, _ ->
addOperationOrder(handlerMethod, operation)
addOperationEeExtension(handlerMethod, operation)
addSelfHostedOperationEeExtension(handlerMethod, operation)
addCloudOperationEeExtension(handlerMethod, operation)
addExtensionFor(handlerMethod, operation, OpenApiEeExtension::class.java, "x-ee")
addExtensionFor(handlerMethod, operation, OpenApiCloudExtension::class.java, "x-cloud")
addExtensionFor(handlerMethod, operation, OpenApiSelfHostedExtension::class.java, "x-self-hosted")
addExtensionFor(handlerMethod, operation, OpenApiUnstableOperationExtension::class.java, "x-unstable")
operation
}
}
Expand All @@ -91,40 +93,21 @@ class OpenApiGroupBuilder(
}
}

private fun addOperationEeExtension(
private fun <T : Annotation> addExtensionFor(
handlerMethod: HandlerMethod,
operation: Operation,
annotationClass: Class<T>,
extensionName: String,
value: ((T) -> Any?)? = null,
) {
val eeExtensionAnnotation =
handlerMethod.getMethodAnnotation(OpenApiEeExtension::class.java)
?: handlerMethod.method.declaringClass.getAnnotation(OpenApiEeExtension::class.java) ?: null
if (eeExtensionAnnotation != null) {
operation.addExtension("x-ee", true)
val annotation =
handlerMethod.getMethodAnnotation(annotationClass)
?: handlerMethod.method.declaringClass.getAnnotation(annotationClass) ?: null
if (annotation == null) {
return
}
}

private fun addCloudOperationEeExtension(
handlerMethod: HandlerMethod,
operation: Operation,
) {
val eeExtensionAnnotation =
handlerMethod.getMethodAnnotation(OpenApiCloudExtension::class.java)
?: handlerMethod.method.declaringClass.getAnnotation(OpenApiCloudExtension::class.java) ?: null
if (eeExtensionAnnotation != null) {
operation.addExtension("x-cloud", true)
}
}

private fun addSelfHostedOperationEeExtension(
handlerMethod: HandlerMethod,
operation: Operation,
) {
val eeExtensionAnnotation =
handlerMethod.getMethodAnnotation(OpenApiSelfHostedExtension::class.java)
?: handlerMethod.method.declaringClass.getAnnotation(OpenApiSelfHostedExtension::class.java) ?: null
if (eeExtensionAnnotation != null) {
operation.addExtension("x-self-hosted", true)
}
operation.addExtension(extensionName, value?.invoke(annotation) ?: true)
}

private fun addTagOrders() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.tolgee.component.machineTranslation.metadata
data class Metadata(
val examples: List<ExampleItem> = emptyList(),
val closeItems: List<ExampleItem> = emptyList(),
val glossaryTerms: List<TranslationGlossaryItem> = emptyList(),
val keyDescription: String?,
val projectDescription: String?,
val languageDescription: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.tolgee.component.machineTranslation.metadata

data class TranslationGlossaryItem(
val source: String,
val target: String? = null,
val description: String? = null,
val isNonTranslatable: Boolean? = null,
val isCaseSensitive: Boolean? = null,
val isAbbreviation: Boolean? = null,
val isForbiddenTerm: Boolean? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum class Feature {
TASKS,
SSO,
ORDER_TRANSLATION,

GLOSSARY,
;

companion object {
Expand Down
4 changes: 4 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ enum class Message {
PLAN_SEAT_LIMIT_EXCEEDED,
INSTANCE_NOT_USING_LICENSE_KEY,
INVALID_PATH,
GLOSSARY_NOT_FOUND,
GLOSSARY_TERM_NOT_FOUND,
GLOSSARY_TERM_TRANSLATION_NOT_FOUND,
GLOSSARY_NON_TRANSLATABLE_TERM_CANNOT_BE_TRANSLATED,
;

val code: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.tolgee.dtos.cacheable

interface OrganizationLanguageDto {
val name: String
val tag: String
val originalName: String?
val flagEmoji: String?
val base: Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ open class LanguageFilters {
description = """Filter languages without id""",
)
var filterNotId: List<Long>? = null

@field:Parameter(
description = """Filter languages by name or tag""",
)
var search: String? = null
}
4 changes: 4 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/model/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.api.ISimpleProject
import io.tolgee.model.automations.Automation
import io.tolgee.model.contentDelivery.ContentDeliveryConfig
import io.tolgee.model.contentDelivery.ContentStorage
import io.tolgee.model.glossary.Glossary
import io.tolgee.model.key.Key
import io.tolgee.model.key.Namespace
import io.tolgee.model.mtServiceConfig.MtServiceConfig
Expand Down Expand Up @@ -93,6 +94,9 @@ class Project(
@ActivityLoggedProp
var defaultNamespace: Namespace? = null

@ManyToMany(fetch = FetchType.LAZY, mappedBy = "assignedProjects")
var glossaries: MutableSet<Glossary> = mutableSetOf()

@ActivityLoggedProp
override var avatarHash: String? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ class ActivityRevision : java.io.Serializable {
*/
var projectId: Long? = null

// /**
// * Glossary of the change
// */
// var glossaryId: Long? = null // TODO

@OneToMany(mappedBy = "activityRevision")
var describingRelations: MutableList<ActivityDescribingEntity> = mutableListOf()

Expand Down
58 changes: 58 additions & 0 deletions backend/data/src/main/kotlin/io/tolgee/model/glossary/Glossary.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.tolgee.model.glossary

import io.tolgee.activity.annotation.ActivityLoggedEntity
import io.tolgee.activity.annotation.ActivityLoggedProp
import io.tolgee.model.Organization
import io.tolgee.model.Project
import io.tolgee.model.SoftDeletable
import io.tolgee.model.StandardAuditModel
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.JoinColumn
import jakarta.persistence.JoinTable
import jakarta.persistence.ManyToMany
import jakarta.persistence.ManyToOne
import jakarta.persistence.OneToMany
import jakarta.persistence.Table
import jakarta.persistence.Temporal
import jakarta.persistence.TemporalType
import jakarta.persistence.UniqueConstraint
import jakarta.validation.constraints.Size
import java.util.Date

@Entity
@ActivityLoggedEntity
@Table(
uniqueConstraints = [
UniqueConstraint(
columnNames = ["organization_owner_id", "name"],
),
],
)
class Glossary(
@field:Size(min = 3, max = 50)
@ActivityLoggedProp
var name: String = "",
@ActivityLoggedProp
@Column(nullable = false)
var baseLanguageTag: String? = null,
) : StandardAuditModel(), SoftDeletable {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "glossary")
var terms: MutableList<GlossaryTerm> = mutableListOf()

@ManyToOne(optional = true, fetch = FetchType.LAZY)
lateinit var organizationOwner: Organization

@ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST])
@JoinTable(
name = "glossary_project",
joinColumns = [JoinColumn(name = "project_id")],
inverseJoinColumns = [JoinColumn(name = "glossary_id")],
)
var assignedProjects: MutableSet<Project> = mutableSetOf()

@Temporal(TemporalType.TIMESTAMP)
override var deletedAt: Date? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.tolgee.model.glossary

import io.tolgee.activity.annotation.ActivityLoggedEntity
import io.tolgee.activity.annotation.ActivityLoggedProp
import io.tolgee.model.StandardAuditModel
import jakarta.persistence.*

@Entity
@ActivityLoggedEntity
class GlossaryTerm(
@Column(columnDefinition = "text")
@ActivityLoggedProp
var description: String? = null,
) : StandardAuditModel() {
@ManyToOne
lateinit var glossary: Glossary

@OneToMany(fetch = FetchType.LAZY, cascade = [CascadeType.REMOVE], mappedBy = "term")
var translations: MutableList<GlossaryTermTranslation> = mutableListOf()

@ActivityLoggedProp
var flagNonTranslatable: Boolean = false

@ActivityLoggedProp
var flagCaseSensitive: Boolean = false

@ActivityLoggedProp
var flagAbbreviation: Boolean = false

@ActivityLoggedProp
var flagForbiddenTerm: Boolean = false
}
Loading
Loading