Draft
Conversation
4f09c13 to
d1f5338
Compare
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
d1f5338 to
325ce9c
Compare
627c037 to
b16f9dc
Compare
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.
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.
2dcc2b7 to
545c0c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the main PR for the entire feature — it accumulates commits as the work progresses through the cycle.
Architecture decisions
POST /v2/projects/{id}/suggest/translation-memorydispatches at runtime: when the project has any readable TM assignments, it usesManagedTranslationMemorySuggestionService(queriestranslation_memory_entry); otherwise it falls back to the legacyTranslationMemoryService(queries thetranslationtable). No URL swap on the frontend, no breaking API change.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).getReadableTmIdsreturns empty and the dispatch picks the legacy path. No migration required.Status
✅ Done
Data model & write pipeline
TranslationMemory,TranslationMemoryEntry,TranslationMemoryProject(entity-backed join with read/write/priority).translation_memory_entry(target_language_tag, source_text).tuidcolumn on entries for TMX round-trip.ProjectCreationService.createProject()for every new project (all plans).TranslationService.save()for any project with writable TM assignments.ProjectHardDeletingServiceremoves project TM + assignments; cascade deletes via DB FK on translation removal.Org-level shared TM management (EE)
MAINTAINERorg role.Project-level TM configuration (EE)
keepData=truesnapshot on disconnect).@dnd-kit), Read/Write chips per row, settings gear (opens unified dialog), "Manage all TMs" button.Suggestion endpoint (CE)
/v2/projects/{id}/suggest/translation-memorydispatches between managed and legacy services.ManagedTranslationMemorySuggestionServicelives inbackend/data(CE), plan-aware viaEnabledFeaturesProvider.TmAutoTranslateProvidersimplified to a single CE impl with the same dispatch — EE override deleted.TMX import / export (EE)
tuid, groups bytuidor source text).tuid+ language).tuid/source).Plan gating
Feature.TRANSLATION_MEMORYflag, backend enforced on management endpoints + batch pre-translate.StartBatchJobController.translate()hard-gated with@RequiresFeatures(regression for free-plan API clients — documented)./suggest/translation-memoryagainst their project TM.🚧 To do
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
TranslationSuggestionControllerTmTest(legacy path),BatchPreTranslateByTmTest(paid happy path + free 400 gate).TranslationSuggestionControllerManagedTmTest(paid + free dispatch, project TM only on free),SharedTranslationMemoryControllerTest,ProjectTranslationMemoryControllerTest,TranslationMemoryEntryControllerTest,TranslationMemoryTmxControllerTest.orgSettings.cy.ts,projectSettings.cy.ts,tmContentBrowser.cy.ts. All pre-existing TM features covered.Test plan
POST /suggest/translation-memory→ 200;POST /start-batch-job/pre-translate-by-tm→ 400feature_not_enabled.