Skip to content

feat: Translation Memory Management#3596

Draft
dkrizan wants to merge 57 commits intomainfrom
dkrizan/translation-memory-management
Draft

feat: Translation Memory Management#3596
dkrizan wants to merge 57 commits intomainfrom
dkrizan/translation-memory-management

Conversation

@dkrizan
Copy link
Copy Markdown
Member

@dkrizan dkrizan commented Apr 8, 2026

This is the main PR for the entire feature — it accumulates commits as the work progresses through the cycle.

Architecture decisions

  • Single suggestion endpoint for all plans. POST /v2/projects/{id}/suggest/translation-memory dispatches at runtime: when the project has any readable TM assignments, it uses ManagedTranslationMemorySuggestionService (queries translation_memory_entry); otherwise it falls back to the legacy TranslationMemoryService (queries the translation table). No URL swap on the frontend, no breaking API change.
  • Plan-aware filter inside the managed service. getReadableTmIdsForSuggestions(projectId, organizationId) returns only the project's own PROJECT-type TM on free plan, or project + shared TMs on paid plan (Feature.TRANSLATION_MEMORY).
  • Free plan opens up to the new TM. Free users get suggestions and single-key auto-translate from their project's own TM. Shared TMs, management UI, TMX import/export, and batch pre-translate by TM remain Business-only.
  • Existing projects fall through to the legacy service. They have no auto-created project TM, so getReadableTmIds returns empty and the dispatch picks the legacy path. No migration required.

Status

✅ Done

Data model & write pipeline

  • Entities: TranslationMemory, TranslationMemoryEntry, TranslationMemoryProject (entity-backed join with read/write/priority).
  • Liquibase schema + GiST trigram index on translation_memory_entry(target_language_tag, source_text).
  • tuid column on entries for TMX round-trip.
  • Auto-create project TM on ProjectCreationService.createProject() for every new project (all plans).
  • Write TM entries on every TranslationService.save() for any project with writable TM assignments.
  • Cleanup hooks: ProjectHardDeletingService removes project TM + assignments; cascade deletes via DB FK on translation removal.

Org-level shared TM management (EE)

  • CRUD API for shared TMs, requires MAINTAINER org role.
  • Frontend: single flat list with All/Shared/Project only filter tabs, type badges per row, kebab menu (Settings + Delete).
  • Unified TM Settings dialog — name, base language, project assignments with Read/Write toggles, single-add project autocomplete, soft-delete with keep/discard confirmation and undo. Replaces the old Edit dialog and the Projects tab on the TM detail view.
  • TM content browser: paginated by distinct source text (language filter narrows columns, never hides source rows); inline click-to-edit, create entry dialog, delete with confirmation.

Project-level TM configuration (EE)

  • API: list / connect / disconnect / update assignments (with keepData=true snapshot on disconnect).
  • Frontend: TM section on Advanced tab with drag-and-drop priority reorder (@dnd-kit), Read/Write chips per row, settings gear (opens unified dialog), "Manage all TMs" button.

Suggestion endpoint (CE)

  • Unified /v2/projects/{id}/suggest/translation-memory dispatches between managed and legacy services.
  • ManagedTranslationMemorySuggestionService lives in backend/data (CE), plan-aware via EnabledFeaturesProvider.
  • TmAutoTranslateProvider simplified to a single CE impl with the same dispatch — EE override deleted.
  • Free plan: project TM only. Paid plan: project TM + assigned shared TMs.

TMX import / export (EE)

  • TMX 1.4b parser (DOM-based, namespace-aware, DTD loading disabled).
  • TMX 1.4b writer (StAX-based, preserves tuid, groups by tuid or source text).
  • Import dialog with keep/override conflict resolution (matched by tuid + language).
  • Import result counts by translation unit (distinct tuid/source).

Plan gating

  • Feature.TRANSLATION_MEMORY flag, backend enforced on management endpoints + batch pre-translate.
  • StartBatchJobController.translate() hard-gated with @RequiresFeatures (regression for free-plan API clients — documented).
  • Frontend: management UI hidden for free users; pre-translate by TM batch option hidden in the operations menu.
  • Free users still see the suggestion panel and use /suggest/translation-memory against their project TM.

🚧 To do

  • Per-row source-TM badge in editor suggestion panel (in progress)
  • Documentation: TM management guide, TMX import/export, migration notes
  • Performance verification on batch pre-translate (40k × 5 baseline)

Deferred / cut

  • Plan gating UI rename ("Translation Memory" → "Similar translations" for free plan) — not needed: free plan now uses the same TM system, so the label stays "Translation Memory".
  • Lazy migration for existing Business plan projects — handled implicitly by the dispatch (existing projects fall through to the legacy service; no backfill required).

Tests

  • CE: TranslationSuggestionControllerTmTest (legacy path), BatchPreTranslateByTmTest (paid happy path + free 400 gate).
  • EE: TranslationSuggestionControllerManagedTmTest (paid + free dispatch, project TM only on free), SharedTranslationMemoryControllerTest, ProjectTranslationMemoryControllerTest, TranslationMemoryEntryControllerTest, TranslationMemoryTmxControllerTest.
  • E2E: orgSettings.cy.ts, projectSettings.cy.ts, tmContentBrowser.cy.ts. All pre-existing TM features covered.

Test plan

  • CI passes
  • Manual smoke: free org new project → suggestion panel works (project TM), management UI hidden, batch pre-translate hidden.
  • Manual smoke: free org old project (no project TM) → suggestion panel still works via legacy path.
  • Manual smoke: paid org → full feature: shared TMs, settings dialog, import/export, batch pre-translate.
  • API: free user POST /suggest/translation-memory → 200; POST /start-batch-job/pre-translate-by-tm → 400 feature_not_enabled.

@dkrizan dkrizan force-pushed the dkrizan/translation-memory-management branch from 4f09c13 to d1f5338 Compare April 9, 2026 07:07
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d79845cf-53a2-411e-abe5-c67573b2ac5d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dkrizan/translation-memory-management

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.

@dkrizan dkrizan force-pushed the dkrizan/translation-memory-management branch from d1f5338 to 325ce9c Compare April 9, 2026 07:53
@dkrizan dkrizan linked an issue Apr 9, 2026 that may be closed by this pull request
@dkrizan dkrizan force-pushed the dkrizan/translation-memory-management branch 4 times, most recently from 627c037 to b16f9dc Compare April 14, 2026 09:57
dkrizan added 21 commits April 21, 2026 20:14
Introduces dedicated Translation Memory entities for the upcoming
shared TM feature, without affecting the existing "similar translations"
suggestion feature which continues querying the translation table directly.

- New entities: TranslationMemory, TranslationMemoryEntry, TranslationMemoryProject
- Liquibase schema + GiST trigram index for similarity search
- Auto-create project TM on project creation via ProjectCreationService
- Write TM entries on translation save (only for projects with TM assignments)
- Clean up TM data on project hard-deletion
- TRANSLATION_MEMORY feature flag for future gating
- Integration tests covering entity lifecycle, write pipeline,
  translation deletion cascade, and project deletion cleanup

Existing "similar translations" feature is unchanged — it will be
renamed on the frontend as part of a later phase.
Introduces the managed Translation Memory feature (Business plan).
All new code lives in the EE module and is gated by
@RequiresFeatures(Feature.TRANSLATION_MEMORY).

Organization-level shared TM CRUD:
- SharedTranslationMemoryService (EE): create/update/delete/find/list
- SharedTranslationMemoryController at /v2/organizations/{orgId}/translation-memories
- Rejects mutations of PROJECT-type TMs via the shared endpoint
- Requires MAINTAINER org role for mutations

Project-level TM configuration:
- ProjectTranslationMemoryConfigService (EE) with assign/unassign/update
- ProjectTranslationMemoryController at /v2/projects/{projectId}/translation-memories
- Individual CRUD endpoints (GET list, POST assign, DELETE unassign, PUT update)
- Cross-organization assignment is rejected
- Cannot unassign the project's own PROJECT-type TM
- Re-assigning an already-assigned TM returns 400

Managed TM suggestions (separate from the original "similar translations"):
- New ManagedTranslationMemorySuggestionService (EE) queries only the
  translation_memory_entry table — it does not touch the translation table.
- New endpoint POST /v2/projects/{projectId}/suggest/managed-translation-memory
- Results come from all assigned TMs (project's own + any readable shared TMs)
- Original /suggest/translation-memory endpoint is completely untouched
  and still queries the translation table for free plan users.

Supporting additions (CE):
- TranslationMemoryRepository: find / findByOrganizationId /
  findByOrganizationIdAndTypePaged / findAssignedToProject
- Message enum: TRANSLATION_MEMORY_NOT_FOUND,
  TRANSLATION_MEMORY_PROJECT_ASSIGNMENT_NOT_FOUND,
  CANNOT_UNASSIGN_PROJECT_FROM_OWN_TRANSLATION_MEMORY,
  CANNOT_MODIFY_PROJECT_TRANSLATION_MEMORY,
  TRANSLATION_MEMORY_ALREADY_ASSIGNED_TO_PROJECT
- ActivityType enum: TRANSLATION_MEMORY_CREATE/UPDATE/DELETE,
  ASSIGN_PROJECT / UNASSIGN_PROJECT / UPDATE_PROJECT_CONFIG

Test data:
- TranslationMemoryTestData gained sharedTm (pre-assigned) and
  unassignedSharedTm (for assign tests); all tests use pre-built data
  instead of inline setup.

Tests (17 new EE tests, all passing):
- SharedTranslationMemoryControllerTest (6 tests)
- ProjectTranslationMemoryControllerTest (7 tests)
- ManagedTranslationMemorySuggestionControllerTest (4 tests)

Existing CE tests for the original "similar translations" feature all
still pass; TranslationMemoryService.kt in the CE module is identical
to origin/main.

Not in this commit (follow-ups):
- Auto-translate / pre-translate integration with shared TMs
- "Keep or delete" choice on shared TM disconnect
- Lazy migration for existing Business plan projects (per pitch)
- TMX import/export
- Frontend UI
Adds keepData query param to DELETE /v2/projects/{id}/translation-memories/{tmId}.
When true, entries from the shared TM are snapshotted into the project's own
PROJECT-type TM before the assignment is removed — the project retains the data
after disconnect. Default (false) preserves previous behavior: assignment is
removed, shared TM and its entries remain intact for other projects.

Snapshot drops translation/key back-references since they belong to the original
source project and have no meaning in the destination.
…Memory

Introduces TmAutoTranslateProvider — a feature-aware dispatcher for TM lookups
used by auto-translate and batch pre-translate.

- OSS impl: delegates to the classic TranslationMemoryService (translation table).
- EE impl (@primary): for projects whose organization has Feature.TRANSLATION_MEMORY
  enabled, routes through ManagedTranslationMemorySuggestionService which queries
  translation_memory_entry. This exposes the project's own TM (including snapshots
  from keepData=true disconnect, TMX imports, and future manual edits) plus all
  shared TMs assigned to the project with read access. Projects without the feature
  fall through to the OSS path.

Both softAutoTranslate (single-key) and autoTranslateUsingTm (batch) now go through
the dispatcher.
New endpoints under /v2/organizations/{orgId}/translation-memories/{tmId}/entries
for listing, creating, updating, and deleting individual TM entries. Used for
direct editing of TM content — complements the write-on-save pipeline, shared-TM
snapshots, and future TMX import.

- Paginated list with optional free-text search (source or target) and
  targetLanguageTag filter
- Organization MAINTAINER role required for create/update/delete; list uses
  @UseDefaultPermissions
- Feature-gated via @RequiresFeatures(Feature.TRANSLATION_MEMORY)
- Entries created this way have no translation/key back-references, matching
  the semantics of TMX imports and keepData snapshots
…O bounds

Must-fix items:
- snapshotEntriesIntoProjectTm: deduplicates against existing project-TM
  triples before saveAll. Hibernate's configured jdbc.batch_size=1000
  groups the INSERTs automatically, so repeat disconnect-with-keepData
  is now idempotent and large snapshots batch correctly.
- Partial unique index on translation_memory_entry(translation_memory_id,
  translation_id) WHERE translation_id IS NOT NULL: prevents concurrent
  write-on-save from producing duplicate rows. upsertEntry falls back to
  re-fetch-and-update on DataIntegrityViolationException so the race is
  handled silently.
- Create/UpdateTranslationMemoryEntryRequest.sourceText/targetText:
  @SiZe(max = 10000) aligned with TolgeeProperties.maxTranslationTextLength
  default, preventing unbounded bodies.
- findByProjectIdAndReadAccessTrue: add FLUSH_MODE=COMMIT query hint for
  consistency with the write variant — same hot-path flush risk applies
  to the auto-translate/suggestion read path.

Should-fix follow-ups:
- Drop redundant CascadeType.REMOVE on TranslationMemory.entries /
  projectAssignments. DB-level ON DELETE CASCADE already handles cleanup;
  the JPA cascade would force an N+1 load before delete.
- Delete unused hasTmAssignments / existsByProjectId (dead code).
- @min(0) on AssignSharedTranslationMemoryRequest.priority and
  UpdateProjectTranslationMemoryAssignmentRequest.priority — reject
  negative priority at the DTO boundary.
- Clarify empty-tmIds guard and transactional contract with code comments.

Tests: idempotent keepData re-snapshot; oversized entry payload → 400.
…ist, E2E tests

- Editor TM panel: conditionally call managed-translation-memory endpoint
  when TRANSLATION_MEMORY feature is enabled (Business plan).
- Org settings: new Translation memories page with list (shared + project
  TMs), create/edit/delete dialogs, empty state, feature-gated banner.
- With-stats endpoint: entry count, assigned projects (first 3 names),
  project count — displayed in the list item.
- Create dialog: supports assigning projects on creation (read+write).
- Refactor: extract BaseLanguageSelect from GlossaryBaseLanguageSelect
  into shared component/languages/.
- Fix: ON DELETE CASCADE on translation_memory_project FKs.
- E2E: orgSettings.cy.ts with navigation, CRUD, permission tests.
  Compounds, test data controller, TRANSLATION_MEMORY feature flag.
- New ProjectSettingsTranslationMemory component: project memory row
  (always-on toggle) + shared TMs as priority-ordered chips + Configure
  button linking to org TM settings.
- Fix auto-incrementing priority when assigning TMs from org-level
  create dialog (was hardcoded to 0).
- E2E: projectSettings.cy.ts with section visibility, always-on state,
  chip display, configure navigation tests.
- New TranslationMemoryView: detail page at /translation-memories/:id
  with breadcrumb, search bar, multi-select language filter.
- Entries grouped by sourceText+keyName, rendered in Translations-view
  style: source text row with flag, target language rows below.
- Multi-select language filter with InfiniteMultiSearchSelect, base
  language shown as checked+disabled, persisted to localStorage.
- Backend: entry query switched to native SQL with bytea::text casts,
  supports comma-separated language tags filter, ordered by source_text.
- TM list items are now clickable — navigate to the detail page.
When no saved preference exists, all languages appear checked in the
selector and no filter is sent to the API. Deselecting a language
switches to explicit mode with all-minus-one.
- Org TM list: split into "Shared" and "Project" sections with compact
  grid for project TMs (no empty projects column/kebab).
- TM detail: "Projects" tab with read/write checkboxes, remove button.
  Backend: GET .../assigned-projects endpoint + HATEOAS model.
- Project settings: replaced chips with priority table, up/down arrows
  for reordering (sequential mutateAsync to avoid race conditions).
- EE/OSS pattern: ProjectSettingsTranslationMemory exported via tg.ee
  module with Empty stub in OSS.
- Entry browser: display all selected languages per group (empty rows
  for missing translations), search moved to inline toolbar.
- Various fixes: checkbox toggle refetch, Manage button layout.
- Inline click-to-edit on target language cells (PUT existing, POST new
  entries for empty cells, DELETE with confirmation). Follows
  GlossaryListTranslationCell pattern: Enter saves, Escape cancels.
- Create entry dialog: source text + multiple target languages at once,
  MUI Select for language picker.
- Backend tests (9 new, 57 total passing): with-stats endpoint (entry
  count, project names, search), assigned-projects endpoint, create with
  assignedProjectIds + auto-incrementing priority, comma-separated
  language filter, ON DELETE CASCADE on TM with assignments.
- TM list alignment: unified 4-column grid for both shared and project
  sections so language flags align vertically.
- Fixed listWrapper border selector for split sections.
- tmContentBrowser.cy.ts (9 tests): navigation, entries display, search,
  inline edit/cancel/delete, create entry dialog, empty state.
- tmProjectsTab.cy.ts (6 tests): tab switch, checkbox states, toggle
  read/write, remove project, empty state.
- projectSettings.cy.ts (2 new): priority table display, arrow states.
- orgSettings.cy.ts: lowercase test titles.
- New compounds: E2TranslationMemoryView (entries + projects tab),
  E2ProjectTmSettings (project settings TM section).
- Updated E2TranslationMemoriesView with findAndVisitTm method.
- Fix: wrap language filter in Box with data-cy for testability.
…TM lists

- Backend: added `type` query param to GET /translation-memories-with-stats
  endpoint. Filters by SHARED or PROJECT when provided, returns all when
  omitted.
- Frontend: two separate API calls (type=SHARED, type=PROJECT) each with
  own PaginatedHateoasList and independent pagination. Replaces the
  client-side split with size=100.
- Backend tests: 3 new tests for type filter (SHARED returns 2, PROJECT
  returns 1, no filter returns all). 21 total SharedTM controller tests
  passing.
- Import TMX 1.4b files into any TM with keep/override conflict resolution
- Export TM entries as TMX file download
- DOM parser with DTD loading disabled for robust XML handling
- StAX writer for valid TMX output with xml:lang attributes
- Entry count shows translation units (distinct source texts) not individual entries
- Import result counts by translation unit for consistency with UI grouping
- Frontend: import dialog with file dropzone + mode selector, export button
- Backend tests (9) and E2E tests (3 new, 12 total in content browser)
Store tuid from TMX <tu> elements in translation_memory_entry.
Conflict resolution (keep/override) now matches by tuid + target
language. Entries with different or null tuid are always created
regardless of source text match.
Org settings:
- Single flat list with All/Shared/Project only filter tabs
- Inline search + filter in one row
- Type badges (Shared/Project only) on every row
- Kebab menu: Settings + Delete (red)

Project settings:
- Drag-and-drop priority reorder via @dnd-kit
- Read/Write chips, settings gear icon per row
- "Manage all TMs" button

Unified TM settings dialog (replaces edit dialog + Projects tab):
- Name, base language, project assignments with read/write toggles
- Add project via single-select autocomplete (unassigned only)
- Soft-delete with keep/discard confirmation and undo
- Backend: assignedProjects with per-project access in update endpoint

Remove Projects tab from TM detail view.
Update E2E tests and compounds to match new UI.
dkrizan and others added 29 commits April 21, 2026 20:15
Entries with the same source text but different keys (or null key) now
appear as separate rows in the TM content browser. Previously they were
merged into one group and only one translation per language was visible.
Previously onTranslationSaved returned early on empty text, leaving
the stale TM entry in place. Now it deletes the entry from all writable
TMs when the translation text is cleared.
- Rename test methods to clearly describe scenario and expected outcome
- Split the language-filter list test into two focused tests
- Fix misleading comments (search test, tuid helpers)
- Add documentation comments to TMX builder helpers and multi-step tests
- Revert broken ObjectMapper-to-DSL conversion in SharedTranslationMemoryControllerTest
  (JsonUnit does not support JSONPath filter expressions; lazy-loading
  issue on detached entity)
Members can browse TM entries but cannot create, edit, import, or
delete. Hide the "+ Entry" button, Import button, inline edit click,
and delete button when the user is not an org owner or maintainer.
Export stays visible (read-only).

Also move inline French entry creation from the comma-separated
language filter test into TranslationMemoryTestData. Update all
affected assertion counts.
When the Formik field value is undefined and organization languages
have loaded, automatically select the first available language.
Improves create dialog UX for both TM and glossary.
Users with TRANSLATIONS_VIEW/EDIT see the TM list on project settings
(read-only) but no longer see the drag handle, settings gear, "Manage
all TMs" button, or the drag-to-reorder hint. Backend already requires
PROJECT_EDIT for assign/unassign/update.
Users with PROJECT_EDIT but not org maintainer role see a simplified
dialog from the gear icon: only read/write access toggles, no TM
name/language/project assignment editing. Org maintainers still get
the full TM settings dialog.
- BaseLanguageSelect: fix useEffect dependency array with ref guard to
  prevent re-render loop on auto-select
- Add onError handlers to all TM mutation calls (settings save, entry
  CRUD, import, create dialog) with messageService.error toasts
- Use TmxImportResult type from generated schema instead of `as any`
- Memoize availableProjects in settings dialog
- Add aria-label to remove-language icon button in create entry dialog
- Fix prettier in ProjectTmAssignmentDialog
Extract TmxParsedEntry and TranslationMemoryEntryGroup into their own
files instead of nesting them inside TmxParser and the management
service respectively.
Existing projects created before the TM management feature have no
project TM, so suggestions silently fell back to the legacy path even
after the org gained TRANSLATION_MEMORY.

Listen for OnOrganizationFeaturesChanged and create a project TM for
each project in the org when the feature is gained. Backfill entries
from existing translations in the same call via a native INSERT ...
SELECT so the TM content browser is populated immediately.

Add getOrCreateProjectTm(projectId) as a lazy safety net used by
snapshot-on-disconnect; re-fetches inside the transaction so async
callers don't trip over detached lazy proxies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two complementary migration paths for legacy projects that never
received a project TM:

- Spring Batch migration job runs on every app startup via the existing
  MigrationJobRunner framework. Iterates non-deleted projects without a
  project-type TM assignment and creates one for each whose organization
  has the TRANSLATION_MEMORY feature enabled. Dedupes via hashed job
  parameters so it only re-runs when the candidate set changes.

- Lazy fallback in the suggestion endpoint (side panel hot path):
  ensureProjectTmIfFeatureEnabled is cheap when the TM exists (one
  indexed query) and short-circuits on free-plan orgs without a feature
  lookup cost.

Together with the existing feature-gained listener this covers: plan
upgrades while running (listener), orgs paid before the migration code
shipped (startup job), and any residual gap (lazy fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Placing the migration job in backend/data meant it was instantiated in
every OSS test context as well. In CE, the feature check always returns
false so the writer is a no-op, but the runner still queries all projects
without a TM and launches the Spring Batch job. In batch-timing sensitive
tests (BatchJobsGeneralWithRedisTest) this was enough to flake assertions
that wait for Tolgee batch state transitions.

The migration job is inherently EE-specific — it only does meaningful
work when the TRANSLATION_MEMORY feature is enabled. Moving both the
runner and configuration to ee-app ensures OSS installs and OSS test
contexts don't instantiate it at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Displays "{N} entries" next to the search field on the TM content
browser, using the totalElements from the paginated response. Helps
users gauge TM size at a glance and provides immediate feedback on
filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rride)

Effective penalty applied at suggestion time is
`coalesce(tmp.penalty, tm.default_penalty, 0)`; project-type TMs are never
penalized. Subtracts percentage points from the trigram similarity before
display/ranking and exposes the raw score as `rawSimilarity` so the editor
can show a "raw% − penalty" tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `DELETE /entries/{entryId}/group` that resolves the entry's source
text + key and bulk-deletes every sibling — i.e. the translation-unit row
visible in the TM content browser. A trash icon on hover of the source
cell triggers the action with a confirmation dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JPQL bulk delete with `:keyName is null` parameter comparisons and implicit
joins on `e.key.name` was matching nothing at runtime. Replaced with a
native SQL query using `cast(:keyName as text) is null` and an EXISTS
subquery against `key`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translation Memory should reflect canonical, merged work — not
work-in-progress on feature branches. The hook now no-ops when
`key.branch` is set and not the project's default branch; translations
land in the TM when their branch merges into default (merge re-saves
them onto the default-branch key, re-triggering the hook).

Matches industry convention: every mature CAT tool (Phrase TMS, memoQ,
Trados) keeps unmerged/unreviewed work out of the shared memory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gles

After a project's base language changes, existing TM entries reference
the old base as `source_text` — no longer meaningful. Wipe + re-backfill
the project TM in the same transaction so suggestions and the content
browser stay consistent. Shared TMs are untouched (org-scoped, keep
their own sourceLanguageTag).

Also finish removing the toggles that never made sense for the project
TM: Read/Write are forced on for PROJECT-type assignments (UI shows a
single "Always on" chip; backend rejects attempts to turn them off).
Penalty was already disabled there.

Backfill query now filters out non-default-branch keys so runtime and
rebuild share the same invariant ("only default branch contributes").
…ject TM row

Two ergonomics fixes on Project → Advanced → Translation memory:

- Add a proper column header (#, Translation memory, Penalty, Access)
  above the draggable list; the old floating caption ("Priority order")
  left the penalty column unlabelled.
- Don't render the penalty chip at all on the Project-type TM row —
  penalty is inapplicable there (SQL forces 0), and the old muted
  "0%" chip suggested there was something to configure.
- Make penalty in the list read-only (same convention as Read/Write
  chips, which are only editable from the TM Settings dialog). Adds
  a `readOnly` prop to `PenaltyCell` that displays the chip without
  click-to-edit.
Header text (inline span) ignored textAlign: center while the row cell
centered its chip via flex, producing visibly offset alignment. Left-
align both to match the rest of the table.

Read-only penalty chip also carried the interactive chip's pink-on-hover
styling — misleading when the cell is display-only. Split into a plain
ReadOnlyChip with no hover state.
Alignment with the header kept drifting depending on locale / column
width. Penalty isn't editable inline anyway (only via TM Settings
dialog), so showing a read-only value here adds noise without benefit.
@dnd-kit reads the sortable item array for its drop animation; until
the two async reorder PUTs resolved + the query refetched, the list
still held the pre-drag order, so the dropped row sprang back before
finally jumping into place after the network round-trip.

Track a local `optimisticOrder` state, set it to arrayMove() on drop so
the row lands where the user dropped it, and clear it when react-query
delivers fresh server data. Roll back on mutation failure.
Changing a per-assignment penalty in the TM Settings dialog and then
clicking Save landed the old value on the server. The dialog's onBlur
committed the new value to parent state via setState, but React batched
the Save click into the same event round — so the submit handler read
stale `projectAccess` before the blur's update flushed. Same race
applies to any mouse path from input → Save.

Commit each valid keystroke directly so the parent state is always in
sync with what's in the input by the time Save fires. Empty input now
commits null (equivalent to the reset button), which is consistent.
…into TestData

MetadataProvider.getExamples now dispatches to the managed suggestion
service when the project has any readable TM, falling back to the
legacy translation-table query only for legacy free-plan projects that
never had a project TM provisioned. Examples carry the penalized
similarity so trust-adjusted TMs contribute proportionally weaker
context to MT prompts. Adds a non-paginated `getSuggestionsList`
overload on ManagedTranslationMemorySuggestionService without
REQUIRES_NEW — MT batches fan chunks out in parallel and a nested tx
per lookup would double the connection-pool footprint.

Test-data hygiene: penalty state now lives in TranslationMemoryTestData
instead of inline mutation inside test bodies. New fixtures:
sharedTmWithPenalty (defaultPenalty=25), sharedTmWithOverride
(defaultPenalty=10 + assignment.penalty=40), projectTm.defaultPenalty=50,
featureBranch + keyOnFeatureBranch. Existing penalty + branch-skip tests
rewritten against the fixtures; list-size assertions in the two
controller tests bumped for the extra TMs.
Replicates the header shape from the design prototype:
- Type chip (Shared / Project only) inline with the TM name, via
  BaseOrganizationSettingsView.titleAdornment.
- Subtitle under the title: "Base language: {L} · {N} projects · Default penalty: {P}%".

Fetches the assignment list in the view so the projects count stays
accurate without an extra model field. The subtitle is rendered at the
top of TranslationMemoryEntriesList so it sits right below the page
title, above the search+filter toolbar.
… browser

TM entries are still grouped per translation unit (one row per distinct
source text — the Crowdin / Phrase TMS / glossary convention), but the
row now has two layouts:

- flat (default): source cell on the left, each language cell flows
  horizontally with horizontal scroll when there are many languages.
  Mirrors the Glossaries list and removes the empty vertical padding
  the old layout produced when the user filtered to just a handful of
  languages.
- stacked: the previous layout, source on the left, target cells
  stacked vertically on the right.

A two-button ButtonGroup (same icons the Translations view uses for
list/table) sits in the toolbar next to the language filter. The
choice persists via useUrlSearchState("layout") so reloads keep it.
The delete-entry-group button was an absolutely positioned overlay on
the source cell that appeared on hover — it overlapped the source text
on narrow cells and felt surprising. Move it out into a dedicated
trailing column on the row, still fading in on hover, so the layout is
explicit and the button never covers content.
Flat-mode rows are now a single CSS grid (minmax(300px, 1fr) +
minmax(200px, 1fr) per target language + 44px action column) with a
sticky header row showing source-language label and one column per
target language — lifted straight from GlossaryViewListHeader so the
tables look consistent. Cells no longer repeat the language label
inline; the column header owns it. Stacked mode is unchanged.

Layout toggle style matches the Translations-view switch: drop the
outlined border; the deselected button uses the default palette for a
subtle (theme-appropriate) background instead of stark white in dark
mode.

e2e
- Extend E2TranslationMemoryView with helpers for the subtitle, the
  new list header, the flat/stacked toggle, and the whole-TU delete.
- Add tests for the type badge, the metadata subtitle, the header
  visibility tied to layout (including URL-state persistence), and
  the row-level group delete.
- Rework projectSettings "read/write badges" test: the first row is
  the project TM which now shows a single "Always on" chip; split into
  one assertion for the project TM row and one for a shared TM row.
@dkrizan dkrizan force-pushed the dkrizan/translation-memory-management branch from 2dcc2b7 to 545c0c0 Compare April 21, 2026 18:37
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.

Translation Memory Management

1 participant