Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

Anty0
Copy link
Collaborator

@Anty0 Anty0 commented Oct 7, 2025

Closes #3103

Summary by CodeRabbit

  • New Features

    • Import glossary terms from CSV (add or replace) via a new import dialog and top-bar controls.
    • Export glossary to CSV from glossary view (download button).
    • Enhanced file uploads: drag-and-drop area, single-file selector, file chips, mode selector, and paste support.
    • Permission-aware dialog controls and import result reporting.
  • Bug Fixes

    • Safer download handling with guaranteed cleanup.
  • Tests

    • New unit and e2e tests for glossary import/export and CSV parsing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

Walkthrough

Adds glossary import/export: new backend controllers, services, CSV parser/exporter, repository methods, activity/message enums, and delimiter detection; frontend import dialog, hooks, drag/drop file components, updated glossary UI, tests, and E2E flows; streaming CSV export and multipart import (replace/add) with bulk persistence.

Changes

Cohort / File(s) Summary
Activity & Constants
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt, backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
Added GLOSSARY_IMPORT(true) activity enum constant and FILE_PROCESSING_FAILED message constant.
Backend Controllers
ee/backend/app/src/main/kotlin/.../GlossaryExportController.kt, .../GlossaryImportController.kt
New REST endpoints: GET .../glossaries/{id}/export (stream CSV) and POST .../import (multipart CSV upload, remove_existing_terms flag, records GLOSSARY_IMPORT).
Backend Services
ee/backend/app/src/main/kotlin/.../GlossaryExportService.kt, .../GlossaryImportService.kt, .../GlossaryTermService.kt, .../GlossaryTermTranslationService.kt
New export/import services, import transaction logic, delimiter detection integration, bulk save/delete and language-tag helpers.
CSV Parser/Exporter & Headers
ee/backend/app/src/main/kotlin/.../formats/csv/in/GlossaryCSVParser.kt, .../out/GlossaryCSVExporter.kt, .../formats/csv/Headers.kt, backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
New parser and exporter with flag handling and header constants; CsvDelimiterDetector moved to util and uses resource-managed stream access.
Repositories
ee/backend/app/src/main/kotlin/.../repository/glossary/GlossaryTermRepository.kt, .../GlossaryTermTranslationRepository.kt
Added deleteAllByGlossary, findByGlossaryWithTranslations, and updated findDistinctLanguageTagsByGlossary to accept a Glossary parameter.
Import Models / Result
ee/backend/app/src/main/kotlin/.../formats/ImportGlossaryTerm.kt, ee/backend/app/src/main/kotlin/.../data/glossary/GlossaryImportResult.kt
Added ImportGlossaryTerm data class and GlossaryImportResult DTO (imported count, @Schema).
CSV Processing Integration
backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt, backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
CsvFileProcessor now uses CsvDelimiterDetector; TableParser simplified to unconditionally drop the first header and first row.
Frontend - Import UI & Hooks
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx, webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts, webapp/src/ee/glossary/hooks/useGlossaryExport.ts
New import dialog component and controller hook, export hook to trigger download; dialog supports replace/add and multipart upload.
Frontend - Glossary View Changes
webapp/src/ee/glossary/components/*.tsx, webapp/src/ee/glossary/views/GlossaryView.tsx
Renamed onCreate → onCreateTerm across components, added onImport, integrated dialog controllers, added import/export buttons and handlers, and adjusted rendering for dialogs.
Frontend - File Components & Utilities
webapp/src/component/common/{DragDropArea,FileDropzone,SingleFileDropzone,FileDropzoneSelectedFile,ModeSelector}.tsx, webapp/src/fixtures/useOnFilePaste.ts, webapp/src/fixtures/FileUploadFixtures.ts
New reusable drag/drop and file components, paste hook; removed public dataTransferItemsToArray helper (inlined).
Screenshot & Import Refactors
webapp/src/views/projects/translations/Screenshots/*, webapp/src/views/projects/import/*
Replaced custom dropzones with DragDropArea, switched to FilesType shape, adapted validate/upload flows, added paste support in import view.
Frontend - Small Changes & Cleanup
webapp/src/views/projects/export/downloadExported.ts, webapp/src/ee/billing/Invoices/DownloadButton.tsx
Improved download cleanup using nested try/finally and URL revocation; response handling refactor.
API Schema & Types
webapp/src/service/apiSchema.generated.ts, e2e/cypress/support/dataCyType.d.ts
OpenAPI client updated with export/import operations and GlossaryImportResult schema; Cypress data-cy identifiers extended.
Tests & Test Utilities
ee/backend/tests/src/test/kotlin/.../GlossaryTestDataBuilder.kt, .../GlossaryTestUtil.kt, .../csv/in/GlossaryCSVParserTest.kt, .../csv/out/GlossaryCSVExporterTest.kt, e2e/cypress/.../E2Glossary*.ts, e2e/cypress/e2e/glossary/importExport.cy.ts
New DSL test builders, assertion utilities, unit tests for parser/exporter, E2E page objects and tests for import/export flows.
Deleted / Removed
webapp/src/views/projects/import/component/ImportFileDropzone.tsx
Removed legacy ImportFileDropzone component and its public exports.

Sequence Diagram(s)

sequenceDiagram
    participant UI as User Interface
    participant Dialog as GlossaryImportDialog
    participant API as GlossaryImportController
    participant Service as GlossaryImportService
    participant Parser as GlossaryCSVParser
    participant Repo as GlossaryTermRepository

    UI->>Dialog: select CSV & submit (file + remove_existing_terms)
    Dialog->>API: POST /v2/organizations/{org}/glossaries/{id}/import (multipart)
    API->>Service: importCsv(glossary, inputStream)
    Service->>Parser: parse(inputStream) (delimiter detect)
    Parser-->>Service: List<ImportGlossaryTerm>
    alt remove_existing_terms == true
        Service->>Repo: deleteAllByGlossary(glossary)
    end
    Service->>Repo: saveAll(terms), saveAll(translations)
    Service-->>API: imported count
    API-->>Dialog: GlossaryImportResult
    Dialog-->>UI: success / close
Loading
sequenceDiagram
    participant UI as User Interface
    participant Hook as useGlossaryExport
    participant API as GlossaryExportController
    participant Service as GlossaryExportService
    participant Exporter as GlossaryCSVExporter

    UI->>Hook: triggerExport()
    Hook->>API: GET /v2/organizations/{org}/glossaries/{id}/export
    API->>Service: exportCsv(glossary)
    Service->>Exporter: export() -> InputStream
    Exporter-->>Service: CSV InputStream
    Service-->>API: InputStream
    API-->>Hook: streaming response (blob)
    Hook->>UI: download CSV file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • JanCizmar

Poem

🐰 I hopped through headers, terms, and CSV rows,

I parsed and exported where the glossary grows,
Dropzones catch carrots (files) with a click,
Replace or add—import does the trick,
A tiny rabbit cheers as the translator shows!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: glossary import and export" is directly aligned with the changeset, which implements comprehensive glossary import and export functionality across both backend and frontend layers. The title clearly summarizes the primary feature being added: new REST endpoints for importing glossaries from CSV files and exporting glossaries to CSV format, along with supporting UI components, services, and data models. The title is concise, uses standard commit convention, and avoids vague or noise-inducing language like emojis or generic terms. A developer scanning commit history would immediately understand that this PR introduces glossary import and export capabilities.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jirikuchynka/glossary-import-export

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (6)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt (1)

7-10: Verify if flag mutability is necessary.

The flag properties are declared as var, making them mutable after construction. If these properties don't need to be modified after the ImportGlossaryTerm is created, consider changing them to val for immutability and thread safety.

If mutability is not required, apply this diff:

   val term: String?,
   val description: String?,
   val translations: Map<String, String>,
-  var flagNonTranslatable: Boolean?,
-  var flagCaseSensitive: Boolean?,
-  var flagAbbreviation: Boolean?,
-  var flagForbiddenTerm: Boolean?,
+  val flagNonTranslatable: Boolean?,
+  val flagCaseSensitive: Boolean?,
+  val flagAbbreviation: Boolean?,
+  val flagForbiddenTerm: Boolean?,
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

52-54: Consider exposing CSV delimiter as an API parameter.

The CSV delimiter is currently hardcoded to comma (default in GlossaryImportService.importCsv). Consider adding an optional request parameter to support other delimiters (e.g., semicolon, tab) for broader CSV format compatibility.

Example enhancement:

   @RequestParam(name = "override_existing_terms", required = false, defaultValue = "false")
   overrideExistingTerms: Boolean,
+  @RequestParam(name = "delimiter", required = false, defaultValue = ",")
+  delimiter: String,
 ): GlossaryImportResult {
   val organization = organizationHolder.organization
   val glossary = glossaryService.get(organization.id, glossaryId)
   if (overrideExistingTerms) {
     glossaryTermService.deleteAllByGlossary(glossary)
   }

   val imported = file.inputStream.use { input ->
-    glossaryImportService.importCsv(glossary, input)
+    glossaryImportService.importCsv(glossary, input, delimiter.firstOrNull() ?: ',')
   }
   return GlossaryImportResult(imported)
 }
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)

40-64: LGTM! Consider more explicit resource management.

The export implementation is correct and uses proper HTTP headers for CSV download. The streaming approach is efficient for large exports.

For more explicit resource management, consider using Kotlin's use for automatic stream closure even if exceptions occur:

   return ResponseEntity.ok().headers(headers).body(
     streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
-      IOUtils.copy(stream, out)
-      stream.close()
-      out.close()
+      stream.use { input ->
+        IOUtils.copy(input, out)
+      }
     },
   )

Note: The OutputStream out is managed by Spring's streaming response framework and doesn't need explicit closing in the lambda.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt (1)

13-17: LGTM! Consider pagination for very large glossaries.

The export implementation is straightforward and correct. All terms are loaded into memory before CSV generation.

For organizations with very large glossaries (thousands of terms), loading all data at once could cause memory pressure. Consider implementing batch processing or streaming if performance issues arise in production. For typical use cases, the current approach should be sufficient.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

15-16: Stabilize column order for language tags

Sets can be unordered; sort for consistent exports.

-    val languageTagsWithoutBaseLanguage by lazy { languageTags - glossary.baseLanguageTag }
+    val languageTagsWithoutBaseLanguage by lazy { (languageTags - glossary.baseLanguageTag).toList().sorted() }

Ensure tests (if any) expect the new deterministic order (sorted lexicographically).

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

14-17: Specify UTF‑8 and close the CSVReader/InputStream

Current code uses platform default charset and never closes the reader/input stream. Create the reader with UTF‑8 and close it within parse().

Suggested refactor inside parse():

fun parse(): List<ImportGlossaryTerm> {
  val csvParser = CSVParserBuilder().withSeparator(delimiter).build()
  InputStreamReader(input, StandardCharsets.UTF_8).use { isr ->
    CSVReaderBuilder(isr).withCSVParser(csvParser).build().use { csv ->
      val rawRows = csv.readAll()
      if (rawRows.isEmpty()) return emptyList()
      val headers = rawRows.first().map { it.trim() }
      val idxTerm = headers.indexOfFirst { it.equals("Term", ignoreCase = true) }.takeIf { it >= 0 }
      // ...compute other indices similarly...
      val specialHeaderIndices = listOfNotNull(idxTerm, /* ... */)
      val idxTranslations = (0 until headers.size).filter { it !in specialHeaderIndices }
      return rawRows.drop(1).mapNotNull { row ->
        // build ImportGlossaryTerm using safe access as above
      }
    }
  }
}

And add imports:

import java.io.InputStreamReader
import java.nio.charset.StandardCharsets

Confirm callers aren’t expected to close the InputStream themselves after parse(); with this refactor, parser takes ownership during parsing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b5315c and 3091b01.

📒 Files selected for processing (14)
  • backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (10)
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1)

83-83: LGTM!

The new GLOSSARY_IMPORT activity type follows the same pattern as the existing IMPORT constant (line 37), correctly sets onlyCountsInList = true, and is logically placed among other glossary-related activities.

backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt (2)

21-21: Approve unconditional header drop

All existing uses of parser.tableParser.rows (e.g. in CsvFileProcessor and CsvImportFormatDetectorTest) correctly treat rows as data only; no code depends on the header being included as a data row.


14-14: Unconditionally drop header column for languages The change to headers?.drop(1) ?: emptyList() handles single‐column inputs correctly and removes a redundant size check; TableParser.languages is only used internally.

ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)

19-25: LGTM! Cleaner API design.

The refactoring to accept a Glossary entity instead of separate IDs simplifies both the method signature and the query logic.

ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)

32-32: LGTM!

The method signature is clear and follows Spring Data JPA conventions.

ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)

5-8: LGTM!

Simple and well-documented DTO for the import result.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (1)

20-31: LGTM! Well-structured refactoring.

The refactoring maintains backward compatibility while providing a cleaner internal API that accepts the domain entity directly.

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

38-55: LGTM! Proper transactional handling.

The @Transactional annotation ensures that if the import fails after deleting existing terms, all changes will be rolled back, preventing data loss.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt (2)

69-81: Helper for “with translations” looks good

Method pair (by ids and by Glossary) reads clean and cohesive.


228-238: Bulk delete/save helpers OK

Transactional wrappers around repository methods are appropriate for import/export flows.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

28-39: Optimize translation lookups for better performance.

Each call to translations.find { it.languageTag == ... } is O(n). For glossaries with many terms and languages, this results in O(terms × languages × translations) complexity. Grouping translations into a map would reduce this to O(terms × languages).

Consider refactoring asColumns() to build a translation map once per term:

 fun GlossaryTerm.asColumns(): Array<String> {
+    val translationMap = translations.associateBy { it.languageTag }
     return arrayOf(
-        translations.find { it.languageTag == glossary.baseLanguageTag }?.text ?: "",
+        translationMap[glossary.baseLanguageTag]?.text ?: "",
         description,
         flagNonTranslatable.toString(),
         flagCaseSensitive.toString(),
         flagAbbreviation.toString(),
         flagForbiddenTerm.toString(),
     ) + languageTagsWithoutBaseLanguage.map { languageTag ->
-        translations.find { it.languageTag == languageTag }?.text ?: ""
+        translationMap[languageTag]?.text ?: ""
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e98ee95 and f5e8085.

📒 Files selected for processing (3)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build frontend 🏗️
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

41-48: Previous issues have been resolved!

The resource leak and encoding concerns from the earlier review have been properly addressed:

  • .use { } now ensures the CSV writer is closed
  • byteInputStream(Charsets.UTF_8) guarantees UTF-8 encoding

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

76-83: Don't coerce unknown boolean tokens to false.

This issue was flagged in a previous review but remains unaddressed. Unrecognized values (e.g., typos like "tru" or invalid entries) are silently mapped to false, which can corrupt data. Return null for unknown values instead.

Apply this diff:

 private fun parseBoolean(value: String?): Boolean? {
     if (value.isNullOrBlank()) return null
     return when (value.trim().lowercase()) {
         "true", "1", "yes", "y", "t" -> true
         "false", "0", "no", "n", "f" -> false
-        else -> false
+        else -> null
     }
 }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

70-74: Add negative index check for defensive programming.

While fixing findHeaderIndex prevents -1 from being passed, getSafe should defensively check both array bounds.

Apply this diff:

 fun Array<String>.getSafe(idx: Int?): String? {
     if (idx == null) return null
-    if (idx >= size) return null
+    if (idx < 0 || idx >= size) return null
     return this[idx]
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5e8085 and 4e422fc.

📒 Files selected for processing (1)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

28-39: Consider caching translation lookups in a map for better performance.

The repeated find calls (lines 30, 37) iterate through the translations collection for each language tag. For terms with many translations or glossaries with many languages, this becomes inefficient.

Apply this diff to optimize translation lookups:

 fun GlossaryTerm.asColumns(): Array<String> {
+    val translationMap = translations.associateBy { it.languageTag }
     return arrayOf(
-        translations.find { it.languageTag == glossary.baseLanguageTag }?.text ?: "",
+        translationMap[glossary.baseLanguageTag]?.text ?: "",
         description,
         flagNonTranslatable.toString(),
         flagCaseSensitive.toString(),
         flagAbbreviation.toString(),
         flagForbiddenTerm.toString(),
     ) + languageTagsWithoutBaseLanguage.map { languageTag ->
-        translations.find { it.languageTag == languageTag }?.text ?: ""
+        translationMap[languageTag]?.text ?: ""
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e422fc and c690f82.

📒 Files selected for processing (1)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (21)
  • GitHub Check: E2E testing ‍🔎 (15, 14)
  • GitHub Check: E2E testing ‍🔎 (15, 12)
  • GitHub Check: E2E testing ‍🔎 (15, 13)
  • GitHub Check: E2E testing ‍🔎 (15, 6)
  • GitHub Check: E2E testing ‍🔎 (15, 11)
  • GitHub Check: E2E testing ‍🔎 (15, 8)
  • GitHub Check: E2E testing ‍🔎 (15, 7)
  • GitHub Check: E2E testing ‍🔎 (15, 5)
  • GitHub Check: E2E testing ‍🔎 (15, 2)
  • GitHub Check: E2E testing ‍🔎 (15, 1)
  • GitHub Check: E2E testing ‍🔎 (15, 4)
  • GitHub Check: E2E testing ‍🔎 (15, 3)
  • GitHub Check: E2E testing ‍🔎 (15, 0)
  • GitHub Check: BT ‍🔎 (ktlint:test)
  • GitHub Check: BT ‍🔎 (data:test)
  • GitHub Check: BT ‍🔎 (security:test)
  • GitHub Check: BT ‍🔎 (server-app:runWebsocketTests)
  • GitHub Check: BT ‍🔎 (server-app:runWithoutEeTests)
  • GitHub Check: BT ‍🔎 (ee-test:test)
  • GitHub Check: BT ‍🔎 (server-app:runStandardTests)
  • GitHub Check: BT ‍🔎 (server-app:runContextRecreatingTests)
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

30-30: Verify that the base language translation is always present.

The code gracefully handles missing base language translations by defaulting to an empty string. However, if glossary.baseLanguageTag is not in a term's translations, the "Term" column will be empty, which might indicate a data consistency issue.

Please confirm whether terms are guaranteed to have a translation in the base language, or if empty base terms are expected. If the former, consider adding validation when loading glossary data to ensure data integrity.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

76-83: Don't coerce unrecognized boolean values to false.

This issue was previously flagged but remains unaddressed. Mapping unknown tokens to false (line 81) can silently corrupt data. Return null instead to surface the issue.

Apply this diff:

 private fun parseBoolean(value: String?): Boolean? {
     if (value.isNullOrBlank()) return null
     return when (value.trim().lowercase()) {
         "true", "1", "yes", "y", "t" -> true
         "false", "0", "no", "n", "f" -> false
-        else -> false
+        else -> null
     }
 }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

70-74: Add negative index bounds check for complete safety.

While callers currently pass valid indices, a method named getSafe should defensively guard both bounds.

Apply this diff:

 fun Array<String>.getSafe(idx: Int?): String? {
     if (idx == null) return null
+    if (idx < 0) return null
     if (idx >= size) return null
     return this[idx]
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c690f82 and a22cbea.

📒 Files selected for processing (1)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

66-68: LGTM! The -1 to null conversion is properly handled.

The ?.takeIf { it >= 0 } correctly converts the -1 result from indexOfFirst to null when no header is found, preventing potential issues downstream.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)

54-56: Prevent duplicate base-language translations
You already add the base-language translation above, but the forEach copies every entry—including the base language again. With the unique constraint on (term_id, language_tag), this will explode as soon as the CSV contains a column for the base language. Skip glossary.baseLanguageTag here.

-    glossaryTerm.translations.forEach { (languageTag, text) ->
-      translations.add(GlossaryTermTranslation(languageTag, text).apply { term = this@applyFrom })
-    }
+    glossaryTerm.translations
+      .filterKeys { it != glossary.baseLanguageTag }
+      .forEach { (languageTag, text) ->
+        translations.add(GlossaryTermTranslation(languageTag, text).apply { term = this@applyFrom })
+      }
🧹 Nitpick comments (1)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)

15-18: Permission check is correct.

The permission check appropriately restricts term creation to users with OWNER or MAINTAINER roles, or admin users. This aligns with the glossary feature's access control requirements.

The permission check logic (['OWNER', 'MAINTAINER'].includes(...)) is duplicated in useGlossaryImportDialog. Consider extracting it into a shared helper like useCanManageGlossary() to reduce duplication:

// New shared hook
export const useCanManageGlossary = () => {
  const { preferredOrganization } = usePreferredOrganization();
  const isUserAdmin = useIsAdmin();
  
  return ['OWNER', 'MAINTAINER'].includes(
    preferredOrganization?.currentUserRole || ''
  ) || isUserAdmin;
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4c0ae and 9baef6d.

📒 Files selected for processing (16)
  • e2e/cypress/support/dataCyType.d.ts (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/component/PublicEnabledFeaturesProvider.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (2 hunks)
  • webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryTermsList.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (4 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryView.tsx (2 hunks)
  • webapp/src/service/apiSchema.generated.ts (4 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • webapp/src/views/projects/export/downloadExported.ts
🧰 Additional context used
🧬 Code graph analysis (9)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
  • GlossaryViewBody (16-73)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (3)
webapp/src/globalContext/helpers.tsx (1)
  • usePreferredOrganization (31-43)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
  • GlossaryImportDialog (90-366)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (2)
webapp/src/views/projects/export/downloadExported.ts (1)
  • downloadExported (3-24)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
  • useGlossaryExport (14-48)
webapp/src/component/layout/BaseViewAddButton.tsx (1)
  • BaseViewAddButton (6-23)
webapp/src/service/apiSchema.generated.ts (1)
webapp/src/service/billingApiSchema.generated.ts (1)
  • components (218-1464)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
  • useGlossaryTermCreateDialog (5-35)
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)
  • useGlossaryImportDialog (5-34)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
  • GlossaryViewTopbar (28-94)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build backend 🏗️
  • GitHub Check: Build frontend 🏗️
🔇 Additional comments (10)
ee/backend/app/src/main/kotlin/io/tolgee/ee/component/PublicEnabledFeaturesProvider.kt (1)

21-21: LGTM! GLOSSARY feature now enabled by default for free tier.

The initialization of forceEnabled with setOf(Feature.GLOSSARY) enables glossary functionality by default in the public/free tier. This aligns with the PR's objective to add glossary import/export capabilities and makes the feature accessible without subscription gating.

webapp/src/ee/glossary/views/GlossaryView.tsx (1)

4-4: LGTM! Clean separation of concerns.

The removal of dialog state management from this component improves the architecture. Dialog handling is now delegated to GlossaryViewBody via hooks (useGlossaryTermCreateDialog, useGlossaryImportDialog), making the component simpler and more focused on layout.

Also applies to: 42-42

webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (2)

51-51: LGTM! Prop rename aligns with new hook-based architecture.

The rename from onCreate to onCreateTerm is consistent with the new hook-based dialog management pattern (useGlossaryTermCreateDialog, useGlossaryImportDialog) used across glossary components.

Also applies to: 58-58, 73-74


90-90: LGTM! Import option now visible by default.

Removing sx={{ display: 'none' }} from the second card makes the import option visible in the empty state, improving discoverability of the glossary import feature.

webapp/src/ee/glossary/components/GlossaryTermsList.tsx (1)

80-81: LGTM! Props updated to support new hook-based dialogs.

The prop updates (onCreateonCreateTerm, added onImport) align with the new hook-based dialog management pattern and are correctly passed through to GlossaryEmptyListMessage.

Also applies to: 94-95, 154-155

e2e/cypress/support/dataCyType.d.ts (1)

332-341: LGTM! Test selectors added for glossary import/export flows.

The new data-cy selectors support E2E testing of the glossary export button, import dialog, dropzone, file controls, import modes, and action buttons.

webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)

20-28: LGTM! Conditional rendering enforces permissions.

The dialog is only rendered when canCreate is true, createTermDialogOpen is true, and preferredOrganization is defined, ensuring proper access control and preventing errors when organization context is missing.

webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)

15-18: LGTM! Permission check and conditional rendering are correct.

The permission check and conditional rendering pattern matches useGlossaryTermCreateDialog, ensuring consistent access control across glossary management operations. The dialog receives the hasExistingTerms prop to conditionally show import mode options.

Also applies to: 20-27

webapp/src/ee/glossary/components/GlossaryViewBody.tsx (2)

8-9: LGTM! Clean integration of glossary creation and import hooks.

The integration of useGlossaryTermCreateDialog and useGlossaryImportDialog follows React best practices. The component correctly:

  • Imports and invokes hooks at the top level
  • Renders dialog components using the conditional pattern (false or React element)
  • Passes derived handlers to child components consistently

This refactor provides better separation of concerns by moving dialog management logic into dedicated hooks.

Also applies to: 34-34, 41-42, 45-46, 60-61


36-37: Import is already gated until glossary data loads
GlossaryViewTopbar only renders when terms is defined, and EmptyState hides the import button while loading is true, so users can’t click import before total is set. Differentiating undefined vs. 0 for hasExistingTerms isn’t needed.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)

7-7: Remove unused import.

The usePreferredOrganization hook is no longer used after the refactor on line 46.

Apply this diff to remove the unused import:

-import { usePreferredOrganization } from 'tg.globalContext/helpers';
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)

57-62: Don't close the servlet output stream.

Spring manages the lifecycle of the StreamingResponseBody output stream. Calling out.close() can trigger IllegalStateException when the container tries to close it again. Remove the explicit out.close() call and let Spring handle it.

Apply this diff to fix the issue:

     return ResponseEntity.ok().headers(headers).body(
       streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
         stream.use { IOUtils.copy(stream, out) }
-        out.close()
       },
     )
🧹 Nitpick comments (3)
webapp/src/component/common/ModeSelector.tsx (1)

42-76: Consider accessibility improvements for screen readers.

While the component is functional and keyboard-accessible via the Radio elements, the current structure has accessibility gaps:

  1. The Radio button lacks a proper label association (no <label> wrapping or aria-labelledby).
  2. The focusable element (Radio) doesn't match the visual clickable area (entire card), which can confuse users navigating by keyboard.
  3. The description text is not programmatically associated with the radio button, so screen readers won't announce it as part of the option.

To improve accessibility, consider wrapping the content in a <label> element or using aria-labelledby and aria-describedby to associate the title and description with the Radio. For example:

<StyledRadioOption
  key={option.value}
  selected={value === option.value}
  onClick={() => onChange(option.value)}
  role="radio"
  aria-checked={value === option.value}
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      onChange(option.value);
    }
  }}
>
  <Box display="flex" flexDirection="column" width="100%">
    <Box
      display="flex"
      justifyContent="space-between"
      alignItems="center"
      mb={0.5}
    >
      <Typography variant="body1" fontWeight={500} id={`option-title-${option.value}`}>
        {option.title}
      </Typography>
      <Radio
        checked={value === option.value}
        data-cy={option.dataCy}
        size="small"
        sx={{ padding: 0.5, pointerEvents: 'none' }}
        aria-hidden="true"
      />
    </Box>
    <Typography variant="body2" color="text.secondary" id={`option-desc-${option.value}`}>
      {option.description}
    </Typography>
  </Box>
</StyledRadioOption>

This approach makes the entire card focusable and keyboard-navigable while ensuring screen readers properly announce the option's title and description.

webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (2)

27-33: Consider simplifying the dialog rendering logic.

The createTermDialog is only rendered when isOpen is true (which means createTermDialogOpen is true), yet the dialog still receives open={createTermDialogOpen}. This prop is redundant at this point.

Consider simplifying to:

 const createTermDialog = isOpen && (
   <GlossaryTermCreateDialog
-    open={createTermDialogOpen}
+    open={true}
     onClose={() => setCreateTermDialogOpen(false)}
     onFinished={() => setCreateTermDialogOpen(false)}
   />
 );

Or if the Dialog component defaults open to true, you could omit it entirely.


35-39: Consider renaming the return value for clarity.

The returned createTermDialogOpen property contains the computed isOpen value (which combines dialog state with permissions), not the raw createTermDialogOpen state. This naming could be confusing for consumers who might expect it to reflect only the dialog state.

Consider renaming to better reflect what it represents:

 return {
   onCreateTerm: canCreate ? onCreateTerm : undefined,
-  createTermDialogOpen: isOpen,
+  isDialogOpen: isOpen,
   createTermDialog,
 };

Or alternatively, return both the raw state and permission check:

 return {
   onCreateTerm: canCreate ? onCreateTerm : undefined,
-  createTermDialogOpen: isOpen,
+  createTermDialogOpen,
+  canCreate,
   createTermDialog,
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9baef6d and dad60e8.

📒 Files selected for processing (19)
  • e2e/cypress/support/dataCyType.d.ts (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
  • webapp/src/views/projects/export/downloadExported.ts
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts
🧰 Additional context used
🧬 Code graph analysis (7)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
  • GlossaryTermCreateDialog (29-85)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
  • hasDescription (39-41)
  • hasTranslation (47-49)
  • isTranslatable (59-61)
  • isNotCaseSensitive (71-73)
  • isNotAbbreviation (79-81)
  • isNotForbidden (87-89)
  • isNotTranslatable (63-65)
  • isCaseSensitive (67-69)
  • isAbbreviation (75-77)
  • isForbidden (83-85)
  • hasNoDescription (43-45)
  • hasNoFlags (91-96)
  • hasNoTerm (55-57)
  • hasNoTranslation (51-53)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (4)
webapp/src/component/common/ModeSelector.tsx (2)
  • ModeOption (23-28)
  • ModeSelector (36-77)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/SingleFileDropzone.tsx (1)
  • SingleFileDropzone (13-32)
webapp/src/component/common/FileDropzone.tsx (2)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (45-96)
webapp/src/component/common/SingleFileDropzone.tsx (1)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (53-68)
  • FileDropzone (70-253)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (2)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
  • glossary (10-47)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (10)
  • glossary (67-69)
  • withBaseLanguageTag (51-53)
  • withDescription (79-81)
  • withTranslations (87-89)
  • translatable (91-93)
  • caseSensitive (95-97)
  • abbreviation (99-101)
  • forbiddenTerm (103-105)
  • withTranslation (83-85)
  • createGlossary (8-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (9)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)

1-302: LGTM!

The test suite provides comprehensive coverage of CSV parsing scenarios, including edge cases like empty rows, case-insensitive headers, various boolean flag formats, and delimiter variations. The test structure is clear and uses well-designed assertion helpers.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

1-47: LGTM!

The exporter implementation properly handles CSV generation with deterministic language tag ordering, explicit UTF-8 encoding, and proper resource management through the use block. The boolean flag inversion is clearly documented.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)

1-97: LGTM!

The test utility provides a clean, fluent DSL for asserting glossary term properties. The helper methods are well-named and focused, making tests more readable and maintainable.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1)

1-310: LGTM!

The test suite thoroughly validates CSV export functionality across multiple scenarios, including delimiter variations, missing translations, special character escaping, and deterministic language tag ordering. The tests are well-structured and use the builder DSL effectively.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)

1-126: LGTM!

The test data builder DSL is well-designed with a fluent API that makes test setup clear and concise. The builder pattern implementation properly manages relationships between glossaries and terms, and the extension functions provide good composability.

webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)

46-46: LGTM! Cleaner ownership resolution.

Using glossary.organizationOwner.id directly is more appropriate than relying on the preferred organization, and it eliminates the need for the null-assertion operator.

webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (3)

1-9: LGTM! Clean hook setup.

The imports and initial context retrieval are well-structured and appropriate for the hook's functionality.


16-20: LGTM! Straightforward state management.

The dialog state management is clean and appropriate for this use case.


22-23: Confirm admin bypass of organization preference or validate current scope.

The create and import dialogs both require isGlossaryUnderPreference even for isUserAdmin (e.g. useGlossaryTermCreateDialog.tsx:23, useGlossaryImportDialog.tsx:23). If global admins should operate across all organizations, let isUserAdmin short-circuit the preference check; otherwise confirm this restriction is intentional.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/views/projects/export/downloadExported.ts (1)

41-41: Fix stateful regex bug with global flag.

The g flag causes exec() to maintain state via lastIndex between calls. Since extensionRegex is module-scoped, subsequent calls to parseExtension() will continue from where the previous call left off, causing intermittent match failures. Additionally, the m flag is unnecessary for single-line header parsing.

Apply this diff to remove the problematic flags:

-const extensionRegex = /^.*\.(\w+)"$/gm;
+const extensionRegex = /^.*\.(\w+)"$/;

Alternatively, for more robust Content-Disposition parsing that handles various formats:

-const extensionRegex = /^.*\.(\w+)"$/gm;
+const extensionRegex = /filename="[^"]*\.(\w+)"/;
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

83-89: Don’t coerce unknown boolean tokens to false

Treating any unrecognized value as false silently corrupts data—for example, an invalid translatable token becomes “non-translatable” on import. Keep known true/false mappings, but return null for everything else so callers can detect the missing/invalid flag.

Recommended change:

   private fun parseBoolean(value: String?): Boolean? {
     if (value.isNullOrBlank()) return null
     return when (value.trim().lowercase()) {
       "true", "1", "yes", "y", "t" -> true
       "false", "0", "no", "n", "f" -> false
-      else -> false
+      else -> null
     }
   }
🧹 Nitpick comments (6)
webapp/src/views/projects/export/downloadExported.ts (1)

30-39: Consider more robust Content-Disposition parsing.

The current regex pattern assumes the header value ends with a quoted filename and doesn't handle edge cases like:

  • Unquoted filenames (valid per RFC 6266)
  • filename* parameter (UTF-8 encoded filenames)
  • Missing filename attribute

While the fallback to format.extension provides resilience, consider using a more comprehensive parser or library for Content-Disposition headers.

Example improvement:

const parseExtension = (response: Response) => {
  const contentDisposition = response.headers.get('Content-Disposition');
  if (contentDisposition) {
    // Try filename* first (RFC 5987), then filename
    const filenameMatch = /filename\*?="?([^";\n]*)"?/i.exec(contentDisposition);
    if (filenameMatch) {
      const filename = filenameMatch[1];
      const extensionMatch = /\.(\w+)$/.exec(filename);
      if (extensionMatch) {
        return extensionMatch[1];
      }
    }
  }
  return null;
};
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)

134-161: Update expectations for invalid boolean tokens

After the parser stops coercing unknown tokens to false, the fourth row in this test should no longer assert negative flags. Please switch the expectations for Term4 to check that the flags remain unset (e.g., hasNoFlags() or explicit null checks).

webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)

7-7: Remove unused import.

The usePreferredOrganization import is no longer used after switching to derive organization context from glossary.organizationOwner.id.

Apply this diff:

-import { usePreferredOrganization } from 'tg.globalContext/helpers';
webapp/src/views/projects/import/component/ImportFileDropzone.tsx (1)

91-97: Good: prevent default on drag over; consider more robust drag depth handling

The onDragOver guard is correct. To avoid drag state flicker from child enter/leave, consider a drag depth counter (as suggested for DragDropArea) instead of comparing event targets.

If aligning with DragDropArea, you can:

  • increment depth on dragEnter, decrement on dragLeave;
  • set active when depth > 0;
  • reset to 0 on drop.

Also applies to: 128-128

webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)

36-41: Expose canImport for consumers; clarify return shape

Returning onImport as undefined requires consumers to null-check. Expose a boolean for simpler usage and consider naming the open flag consistently.

Apply:

   return {
-    onImport: canImport ? onImport : undefined,
-    importDialogOpen: isOpen,
-    importDialog,
+    canImport,
+    onImport: canImport ? onImport : undefined,
+    isOpen,
+    importDialog,
   };
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)

44-44: Replace deprecated React.VFC with React.FC.

React.VFC was removed in React 18. Use React.FC instead.

Apply this diff:

-export const GlossaryImportDialog: React.VFC<Props> = ({
+export const GlossaryImportDialog: React.FC<Props> = ({
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9baef6d and 089687b.

📒 Files selected for processing (19)
  • e2e/cypress/support/dataCyType.d.ts (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
  • GlossaryImportDialog (44-148)
webapp/src/component/common/SingleFileDropzone.tsx (1)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (53-68)
  • FileDropzone (70-253)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
  • hasDescription (39-41)
  • hasTranslation (47-49)
  • isTranslatable (59-61)
  • isNotCaseSensitive (71-73)
  • isNotAbbreviation (79-81)
  • isNotForbidden (87-89)
  • isNotTranslatable (63-65)
  • isCaseSensitive (67-69)
  • isAbbreviation (75-77)
  • isForbidden (83-85)
  • hasNoDescription (43-45)
  • hasNoFlags (91-96)
  • hasNoTerm (55-57)
  • hasNoTranslation (51-53)
webapp/src/component/common/FileDropzone.tsx (2)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (45-96)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (4)
webapp/src/component/common/ModeSelector.tsx (2)
  • ModeOption (23-28)
  • ModeSelector (36-77)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/SingleFileDropzone.tsx (1)
  • SingleFileDropzone (13-32)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (2)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (11)
  • glossary (69-71)
  • withBaseLanguageTag (53-55)
  • glossaryTerm (124-128)
  • withDescription (81-83)
  • withTranslations (89-91)
  • translatable (93-95)
  • caseSensitive (97-99)
  • abbreviation (101-103)
  • forbiddenTerm (105-107)
  • withTranslation (85-87)
  • createGlossary (8-13)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
  • glossary (10-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (5)
e2e/cypress/support/dataCyType.d.ts (1)

332-338: LGTM! Clean addition of glossary import/export test selectors.

The new data-cy selectors follow established naming conventions, maintain alphabetical order, and comprehensively cover the UI elements for the glossary import/export feature (export button, import button, dialog, mode switches, and action buttons).

webapp/src/views/projects/export/downloadExported.ts (1)

11-27: LGTM: Proper resource cleanup with try/finally.

The try/finally block ensures the object URL is always revoked, preventing memory leaks. This is a good improvement over the previous implementation.

webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)

46-46: No null check needed for glossary.organizationOwner: GlossaryModel defines organizationOwner as a required, non-nullable property per the generated API schema.

Likely an incorrect or invalid review comment.

webapp/src/component/common/ModeSelector.tsx (1)

1-77: LGTM! Clean and accessible implementation.

The component is well-structured with:

  • Proper TypeScript generics for flexible option types
  • Theme-aware styling with hover states
  • Redundant click handlers (box + radio) ensuring good UX and accessibility
  • Clear separation of concerns
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)

104-120: acceptedFileTypes prop signature is correct; CsvLogo is a React component matching FileType.icon.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (6)
webapp/src/service/apiSchema.generated.ts (1)

8721-8747: Fix export response media type

The export endpoint still advertises "application/json" for its 200 response even though it streams a CSV file. Client SDKs generated from this schema will try to JSON‑decode the CSV stream and fail. Please declare the real media type (e.g. "text/csv" or another appropriate non‑JSON type) for the success response.

-      200: {
-        content: {
-          "application/json": components["schemas"]["StreamingResponseBody"];
-        };
-      };
+      200: {
+        content: {
+          "text/csv": components["schemas"]["StreamingResponseBody"];
+        };
+      };
e2e/cypress/support/dataCyType.d.ts (1)

335-341: Unused glossary-import-mode selectors remain unresolved.

The previous review identified that glossary-import-mode-add and glossary-import-mode-replace are unused in the UI. Since this PR is still in draft, either:

  1. Add the corresponding data-cy attributes to the import mode controls in the UI
  2. Remove these declarations if the mode selection UI won't be implemented

Verify if the other glossary selectors are used:

#!/bin/bash
# Description: Verify usage of glossary export/import selectors (excluding mode selectors already flagged)

echo "Checking usage of glossary import/export selectors..."
for selector in "glossary-export-button" "glossary-import-button" "glossary-import-cancel-button" "glossary-import-dialog" "glossary-import-submit-button"; do
  echo "---"
  echo "Searching for: $selector"
  rg -n "data-cy=['\"]$selector['\"]" -g '*.{ts,tsx,js,jsx}' || echo "  No matches found"
done
webapp/src/component/common/SingleFileDropzone.tsx (1)

5-11: Minor typing/readability nits

  • Prefer a SingleFile alias (FilesType[number]) over FilesType[0].
  • Use nullish coalescing for brevity: onFileSelect(newFiles[0] ?? null).
+type SingleFile = FilesType[number];
 export type SingleFileDropzoneProps = Omit<
   FileDropzoneProps,
   'files' | 'onFilesSelect' | 'maxFiles'
 > & {
-  file: FilesType[0] | null;
-  onFileSelect: (file: FilesType[0] | null) => void;
+  file: SingleFile | null;
+  onFileSelect: (file: SingleFile | null) => void;
 };
@@
-  const handleFilesSelect = (newFiles: FilesType) => {
-    onFileSelect(newFiles.length > 0 ? newFiles[0] : null);
-  };
+  const handleFilesSelect = (newFiles: FilesType) => {
+    onFileSelect(newFiles[0] ?? null);
+  };

Also applies to: 20-22

webapp/src/component/common/DragDropArea.tsx (2)

71-101: Stabilize drag state with a depth counter; reset on drop

Target equality is fragile and can flicker. Track depth and reset reliably.

-  const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null);
-  const [dragEnterTarget, setDragEnterTarget] = useState<EventTarget | null>(
-    null
-  );
+  const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null);
+  const [dragDepth, setDragDepth] = useState(0);
@@
-  const handleDragEnter = (e: React.DragEvent) => {
-    if (!active) return;
+  const handleDragEnter = (e: React.DragEvent) => {
+    if (!active) {
+      e.preventDefault();
+      e.stopPropagation();
+      return;
+    }
     e.preventDefault();
     e.stopPropagation();
-    setDragEnterTarget(e.target);
+    setDragDepth((d) => d + 1);
@@
-  const handleDragLeave = (e: React.DragEvent) => {
-    if (!active) return;
+  const handleDragLeave = (e: React.DragEvent) => {
+    if (!active) {
+      e.preventDefault();
+      e.stopPropagation();
+      return;
+    }
     e.preventDefault();
     e.stopPropagation();
-    if (e.target === dragEnterTarget) {
-      setDragOver(null);
-      setDragEnterTarget(null);
-    }
+    setDragDepth((d) => Math.max(0, d - 1));
+    if (dragDepth <= 1) setDragOver(null);
   };
@@
-  const handleDrop = async (e: React.DragEvent) => {
-    if (!active) return;
+  const handleDrop = async (e: React.DragEvent) => {
+    if (!active) {
+      e.preventDefault();
+      e.stopPropagation();
+      return;
+    }
     e.preventDefault();
     e.stopPropagation();
@@
     onFilesReceived(files);
     setDragOver(null);
-    setDragEnterTarget(null);
+    setDragDepth(0);
   };

Also applies to: 108-117


122-160: Add basic keyboard/ARIA support and prevent default when inactive

Make the surface operable via keyboard and non-destructive when disabled.

   return (
-    <Box
+    <Box
       position="relative"
       overflow="visible"
-      onClick={onClick}
+      onClick={onClick}
+      role={onClick ? 'button' : undefined}
+      tabIndex={onClick ? 0 : -1}
+      aria-disabled={!active || undefined}
+      onKeyDown={(e) => {
+        if (!onClick) return;
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          onClick();
+        }
+      }}
       data-cy={dataCy}
       onDragEnter={handleDragEnter}
       onDragLeave={handleDragLeave}
       onDragOver={handleDragOver}
       onDrop={handleDrop}
     >
webapp/src/component/common/FileDropzone.tsx (1)

86-92: Reset file input after selection to allow re-selecting the same file

Without clearing the input value, choosing the same file again will not trigger onChange.

Apply this diff:

   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const selectedFiles = e.target.files;
     if (selectedFiles && selectedFiles.length > 0) {
       const fileArray = Array.from(selectedFiles);
       handleFilesReceived(fileArray.map((f) => ({ file: f, name: f.name })));
+      e.currentTarget.value = '';
     }
   };
🧹 Nitpick comments (13)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (2)

32-45: Consider clarifying the unused path parameter.

The organizationId path parameter is declared but never used. Instead, the code correctly uses organizationHolder.organization.id (line 47), which is the right approach for security—it ensures users can only import into their own organization's glossaries. However, having the unused parameter in the path can be confusing for API consumers who might expect it to be validated or used.

Consider either:

  • Removing organizationId from the path if it's not semantically necessary for the API design
  • Or keeping it for REST convention but adding a comment explaining why it's not directly used (authentication context takes precedence)

46-56: Solid implementation with proper resource management.

The method body correctly:

  • Uses authenticated organization context for security
  • Ensures atomic delete+import via @Transactional
  • Manages file input stream with Kotlin's use{} (try-with-resources equivalent)

Optional improvements:

  • Consider adding explicit validation that the file is not empty before processing
  • Document the expected CSV format in the @Operation annotation or API docs
  • For better UX, consider returning more detailed results (e.g., rows skipped, validation errors) instead of just the imported count
webapp/src/component/common/FileDropzoneSelectedFile.tsx (2)

45-53: Add accessible name to remove button

Provide aria-label so screen readers announce the action.

-      <IconButton
+      <IconButton
         size="small"
         onClick={(e) => {
           e.stopPropagation();
           onRemove?.();
         }}
+        aria-label={`Remove ${file.name}`}
         data-cy={`file-dropzone-remove-button`}
       >

42-44: Expose full filename on hover

Long names may be truncated; add title for discoverability.

-      <Typography variant="body2" sx={{ flex: 1 }}>
+      <Typography variant="body2" sx={{ flex: 1 }} title={file.name}>
         {file.name}
       </Typography>
webapp/src/views/projects/import/component/ImportFileInput.tsx (3)

193-199: Use native click() instead of dispatchEvent

click() is simpler and more reliable for file inputs.

-              <Button
-                onClick={() =>
-                  fileRef.current?.dispatchEvent(new MouseEvent('click'))
-                }
+              <Button
+                onClick={() => fileRef.current?.click()}
                 variant="outlined"
                 color="primary"
               >

153-158: Optional: Make the whole drop area open the picker on click

Improves UX; consistent with other dropzones.

-    <DragDropArea
-      onFilesReceived={onNewFiles}
-      data-cy="dropzone"
-      active={!props.isProgressOverlayActive}
-      maxItems={MAX_FILE_COUNT}
-    >
+    <DragDropArea
+      onFilesReceived={onNewFiles}
+      data-cy="dropzone"
+      active={!props.isProgressOverlayActive}
+      maxItems={MAX_FILE_COUNT}
+      onClick={() => fileRef.current?.click()}
+    >

151-187: Scope TS suppression to the non-standard attribute, not the whole return

Avoid broad @ts-ignore; annotate just the input element for webkitdirectory.

-  /* @ts-ignore */
   return (
@@
-              <input
+              {/* @ts-expect-error: non-standard attribute not in React types */}
+              <input
                 key={resetKey}
                 data-cy={'import-file-input'}
                 type="file"
                 style={{ display: 'none' }}
                 ref={fileRef}
                 onChange={(e) => onFileSelected(e)}
                 multiple
                 webkitdirectory
               />
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)

31-38: Remove no-op onClick

Unnecessary handler; omit to reduce noise.

-    <DragDropArea
-      onFilesReceived={validateAndUpload}
-      onClick={() => {}}
+    <DragDropArea
+      onFilesReceived={validateAndUpload}
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)

127-129: Use native click() instead of dispatchEvent

Simpler and consistent with other places.

-  function openFiles() {
-    fileRef.current?.dispatchEvent(new MouseEvent('click'));
-  }
+  function openFiles() {
+    fileRef.current?.click();
+  }

85-93: Avoid as any if API typing allows File/Blob

If the OpenAPI types accept File/Blob, drop the cast.

-                  screenshot: file.file as any,
+                  screenshot: file.file,
webapp/src/component/common/FileDropzone.tsx (3)

58-69: Document or validate extension format assumption

The code assumes extension includes the leading dot (e.g., ".csv"):

  • Line 60 uses endsWith(config.extension), which would incorrectly match if the dot is missing (e.g., extension "sv" would match filename "data.asv")
  • Line 128 passes extensions to the accept attribute, which per HTML spec requires a leading dot for extension-based file types

Consider adding TypeScript branding, JSDoc, or runtime validation to ensure extensions are properly formatted.

Example validation in the component:

  const findFileType = (fileName: string) =>
    acceptedFileTypes.find((config) =>
      fileName.toLowerCase().endsWith(config.extension.toLowerCase())
    );
+
+ // Validate extensions format on mount (development only)
+ if (process.env.NODE_ENV === 'development') {
+   acceptedFileTypes.forEach((fileType) => {
+     if (!fileType.extension.startsWith('.')) {
+       console.warn(
+         `FileDropzone: extension "${fileType.extension}" should start with a dot`
+       );
+     }
+   });
+ }

71-84: Consider notifying users when files exceed the limit

Line 81 silently truncates files beyond maxFiles. If a user drags 5 files but maxFiles is 1, only the first file is kept without notification, which may be confusing.

Optional enhancement:

  const handleFilesReceived = (receivedFiles: FilesType) => {
    const validFiles = receivedFiles.filter((receivedFile) => {
      if (!isValidFileType(receivedFile.name)) {
        messageService.error(<T keyName="error_message_invalid_file_type" />);
        return false;
      }
      return true;
    });

    if (validFiles.length > 0) {
      const newFiles = [...files, ...validFiles].slice(0, maxFiles);
+     const droppedCount = files.length + validFiles.length - newFiles.length;
+     if (droppedCount > 0) {
+       messageService.error(
+         <T keyName="error_message_too_many_files" params={{ count: droppedCount }} />
+       );
+     }
      onFilesSelect(newFiles);
    }
  };

33-36: Consider stronger typing for icon component

The icon property uses React.ComponentType<any>, which sacrifices type safety. If all icons share a common interface (e.g., MUI's SvgIconComponent or a custom IconProps), consider using that type instead.

Example:

+import { SvgIconComponent } from '@mui/material';
+
 export type FileType = {
   extension: string;
-  icon: React.ComponentType<any>;
+  icon: SvgIconComponent;
 };

Note: Only apply this if the icon components conform to SvgIconComponent. Otherwise, you could define a minimal shared interface.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 089687b and fbf86ef.

📒 Files selected for processing (13)
  • e2e/cypress/support/dataCyType.d.ts (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/FileDropzoneSelectedFile.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/fixtures/FileUploadFixtures.ts (0 hunks)
  • webapp/src/service/apiSchema.generated.ts (4 hunks)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx (0 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2 hunks)
  • webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (5 hunks)
💤 Files with no reviewable changes (2)
  • webapp/src/fixtures/FileUploadFixtures.ts
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
webapp/src/views/projects/import/component/ImportFileInput.tsx (1)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (62-161)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • ALLOWED_UPLOAD_TYPES (17-17)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
  • FilesType (42-45)
  • getFilesAsync (3-40)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (38-47)
  • FileDropzone (49-199)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/apiSchema.generated.ts (1)
webapp/src/service/billingApiSchema.generated.ts (1)
  • components (218-1464)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (62-161)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • MAX_FILE_COUNT (16-16)
webapp/src/component/common/FileDropzone.tsx (4)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (36-57)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (62-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (4)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

23-31: LGTM! Clean controller structure.

The class declaration follows Spring and Kotlin best practices with constructor-based dependency injection and clear REST API boundaries.

webapp/src/component/common/SingleFileDropzone.tsx (1)

9-22: Single-file typing and mapping look correct

Props and handler match FileDropzone API; single-file wrapping is correct.

webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)

1-10: Approve FilesType migration
All validateAndUpload call sites now use FilesType (verified in useScreenshotUpload.tsx).

webapp/src/component/common/FileDropzone.tsx (1)

49-198: Clean implementation with good separation of concerns

The component demonstrates solid patterns:

  • Proper ref management for the hidden file input
  • Conditional rendering based on file selection state
  • Appropriate use of stopPropagation for nested click handlers
  • Integration with DragDropArea for drag-and-drop functionality
  • Data-cy attributes for testability
  • User feedback via messageService for validation errors

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
webapp/src/component/common/FileDropzone.tsx (1)

85-91: Reset file input after selection to allow re-selecting the same file

Without clearing the input value, choosing the same file again won’t trigger onChange.

Apply:

   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const selectedFiles = e.target.files;
     if (selectedFiles && selectedFiles.length > 0) {
       const fileArray = Array.from(selectedFiles);
       handleFilesReceived(fileArray.map((f) => ({ file: f, name: f.name })));
+      // allow selecting the same file again
+      e.currentTarget.value = '';
     }
   };
🧹 Nitpick comments (2)
webapp/src/fixtures/useOnFilePaste.ts (1)

6-18: Consider simplifying the file collection logic.

The manual loop through clipboardData.files can be simplified using Array.from() for cleaner code.

Apply this diff to simplify the implementation:

 const pasteListener = (e: ClipboardEvent) => {
-  const files: File[] = [];
   if (!e.clipboardData?.files.length) {
     return;
   }
-  for (let i = 0; i < e.clipboardData.files.length; i++) {
-    const item = e.clipboardData.files.item(i);
-    if (item) {
-      files.push(item);
-    }
-  }
+  const files = Array.from(e.clipboardData.files);
   onFilePaste(files.map((f) => ({ file: f, name: f.name })));
 };
webapp/src/component/common/FileDropzone.tsx (1)

110-119: Accessibility: add aria-label to remove button (in FileDropzoneSelectedFile)

The remove IconButton lives in FileDropzoneSelectedFile and lacks an aria-label. Add an accessible label using the file name.

Proposed change in webapp/src/component/common/FileDropzoneSelectedFile.tsx:

-      <IconButton
+      <IconButton
         size="small"
         onClick={(e) => {
           e.stopPropagation();
           onRemove?.();
         }}
         data-cy={`file-dropzone-remove-button`}
+        aria-label={`Remove ${file.name}`}
       >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fbf86ef and 886397a.

📒 Files selected for processing (5)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/fixtures/useOnFilePaste.ts (1 hunks)
  • webapp/src/views/projects/import/ImportView.tsx (1 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (36-57)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (62-161)
webapp/src/views/projects/import/ImportView.tsx (2)
webapp/src/views/projects/import/hooks/useImportDataHelper.tsx (1)
  • useImportDataHelper (28-138)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (4)
webapp/src/views/projects/import/ImportView.tsx (2)

25-25: LGTM!

The import statement correctly adds the new useOnFilePaste hook following the project's import path conventions.


29-29: LGTM!

The hook invocation correctly integrates paste functionality into the import flow by passing dataHelper.onNewFiles as the callback handler.

webapp/src/fixtures/useOnFilePaste.ts (1)

20-24: LGTM!

The event listener setup and cleanup are correctly implemented, preventing memory leaks by removing the listener on unmount.

webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)

33-45: No null checks required for glossary.organizationOwner.id. The GlossaryProvider only renders its children once glossary.data is loaded, and the GlossaryModel schema guarantees both organizationOwner.id and id are defined.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)

312-312: LGTM! Consider grouping with related constants.

The new FILE_PROCESSING_FAILED constant is correctly added and follows the enum's naming convention. It aligns well with the PR's glossary import/export functionality.

For improved organization, you might consider positioning it near related file/import error constants like CANNOT_PARSE_FILE (line 68), IMPORT_FAILED (line 221), or NO_DATA_TO_IMPORT (line 223), though placing it at the end is also acceptable as it minimizes merge conflicts.

webapp/src/fixtures/useOnFilePaste.ts (1)

11-16: Consider simplifying the FileList iteration.

The standard for-loop works correctly, but you could simplify it using modern array methods.

Apply this diff for a more concise implementation:

-      for (let i = 0; i < e.clipboardData.files.length; i++) {
-        const item = e.clipboardData.files.item(i);
-        if (item) {
-          files.push(item);
-        }
-      }
-      onFilePaste(files.map((f) => ({ file: f, name: f.name })));
+      const clipboardFiles = Array.from(e.clipboardData.files);
+      onFilePaste(clipboardFiles.map((f) => ({ file: f, name: f.name })));

This eliminates the need for the temporary files array and the null check, since Array.from() handles the FileList conversion cleanly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 886397a and ec4dbe2.

📒 Files selected for processing (9)
  • backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/FileDropzoneSelectedFile.tsx (1 hunks)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/fixtures/useOnFilePaste.ts (1 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
  • webapp/src/component/common/FileDropzoneSelectedFile.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (39-48)
  • FileDropzone (50-208)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (37-60)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (62-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (3)
webapp/src/fixtures/useOnFilePaste.ts (1)

5-25: Stale closure issue fixed.

The dependency array now correctly includes onFilePaste, resolving the previous stale closure concern. The listener will properly update when the callback changes.

Note: This approach re-registers the listener whenever onFilePaste changes. If the parent component doesn't memoize the callback (e.g., with useCallback), this could cause frequent re-registrations. If that becomes a concern, consider the ref-based approach suggested in the previous review to avoid re-attaching the DOM listener.

webapp/src/component/common/SingleFileDropzone.tsx (1)

9-22: Props now wrap the single file correctly

Using FilesType[number] for the prop and unwrapping the first element keeps the dropzone contract intact without nesting arrays. Nicely aligned.

webapp/src/component/common/FileDropzone.tsx (1)

68-111: Paste flow now reuses full validation pipeline

Routing useOnFilePaste through handleFilesReceived keeps the drag/drop, paste, and browse paths consistent on type checks and limits. Looks solid.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)

37-37: Remove unnecessary double type cast.

The res.blob() method already returns a Promise<Blob>, so the double cast as any as Blob is unnecessary.

Apply this diff:

-          const url = URL.createObjectURL(data as any as Blob);
+          const url = URL.createObjectURL(data);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec4dbe2 and 0036779.

📒 Files selected for processing (4)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/ee/billing/Invoices/DownloadButton.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts
  • webapp/src/component/common/ModeSelector.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (3)
webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)

38-52: LGTM! Nested try/finally ensures proper cleanup.

The nested try/finally blocks guarantee that:

  1. The anchor element is removed even if the click fails
  2. The blob URL is revoked after a delay to avoid Safari download issues

The 7000ms delay is conservative but safe for ensuring downloads complete before URL cleanup.

webapp/src/views/projects/export/downloadExported.ts (2)

37-37: LGTM! Regex reset prevents state issues.

The explicit reset of lastIndex is necessary because extensionRegex uses the global flag (g), which maintains state between exec() calls. The comment clearly explains the purpose.


9-31: LGTM! Download flow refactored with proper cleanup and Safari fix.

The nested try/finally structure ensures:

  1. The anchor element is removed after the click
  2. The blob URL is revoked after a 7000ms delay

The 7000ms delay addresses the Safari zero-byte download issue flagged in the previous review (which suggested setTimeout(..., 0)). While more conservative than the suggested 0ms, the longer delay ensures downloads complete reliably across all browsers before URL cleanup.

Based on past review comments.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1)

15-21: Resource leak: detector's input stream is never closed.

The detector is initialized with an input stream (line 15), reads from it when detector.delimiter is accessed (line 19), but never closes the stream. Since a second stream is created for the parser (line 18), the first stream should be explicitly closed after delimiter detection.

Apply this diff to ensure the stream is closed:

-    val detector = CsvDelimiterDetector(context.file.data.inputStream())
+    val delimiter = context.file.data.inputStream().use { stream ->
+      CsvDelimiterDetector(stream).delimiter
+    }
     val parser =
       CsvFileParser(
         inputStream = context.file.data.inputStream(),
-        delimiter = detector.delimiter,
+        delimiter = delimiter,
         languageFallback = firstLanguageTagGuessOrUnknown,
       )
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)

11-11: Resource leak: reader and bufferedReader are never closed.

The reader() and buffered() calls create resources that remain open after reading the first line. Since the delimiter property is lazily evaluated and the input stream is provided externally, the detector should close its internal readers to prevent resource leaks.

Apply this diff to properly close resources:

   val delimiter by lazy {
-    val headerLine = inputStream.reader().buffered().lineSequence().firstOrNull() ?: ""
+    val headerLine = inputStream.reader().use { reader ->
+      reader.buffered().use { buffered ->
+        buffered.lineSequence().firstOrNull() ?: ""
+      }
+    }
     val counts =
       DELIMITERS.map { delimiter ->
♻️ Duplicate comments (3)
webapp/src/service/apiSchema.generated.ts (1)

8723-8752: Fix export response media type

GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export returns a CSV stream, but the schema still advertises "application/json" with StreamingResponseBody. Any generated client will try to JSON-decode the stream and crash at runtime. Please expose the real media type (e.g. "text/csv" or the exact type you stream) so clients handle it correctly.

webapp/src/component/common/DragDropArea.tsx (2)

70-72: Drag state tracking via event target is fragile.

The dragEnterTarget comparison on line 94 can cause flickering when dragging over child elements. A drag depth counter would be more reliable.

This issue was raised in a previous review. As per the past comment, replace dragEnterTarget with a dragCounter (number) that increments on dragenter and decrements on dragleave, setting dragOver when the counter is positive.

Also applies to: 94-97


120-159: Missing keyboard accessibility for the drop area.

The drop zone lacks keyboard navigation support. Users who cannot use a mouse cannot activate the file picker or interact with the drop area.

This issue was raised in a previous review. As per the past comment, add:

  • role="button" or appropriate ARIA role
  • tabIndex={0} to make it focusable
  • onKeyDown handler to activate onClick on Enter/Space
  • Consider adding visual focus indicators

Apply this diff:

     <Box
       position="relative"
       overflow="visible"
       onClick={onClick}
       data-cy="dropzone"
+      role="button"
+      tabIndex={onClick ? 0 : undefined}
+      onKeyDown={(e) => {
+        if (onClick && (e.key === 'Enter' || e.key === ' ')) {
+          e.preventDefault();
+          onClick();
+        }
+      }}
       onDragEnter={handleDragEnter}
       onDragLeave={handleDragLeave}
       onDragOver={handleDragOver}
       onDrop={handleDrop}
     >
🧹 Nitpick comments (9)
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)

12-21: Consider handling quoted strings in delimiter detection.

The current heuristic counts delimiter occurrences in the header line, which can be misled by delimiters inside quoted values. For example, a CSV with header "Name","Description,with,comma","Value" would count 4 commas as field separators when only 2 are actual delimiters.

While the simple heuristic works for typical well-formed CSVs, consider enhancing the logic to respect quoted strings for more robust delimiter detection.

webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)

33-33: Address empty onClick in ScreenshotDropzone
ScreenshotDropzone.tsx:33 sets onClick={() => {}}, which is a no-op. Either remove this prop to disable clicks or implement a file-picker trigger (e.g. use a hidden <input> and call inputRef.current?.click(), as in webapp/src/component/common/FileDropzone.tsx).

e2e/cypress/e2e/glossary/importExport.cy.ts (5)

5-7: Remove unused imports to satisfy lints.

E2GlossariesView and E2GlossaryCreateEditDialog are unused. Clean them up to fix static warnings.

-import { E2GlossariesView } from '../../compounds/glossaries/E2GlossariesView';
 import { E2GlossaryView } from '../../compounds/glossaries/E2GlossaryView';
-import { E2GlossaryCreateEditDialog } from '../../compounds/glossaries/E2GlossaryCreateEditDialog';

23-38: Wait on the import API for determinism.

Add an intercept and assert 200 before DOM checks to reduce flakiness.

   it('imports into an empty glossary', () => {
     login('Owner');
     const view = new E2GlossaryView();
     view.findAndVisit(data, 'Owner', 'Empty Glossary');

+    cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as(
+      'importGlossary'
+    );
     const importDialog = view.openImportDialogWhenGlossaryIsEmpty();
     importDialog.selectFile('glossary/import_basic.csv');
     importDialog.submit();
+    cy.wait('@importGlossary').its('response.statusCode').should('eq', 200);

     gcy('glossary-term-list-item')
       .filter(':contains("Imported Term One")')
       .should('be.visible');
     gcy('glossary-term-list-item')
       .filter(':contains("Imported Term Two")')
       .should('be.visible');
   });

45-55: Replace mode: wait on import and scope the negative assertion.

  • Wait for POST /import to complete.
  • Scope “A.B.C Inc” assertion to the term list to avoid false positives.
     const importDialog = view.openImportDialog();
+    cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as(
+      'importGlossary'
+    );
     importDialog.selectFile('glossary/import_basic.csv');
     importDialog.chooseReplace();
     importDialog.submit();
+    cy.wait('@importGlossary').its('response.statusCode').should('eq', 200);

     gcy('glossary-term-list-item')
       .filter(':contains("Imported Term One")')
       .should('be.visible');

-    cy.contains('A.B.C Inc').should('not.exist');
+    gcy('glossary-term-list-item')
+      .filter(':contains("A.B.C Inc")')
+      .should('not.exist');

62-72: Add mode: wait on import and scope the positive assertion.

Make the check deterministic and scoped.

     const importDialog = view.openImportDialog();
+    cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as(
+      'importGlossary'
+    );
     importDialog.selectFile('glossary/import_basic.csv');
     importDialog.chooseAdd();
     importDialog.submit();
+    cy.wait('@importGlossary').its('response.statusCode').should('eq', 200);

     gcy('glossary-term-list-item')
       .filter(':contains("Imported Term One")')
       .should('be.visible');

-    cy.contains('A.B.C Inc').should('be.visible');
+    gcy('glossary-term-list-item')
+      .filter(':contains("A.B.C Inc")')
+      .should('be.visible');

79-91: Strengthen export assertions.

Also assert content-type is CSV and content-disposition includes attachment.

     cy.wait('@exportGlossary').then(({ response }) => {
       expect(response?.statusCode).to.eq(200);
       const disp = response?.headers?.['content-disposition'] as string;
       expect(disp).to.exist;
       expect(disp).to.include('.csv');
+      expect(response?.headers?.['content-type']).to.contain('text/csv');
+      expect(disp).to.match(/attachment/i);
     });
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (2)

1-2: Register cypress-file-upload in support for global availability.

Importing the plugin per component works but scatters side-effects and types. Prefer adding it once in Cypress support (e.g., cypress/support/e2e.ts or commands.ts) and remove local imports.

Based on learnings


26-29: Offer helper to submit and wait on network alias.

To reduce duplication in tests, add a submit helper that waits on an alias (e.g., @importGlossary) before asserting dialog close.

Example:

submitAndWaitFor(alias = 'importGlossary') {
  gcy('glossary-import-submit-button').click();
  cy.wait(`@${alias}`).its('response.statusCode').should('eq', 200);
  gcy('glossary-import-dialog').should('not.exist');
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0036779 and 30ef02e.

⛔ Files ignored due to path filters (1)
  • e2e/cypress/fixtures/glossary/import_basic.csv is excluded by !**/*.csv
📒 Files selected for processing (12)
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (2 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2 hunks)
  • e2e/cypress/e2e/glossary/importExport.cy.ts (1 hunks)
  • e2e/cypress/support/dataCyType.d.ts (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/service/apiSchema.generated.ts (6 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx
  • webapp/src/component/common/FileDropzone.tsx
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
🧰 Additional context used
🧬 Code graph analysis (5)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
  • glossaryTestData (131-131)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
  • E2GlossaryView (11-96)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • MAX_FILE_COUNT (16-16)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
  • E2GlossaryImportDialog (4-35)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
  • FilesType (42-45)
  • getFilesAsync (3-40)
🪛 GitHub Check: E2E Static Check 🪲
e2e/cypress/e2e/glossary/importExport.cy.ts

[warning] 7-7:
'E2GlossaryCreateEditDialog' is defined but never used. Allowed unused vars must match /^_/u


[warning] 5-5:
'E2GlossariesView' is defined but never used. Allowed unused vars must match /^_/u

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (9)
e2e/cypress/support/dataCyType.d.ts (1)

295-297: New selector declarations are well-formatted, but prior unused selector issues must be resolved.

The ten new data-cy selector declarations follow the correct format, maintain alphabetical ordering, and use consistent naming conventions. However, past reviews identified three unused selectors that still need attention:

  1. Lines 296: file-dropzone-remove-button - not found in FileDropzone.tsx
  2. Lines 339-340: glossary-import-mode-add and glossary-import-mode-replace - not found in glossary import UI components

Before merging, either add the missing data-cy attributes to the corresponding UI elements or remove these declarations to maintain type/implementation consistency.

Also applies to: 335-341

backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)

1-1: LGTM: Package relocation is appropriate.

Moving CsvDelimiterDetector to the io.tolgee.util package makes sense, as this is a reusable utility that can be leveraged across different modules (e.g., CSV import and glossary import/export).

webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2)

18-28: LGTM - Global drag/drop prevention is appropriate.

The global event listeners correctly prevent the browser's default behavior of opening dropped files in a new tab, which would disrupt the application flow.


30-40: LGTM - DragDropArea integration is well-structured.

The component correctly uses DragDropArea with permission-based activation, max items limit, and appropriate callbacks.

webapp/src/component/common/DragDropArea.tsx (3)

7-50: LGTM - Styled components are well-structured.

The overlay, icon wrapper, and icon components use appropriate theming and transitions. The styling provides clear visual feedback for valid and invalid drag states.


74-88: LGTM - Drag enter validation logic is correct.

The handler appropriately checks the active flag, prevents default behavior, and validates the item count against maxItems to provide immediate visual feedback.


106-115: LGTM - File drop processing is properly async.

The handler correctly uses getFilesAsync to handle the drag-and-drop file system entries and resets state after processing.

e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)

3-3: Import aligns with new dialog usage. LGTM.


81-95: New import/export helpers read well and are consistent.

Good page-object API, visibility checks included, returns dialog appropriately.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)

11-21: Delimiter detection doesn't handle CSV quoting or escaping.

The current logic counts raw character occurrences in the header line without accounting for CSV quoting rules. If the header contains commas inside quoted strings (e.g., "Name, Title",Email), the detector may incorrectly choose comma as the delimiter when the actual delimiter is a different character.

Additionally, when the file is empty or has no header line, the logic defaults to returning comma (index 0), which may not be appropriate for all cases.

Consider enhancing the detection logic to:

  1. Parse quoted strings correctly before counting delimiters
  2. Validate that the detected delimiter produces consistent column counts across multiple rows
  3. Handle edge cases like empty files more explicitly

Example enhancement:

  val delimiter by lazy {
-   val headerLine = inputStream.reader().buffered().lineSequence().firstOrNull() ?: ""
+   val lines = inputStream.reader().buffered().lineSequence().take(5).toList()
+   if (lines.isEmpty()) return@lazy ','
+   
+   // Count delimiters outside of quoted strings
    val counts =
      DELIMITERS.map { delimiter ->
-       headerLine.count { it == delimiter }
+       lines.first().count { it == delimiter && !isInsideQuotes(it) }
      }
    val bestIndex =
      counts.foldIndexed(0) { index, maxIndex, value ->
        val maxValue = counts[maxIndex]
        index.takeIf { value > maxValue } ?: maxIndex
      }
    DELIMITERS[bestIndex]
  }
♻️ Duplicate comments (2)
e2e/cypress/support/dataCyType.d.ts (1)

335-341: Verify usage of glossary import/export selectors in UI components.

The seven glossary import/export selectors are correctly formatted and follow naming conventions. However, previous review comments flagged glossary-import-mode-add and glossary-import-mode-replace as unused. Confirm all selectors are now properly used in the UI components.

Run the following script to verify usage:

#!/bin/bash
# Description: Verify all glossary import/export selectors are used in UI components

echo "Checking usage of glossary import/export selectors..."
for selector in "glossary-export-button" "glossary-import-button" "glossary-import-cancel-button" "glossary-import-dialog" "glossary-import-mode-add" "glossary-import-mode-replace" "glossary-import-submit-button"; do
  echo "---"
  echo "Searching for: $selector"
  rg -n "data-cy=['\"\`]$selector['\"\`]" -g '*.{ts,tsx,js,jsx}' || echo "  No matches found"
done
webapp/src/service/apiSchema.generated.ts (1)

8724-8726: Fix the export media type declaration

The export endpoint still claims the 200 response is "application/json", but the backend streams CSV. Generated clients will try to decode CSV as JSON and fail. Change the response content type to the actual CSV media type (e.g. "text/csv").

🧹 Nitpick comments (3)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (2)

2-2: Prefer Cypress built-in .selectFile() over the plugin.

The cypress-file-upload plugin is 4+ years old. Cypress 9.3+ includes a built-in .selectFile() command that provides similar functionality and is actively maintained. Consider migrating to the built-in command for better long-term support.

Based on learnings


5-10: Consider using Cypress built-in .selectFile() for better maintainability.

While the current implementation works, the built-in .selectFile() command is more modern and doesn't require an external plugin.

Example migration:

  selectFile(fixturePath: string) {
-   gcy('file-dropzone-select-button').click();
-   gcy('glossary-import-dialog')
-     .find('input[type="file"]')
-     .attachFile(fixturePath, { subjectType: 'input' });
+   gcy('glossary-import-dialog')
+     .find('input[type="file"]')
+     .selectFile(`cypress/fixtures/${fixturePath}`);
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)

26-32: Consider streaming approach for very large files.

The current implementation loads the entire CSV into memory before parsing. While this is acceptable for typical glossary sizes, consider a streaming approach if you expect to handle very large imports (100K+ terms).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0036779 and 22bdc9b.

⛔ Files ignored due to path filters (1)
  • e2e/cypress/fixtures/glossary/import_basic.csv is excluded by !**/*.csv
📒 Files selected for processing (12)
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2 hunks)
  • e2e/cypress/e2e/glossary/importExport.cy.ts (1 hunks)
  • e2e/cypress/support/dataCyType.d.ts (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/service/apiSchema.generated.ts (6 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
  • webapp/src/component/common/DragDropArea.tsx
  • webapp/src/component/common/FileDropzone.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
  • glossaryTestData (131-131)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
  • E2GlossaryView (11-96)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
  • E2GlossaryImportDialog (4-35)
webapp/src/views/projects/import/component/ImportFileInput.tsx (2)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • MAX_FILE_COUNT (16-16)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (20)
webapp/src/views/projects/import/component/ImportFileInput.tsx (1)

137-141: Remove incorrect type mismatch concern The DragDropArea’s onFilesReceived prop already uses FilesType, matching the onNewFiles callback signature.

Likely an incorrect or invalid review comment.

e2e/cypress/compounds/glossaries/E2GlossaryView.ts (4)

3-3: LGTM!

The import is necessary for the new methods and properly placed.


81-85: LGTM!

The method follows the established pattern in the class and properly returns a helper instance for chaining.


87-91: LGTM!

The method is well-structured and maintains consistency with other dialog helpers in the class.


93-95: LGTM!

The method appropriately triggers the export action. Tests can intercept the API call to verify the download.

e2e/cypress/e2e/glossary/importExport.cy.ts (5)

1-19: LGTM!

The test setup properly manages test data lifecycle with cleanup before and after each test.


21-36: LGTM!

The test case correctly verifies the import flow for an empty glossary and asserts the presence of imported terms.


38-53: LGTM!

The test case correctly verifies that the replace mode removes old terms and imports new ones.


55-70: LGTM!

The test case correctly verifies that the add mode preserves existing terms while importing new ones.


72-89: LGTM!

The test case properly intercepts the export API call and verifies the response status and CSV content-disposition header.

e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (4)

12-17: LGTM!

The method properly selects the replace mode and verifies the radio button state.


19-24: LGTM!

The method correctly selects the add mode and verifies the state, maintaining consistency with chooseReplace().


26-29: LGTM!

The method correctly submits the import and verifies the dialog closes, ensuring the action completes.


31-34: LGTM!

The method properly cancels the import and verifies the dialog closes, maintaining consistency with the submit() method.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (4)

1-20: LGTM! Clean service structure.

The service declaration follows Spring best practices with proper dependency injection. The imports are relevant and well-organized.


40-43: Verify if separate translation save is necessary.

Line 43 explicitly saves translations after saving terms. If GlossaryTerm.translations has CascadeType.PERSIST configured (as indicated by the addressed past review), this explicit save may be redundant—saving terms should cascade to their translations.

Confirm whether:

  1. The cascade configuration is working as expected
  2. There's a specific reason to flush translations separately (e.g., to ensure proper ordering or to handle large batches)

If cascade is working and no explicit flush is needed, consider removing line 43 to simplify the code.


48-73: LGTM! Well-structured translation mapping.

The applyFrom extension function correctly:

  • Sets back-references on all GlossaryTermTranslation instances (lines 53, 69)
  • Handles the duplicate base language scenario by preferring the term value over a translations column entry (lines 64-68)
  • Uses Kotlin's smart cast to safely access glossaryTerm.term after null check
  • Employs the setIfNotNull helper to cleanly assign optional fields

The logic addresses the concerns raised in previous reviews.


75-75: LGTM! Elegant helper function.

The setIfNotNull infix extension is a clean, type-safe utility that improves readability when conditionally assigning optional fields.

backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1)

8-8: LGTM!

The import correctly reflects the package move of CsvDelimiterDetector to io.tolgee.util. The usage pattern in lines 15-19 is also correct, creating separate input streams for the detector and parser to avoid stream consumption issues.

backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)

1-1: CsvDelimiterDetector references updated to new package
No remaining imports or fully qualified references to io.tolgee.formats.csv.in.CsvDelimiterDetector—all code uses io.tolgee.util.CsvDelimiterDetector.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
webapp/src/component/common/DragDropArea.tsx (1)

69-88: Stabilize drag state (depth counter) and add keyboard accessibility; also guard clicks when inactive

Current target-based tracking flickers and isn’t keyboard-accessible. Use a drag depth counter, handle Enter/Space, and ignore clicks when inactive.

@@
-  const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null);
-  const [dragEnterTarget, setDragEnterTarget] = useState<EventTarget | null>(
-    null
-  );
+  const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null);
+  const [dragCounter, setDragCounter] = useState(0);
@@
   const handleDragEnter = (e: React.DragEvent) => {
     if (!active) return;
     e.preventDefault();
     e.stopPropagation();
-    setDragEnterTarget(e.target);
+    setDragCounter((c) => c + 1);
@@
   const handleDragLeave = (e: React.DragEvent) => {
     if (!active) return;
     e.preventDefault();
     e.stopPropagation();
-    if (e.target === dragEnterTarget) {
-      setDragOver(null);
-      setDragEnterTarget(null);
-    }
+    setDragCounter((c) => {
+      const next = Math.max(0, c - 1);
+      if (next === 0) setDragOver(null);
+      return next;
+    });
   };
@@
   const handleDrop = async (e: React.DragEvent) => {
     if (!active) return;
     e.preventDefault();
     e.stopPropagation();
 
-    const files = await getFilesAsync(e.dataTransfer);
+    const dt = e.dataTransfer;
+    const files = await getFilesAsync(dt);
     onFilesReceived(files);
     setDragOver(null);
-    setDragEnterTarget(null);
+    setDragCounter(0);
   };
@@
   return (
     <Box
       position="relative"
       overflow="visible"
-      onClick={onClick}
+      onClick={(e) => {
+        if (!active) {
+          e.preventDefault();
+          return;
+        }
+        onClick?.();
+      }}
+      role="button"
+      tabIndex={0}
+      aria-disabled={!active}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          if (active) onClick?.();
+        }
+        if (e.key === 'Escape') {
+          setDragOver(null);
+          setDragCounter(0);
+        }
+      }}
       data-cy="dropzone"
       onDragEnter={handleDragEnter}
       onDragLeave={handleDragLeave}
       onDragOver={handleDragOver}
       onDrop={handleDrop}
     >

Also applies to: 90-98, 106-115, 120-130

🧹 Nitpick comments (5)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)

75-86: Consider adding a loading indicator for better UX.

The export button handles the loading state by disabling itself, but users might not realize an export is in progress without a visual indicator.

Consider adding a CircularProgress spinner:

           <Tooltip title={t('glossary_export_button')}>
             <IconButton
               size="small"
               color="primary"
               onClick={triggerExport}
               data-cy="glossary-export-button"
               disabled={exportLoading}
               aria-label={t('glossary_export_button')}
             >
-              <FileDownload03 height={20} width={20} />
+              {exportLoading ? (
+                <CircularProgress size={20} />
+              ) : (
+                <FileDownload03 height={20} width={20} />
+              )}
             </IconButton>
           </Tooltip>

Additionally, verify whether the export button should always be visible or if it should be conditionally rendered like the import button (e.g., based on whether there are terms to export or user permissions).

webapp/src/component/common/FileDropzone.tsx (4)

1-1: Memoize handlers to avoid paste-listener churn

Stabilize findFileType and handleFilesReceived so useOnFilePaste doesn’t reattach on every render.

- import React, { useRef, ReactNode } from 'react';
+ import React, { useRef, ReactNode, useCallback, useMemo } from 'react';
@@
-  const findFileType = (fileName: string) =>
-    acceptedFileTypes.find((config) =>
-      fileName.toLowerCase().endsWith(config.extension.toLowerCase())
-    );
+  const findFileType = useCallback(
+    (fileName: string) =>
+      acceptedFileTypes.find((config) =>
+        fileName.toLowerCase().endsWith(config.extension.toLowerCase())
+      ),
+    [acceptedFileTypes]
+  );
@@
-  const handleFilesReceived = (receivedFiles: FilesType) => {
+  const handleFilesReceived = useCallback((receivedFiles: FilesType) => {
     const validFiles = receivedFiles.filter((receivedFile) => {
       if (!isValidFileType(receivedFile.name)) {
         messageService.error(<T keyName="error_message_invalid_file_type" />);
         return false;
       }
       return true;
     });
 
     if (validFiles.length > 0) {
       const allFiles = [...files, ...validFiles];
       const newFiles = allFiles.slice(0, maxFiles);
       if (newFiles.length !== allFiles.length) {
         messageService.error(
           <T keyName="error_message_too_many_files" params={{ maxFiles }} />
         );
       }
       onFilesSelect(newFiles);
     }
-  };
+  }, [files, maxFiles, onFilesSelect, findFileType]);

Also applies to: 74-81, 83-102


72-73: Normalize accept attribute (ensure leading dot)

Ensure accept strings are valid (e.g., ".csv,.json") even if extensions are provided without a dot.

   const fileInputRef = useRef<HTMLInputElement>(null);
+  const acceptAttr = useMemo(
+    () =>
+      acceptedFileTypes
+        .map((config) =>
+          config.extension.startsWith('.')
+            ? config.extension
+            : `.${config.extension}`
+        )
+        .join(','),
+    [acceptedFileTypes]
+  );
@@
-        accept={acceptedFileTypes.map((config) => config.extension).join(',')}
+        accept={acceptAttr}

Also applies to: 161-168


170-174: Pass active and clamp maxItems to avoid negative values

Disable interactions when the quota is reached and ensure maxItems isn’t negative.

-      <DragDropArea
-        onFilesReceived={handleFilesReceived}
-        onClick={handleClick}
-        maxItems={maxFiles - files.length}
-      >
+      <DragDropArea
+        onFilesReceived={handleFilesReceived}
+        onClick={handleClick}
+        active={canAddMoreFiles}
+        maxItems={Math.max(0, maxFiles - files.length)}
+      >

221-226: Add noreferrer with target="_blank"

Minor security/privacy tweak.

-            <Link
+            <Link
               href={helpLink.href}
               target="_blank"
-              rel="noopener"
+              rel="noopener noreferrer"
               onClick={(e) => e.stopPropagation()}
             >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 150b487 and f95dad6.

📒 Files selected for processing (4)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
  • FilesType (42-45)
  • getFilesAsync (3-40)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (37-60)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
  • useGlossaryExport (22-56)
webapp/src/component/layout/BaseViewAddButton.tsx (1)
  • BaseViewAddButton (6-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build backend 🏗️
  • GitHub Check: Frontend static check 🪲
🔇 Additional comments (8)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (6)

1-1: LGTM! Imports are appropriate.

The new imports (IconButton, Tooltip, icons, and useGlossaryExport hook) are all necessary for the import/export functionality and are used correctly in the component.

Also applies to: 7-8


19-20: LGTM! Props are well-defined.

The rename from onCreate to onCreateTerm improves clarity, and the new onImport prop follows the same optional pattern for conditional rendering of the import button.


29-30: LGTM! Component setup is correct.

The updated destructuring matches the new props interface, and the useGlossaryExport hook is properly initialized with both triggerExport and exportLoading being used in the render.

Also applies to: 38-38


54-54: Minor UI spacing adjustment.

The gap reduction from 2 to 1 appears intentional to accommodate the new import/export buttons in the action row.


62-74: LGTM! Import button is well-implemented.

The import button is properly conditionally rendered, includes appropriate accessibility attributes (aria-label), has test identifiers (data-cy), and uses consistent styling with the export button.


87-92: LGTM! Add button correctly updated.

The add button properly uses the renamed onCreateTerm prop and maintains consistent conditional rendering with the other action buttons.

webapp/src/component/common/FileDropzone.tsx (2)

110-113: Nice: input reset after selection

Good call resetting the input to allow re-selecting the same file.


125-126: Paste routed through the same handler

Correctly reusing handleFilesReceived for paste. LGTM.

@Anty0 Anty0 marked this pull request as ready for review October 14, 2025 14:59
@JanCizmar JanCizmar self-requested a review October 15, 2025 14:52
Copy link
Contributor

@JanCizmar JanCizmar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Job! Thanks!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (3)

26-26: Consider streaming for large CSV files.

readAllBytes() loads the entire CSV into memory, which may cause issues with large glossary files. If large imports are expected, consider using streaming or chunked processing.


42-43: Redundant translation save if cascade is configured.

Since the past review comments confirm that cascade (PERSIST/MERGE) was added to GlossaryTerm.translations, saving translations separately after saving terms is redundant. The cascade should automatically persist translations when terms are saved.

Consider removing the explicit translation save:

     val translations = terms.flatMap { it.translations }

     glossaryTermService.saveAll(terms)
-    glossaryTermTranslationService.saveAll(translations)

If you need the separate save for a specific reason (e.g., ensuring translations get IDs immediately), please add a comment explaining why.

Based on past review comments indicating cascade was configured.


51-57: Consider validating base translation is not blank.

While null is handled with the let block, an empty or blank string could still create a translation with no meaningful content. Consider adding a check:

     baseTranslation?.let {
       // We've found a base translation.
+      if (it.isBlank()) return this
       translations.add(
         GlossaryTermTranslation(glossary.baseLanguageTag, it)
           .apply { term = this@applyFrom }
       )
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f95dad6 and d270783.

📒 Files selected for processing (2)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build backend 🏗️
  • GitHub Check: Frontend static check 🪲
🔇 Additional comments (5)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)

81-82: Nice Kotlin idiom!

The setIfNotNull infix extension provides a clean, readable way to conditionally set properties. This is good Kotlin style.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (4)

10-20: LGTM!

The class structure is well-designed with immutable properties and lazy initialization. The deterministic ordering of language tags (via .sorted()) ensures consistent CSV output across runs.


41-48: LGTM!

The export implementation correctly uses .use{} for resource management and explicitly specifies UTF-8 encoding for deterministic output.


50-52: LGTM!

Clean helper function for boolean-to-string conversion.


27-27: No changes needed for translatable flag header. The header GLOSSARY_CSV_HEADER_FLAG_TRANSLATABLE correctly reflects the inverted !flagNonTranslatable logic.

@Anty0 Anty0 force-pushed the jirikuchynka/glossary-import-export branch from d270783 to 0a12bb8 Compare October 16, 2025 12:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (4)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)

57-61: Don’t close servlet OutputStream; use the use { … } stream variable.

Spring closes the response stream. Manually closing can throw and isn’t needed. Also copy from the use lambda param.

-    return ResponseEntity.ok().headers(headers).body(
-      streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
-        stream.use { IOUtils.copy(stream, out) }
-        out.close()
-      },
-    )
+    return ResponseEntity.ok().headers(headers).body(
+      streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
+        stream.use { IOUtils.copy(it, out) }
+      },
+    )
webapp/src/component/common/DragDropArea.tsx (1)

69-115: Consider adding keyboard accessibility.

The drag-and-drop implementation works correctly, but lacks keyboard accessibility. Users who cannot use drag-and-drop (keyboard-only, assistive tech) cannot interact with this component.

Add keyboard support by making the container focusable and handling keyboard events:

   return (
     <Box
       position="relative"
       overflow="visible"
       onClick={onClick}
       data-cy="dropzone"
       onDragEnter={handleDragEnter}
       onDragLeave={handleDragLeave}
       onDragOver={handleDragOver}
       onDrop={handleDrop}
+      role="button"
+      tabIndex={0}
+      onKeyDown={(e) => {
+        if ((e.key === 'Enter' || e.key === ' ') && onClick) {
+          e.preventDefault();
+          onClick();
+        }
+      }}
     >

Note: The past review also suggested using a drag counter instead of dragEnterTarget to prevent flicker with nested elements. While the current implementation is functional, the counter approach would be more robust if you encounter visual issues.

webapp/src/service/apiSchema.generated.ts (1)

9118-9125: Fix export media type advertisement.

GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export is still declared as returning "application/json" even though the controller streams CSV. Any generated client will keep trying to JSON‑parse the CSV stream and blow up at runtime (exactly what I flagged earlier). Update the OpenAPI definition to advertise the real media type (e.g. "text/csv": string like other CSV endpoints in this repo) or clients remain broken.

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

52-54: Service-level parsing/validation handling (duplicate of earlier feedback)

Reiterating prior note: importCsv should translate CSV parsing/validation errors into user-friendly responses and ensure all-or-nothing behavior.

🧹 Nitpick comments (12)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)

9-36: Consider extracting the common find-assert-apply pattern.

All three finder functions (assertTerm, assertTermWithTranslation, assertTermWithDescription) follow an identical pattern: find by predicate, assert non-null with description, apply block. This duplication increases maintenance burden.

Consider refactoring to a common helper:

+private fun List<ImportGlossaryTerm>.assertTermBy(
+    description: String,
+    predicate: (ImportGlossaryTerm) -> Boolean,
+    block: TermAssertion.() -> Unit = {}
+) {
+    val foundTerm = find(predicate)
+    assertThat(foundTerm).describedAs(description).isNotNull
+    TermAssertion(foundTerm!!).apply(block)
+}
+
 fun List<ImportGlossaryTerm>.assertTerm(term: String, block: TermAssertion.() -> Unit = {}) {
-    val foundTerm = find { it.term == term }
-    assertThat(foundTerm).describedAs("Term '$term' should be found").isNotNull
-    TermAssertion(foundTerm!!).apply(block)
+    assertTermBy("Term '$term' should be found", { it.term == term }, block)
 }

 fun List<ImportGlossaryTerm>.assertTermWithTranslation(
     languageTag: String,
     translation: String,
     block: TermAssertion.() -> Unit = {}
 ) {
-    val foundTerm = find { it.translations[languageTag] == translation }
-    assertThat(foundTerm)
-        .describedAs("Term with translation '$languageTag'='$translation' should be found")
-        .isNotNull
-    TermAssertion(foundTerm!!).apply(block)
+    assertTermBy(
+        "Term with translation '$languageTag'='$translation' should be found",
+        { it.translations[languageTag] == translation },
+        block
+    )
 }

 fun List<ImportGlossaryTerm>.assertTermWithDescription(
     description: String,
     block: TermAssertion.() -> Unit = {}
 ) {
-    val foundTerm = find { it.description == description }
-    assertThat(foundTerm)
-        .describedAs("Term with description '$description' should be found")
-        .isNotNull
-    TermAssertion(foundTerm!!).apply(block)
+    assertTermBy(
+        "Term with description '$description' should be found",
+        { it.description == description },
+        block
+    )
 }
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (2)

6-6: Remove unused import.

The import io.tolgee.model.glossary.Glossary_.terms (JPA metamodel) is not used in this file.

-import io.tolgee.model.glossary.Glossary_.terms

73-122: Consider renaming for clarity.

The translatable(value: Boolean = true) method (lines 93-95) sets flagNonTranslatable = !value, creating a double-negative that may confuse readers. While functionally correct, consider either:

  1. Renaming to nonTranslatable(value: Boolean = false) to match the flag name directly, or
  2. Keep translatable() but add a brief comment explaining the inversion

The rest of the builder implementation is well-structured.

- fun translatable(value: Boolean = true) = apply {
-   flagNonTranslatable = !value
+ fun nonTranslatable(value: Boolean = false) = apply {
+   flagNonTranslatable = value
e2e/cypress/e2e/glossary/importExport.cy.ts (1)

72-89: Consider verifying CSV content.

While the export test validates the HTTP status and content-disposition header, it doesn't verify the actual CSV content. Consider adding assertions on the response body to ensure the exported data is correct.

You could enhance the test by checking the CSV contains expected term data:

 cy.wait('@exportGlossary').then(({ response }) => {
   expect(response?.statusCode).to.eq(200);
   const disp = response?.headers?.['content-disposition'] as string;
   expect(disp).to.exist;
   expect(disp).to.include('.csv');
+  expect(response?.body).to.include('A.B.C Inc');
 });
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (1)

30-43: Reset the file input after processing to allow re‑selecting the same file(s).

Without clearing, selecting the same files may not trigger change again. Clear both the event target and the ref.

   function onFileSelected(e: SyntheticEvent) {
     const files = (e.target as HTMLInputElement).files;
     if (!files) {
       return;
     }
     const toUpload: FilesType = [];
     for (let i = 0; i < files.length; i++) {
       const item = files.item(i);
       if (item) {
         toUpload.push({ file: item, name: item.name });
       }
     }
     validateAndUpload(toUpload);
+    // Clear to allow selecting the same files again
+    (e.target as HTMLInputElement).value = '';
+    fileRef.current && (fileRef.current.value = '');
   }
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)

40-45: Consider relying on cascade to avoid double persistence.

If GlossaryTerm.translations has CascadeType.PERSIST (as previously discussed), saving parents will persist children; the extra saveAll(translations) adds overhead.

-    val translations = terms.flatMap { it.translations }
-
     glossaryTermService.saveAll(terms)
-    glossaryTermTranslationService.saveAll(translations)

Note: Keep explicit save if cascade isn’t guaranteed. [Based on learnings]

webapp/src/component/common/FileDropzone.tsx (3)

83-101: Consolidate validation messages and enforce per‑batch limit without silent drops

Aggregate invalid-type errors to one toast and add only remaining slots, informing when files are trimmed.

-  const handleFilesReceived = (receivedFiles: FilesType) => {
-    const validFiles = receivedFiles.filter((receivedFile) => {
-      if (!isValidFileType(receivedFile.name)) {
-        messageService.error(<T keyName="error_message_invalid_file_type" />);
-        return false;
-      }
-      return true;
-    });
-
-    if (validFiles.length > 0) {
-      const allFiles = [...files, ...validFiles];
-      const newFiles = allFiles.slice(0, maxFiles);
-      if (newFiles.length !== allFiles.length) {
-        messageService.error(
-          <T keyName="error_message_too_many_files" params={{ maxFiles }} />
-        );
-      }
-      onFilesSelect(newFiles);
-    }
-  };
+  const handleFilesReceived = (receivedFiles: FilesType) => {
+    const validFiles = receivedFiles.filter((f) => isValidFileType(f.name));
+    const invalidCount = receivedFiles.length - validFiles.length;
+
+    if (invalidCount > 0) {
+      messageService.error(<T keyName="error_message_invalid_file_type" />);
+    }
+
+    const remaining = Math.max(0, maxFiles - files.length);
+    const toAdd = validFiles.slice(0, remaining);
+
+    if (toAdd.length < validFiles.length) {
+      messageService.error(
+        <T keyName="error_message_too_many_files" params={{ maxFiles }} />
+      );
+    }
+
+    if (toAdd.length > 0) {
+      onFilesSelect([...files, ...toAdd]);
+    }
+  };

164-166: Normalize accept attr and reflect remaining slots for selection/drag

Ensure extensions are prefixed with “.” (or allow MIME), set multiple by remaining capacity, and clamp maxItems.

-        accept={acceptedFileTypes.map((config) => config.extension).join(',')}
-        multiple={maxFiles > 1}
+        accept={acceptedFileTypes
+          .map((c) =>
+            c.extension.includes('/') || c.extension.startsWith('.')
+              ? c.extension
+              : `.${c.extension}`
+          )
+          .join(',')}
+        multiple={maxFiles - files.length > 1}
@@
-        maxItems={maxFiles - files.length}
+        maxItems={Math.max(0, maxFiles - files.length)}

Also applies to: 173-173


141-141: Use a stable key to avoid re‑mounts on reorder

Index keys can cause unnecessary re-renders.

-        key={`${file.name}-${index}`}
+        key={`${file.name}-${file.file.lastModified}-${file.file.size}`}
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (3)

43-45: Use Kotlin default value for query param instead of required=false + defaultValue

Cleaner and consistent with Kotlin style; keeps API behavior identical.

-    @RequestParam(required = false, defaultValue = "false")
-    removeExistingTerms: Boolean,
+    @RequestParam("removeExistingTerms")
+    removeExistingTerms: Boolean = false,

38-39: Avoid @transactional on controllers; move Tx boundary to service

Place the transaction in GlossaryImportService to keep web layer thin and avoid long-lived HTTP-bound transactions.

-  @Transactional
   fun importCsv(

Follow-up: wrap delete+import in a single @transactional service method (e.g., glossaryImportService.importCsv(glossary, input, removeExistingTerms)).


40-41: Silence unused path variable by binding explicitly and prefixing with underscore

Prevents IDE warnings while preserving binding.

-    @PathVariable organizationId: Long,
+    @PathVariable("organizationId") _organizationId: Long,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d270783 and 0a12bb8.

⛔ Files ignored due to path filters (1)
  • e2e/cypress/fixtures/glossary/import_basic.csv is excluded by !**/*.csv
📒 Files selected for processing (51)
  • backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (2 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2 hunks)
  • e2e/cypress/e2e/glossary/importExport.cy.ts (1 hunks)
  • e2e/cypress/support/dataCyType.d.ts (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/FileDropzoneSelectedFile.tsx (1 hunks)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/billing/Invoices/DownloadButton.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryTermsList.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (4 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryView.tsx (2 hunks)
  • webapp/src/fixtures/FileUploadFixtures.ts (0 hunks)
  • webapp/src/fixtures/useOnFilePaste.ts (1 hunks)
  • webapp/src/service/apiSchema.generated.ts (6 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
  • webapp/src/views/projects/import/ImportView.tsx (1 hunks)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx (0 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2 hunks)
  • webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (5 hunks)
💤 Files with no reviewable changes (2)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx
  • webapp/src/fixtures/FileUploadFixtures.ts
✅ Files skipped from review due to trivial changes (1)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
🚧 Files skipped from review as they are similar to previous changes (17)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
  • backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt
  • webapp/src/views/projects/import/ImportView.tsx
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
  • backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
  • webapp/src/views/projects/import/component/ImportFileInput.tsx
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
  • webapp/src/component/common/ModeSelector.tsx
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
🧰 Additional context used
🧬 Code graph analysis (16)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
  • glossaryTestData (131-131)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
  • E2GlossaryView (11-96)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
  • FilesType (42-45)
  • getFilesAsync (3-40)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
  • hasDescription (39-41)
  • hasTranslation (47-49)
  • isTranslatable (59-61)
  • isNotCaseSensitive (71-73)
  • isNotAbbreviation (79-81)
  • isNotForbidden (87-89)
  • isNotTranslatable (63-65)
  • isCaseSensitive (67-69)
  • isAbbreviation (75-77)
  • isForbidden (83-85)
  • hasNoDescription (43-45)
  • hasNoFlags (91-96)
  • hasNoTerm (55-57)
  • hasNoTranslation (51-53)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (5)
webapp/src/component/common/ModeSelector.tsx (2)
  • ModeOption (23-28)
  • ModeSelector (36-81)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/component/common/SingleFileDropzone.tsx (1)
  • SingleFileDropzone (13-32)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
  • useGlossaryExport (22-56)
webapp/src/component/layout/BaseViewAddButton.tsx (1)
  • BaseViewAddButton (6-23)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (54-63)
  • FileDropzone (65-236)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
  • GlossaryViewBody (16-73)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (37-60)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
  • E2GlossaryImportDialog (4-35)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • ALLOWED_UPLOAD_TYPES (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build frontend 🏗️
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (30)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)

312-312: LGTM – FILE_PROCESSING_FAILED is referenced in GlossaryImportService.

webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)

38-52: Excellent cleanup pattern.

The nested try/finally blocks guarantee proper resource cleanup (anchor removal and URL revocation) even if errors occur during the download. The 7-second delay before revoking the blob URL is appropriate for invoice PDFs.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (2)

1-7: LGTM! Clean utility setup.

The package structure and initial assertion helper are straightforward and appropriate for test utilities.


38-57: LGTM! Description and translation assertions are well-structured.

The assertion methods for descriptions, translations, and term presence are clear and correctly implemented.

webapp/src/views/projects/export/downloadExported.ts (2)

14-28: LGTM! Clean separation of resource cleanup.

The nested try/finally structure properly ensures both DOM cleanup (anchor removal) and blob URL cleanup (revocation) happen independently, even if errors occur during the download process.


37-37: LGTM! Correct regex reset for global flag.

Resetting lastIndex is necessary because the global flag causes it to advance after each exec() call. The explanatory comment is helpful for maintainability.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (6)

8-13: LGTM!

Clean factory function with sensible defaults for test data creation.


15-20: LGTM!

Proper initialization of the term with mutable collections for translations.


22-29: LGTM!

Correctly establishes the bidirectional relationship between term and translation while enabling fluent chaining.


31-42: LGTM!

Good use of nullable parameters with safe-call operators to enable selective flag updates.


69-71: LGTM!

Clean DSL entry point following standard Kotlin builder patterns.


124-128: LGTM!

Elegant extension enabling nested DSL syntax for adding terms within a glossary builder.

webapp/src/fixtures/useOnFilePaste.ts (1)

1-26: LGTM! Dependency array issue resolved.

The hook correctly includes onFilePaste in the dependency array (line 25), which addresses the previously flagged stale closure concern. The paste listener logic is sound: it reads clipboard files, filters empty pastes, and invokes the callback with the expected FilesType format.

ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)

1-8: LGTM! Clean DTO for import response.

The data class is well-structured for its purpose as an API response. The @Schema annotation provides helpful documentation for API consumers.

ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)

15-25: LGTM! Type-safe refactoring.

The method signature change from separate organizationId and glossaryId parameters to a single Glossary entity improves type safety and aligns with similar methods in this repository (e.g., updateBaseLanguage). The JPQL query is cleaner when working with the entity directly.

webapp/src/ee/glossary/views/GlossaryView.tsx (1)

1-45: LGTM! Good separation of concerns.

The refactoring moves dialog management from GlossaryView to GlossaryViewBody, which now handles creation and import dialogs via hooks (useGlossaryTermCreateDialog and useGlossaryImportDialog). This simplifies GlossaryView to focus on layout and navigation.

webapp/src/component/common/SingleFileDropzone.tsx (1)

1-32: LGTM! Type-safe single-file wrapper.

The component correctly handles the conversion between a single file (FilesType[number]) and the array type (FilesType) that FileDropzone expects. The type signatures are now correct, addressing the concerns raised in past reviews.

e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)

81-95: LGTM! Well-structured test helpers.

The three new methods provide clear entry points for testing import/export flows. They follow the established patterns in this class, use proper data-cy selectors, and return appropriate page objects for chaining.

e2e/cypress/e2e/glossary/importExport.cy.ts (1)

1-90: LGTM! Comprehensive E2E test coverage.

The test suite thoroughly covers the import/export functionality:

  • Import into empty glossary
  • Replace existing terms
  • Add to existing terms
  • CSV export with proper response validation

The tests follow good practices with proper setup/teardown, page objects, and meaningful assertions.

e2e/cypress/support/dataCyType.d.ts (1)

295-297: file-dropzone-remove-button verified in UI. Found in webapp/src/component/common/FileDropzoneSelectedFile.tsx:54.

webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)

80-92: LGTM: FilesType adoption + multipart payload.

Mapping FilesType to multipart via screenshot: file.file is consistent and clear.


57-74: Verify config.maxUploadFileSize unit. Unable to locate its definition in the front-end config; confirm whether maxUploadFileSize is specified in kilobytes (making * 1024 correct) or already in bytes (in which case the multiplier should be removed).

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (1)

26-33: LGTM: delegation to glossary-based query and bulk save helper.

Lazy injection prevents circular deps; the glossary overload and saveAll helper are straightforward.

Also applies to: 111-113

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt (1)

3-17: LGTM: stable CSV header constants and ordered list.

Names and ordering are clear and consistent for import/export.

webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)

1-60: LGTM! Clean component implementation.

The component is well-structured with proper TypeScript typing, accessibility attributes, event handling, and test coverage. The use of styled components for layout, translation hooks for i18n, and proper event propagation control demonstrates good practices.

webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)

44-140: LGTM! All previous concerns addressed.

The component now correctly:

  • Uses glossary.organizationOwner.id instead of the nullable preferredOrganization!.id (line 68)
  • Stores the single file wrapper as FilesType[number] | null (line 52)
  • Passes the raw File blob via file.file to the mutation (line 73)
  • Includes proper mode selection UI, accessibility attributes, and loading states

The as any assertion on line 73 is acceptable given OpenAPI schema typing constraints.

webapp/src/component/common/DragDropArea.tsx (1)

117-159: LGTM! Overlay and icon logic is well-implemented.

The overlay visibility logic correctly handles three states (canShow, forceShow, dragOver), and the valid/invalid icon selection provides clear visual feedback. The StyledOverlay with transition and backdrop-filter creates a polished UX.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

1-53: LGTM! All previous concerns resolved.

The exporter implementation now correctly:

  • Ensures deterministic column ordering with .sorted() (line 16)
  • Closes the CSV writer with .use { } to prevent resource leaks (line 43)
  • Uses explicit UTF-8 encoding with byteInputStream(Charsets.UTF_8) (line 47)
  • Properly handles non-translatable terms by replicating base text (lines 32-37)
  • Documents the inverted flagNonTranslatable logic (line 27)

The code is clean, well-structured, and production-ready.

webapp/src/component/common/FileDropzone.tsx (1)

79-81: Empty acceptedFileTypes rejects everything — confirm intent

As written, an empty list makes every file invalid. Is that desired? If not, either enforce non-empty via prop validation or allow all types when empty.

Option if allowing all:

const isValidFileType = (fileName: string) =>
  acceptedFileTypes.length === 0 || findFileType(fileName) !== undefined;
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

34-36: ```markdown

Confirm tokenType fits UI flow

GlossaryImportController.kt:34 uses @AllowApiAccess(AuthTokenType.ONLY_PAT), which restricts to PATs (default = ANY allows session/JWT). If the EE UI invokes this import endpoint with session/JWT, switch to default.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)

12-13: Verify null-safety of organization comparison.

If preferredOrganization is null or undefined, and glossary.organizationOwner.id is also null/undefined, this comparison evaluates to true, potentially granting unintended permissions. This issue is shared with useGlossaryTermCreateDialogController.ts. Ensure that both values are defined before comparing.

This issue is also present in useGlossaryTermCreateDialogController.ts at lines 12-13. See the verification script in that file's review comment.

🧹 Nitpick comments (2)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)

9-11: Naming inconsistency: isUserMaintainer checks both OWNER and MAINTAINER roles.

The variable name isUserMaintainer only mentions "Maintainer" but checks for both 'OWNER' and 'MAINTAINER' roles. This is inconsistent with the parallel hook in useGlossaryImportDialogController.ts which uses the more accurate name isUserMaintainerOrOwner. Consider renaming for clarity.

Apply this diff to improve naming consistency:

-  const isUserMaintainer = ['OWNER', 'MAINTAINER'].includes(
+  const isUserMaintainerOrOwner = ['OWNER', 'MAINTAINER'].includes(
     preferredOrganization?.currentUserRole || ''
   );
   const isGlossaryUnderPreference =
     glossary.organizationOwner.id === preferredOrganization?.id;
 
   const [createTermDialogOpen, setCreateTermDialogOpen] = useState(false);
 
   const onCreateTerm = () => {
     setCreateTermDialogOpen(true);
   };
 
   const canCreate =
-    isGlossaryUnderPreference && (isUserMaintainer || isUserAdmin);
+    isGlossaryUnderPreference && (isUserMaintainerOrOwner || isUserAdmin);
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)

5-35: Extract shared logic to reduce code duplication.

This hook is almost identical to useGlossaryTermCreateDialogController.ts with only naming differences (import vs create, isUserMaintainerOrOwner vs isUserMaintainer). Consider extracting the common pattern into a shared generic hook to improve maintainability and reduce duplication.

For example, create a generic controller hook:

// useGlossaryDialogController.ts
export const useGlossaryDialogController = (actionType: 'create' | 'import') => {
  const { preferredOrganization } = usePreferredOrganization();
  const glossary = useGlossary();
  const isUserAdmin = useIsAdmin();
  const isUserMaintainerOrOwner = ['OWNER', 'MAINTAINER'].includes(
    preferredOrganization?.currentUserRole || ''
  );
  const isGlossaryUnderPreference =
    glossary.organizationOwner.id === preferredOrganization?.id;

  const [dialogOpen, setDialogOpen] = useState(false);

  const onAction = () => {
    setDialogOpen(true);
  };

  const canPerformAction =
    isGlossaryUnderPreference && (isUserMaintainerOrOwner || isUserAdmin);

  const isOpen = dialogOpen && canPerformAction;

  return {
    onAction: canPerformAction ? onAction : undefined,
    dialogOpen: isOpen,
    dialogProps: {
      open: isOpen,
      onClose: () => setDialogOpen(false),
      onFinished: () => setDialogOpen(false),
    },
  };
};

Then use it in both files:

// useGlossaryTermCreateDialogController.ts
export const useGlossaryTermCreateDialogController = () => {
  const { onAction, dialogOpen, dialogProps } = useGlossaryDialogController('create');
  return {
    onCreateTerm: onAction,
    createTermDialogOpen: dialogOpen,
    createTermDialogProps: dialogProps,
  };
};

// useGlossaryImportDialogController.ts
export const useGlossaryImportDialogController = () => {
  const { onAction, dialogOpen, dialogProps } = useGlossaryDialogController('import');
  return {
    onImport: onAction,
    importDialogOpen: dialogOpen,
    importDialogProps: dialogProps,
  };
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a12bb8 and 3f2f834.

📒 Files selected for processing (3)
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (31-43)
  • useIsAdmin (21-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)

12-13: Verify null-safety of organization comparison
Ensure both preferredOrganization and glossary.organizationOwner.id are non-null before comparing (e.g. guard with if (!preferredOrganization?.id || !glossary.organizationOwner.id) return false;) to avoid unintended true matches.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

83-90: Critical: Revert to coercing unknown values to false instead of null.

Line 88 returns false for unrecognized boolean values, which can silently corrupt data. Past review comment #2 recommended changing this to return null for unknown values and was marked as addressed, but the fix appears to have been reverted or not applied in the current code.

Apply this diff to handle unrecognized values safely:

 private fun parseBoolean(value: String?): Boolean? {
   if (value.isNullOrBlank()) return null
   return when (value.trim().lowercase()) {
     "true", "1", "yes", "y", "t" -> true
     "false", "0", "no", "n", "f" -> false
-    else -> false
+    else -> null
   }
 }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

77-81: Add negative index guard for defensive coding.

The function checks the upper bound but not the lower bound. While current callers (via findHeaderIndex) won't pass negative indices, adding a check for idx < 0 would make this helper more robust against future misuse.

Apply this diff to add the defensive check:

 fun Array<String>.getSafe(idx: Int?): String? {
   if (idx == null) return null
+  if (idx < 0) return null
   if (idx >= size) return null
   return this[idx].ifBlank { null }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f2f834 and 3a6f910.

📒 Files selected for processing (1)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (6)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (6)

15-23: LGTM!

The constructor and lazy reader initialization are correctly implemented. The use of CSVReaderBuilder with the custom delimiter configuration is appropriate.


25-33: LGTM!

The data properties correctly handle CSV structure: reading all rows, extracting trimmed headers from the first row, and separating data rows.


35-48: LGTM!

The header index computation correctly identifies special columns and derives translation columns as the remaining indices. The lazy evaluation strategy is appropriate for this parsing workflow.


50-52: LGTM!

The parse method is concise and correct, delegating row-to-term conversion to the extension function and filtering null results.


54-71: LGTM!

The extension correctly converts array rows to ImportGlossaryTerm. The non-null assertion on line 64 is safe because idxTranslations would be empty if headers were null. The logic for filtering empty rows and blank translations is appropriate.


73-75: LGTM!

The findHeaderIndex method correctly returns null when the header is not found or when headers are absent, avoiding the -1 issue from previous reviews.

@JanCizmar JanCizmar self-requested a review October 16, 2025 17:04
@Anty0 Anty0 force-pushed the jirikuchynka/glossary-import-export branch from 3a6f910 to c69541e Compare October 17, 2025 11:06
@Anty0
Copy link
Collaborator Author

Anty0 commented Oct 17, 2025

(resolved conflicts caused by the supporter being merged; no changes otherwise)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
webapp/src/views/projects/export/downloadExported.ts (1)

34-44: Make parseExtension robust (RFC 6266) and drop stateful global regex.

Current regex fails for filename* (UTF‑8) or when extra params follow filename; global flag + lastIndex is brittle.

-const parseExtension = (response: Response) => {
-  const contentDisposition = response.headers.get('Content-Disposition');
-  if (contentDisposition) {
-    extensionRegex.lastIndex = 0; // Reset the regex to the start of the string
-    const match = extensionRegex.exec(contentDisposition);
-    if (match) {
-      return match[1];
-    }
-  }
-  return null;
-};
-
-const extensionRegex = /^.*\.(\w+)"$/gm;
+const parseExtension = (response: Response) => {
+  const cd = response.headers.get('Content-Disposition');
+  if (!cd) return null;
+  let filename: string | null = null;
+  // Prefer RFC 5987/6266 filename*
+  const star = cd.match(/filename\*\s*=\s*([^;]+)/i);
+  if (star && star[1]) {
+    const raw = star[1].trim().replace(/^"(.*)"$/, '$1');
+    const unprefixed = raw.replace(/^UTF-8''/i, '');
+    try {
+      filename = decodeURIComponent(unprefixed);
+    } catch {
+      filename = unprefixed;
+    }
+  }
+  if (!filename) {
+    const normal = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i);
+    if (normal) filename = (normal[1] || normal[2]).trim();
+  }
+  if (!filename) return null;
+  const dot = filename.lastIndexOf('.');
+  return dot > -1 ? filename.slice(dot + 1) : null;
+};

Also applies to: 46-46

ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)

36-46: Refactor glossary term search queries to handle empty sets and fix language filtering semantics.

The review comment correctly identifies two critical bugs:

  1. Empty set SQL failure: When controller converts empty request parameter to empty Set via toSet(), the SQL IN () clause fails. The is null check only catches null, not empty sets.

  2. Search semantics failure: Queries join only to baseLanguageTag translations, so searching with non-base languages (e.g., languageTags={"cs","de"} where base is "en") returns no results because the translation join yields nothing.

All three methods are affected identically:

  • findByGlossaryPaged (lines 34-53)
  • findByGlossaryIdsPaged (lines 55-75)
  • findAllIds (lines 95-113)

The suggested EXISTS-based refactoring correctly addresses both issues by removing the problematic left join and allowing independent language filtering per translation. Additionally, ensure the controller or service layer converts empty lists to null before passing to repository (or apply the refactor, which eliminates the need for that conversion).

♻️ Duplicate comments (9)
webapp/src/component/common/FileDropzone.tsx (1)

221-226: Add noreferrer to prevent referrer leakage.

External links opened in a new tab should include noreferrer to prevent the Referer header from being sent to the target site, improving privacy and security.

Apply:

-              rel="noopener"
+              rel="noopener noreferrer"
webapp/src/views/projects/export/downloadExported.ts (1)

29-31: Reduce the 7s URL revoke delay to avoid memory bloat.

A 7000 ms timeout holds large blobs unnecessarily; next‑tick or ~100 ms is enough for Safari/WebKit.

-    setTimeout(() => URL.revokeObjectURL(url), 7000);
+    // Delay just enough for Safari/WebKit to start reading the blob
+    setTimeout(() => URL.revokeObjectURL(url), 100);
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)

42-46: Type cast bypasses type safety.

The double cast response as unknown as Response remains from the previous review and bypasses TypeScript's type checking. This occurs because useApiMutation with rawResponse: true doesn't correctly type the response as Response.

As noted in the previous review, the root cause should be addressed in the API schema generation or useApiMutation generic signature to properly type raw responses, eliminating the need for this unsafe cast.

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

52-54: Ensure parse/validation failures abort the import (no partial or empty glossaries).

If parsing or validation fails after delete, users could end up with data loss unless exceptions trigger rollback. Make importCsv surface deterministic errors and transactional semantics so failures roll back deletes/inserts. This mirrors prior feedback.

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)

57-61: Don’t close the servlet OutputStream; let Spring manage it. Also use the use receiver.

Remove out.close() and copy from the use receiver to avoid double-close and confusion.

-    return ResponseEntity.ok().headers(headers).body(
-      streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
-        stream.use { IOUtils.copy(stream, out) }
-        out.close()
-      },
-    )
+    return ResponseEntity.ok().headers(headers).body(
+      streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream ->
+        stream.use { IOUtils.copy(it, out) }
+      },
+    )
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

83-89: Don’t coerce unknown boolean tokens to false.

Return null for unrecognized values to avoid silent data corruption; this was flagged before.

   private fun parseBoolean(value: String?): Boolean? {
     if (value.isNullOrBlank()) return null
     return when (value.trim().lowercase()) {
       "true", "1", "yes", "y", "t" -> true
       "false", "0", "no", "n", "f" -> false
-      else -> false
+      else -> null
     }
   }
webapp/src/service/apiSchema.generated.ts (1)

9170-9177: Fix the export media type.

GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export streams CSV, yet the schema announces "application/json". Clients generated from this spec will JSON-decode the stream and fail. Align the 200 response with the real payload (e.g. "text/csv" like other CSV endpoints).
Example fix:

-        content: {
-          "application/json": components["schemas"]["StreamingResponseBody"];
-        };
+        content: {
+          "text/csv": components["schemas"]["StreamingResponseBody"];
+        };
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)

59-96: Fix nullable flag handling as previously noted.

The type inconsistency issue with nullable Boolean flags remains unaddressed. As noted in the previous review, hasNoFlags() expects nullable flags but methods like isTranslatable(), isCaseSensitive(), etc., use isTrue()/isFalse() which don't handle nullability semantically. When a flag is null (unset), these assertions may not behave as intended.

Please apply the fix suggested in the previous review to handle nullability correctly.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)

44-67: Address uninitialized baseLanguageTag as previously noted.

The issue with the uninitialized lateinit var baseLanguageTag remains unresolved. If build() is called without first calling withBaseLanguageTag(), it will throw UninitializedPropertyAccessException, leading to confusing test failures.

Please implement one of the solutions suggested in the previous review (either add validation in build() or make it a required constructor parameter).

🧹 Nitpick comments (10)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)

93-95: Consider adding a visibility check before clicking.

The export likely triggers a download rather than opening a dialog, which explains the minimal implementation. For better test robustness, you could add a visibility or enabled check before clicking:

 clickExport() {
+  gcy('glossary-export-button').should('be.visible');
   gcy('glossary-export-button').click();
 }

This ensures the button is available before attempting to click it, making test failures more informative.

webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)

38-52: Nested try/finally ensures proper cleanup; however, consider documenting the 7-second timeout rationale.

The nested try/finally blocks correctly guarantee cleanup of both the anchor element and object URL, even if errors occur. This pattern is consistently applied across the codebase (e.g., useGlossaryExport.ts, downloadExported.ts).

However, web best practices recommend revoking blob URLs after 300–1000ms (once the download starts or via completion events), whereas this code uses 7000ms. While this conservative timeout is used consistently across multiple download/export utilities and appears intentional, consider adding a brief inline comment documenting why the 7-second delay is necessary. If this addresses specific browser compatibility or large-file-handling requirements discovered during development, that context will help future maintainers understand the choice.

webapp/src/component/common/SingleFileDropzone.tsx (1)

18-18: Consider explicit type annotation for clarity.

While TypeScript can infer the type, adding an explicit annotation improves readability and prevents future type-related confusion.

Apply:

-  const files = file ? [file] : [];
+  const files: FilesType = file ? [file] : [];
webapp/src/views/projects/export/downloadExported.ts (1)

14-28: Append the anchor before click for cross‑browser reliability.

Calling click() on a detached node can be flaky on some Safari/WebKit builds; remove() is a no‑op if never appended.

   try {
     const a = document.createElement('a');
     try {
+      document.body.appendChild(a);
       a.href = url;
-      if (data.type === 'application/zip') {
-        a.download = projectName + dateStr + '.zip';
-      } else {
-        const extension = parseExtension(response) || format.extension;
-        a.download =
-          projectName + onlyPossibleLanguageString + dateStr + '.' + extension;
-      }
+      // set below (unified naming)
       a.click();
     } finally {
       a.remove();
     }
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)

1-1: Stabilize handlers/props to reduce re-renders.

Wrap onImport in useCallback and memoize importDialogProps.

-import { useState } from 'react';
+import { useState, useCallback, useMemo } from 'react';
@@
-  const onImport = () => {
-    setImportDialogOpen(true);
-  };
+  const onImport = useCallback(() => {
+    setImportDialogOpen(true);
+  }, []);
@@
-  return {
-    onImport: canImport ? onImport : undefined,
-    importDialogOpen: isOpen,
-    importDialogProps: {
-      open: isOpen,
-      onClose: () => setImportDialogOpen(false),
-      onFinished: () => setImportDialogOpen(false),
-    },
-  };
+  const importDialogProps = useMemo(
+    () => ({
+      open: isOpen,
+      onClose: () => setImportDialogOpen(false),
+      onFinished: () => setImportDialogOpen(false),
+    }),
+    [isOpen]
+  );
+
+  return {
+    onImport: canImport ? onImport : undefined,
+    importDialogOpen: isOpen,
+    importDialogProps,
+  };

Also applies to: 17-19, 26-34

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)

38-45: Avoid controller-scoped transaction over file I/O; validate before delete.

Parsing/upload can be slow; holding a DB tx here risks long locks. Parse and validate first (outside tx), then perform delete-and-bulk-insert inside a short service-level @transactional to guarantee rollback if any step fails. Also only delete after successful parse.

webapp/src/ee/glossary/components/GlossaryViewBody.tsx (2)

18-18: Prefer React.FC or a typed function over deprecated React.VFC.

VFC was removed from @types/react; using it may break on newer React typings. Switch to React.FC<Props> or a plain function component.


45-53: Conditional mounting of dialogs is redundant.

You can always render the dialogs and control visibility via open; simplifies diffing and avoids remount side effects.

-      {createTermDialogOpen && (
-        <GlossaryTermCreateDialog {...createTermDialogProps} />
-      )}
-      {importDialogOpen && (
-        <GlossaryImportDialog
-          {...importDialogProps}
-          hasExistingTerms={hasExistingTerms}
-        />
-      )}
+      <GlossaryTermCreateDialog {...createTermDialogProps} />
+      <GlossaryImportDialog
+        {...importDialogProps}
+        hasExistingTerms={hasExistingTerms}
+      />
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)

77-81: Guard negative indices in getSafe.

Extra safety if callers ever pass -1.

   fun Array<String>.getSafe(idx: Int?): String? {
     if (idx == null) return null
-    if (idx >= size) return null
+    if (idx < 0 || idx >= size) return null
     return this[idx].ifBlank { null }
   }
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)

15-20: Consider preserving null description values.

Line 17 converts null descriptions to empty strings, which loses the semantic difference between "no description provided" and "explicitly empty description." If the model supports nullable descriptions, consider preserving the null value.

If intentional, this is fine. Otherwise, apply this diff:

 fun createTerm(description: String? = null): GlossaryTerm {
   return GlossaryTerm().apply {
-    this.description = description ?: ""
+    this.description = description
     this.translations = mutableListOf()
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6f910 and c69541e.

⛔ Files ignored due to path filters (1)
  • e2e/cypress/fixtures/glossary/import_basic.csv is excluded by !**/*.csv
📒 Files selected for processing (51)
  • backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt (1 hunks)
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (2 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1 hunks)
  • e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2 hunks)
  • e2e/cypress/e2e/glossary/importExport.cy.ts (1 hunks)
  • e2e/cypress/support/dataCyType.d.ts (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (2 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1 hunks)
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1 hunks)
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1 hunks)
  • webapp/src/component/common/DragDropArea.tsx (1 hunks)
  • webapp/src/component/common/FileDropzone.tsx (1 hunks)
  • webapp/src/component/common/FileDropzoneSelectedFile.tsx (1 hunks)
  • webapp/src/component/common/ModeSelector.tsx (1 hunks)
  • webapp/src/component/common/SingleFileDropzone.tsx (1 hunks)
  • webapp/src/ee/billing/Invoices/DownloadButton.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/components/GlossaryTermsList.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3 hunks)
  • webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (4 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1 hunks)
  • webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1 hunks)
  • webapp/src/ee/glossary/views/GlossaryView.tsx (2 hunks)
  • webapp/src/fixtures/FileUploadFixtures.ts (0 hunks)
  • webapp/src/fixtures/useOnFilePaste.ts (1 hunks)
  • webapp/src/service/apiSchema.generated.ts (6 hunks)
  • webapp/src/views/projects/export/downloadExported.ts (1 hunks)
  • webapp/src/views/projects/import/ImportView.tsx (1 hunks)
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx (0 hunks)
  • webapp/src/views/projects/import/component/ImportFileInput.tsx (3 hunks)
  • webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2 hunks)
  • webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (5 hunks)
💤 Files with no reviewable changes (2)
  • webapp/src/fixtures/FileUploadFixtures.ts
  • webapp/src/views/projects/import/component/ImportFileDropzone.tsx
🚧 Files skipped from review as they are similar to previous changes (17)
  • webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
  • e2e/cypress/e2e/glossary/importExport.cy.ts
  • e2e/cypress/support/dataCyType.d.ts
  • backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
  • webapp/src/component/common/ModeSelector.tsx
  • backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
  • webapp/src/fixtures/useOnFilePaste.ts
  • backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
  • e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
  • webapp/src/component/common/DragDropArea.tsx
  • webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
🧰 Additional context used
🧬 Code graph analysis (16)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
  • GlossaryViewBody (18-84)
webapp/src/views/projects/import/component/ImportFileInput.tsx (2)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • MAX_FILE_COUNT (16-16)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (2)
webapp/src/views/projects/export/downloadExported.ts (1)
  • downloadExported (3-32)
webapp/src/service/http/useQueryApi.ts (1)
  • useApiMutation (224-265)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • MAX_FILE_COUNT (16-16)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (42-54)
  • useIsAdmin (21-22)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
  • ALLOWED_UPLOAD_TYPES (17-17)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
  • FileDropzoneProps (54-63)
  • FileDropzone (65-236)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
  • hasDescription (39-41)
  • hasTranslation (47-49)
  • isTranslatable (59-61)
  • isNotCaseSensitive (71-73)
  • isNotAbbreviation (79-81)
  • isNotForbidden (87-89)
  • isNotTranslatable (63-65)
  • isCaseSensitive (67-69)
  • isAbbreviation (75-77)
  • isForbidden (83-85)
  • hasNoDescription (43-45)
  • hasNoFlags (91-96)
  • hasNoTerm (55-57)
  • hasNoTranslation (51-53)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (5)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
  • useGlossaryTermCreateDialogController (5-35)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
  • useGlossaryImportDialogController (5-35)
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
  • GlossaryTermCreateDialog (28-84)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
  • GlossaryImportDialog (44-140)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
  • GlossaryViewTopbar (28-98)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
  • useGlossaryExport (22-56)
webapp/src/component/layout/BaseViewAddButton.tsx (1)
  • BaseViewAddButton (6-23)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
  • GlossaryTermTranslation (11-49)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
  • FilesType (42-45)
webapp/src/service/MessageService.tsx (1)
  • messageService (6-18)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
  • FileDropzoneSelectedFile (37-60)
webapp/src/component/common/DragDropArea.tsx (1)
  • DragDropArea (61-160)
webapp/src/views/projects/import/ImportView.tsx (2)
webapp/src/views/projects/import/hooks/useImportDataHelper.tsx (1)
  • useImportDataHelper (28-138)
webapp/src/fixtures/useOnFilePaste.ts (1)
  • useOnFilePaste (4-26)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
  • usePreferredOrganization (42-54)
  • useIsAdmin (21-22)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
  • gcy (28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
  • E2GlossaryImportDialog (4-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Frontend static check 🪲
  • GitHub Check: Build backend 🏗️
🔇 Additional comments (30)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)

314-314: LGTM! Enum constant is properly utilized.

The new FILE_PROCESSING_FAILED constant is correctly positioned at the end of the enum and verified to be actively used in the glossary import error handling—specifically thrown when CSV parsing fails in GlossaryImportService.kt (line 31). The naming and implementation are appropriate for the feature's requirements.

e2e/cypress/compounds/glossaries/E2GlossaryView.ts (3)

3-3: LGTM!

The import is necessary for the new methods and follows the established pattern.


81-85: LGTM!

The method follows the established pattern (click → verify visibility → return dialog object) and is consistent with similar methods like openCreateTermDialog.


87-91: LGTM!

The method correctly implements the dialog-opening pattern and differentiates from the empty-state import flow by using a different button trigger.

webapp/src/views/projects/import/ImportView.tsx (1)

25-29: LGTM! Clean integration of paste handling.

The useOnFilePaste hook is correctly wired to the existing dataHelper.onNewFiles handler, enabling paste-triggered file uploads at the view level.

webapp/src/views/projects/import/component/ImportFileInput.tsx (1)

137-141: LGTM! Successful migration to unified DragDropArea.

The component correctly migrates from ImportFileDropzone to DragDropArea with proper prop wiring and maxItems enforcement.

webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)

31-39: LGTM! Clean refactor to DragDropArea.

The component correctly delegates drag-and-drop handling to DragDropArea while maintaining permission-based control and maxItems enforcement.

webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (1)

35-42: LGTM! Correct FilesType migration.

The conversion from File[] to FilesType is properly implemented with correct property mapping and access patterns throughout the validation and upload flow.

webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)

31-60: LGTM! Well-designed file chip component.

Clean implementation with proper accessibility (aria-label), event handling (stopPropagation), and type safety (FilesType[number]).

webapp/src/component/common/FileDropzone.tsx (2)

83-102: LGTM! Robust file handling with validation and user feedback.

The implementation correctly validates file types, enforces maxFiles limits, and provides clear error messages when files are rejected or the limit is exceeded.


125-125: LGTM! Paste integration correctly routes through validation.

Calling useOnFilePaste with handleFilesReceived ensures pasted files undergo the same validation, type checking, and maxFiles enforcement as dropped or selected files.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (2)

18-19: Circular dependency detected.

The @Lazy annotation on glossaryService indicates a circular dependency between GlossaryTermTranslationService and GlossaryService. While @Lazy resolves the injection issue, it suggests a potential design concern that may complicate testing and maintenance.

Consider verifying whether the services can be refactored to break the circular dependency by introducing an intermediary service or moving shared logic to a separate component.


22-33: LGTM!

The refactored getDistinctLanguageTags method cleanly delegates to the new overload, improving reusability by accepting a Glossary object directly. This aligns well with the broader glossary export/import flow.

webapp/src/ee/glossary/views/GlossaryView.tsx (1)

1-45: LGTM!

The refactoring simplifies GlossaryView by moving dialog state management to dedicated hooks (controller pattern). This reduces component complexity and improves separation of concerns, aligning with the new glossary import/export architecture.

ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)

1-8: LGTM!

The GlossaryImportResult class is a clean, well-documented DTO with appropriate OpenAPI schema annotations. The structure is simple and suitable for its purpose.

backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1)

83-83: LGTM!

The new GLOSSARY_IMPORT activity type follows the established pattern (using onlyCountsInList = true like IMPORT) and is properly positioned among other glossary-related activities.

ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)

15-25: LGTM!

The refactored findDistinctLanguageTagsByGlossary method simplifies the query by accepting a Glossary object instead of separate IDs. This improves type safety and aligns with the service layer changes.

webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)

1-35: LGTM!

The hook cleanly encapsulates permission checking and dialog state management. The authorization logic correctly combines organization role, admin status, and glossary ownership checks. The canCreate guard ensures the dialog and callback are only available to authorized users.

webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (1)

48-113: LGTM!

The prop rename from onCreate to onCreateTerm improves clarity and consistency with the broader glossary architecture. The removal of display: 'none' from the import card (line 90) properly exposes the import functionality, aligning with the new glossary import/export feature.

webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)

4-20: LGTM!

The memory leak has been properly addressed. The blob URL is now revoked after a 7-second delay in a finally block, and the temporary anchor element is removed from the DOM. This matches the pattern used in the existing downloadExported helper for project exports.

webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)

9-14: Confirm org-based gating source.

canImport depends on preferredOrganization role and glossary being under that org. If users can access a glossary outside their “preferred” org, this will hide import even when they have rights on that glossary. Confirm this mirrors the create-term controller behavior and the intended UX.

Also applies to: 21-29

webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)

62-87: Import/export controls wired correctly.

Buttons are gated/disabled appropriately; aria-labels and data-cy are set. No issues.

Also applies to: 75-86, 88-91

webapp/src/ee/glossary/components/GlossaryTermsList.tsx (1)

80-82: Props correctly renamed and wired throughout component hierarchy.

Verification complete:

  • GlossaryTermsList props (lines 80-82): ✅ onCreateTerm, onImport, onFetchNextPageHint correctly declared
  • Destructuring (lines 94-96): ✅ Props extracted correctly
  • GlossaryViewBody usage (line 71-73): ✅ Parent passes onCreateTerm, onImport correctly
  • GlossaryEmptyListMessage forwarding (line 154): ✅ Props passed to child
  • No stale onCreate prop assignments found in glossary module
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)

32-33: No issues found—cascading delete is properly configured.

The bulk delete operation in deleteAllByGlossary() is safe:

  • Cascade configured: GlossaryTerm.translations has cascade = [CascadeType.REMOVE] (line 25 of GlossaryTerm.kt), so deleting a term automatically cascades to all related translations.
  • FK constraint proper: GlossaryTermTranslation.term is @ManyToOne(optional = false), ensuring referential integrity.
  • Transactional: The service method is wrapped with @Transactional (line 228 of GlossaryTermService.kt), and the controller caller is also transactional (line 38 of GlossaryImportController.kt).

No orphan rows or FK violations will occur.

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)

41-48: LGTM: writer lifecycle and UTF‑8 are handled.

use {} closes the CSV writer and returning byteInputStream(Charsets.UTF_8) ensures deterministic encoding.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (2)

5-36: LGTM! Well-structured assertion helpers.

The extension functions provide a clear, fluent API for finding and asserting glossary terms by different criteria. The use of describedAs ensures meaningful test failure messages.


38-57: LGTM! Clear and straightforward assertion methods.

The assertion methods for description, translation, and term presence are well-implemented.

ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (3)

22-42: LGTM! Proper relationship management.

The addTranslation extension correctly establishes bidirectional relationships, and withFlags appropriately uses nullable parameters to distinguish between "not set" (null) and "explicitly false."


69-71: LGTM! Clean DSL entry point.


124-128: LGTM! Good builder composition.

The extension properly composes the term builder within the glossary builder DSL.

Comment on lines +86 to +93
@Query(
"""
from GlossaryTerm te
left join fetch te.translations
where te.glossary = :glossary
""",
)
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid duplicate parents with fetch-join. Use DISTINCT (and consider stable ordering).

left join fetch te.translations on a to-many can duplicate GlossaryTerm rows. Add DISTINCT to the select (and optionally order for determinism).

Apply this diff:

   @Query(
     """
-    from GlossaryTerm te
-    left join fetch te.translations
-    where te.glossary = :glossary
+    select distinct te
+    from GlossaryTerm te
+    left join fetch te.translations
+    where te.glossary = :glossary
+    order by te.id
   """,
   )
   fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Query(
"""
from GlossaryTerm te
left join fetch te.translations
where te.glossary = :glossary
""",
)
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm>
@Query(
"""
select distinct te
from GlossaryTerm te
left join fetch te.translations
where te.glossary = :glossary
order by te.id
""",
)
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm>
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
around lines 86-93, the fetch-join on te.translations can produce duplicate
GlossaryTerm rows for to-many relations; update the JPQL to use SELECT DISTINCT
(e.g. "select distinct te from GlossaryTerm te ...") to de-duplicate results
and, for deterministic results, add an explicit ORDER BY (for example ORDER BY
te.id) as needed.

Comment on lines +19 to +23
val reader: CSVReader by lazy {
CSVReaderBuilder(input.reader()).withCSVParser(
CSVParserBuilder().withSeparator(delimiter).build()
).build()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix charset and close the reader.

Default platform charset can corrupt data (Windows-1252 vs UTF‑8). Wrap with InputStreamReader(input, UTF_8) and close the reader.

-  val reader: CSVReader by lazy {
-    CSVReaderBuilder(input.reader()).withCSVParser(
-      CSVParserBuilder().withSeparator(delimiter).build()
-    ).build()
-  }
+  val reader: CSVReader by lazy {
+    val parser = CSVParserBuilder().withSeparator(delimiter).build()
+    CSVReaderBuilder(java.io.InputStreamReader(input, java.nio.charset.StandardCharsets.UTF_8))
+      .withCSVParser(parser)
+      .build()
+  }

Optionally, read all rows with reader.use { it.readAll() } in rawRows to ensure closure.

-  val rawRows: List<Array<String>> by lazy { reader.readAll() }
+  val rawRows: List<Array<String>> by lazy { reader.use { it.readAll() } }
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
around lines 19-23, the CSVReader is created with the platform default charset
and is never closed; change the reader creation to wrap the InputStream with an
InputStreamReader using StandardCharsets.UTF_8 (InputStreamReader(input, UTF_8))
and ensure the CSVReader is closed by using Kotlin's use/try-with-resources
pattern — e.g., replace lazy reader usage with reading logic that calls
reader.use { it.readAll() } (or otherwise closes the CSVReader) so the stream is
read with UTF-8 and properly closed.

Comment on lines +27 to +29
val headers: List<String>? by lazy {
rawRows.firstOrNull()?.map { it.trim() }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Strip UTF‑8 BOM from the first header cell.

A BOM will prevent header matching. Remove it on the first header.

-  val headers: List<String>? by lazy {
-    rawRows.firstOrNull()?.map { it.trim() }
-  }
+  val headers: List<String>? by lazy {
+    rawRows.firstOrNull()?.mapIndexed { i, s ->
+      val t = s.trim()
+      if (i == 0) t.removePrefix("\uFEFF") else t
+    }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val headers: List<String>? by lazy {
rawRows.firstOrNull()?.map { it.trim() }
}
val headers: List<String>? by lazy {
rawRows.firstOrNull()?.mapIndexed { i, s ->
val t = s.trim()
if (i == 0) t.removePrefix("\uFEFF") else t
}
}
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
around lines 27 to 29, the headers lazy property currently trims cells but does
not remove a UTF‑8 BOM from the first header cell; update the headers
computation to strip any leading BOM (e.g. removePrefix("\uFEFF") or equivalent)
from the first element before or after trimming so header matching works
correctly when files include a BOM, while preserving null-safety and the
existing trimming behavior for all cells.

Comment on lines +111 to +113
fun saveAll(translations: List<GlossaryTermTranslation>) {
glossaryTermTranslationRepository.saveAll(translations)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing @Transactional annotation on bulk operation.

The saveAll method performs bulk persistence but lacks a @Transactional annotation, unlike other mutation methods in this service (e.g., create, updateOrCreate, updateBaseLanguage). Without a transaction boundary, partial saves may occur on failure, leading to inconsistent state.

Apply this diff to ensure atomicity:

+  @Transactional
   fun saveAll(translations: List<GlossaryTermTranslation>) {
     glossaryTermTranslationRepository.saveAll(translations)
   }
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
around lines 111 to 113, the bulk persistence method saveAll lacks a
@Transactional annotation which can lead to partial commits on failure; add the
@Transactional annotation to the saveAll method (and the corresponding import if
missing) so the entire bulk save runs within a single transaction, ensuring
atomicity and consistency.

Comment on lines +9 to +302
@Test
fun `parses CSV with all fields`() {
val csvContent = """
term,description,translatable,casesensitive,abbreviation,forbidden,en,cs,de
Apple,A fruit,true,false,false,false,Apple,Jablko,Apfel
API,Application Programming Interface,false,true,true,false,API,API,API
BadWord,Forbidden term,true,false,false,true,BadWord,SpatneSlovo,SchlechteWort
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(3)

terms.assertTerm("Apple") {
hasDescription("A fruit")
hasTranslation("en", "Apple")
hasTranslation("cs", "Jablko")
hasTranslation("de", "Apfel")
isTranslatable()
isNotCaseSensitive()
isNotAbbreviation()
isNotForbidden()
}

terms.assertTerm("API") {
hasDescription("Application Programming Interface")
hasTranslation("en", "API")
hasTranslation("cs", "API")
hasTranslation("de", "API")
isNotTranslatable()
isCaseSensitive()
isAbbreviation()
isNotForbidden()
}

terms.assertTerm("BadWord") {
hasDescription("Forbidden term")
hasTranslation("en", "BadWord")
hasTranslation("cs", "SpatneSlovo")
hasTranslation("de", "SchlechteWort")
isTranslatable()
isNotCaseSensitive()
isNotAbbreviation()
isForbidden()
}
}

@Test
fun `parses CSV with minimal data`() {
val csvContent = """
term,en
MinimalTerm,Minimal
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(1)
terms.assertTerm("MinimalTerm") {
hasNoDescription()
hasTranslation("en", "Minimal")
hasNoFlags()
}
}

@Test
fun `parses CSV with only translations`() {
val csvContent = """
en,cs,de
Hello,Ahoj,Hallo
World,Svět,Welt
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(2)
terms.assertTermWithTranslation("en", "Hello") {
hasNoTerm()
hasNoDescription()
hasTranslation("en", "Hello")
hasTranslation("cs", "Ahoj")
hasTranslation("de", "Hallo")
hasNoFlags()
}

terms.assertTermWithTranslation("en", "World") {
hasNoTerm()
hasNoDescription()
hasTranslation("en", "World")
hasTranslation("cs", "Svět")
hasTranslation("de", "Welt")
hasNoFlags()
}
}

@Test
fun `parses CSV with semicolon delimiter`() {
val csvContent = """
term;description;en;cs
TestTerm;Test description;TestValue;TestHodnota
""".trimIndent()

val terms = parseCSV(csvContent, ';')

terms.assertSize(1)
terms.assertTerm("TestTerm") {
hasDescription("Test description")
hasTranslation("en", "TestValue")
hasTranslation("cs", "TestHodnota")
}
}

@Test
fun `parses boolean flag variations`() {
val csvContent = """
term,translatable,casesensitive,abbreviation,forbidden,en
Term1,true,1,yes,t,Value1
Term2,false,0,no,f,Value2
Term3,TRUE,Y,T,FALSE,Value3
Term4,invalid,n,false,gibberish,Value4
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(4)

terms.assertTerm("Term1") {
isTranslatable()
isCaseSensitive()
isAbbreviation()
isForbidden()
}

terms.assertTerm("Term2") {
isNotTranslatable()
isNotCaseSensitive()
isNotAbbreviation()
isNotForbidden()
}

terms.assertTerm("Term3") {
isTranslatable()
isCaseSensitive()
isAbbreviation()
isNotForbidden()
}

terms.assertTerm("Term4") {
isNotTranslatable() // invalid defaults to false
isNotCaseSensitive()
isNotForbidden() // invalid defaults to false
isNotAbbreviation()
}
}

@Test
fun `skips empty rows`() {
val csvContent = """
term,en
ValidTerm,ValidValue
,,
AnotherTerm,AnotherValue
,
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(2)
terms.assertTerm("ValidTerm") {
hasTranslation("en", "ValidValue")
}
terms.assertTerm("AnotherTerm") {
hasTranslation("en", "AnotherValue")
}
}

@Test
fun `handles case insensitive headers`() {
val csvContent = """
TERM,DESCRIPTION,TRANSLATABLE,CASESENSITIVE,ABBREVIATION,FORBIDDEN,EN
TestTerm,Test Desc,true,false,false,false,TestValue
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(1)
terms.assertTerm("TestTerm") {
hasDescription("Test Desc")
hasTranslation("EN", "TestValue")
isTranslatable()
isNotCaseSensitive()
isNotAbbreviation()
isNotForbidden()
}
}

@Test
fun `handles mixed header spacing`() {
val csvContent = """
term , description, en ,cs
TestTerm,Test description,TestValue,TestHodnota
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(1)
terms.assertTerm("TestTerm") {
hasDescription("Test description")
hasTranslation("en", "TestValue")
hasTranslation("cs", "TestHodnota")
}
}

@Test
fun `returns empty list for empty CSV`() {
val csvContent = """
term,en
""".trimIndent()

val terms = parseCSV(csvContent)
terms.assertSize(0)
}

@Test
fun `returns empty list for headers only`() {
val csvContent = "term,description,en,cs"

val terms = parseCSV(csvContent)
terms.assertSize(0)
}

@Test
fun `handles empty translation values`() {
val csvContent = """
term,en,cs,de
TestTerm,TestValue,,
AnotherTerm,,AnotherValue,
EmptyTerm,,,
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(3)

terms.assertTerm("TestTerm") {
hasTranslation("en", "TestValue")
hasNoTranslation("cs")
hasNoTranslation("de")
}

terms.assertTerm("AnotherTerm") {
hasNoTranslation("en")
hasTranslation("cs", "AnotherValue")
hasNoTranslation("de")
}

terms.assertTerm("EmptyTerm") {
hasNoTranslation("en")
hasNoTranslation("cs")
hasNoTranslation("de")
}
}

@Test
fun `handles term with description only`() {
val csvContent = """
description,en,cs
Just a description,,
Another description,Value,
""".trimIndent()

val terms = parseCSV(csvContent)

terms.assertSize(2)

terms.assertTermWithDescription("Just a description") {
hasNoTerm()
hasDescription("Just a description")
hasNoTranslation("en")
hasNoTranslation("cs")
}

terms.assertTermWithDescription("Another description") {
hasNoTerm()
hasDescription("Another description")
hasTranslation("en", "Value")
hasNoTranslation("cs")
}
}

private fun parseCSV(csvContent: String, delimiter: Char = ','): List<ImportGlossaryTerm> {
return GlossaryCSVParser(ByteArrayInputStream(csvContent.toByteArray()), delimiter).parse()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Great coverage; add a few edge‑case scenarios.

  • Quoted fields with delimiters/newlines, Windows CRLF, UTF‑8 BOM handling.
  • Very long fields and trailing/leading spaces in cell values.

Example additions:

  • '"API, v2"' in a term/translation
  • Headers/rows with CRLF
  • File starting with BOM

Also, the “case insensitive headers” test asserts EN (uppercase). Confirm whether parser preserves header case or normalizes to canonical tags (lowercase). Align with import service to avoid mixed‑case language tags in persistence.

Locate normalization (if any):


🏁 Script executed:

#!/bin/bash
rg -nP -C2 'languageTag.*(lowercase|toLowerCase|Locale\.|normalize)' ee/backend/app/src/main/kotlin
rg -nP -C2 'ImportGlossaryTerm|GlossaryCSVParser' ee/backend/app/src/main/kotlin

Length of output: 6822


🏁 Script executed:

#!/bin/bash
# Inspect GlossaryCSVParser full implementation, focus on header parsing and translation map construction
cat ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt

Length of output: 3437


🏁 Script executed:

#!/bin/bash
# Check GlossaryImportService.applyFrom to see if normalization happens during import
rg -nP -A20 'private fun GlossaryTerm.applyFrom' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt

Length of output: 930


🏁 Script executed:

#!/bin/bash
# Inspect ImportGlossaryTerm data class definition
cat ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check test helper methods to understand what hasTranslation asserts
rg -nP -B5 -A10 'fun hasTranslation|fun hasNoTranslation' ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any language tag normalization in glossary-related code
rg -nP 'translations\[.*\]|\.get\(.*language' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary --type=kotlin

Length of output: 187


🏁 Script executed:

#!/bin/bash
# Find the test class definition and helper methods
rg -nP 'class GlossaryCSVParserTest|fun.*assertTerm|fun.*hasTranslation' ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/ -B2 -A5

Length of output: 1064


🏁 Script executed:

#!/bin/bash
# Look for assertion helper in parent class or test utilities
fd -e kt -path '*/test*' -name '*.kt' | xargs rg -l 'assertTerm|hasTranslation' | head -5

Length of output: 362


🏁 Script executed:

#!/bin/bash
# Check how GlossaryImportService iterates/stores translations from ImportGlossaryTerm
rg -nP -A30 'glossaryTerm.translations' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt

Length of output: 1311


🏁 Script executed:

#!/bin/bash
# Inspect the test utility
cat ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt

Length of output: 2767


Add edge-case scenarios and fix language tag case handling.

The current tests cover basic scenarios well, but miss important edge cases:

  • Quoted fields containing delimiters or newlines, Windows CRLF, UTF-8 BOM handling
  • Very long fields and leading/trailing spaces in cell values

Critical issue with language tags: The parser preserves header case (e.g., "EN", "En") in the translations map keys, and the import service stores these as-is into GlossaryTermTranslation. However, the query layer uses lowercase normalized tags via Locale.forLanguageTag(). This mismatch means uppercase or mixed-case language headers in the CSV will result in language tags that fail to match during lookups.

The test "handles case insensitive headers" is misleading—it verifies case-insensitive header matching in the parser, but does not normalize the resulting language tag keys. The assertion hasTranslation("EN", ...) passes only because the test directly inspects the ImportGlossaryTerm before it reaches the import service.

Recommended fixes:

  • Normalize language tag headers to lowercase in the parser (line ~65) when building the translations map, or
  • Apply normalization in GlossaryImportService.applyFrom() before storing.
  • Add tests for the full import flow to catch case mismatches between storage and query layers.
🤖 Prompt for AI Agents
In
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
around lines 9-302, the review points out that language header case is preserved
in the parser which later causes mismatches with lowercased query tags; also
tests miss edge cases like quoted fields with delimiters/newlines, CRLF, UTF-8
BOM, very long fields and leading/trailing spaces. Fix by normalizing language
header keys to a canonical form (e.g., lowercased Locale.forLanguageTag() or
.lowercase()) when building the translations map in the parser (around line ~65)
or alternatively normalize in GlossaryImportService.applyFrom() before
persisting, and add new tests in this file that validate: headers with
mixed/upper case produce normalized tags that survive the full import flow,
quoted fields with delimiters/newlines, CRLF and BOM handling, trimming of cell
values, and a test with very long field values to ensure no truncation.

Comment on lines +73 to +122
class GlossaryTermBuilder {
private var description: String? = null
private val translations = mutableListOf<Pair<String, String>>()
private var flagNonTranslatable: Boolean = false
private var flagCaseSensitive: Boolean = false
private var flagAbbreviation: Boolean = false
private var flagForbiddenTerm: Boolean = false

fun withDescription(description: String) = apply {
this.description = description
}

fun withTranslation(languageTag: String, text: String) = apply {
translations.add(languageTag to text)
}

fun withTranslations(vararg translations: Pair<String, String>) = apply {
this.translations.addAll(translations)
}

fun translatable(value: Boolean = true) = apply {
flagNonTranslatable = !value
}

fun caseSensitive(value: Boolean = true) = apply {
flagCaseSensitive = value
}

fun abbreviation(value: Boolean = true) = apply {
flagAbbreviation = value
}

fun forbiddenTerm(value: Boolean = true) = apply {
flagForbiddenTerm = value
}

fun build(): GlossaryTerm {
return createTerm(description).let {
translations.forEach { (languageTag, text) ->
it.addTranslation(languageTag, text)
}
it.withFlags(
nonTranslatable = flagNonTranslatable,
caseSensitive = flagCaseSensitive,
abbreviation = flagAbbreviation,
forbiddenTerm = flagForbiddenTerm,
)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix flag semantics: use nullable Booleans to support "unset" state.

The builder initializes all flags to false (lines 76-79), which means flags are always explicitly set when build() is called (lines 114-119). This prevents creating terms with unset (null) flags, contradicting the nullable semantics of withFlags().

Apply this diff to use nullable Booleans:

 class GlossaryTermBuilder {
   private var description: String? = null
   private val translations = mutableListOf<Pair<String, String>>()
-  private var flagNonTranslatable: Boolean = false
-  private var flagCaseSensitive: Boolean = false
-  private var flagAbbreviation: Boolean = false
-  private var flagForbiddenTerm: Boolean = false
+  private var flagNonTranslatable: Boolean? = null
+  private var flagCaseSensitive: Boolean? = null
+  private var flagAbbreviation: Boolean? = null
+  private var flagForbiddenTerm: Boolean? = null

This allows the builder to distinguish between "explicitly set to false" and "not set at all."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class GlossaryTermBuilder {
private var description: String? = null
private val translations = mutableListOf<Pair<String, String>>()
private var flagNonTranslatable: Boolean = false
private var flagCaseSensitive: Boolean = false
private var flagAbbreviation: Boolean = false
private var flagForbiddenTerm: Boolean = false
fun withDescription(description: String) = apply {
this.description = description
}
fun withTranslation(languageTag: String, text: String) = apply {
translations.add(languageTag to text)
}
fun withTranslations(vararg translations: Pair<String, String>) = apply {
this.translations.addAll(translations)
}
fun translatable(value: Boolean = true) = apply {
flagNonTranslatable = !value
}
fun caseSensitive(value: Boolean = true) = apply {
flagCaseSensitive = value
}
fun abbreviation(value: Boolean = true) = apply {
flagAbbreviation = value
}
fun forbiddenTerm(value: Boolean = true) = apply {
flagForbiddenTerm = value
}
fun build(): GlossaryTerm {
return createTerm(description).let {
translations.forEach { (languageTag, text) ->
it.addTranslation(languageTag, text)
}
it.withFlags(
nonTranslatable = flagNonTranslatable,
caseSensitive = flagCaseSensitive,
abbreviation = flagAbbreviation,
forbiddenTerm = flagForbiddenTerm,
)
}
}
}
class GlossaryTermBuilder {
private var description: String? = null
private val translations = mutableListOf<Pair<String, String>>()
private var flagNonTranslatable: Boolean? = null
private var flagCaseSensitive: Boolean? = null
private var flagAbbreviation: Boolean? = null
private var flagForbiddenTerm: Boolean? = null
fun withDescription(description: String) = apply {
this.description = description
}
fun withTranslation(languageTag: String, text: String) = apply {
translations.add(languageTag to text)
}
fun withTranslations(vararg translations: Pair<String, String>) = apply {
this.translations.addAll(translations)
}
fun translatable(value: Boolean = true) = apply {
flagNonTranslatable = !value
}
fun caseSensitive(value: Boolean = true) = apply {
flagCaseSensitive = value
}
fun abbreviation(value: Boolean = true) = apply {
flagAbbreviation = value
}
fun forbiddenTerm(value: Boolean = true) = apply {
flagForbiddenTerm = value
}
fun build(): GlossaryTerm {
return createTerm(description).let {
translations.forEach { (languageTag, text) ->
it.addTranslation(languageTag, text)
}
it.withFlags(
nonTranslatable = flagNonTranslatable,
caseSensitive = flagCaseSensitive,
abbreviation = flagAbbreviation,
forbiddenTerm = flagForbiddenTerm,
)
}
}
}

Comment on lines +18 to +24
if (data.type === 'application/zip') {
a.download = projectName + dateStr + '.zip';
} else {
const extension = parseExtension(response) || format.extension;
a.download =
projectName + onlyPossibleLanguageString + dateStr + '.' + extension;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unify extension detection and zip handling (header or blob type).

Relying solely on data.type === 'application/zip' misses 'octet‑stream' or other zip CTs; header may still be .zip. Also avoids wrong fallback (e.g., naming a .zip as .json).

-      if (data.type === 'application/zip') {
-        a.download = projectName + dateStr + '.zip';
-      } else {
-        const extension = parseExtension(response) || format.extension;
-        a.download =
-          projectName + onlyPossibleLanguageString + dateStr + '.' + extension;
-      }
+      const headerExt = parseExtension(response);
+      const isZip =
+        (!!data.type && data.type.includes('zip')) || headerExt === 'zip';
+      const ext = isZip ? 'zip' : (headerExt || format.extension);
+      const languagePart = isZip ? '' : onlyPossibleLanguageString;
+      a.download = `${projectName}${languagePart}${dateStr}.${ext}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (data.type === 'application/zip') {
a.download = projectName + dateStr + '.zip';
} else {
const extension = parseExtension(response) || format.extension;
a.download =
projectName + onlyPossibleLanguageString + dateStr + '.' + extension;
}
const headerExt = parseExtension(response);
const isZip =
(!!data.type && data.type.includes('zip')) || headerExt === 'zip';
const ext = isZip ? 'zip' : (headerExt || format.extension);
const languagePart = isZip ? '' : onlyPossibleLanguageString;
a.download = `${projectName}${languagePart}${dateStr}.${ext}`;
🤖 Prompt for AI Agents
In webapp/src/views/projects/export/downloadExported.ts around lines 18 to 24,
the code only treats blobs with data.type === 'application/zip' as zip files
which misses cases like application/octet-stream or other CTs where the response
headers or content-disposition filename indicate a .zip; change the logic to
first try to extract an extension from the response (content-disposition
filename if present, then response.headers['content-type'] mapping to known
extensions), then treat the file as zip if either the resolved extension ===
'zip' or the blob type explicitly contains 'zip'; otherwise fall back to
parseExtension(response) || format.extension; set a.download using the resolved
extension so .zip files are always named .zip even when the content-type is
generic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Glossary import/export

2 participants