Skip to content

fix: sort export translations by key name (Sentry TOLGEE-BACKEND-3ED) - #3470

Merged
bdshadow merged 1 commit into
mainfrom
bdshadow/fix-nested-structure-export-sorting
Feb 19, 2026
Merged

fix: sort export translations by key name (Sentry TOLGEE-BACKEND-3ED)#3470
bdshadow merged 1 commit into
mainfrom
bdshadow/fix-nested-structure-export-sorting

Conversation

@bdshadow

@bdshadow bdshadow commented Feb 14, 2026

Copy link
Copy Markdown
Member

The StructureModelBuilder requires translations sorted alphabetically by key name path, but the database collation may return them in a different order. Sort in GenericStructuredFileExporter.prepare() to ensure correct ordering regardless of database collation.

Likely triggered by the branching feature (#3246, merged Feb 6) which added a LEFT JOIN on Branch to the export query. The additional join changed the query execution plan, surfacing a collation mismatch between PostgreSQL's locale-aware sorting and Java's byte-by-byte string comparison for keys with punctuation (e.g. "profile.my-goals" vs "profile.my-goals.plan_other").

Summary by CodeRabbit

  • Bug Fixes
    • Export validation now detects and prevents collisions between plural key suffixes and existing keys during file export operations. Users receive a clear error message when conflicts are detected, specifying the involved keys and suffix to enable quick resolution.

@bdshadow
bdshadow requested a review from JanCizmar February 14, 2026 23:01
@coderabbitai

coderabbitai Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR adds plural key suffix collision detection to the export functionality. When pluralsViaSuffixes is enabled, the exporter validates that plural form suffixes don't collide with existing keys, throwing a new exception if conflicts are detected.

Changes

Cohort / File(s) Summary
Export Collision Detection
backend/data/src/main/kotlin/io/tolgee/formats/genericStructuredFile/out/GenericStructuredFileExporter.kt, backend/data/src/test/kotlin/io/tolgee/unit/formats/i18next/out/I18nextFileExporterTest.kt
Adds validation method checkPluralSuffixCollisions() to detect conflicts between plural suffixes and existing keys, with test coverage verifying exception is thrown on collision.
Exception & Message Infrastructure
backend/data/src/main/kotlin/io/tolgee/exceptions/ExportCollidingKeysException.kt, backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
Introduces new ExportCollidingKeysException class with pluralKey, collidingKey, and suffix properties, paired with new EXPORT_KEY_PLURAL_SUFFIX_COLLISION message constant.
Frontend Error Handling & Localization
webapp/src/translationTools/useErrorTranslation.ts, webapp/src/service/apiSchema.generated.ts, webapp/src/i18n/en.json
Adds error code handling, API schema enumeration, and English localization for the collision error with parameters for affected keys and suffix.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • Anty0

Poem

🐰 A collision we shall catch, before keys clash and match,
With suffixes so sly, we keep your exports dry,
No plural pranks today, they're validated away! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title claims to sort export translations by key name, but the actual changes implement plural suffix collision detection, not sorting. Update the title to reflect the actual change: 'fix: detect and throw on plural suffix collisions in exports' or similar, which accurately describes the added collision detection logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 bdshadow/fix-nested-structure-export-sorting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@bdshadow
bdshadow marked this pull request as draft February 15, 2026 06:32
@bdshadow
bdshadow removed the request for review from JanCizmar February 15, 2026 06:32
@bdshadow
bdshadow force-pushed the bdshadow/fix-nested-structure-export-sorting branch from 226d7f8 to 5109c75 Compare February 15, 2026 20:35
@bdshadow
bdshadow marked this pull request as ready for review February 15, 2026 21:12
@bdshadow
bdshadow requested a review from JanCizmar February 15, 2026 21:12

@Anty0 Anty0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this is a good way to fix this. This would be a breaking change - the order of keys in the export shouldn't change. I think there are people who depend on sorting being static. Can we solve this without affecting existing sorting?

@bdshadow
bdshadow force-pushed the bdshadow/fix-nested-structure-export-sorting branch from 5109c75 to a4d9fbc Compare February 19, 2026 13:31
…BACKEND-3ED)

When exporting to i18next format, plural keys get suffixed (e.g.,
"plan" → "plan_other"). If a non-plural key with the same suffixed
name already exists, the export crashed with an IllegalStateException.

Now detect such collisions upfront and throw a BadRequestException
with a clear message identifying both the plural key and the
colliding key, so the user can resolve the naming conflict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bdshadow
bdshadow force-pushed the bdshadow/fix-nested-structure-export-sorting branch from a4d9fbc to 3a676c4 Compare February 19, 2026 14:33
@bdshadow
bdshadow marked this pull request as draft February 19, 2026 14:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
webapp/src/i18n/en.json (1)

741-741: New key inserted out of alphabetical order

export_key_plural_suffix_collision is placed between two cannot_* keys at line 741, breaking the alphabetical ordering of the file. All other export_* keys appear around lines 929–950. Moving it there keeps the file consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/i18n/en.json` at line 741, The new JSON entry
"export_key_plural_suffix_collision" is out of alphabetical order; cut the
"export_key_plural_suffix_collision" key-value pair from its current location
and insert it with the other "export_*" keys (around the existing export block)
maintaining alphabetical order among export_* entries; ensure you preserve
commas and valid JSON syntax after moving so the file remains parseable.
backend/data/src/main/kotlin/io/tolgee/formats/genericStructuredFile/out/GenericStructuredFileExporter.kt (1)

52-66: Redundant per-language iteration in collision check

translations contains one entry per key per language, so a plural key exported for L languages causes the same name collision check to run L times. Deduplicate by key name first:

♻️ Proposed refactor
 private fun checkPluralSuffixCollisions() {
   val allKeyNames = translations.map { it.key.name }.toSet()
-  translations.filter { it.key.isPlural }.forEach { translation ->
+  val pluralKeyNames = translations.filter { it.key.isPlural }.map { it.key.name }.toSet()
+  for (pluralKeyName in pluralKeyNames) {
     for (suffix in formKeywords) {
-      val suffixedName = "${translation.key.name}_$suffix"
+      val suffixedName = "${pluralKeyName}_$suffix"
       if (suffixedName in allKeyNames) {
         throw ExportCollidingKeysException(
-          pluralKey = translation.key.name,
+          pluralKey = pluralKeyName,
           collidingKey = suffixedName,
           suffix = suffix,
         )
       }
     }
-  }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/data/src/main/kotlin/io/tolgee/formats/genericStructuredFile/out/GenericStructuredFileExporter.kt`
around lines 52 - 66, The checkPluralSuffixCollisions function is running the
same collision check once per language because translations contains entries per
key per language; to fix it, deduplicate by key name before iterating: compute
allKeyNames = translations.map { it.key.name }.toSet() as now, then build a set
of pluralKeyNames from translations (e.g. translations.filter { it.key.isPlural
}.map { it.key.name }.toSet()) and iterate that set instead of translations,
checking for each suffix in formKeywords whether "${pluralKey}_$suffix" is in
allKeyNames and throwing ExportCollidingKeysException(pluralKey = pluralKey,
collidingKey = suffixedName, suffix = suffix) when found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@backend/data/src/main/kotlin/io/tolgee/formats/genericStructuredFile/out/GenericStructuredFileExporter.kt`:
- Around line 52-66: The checkPluralSuffixCollisions function is running the
same collision check once per language because translations contains entries per
key per language; to fix it, deduplicate by key name before iterating: compute
allKeyNames = translations.map { it.key.name }.toSet() as now, then build a set
of pluralKeyNames from translations (e.g. translations.filter { it.key.isPlural
}.map { it.key.name }.toSet()) and iterate that set instead of translations,
checking for each suffix in formKeywords whether "${pluralKey}_$suffix" is in
allKeyNames and throwing ExportCollidingKeysException(pluralKey = pluralKey,
collidingKey = suffixedName, suffix = suffix) when found.

In `@webapp/src/i18n/en.json`:
- Line 741: The new JSON entry "export_key_plural_suffix_collision" is out of
alphabetical order; cut the "export_key_plural_suffix_collision" key-value pair
from its current location and insert it with the other "export_*" keys (around
the existing export block) maintaining alphabetical order among export_*
entries; ensure you preserve commas and valid JSON syntax after moving so the
file remains parseable.

@bdshadow
bdshadow requested a review from Anty0 February 19, 2026 16:28
@bdshadow
bdshadow marked this pull request as ready for review February 19, 2026 16:28

@Anty0 Anty0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's definitely better, but I'm not sure it's an ideal solution. If we miss an edge case, we still end up with an IllegalStateException from throwIfExists. Maybe we can add another exception that just tells the user there is a conflict at the path buildPath(fullPath) if the throw in throwIfExists is reached - I'd just replace the IllegalStateException.

Comment thread webapp/src/i18n/en.json
"cannot_change_your_own_access_tooltip": "Cannot revoke access to yourself",
"cannot_delete_base_language_message": "Cannot delete base language",
"cannot_delete_branch_with_children": "Cannot delete this branch because other branches were created from it.",
"export_key_plural_suffix_collision": "Export failed: plural key \"{pluralKey}\" with suffix \"_{suffix}\" collides with existing key \"{collidingKey}\". Rename one of the keys to resolve the conflict.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't be part of the changeset.

@Anty0 Anty0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We'll deal with the other exception in a separate PR.

@bdshadow
bdshadow merged commit 320d87a into main Feb 19, 2026
71 of 74 checks passed
@bdshadow
bdshadow deleted the bdshadow/fix-nested-structure-export-sorting branch February 19, 2026 17:41
TolgeeMachine added a commit that referenced this pull request Feb 20, 2026
# [3.163.0](v3.162.0...v3.163.0) (2026-02-20)

### Bug Fixes

* gh-982 - add tooltips ([#3417](#3417)) ([52d5933](52d5933)), closes [#982](#982)
* Resilient Redis cache reads for rate limiting ([#3455](#3455)) ([a33e1d0](a33e1d0))
* sort export translations by key name (Sentry TOLGEE-BACKEND-3ED) ([#3470](#3470)) ([320d87a](320d87a)), closes [#3246](#3246)

### Features

* rate limit translation and activity endpoints ([#3483](#3483)) ([2f125da](2f125da))
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.

2 participants