Skip to content

✨ feat(context-preview): in-context review panel with segment-aware highlighting#4429

Open
riccio82 wants to merge 119 commits intodevelopfrom
context-review
Open

✨ feat(context-preview): in-context review panel with segment-aware highlighting#4429
riccio82 wants to merge 119 commits intodevelopfrom
context-review

Conversation

@riccio82
Copy link
Copy Markdown
Collaborator

@riccio82 riccio82 commented Mar 16, 2026

Summary

Adds a context-preview panel to the CAT tool that displays the source document with segment-aware highlighting, screenshot support, and keyboard shortcuts. Also includes significant backend refactoring: typed event dispatch for FeatureSet hooks, superglobal replacement with Klein abstractions, parameterized queries replacing escape(), and PHPStan level 8 compliance.

Type

  • feat — new user-facing feature
  • fix — bug fix
  • refactor — restructure without behavior change
  • chore — build, deps, config, docs
  • perf — performance improvement
  • test — test coverage

Changes

File Change
lib/Controller/API/App/ContextUrlController.php New controller for context-preview URL routing
public/js/components/context-preview/ Frontend panel with segment highlighting, tab animation, zoom
lib/Model/FeatureSet.php Typed Event DTO dispatch for all 44 hooks
lib/Controller/API/App/SetTranslationController.php Decompose translate() god method, extract db commit
lib/Controller/API/ (multiple) Replace $_POST/$_GET/$_REQUEST/$_SERVER with Klein abstractions
lib/Model/Search/SearchModel.php Replace escape() with parameterized queries
lib/Model/DataAccess/Database.php Fix ON DUPLICATE KEY bind values from buildInsertStatement()
plugins/airbnb/, plugins/translated/ PHPStan level 8 fixes
phpstan.neon Add plugins path, exception checking rules
docs/ Codebase review, hook changelog, context-review spec, xfetch docs
tests/unit/TestDatabase/ New tests for buildInsertStatement and ON DUPLICATE KEY
tests/unit/Search/SearchModelTest.php New parameterized query tests

Testing

  • vendor/bin/phpunit --exclude-group=ExternalServices --no-coverage passes
  • ./vendor/bin/phpstan passes (0 errors, with baseline)
  • Manual testing performed (describe below)
  • New tests added for changed behavior
  • Regression tests added for bug fixes

Manual testing of context-preview panel: segment navigation, highlight sync, tab collapse/expand animation, screenshot zoom, keyboard shortcut. New unit tests for Database buildInsertStatement and SearchModel parameterized queries.

AI Disclosure

  • No AI tools were used in this PR
  • AI tools were used — details below

Claude Code (claude-opus-4-6) — refactoring (controllers, hooks, FeatureSet typing), documentation, tests, PR description

Notes

  • Large PR combining context-preview feature with backend modernization
  • FeatureSet hooks: 20 unused hooks removed, 5 renamed from snake_case to camelCase (see docs/featureset-hooks-changelog.md)
  • filter()/run() removed from FeatureSet in favor of typed pipeline customization
  • 328 files changed across frontend and backend

riccio82 and others added 24 commits March 17, 2026 14:24
… and collection

Introduce SegmentMetadataMarshaller enum as single source of truth for
valid metadata keys with marshall/unmarshall type conversion.

Add SegmentMetadataMapper to map XLIFF trans-unit attributes to DB-ready
SegmentMetadataStruct entries, and SegmentMetadataCollection as a
purpose-built read-only collection with typed find() lookup.

Refactor all consumers (SegmentExtractor, SegmentStorageService,
SizeRestrictionChecker, QA, controllers) to use the centralized
components instead of scattered constants and inline logic.
…Type enum

Add resname and restype attributes to the segment metadata pipeline, enabling XLIFF context mapping storage with validated lookup strategies.
GetSegmentsController now calls jsonSerialize() on SegmentMetadataCollection,
returning typed values (int, string) instead of raw DB strings.
- Add proper error reporting: check isValid() and throw with aggregated
  exception messages instead of silently accepting invalid metadata
- Remove unused imports (InjectableFiltersTags, HandlersSorter,
  JobsMetadataDao, MetadataDao)
…nstraints

- XLIFF 2.0 test fixture: use matecat: namespace prefix (matecat:resname,
  matecat:restype) with proper xmlns:matecat declaration per spec
- Feature spec Section 8: clean constraints from 16 → 13 active items,
  remove implemented/outdated entries, add new architecture decisions
- Section 7.2: context-url pipeline task list (7 items) — FilesMetadataMarshaller,
  CONTEXT_URL cases, fallback resolver, 3 APIs, GetSegmentsController
- Section 11: full architecture — three-level storage, dual ingestion paths,
  read-time fallback resolution, FilesMetadataMarshaller Pattern B design
- Section 8: three new constraints (naming convention, dual ingestion, fallback order)
…lers

Add 'context-url' as a new metadata key recognized at both segment
level (SegmentMetadataMarshaller, 9th case) and project level
(ProjectsMetadataMarshaller, 31st case). Both use simple string
pass-through for marshall/unMarshall. TDD: +8 tests.
Ostico and others added 9 commits April 10, 2026 09:36
- Add screenshot metadata extraction to contextReviewUtils
- Create HtmlContextPanel and ScreenshotContextPanel components
- Implement zoom controls (50%-200%) for both HTML and screenshot views
- Add toolbar toggle to switch between HTML and screenshot content views
- Fix scrolling behavior for zoomed content in both views
- Prevent nested scrollbars in HTML view with overflow:visible enforcement
- Add responsive zoom with dynamic margin for proper scroll bounds
- All 235 tests passing
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Rename all frontend JS/SCSS files, utilities, hooks, components, and
sample files from context-review to context-preview. Add
_context_preview.html template (served via the existing
/context-review route) with updated body/div class names matching the
renamed frontend selectors. Webpack entry point updated accordingly.
…calls

On incremental calls to tagSegments, tier1 elements (resolved via the
strategy pass using resname/restype) were not added to tier1Nodes.
This allowed Pass 2 to append other segments to those elements via
text match, causing tier1 segments to appear on multiple nodes and
showing spurious navigation buttons.

Fix: in the alreadyTagged branch of the strategy pass, re-find the
element via findElementByMetadata and add it to tier1Nodes so Pass 2
correctly excludes it on subsequent calls.

Add regression test covering the incremental call scenario.
Copilot AI review requested due to automatic review settings April 17, 2026 09:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a “context preview/review” capability and modernizes several backend/plugin hook integrations by introducing typed hook event objects, while also cleaning up legacy test assets and tightening data-handling in a few critical paths.

Changes:

  • Introduce Context Preview UI building blocks (new panels, resizing hook, layout styles) and wire segment highlight/translation updates to a ContextPreview channel.
  • Replace multiple FeatureSet filter()/run() calls with typed dispatchFilter()/dispatchRun() events across controllers, workers, and LQA components.
  • Update segment/file/project metadata handling (new metadata marshallers/collections, context-url support) and enforce a unique constraint on segment_metadata.

Reviewed changes

Copilot reviewed 268 out of 328 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
public/js/hooks/useSegmentsLoader.js Remove unused import from segments loader hook.
public/js/hooks/useResizable.js New hook for draggable vertical resizing.
public/js/components/segments/utils/DraftMatecatUtils/tagUtils.js Adjust tag placeholder handling during text transform.
public/js/components/segments/SegmentsContainer.js Recalculate viewport height with context preview wrapper + send highlight messages.
public/js/components/segments/Editarea.js Send translation updates to context preview channel.
public/js/components/contextPreview/index.js Export context preview panels.
public/js/components/contextPreview/ScreenshotContextPanel.js New screenshot-based context preview panel.
public/js/components/contextPreview/LivePreviewPanel.js New live preview container panel with zoom scaling.
public/img/icons/ChangePassword.js Fix React SVG attribute casing (clipPath).
public/css/sass/cattool.scss Add context preview header/container/resize-handle styles.
plugins/uber Bump submodule pointer.
plugins/translated Bump submodule pointer.
plugins/airbnb Bump submodule pointer.
phpstan.neon Add PHPStan configuration.
old_tests/support/lib/Utils/ZipArchiveExtendedTest.php Remove legacy test placeholder.
old_tests/support/lib/UnitTestInitializer.php Remove legacy integration test initializer.
old_tests/support/lib/IntegrationTest.php Remove legacy integration test base class.
old_tests/support/lib/Factory/User.php Remove legacy factory.
old_tests/support/lib/Factory/OwnerFeature.php Remove legacy factory.
old_tests/support/lib/Factory/Base.php Remove legacy factory base.
old_tests/support/lib/Factory/ApiKey.php Remove legacy factory.
old_tests/support/fixtures/users.yml Remove legacy fixtures.
old_tests/support/fixtures/segments.yml Remove legacy fixtures.
old_tests/support/fixtures/segment_translations.yml Remove legacy fixtures.
old_tests/support/fixtures/segment_translation_versions.yml Remove legacy fixtures.
old_tests/support/fixtures/qa_models.yml Remove legacy fixtures.
old_tests/support/fixtures/qa_chunk_reviews.yml Remove legacy fixtures.
old_tests/support/fixtures/qa_categories.yml Remove legacy fixtures.
old_tests/support/fixtures/projects.yml Remove legacy fixtures.
old_tests/support/fixtures/jobs.yml Remove legacy fixtures.
old_tests/support/fixtures/files_job.yml Remove legacy fixtures.
old_tests/support/fixtures/files.yml Remove legacy fixtures.
old_tests/support/files/zip-with-model-json/amex-test.docx.xlf Remove legacy test file.
old_tests/support/files/zip-with-model-json/__meta/qa_model.json Remove legacy test metadata file.
old_tests/support/files/xliff/sdlxliff-with-mrk-and-note.xlf.sdlxliff Remove legacy test file.
old_tests/support/files/xliff/file-with-preserve-white-space.xliff Remove legacy test file.
old_tests/support/files/xliff/file-with-notes-nobase64.po.sdlxliff Remove legacy test file.
old_tests/support/files/xliff/file-with-hello-world.xliff Remove legacy test file.
old_tests/support/files/xliff/amex-test.docx.xlf Remove legacy test file.
old_tests/support/files/txt/hello.txt Remove legacy test file.
old_tests/support/files/tmx/exampleForTestOriginal.tmx Remove legacy test file.
old_tests/support/files/test-propagation.xlf Remove legacy test file.
old_tests/support/files/small-with-notes.sdlxliff Remove legacy test file.
old_tests/support/files/json/schema/schema_1.json Remove legacy schema fixture.
old_tests/support/files/json/schema/invalid.json Remove legacy invalid schema fixture.
old_tests/support/files/json/files/valid.json Remove legacy JSON fixture.
old_tests/support/files/json/files/invalid_maxItems.json Remove legacy JSON fixture.
old_tests/support/files/json/files/invalid.json Remove legacy JSON fixture.
old_tests/support/files/glossary/GlossaryInvalidHeader.csv Remove legacy glossary fixture.
old_tests/support/files/glossary/Final-Matecat-new_glossary_format-InvalidTargetLang.csv Remove legacy glossary fixture.
old_tests/support/files/glossary/Final-Matecat-new_glossary_format-Glossary.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/mixed-valid.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/minimal-valid.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/minimal-invalid.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/invalid-structure.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/invalid-language.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/full-structure-valid.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Header-vuoti.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato solo blacklist language-specific.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato solo blacklist combinata.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato semplice solo lingue.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato lingue + campi termine.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato lingue + campi termine (non per tutte le lingue).csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/V - Formato completo.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV -Formato solo blacklist generale.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Header vuoto.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato una sola lingua solo note.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato una sola lingua solo esempi.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato una sola lingua completa.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato completo con colonne lingua spostate.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato campi concetto + una sola lingua.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato campi concetto + una sola lingua solo note.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Formato campi concetto + una sola lingua solo esempi.csv Remove legacy glossary fixture.
old_tests/support/files/csv/glossary/NV - Campi concetto + una sola lingua solo termini.csv Remove legacy glossary fixture.
old_tests/integration/Features/TranslationVersions/setTranslationWithVersioningDisabledTest.php Remove legacy integration test.
old_tests/integration/Features/ReviewImproved/AssignQualityModelToProjectTest.php Remove legacy integration test.
old_tests/integration/Features/ProjectCompletion/JobStatusTest.php Remove legacy integration test.
old_tests/integration/CreateProjectController/sourceAndTargetLangValidationTest.php Remove legacy integration test.
old_tests/integration/CreateProjectController/setPrivateTMKeyTest.php Remove legacy integration test.
old_tests/integration/Converters/ConversionsTest.php Remove legacy integration test.
old_tests/integration/API/V2/SegmentVersionTest.php Remove legacy integration test.
old_tests/integration/API/V2/SegmentTranslationIssueTest.php Remove legacy integration test placeholder.
old_tests/integration/API/V2/ProjectUrlsTest.php Remove legacy integration test.
old_tests/integration/API/V2/ProjectUpdateTest.php Remove legacy integration test.
old_tests/integration/API/V2/JobMergeTest.php Remove legacy integration test.
old_tests/integration/API/V2/CreateTeamTest.php Remove legacy integration test.
old_tests/integration/API/V1/ValidateSourceAndTargetLanguagesTest.php Remove legacy integration test.
old_tests/integration/API/V1/StatusTest.php Remove legacy integration test.
old_tests/integration/API/V1/NewWithTeamTest.php Remove legacy integration test.
old_tests/integration/API/V1/NewWithRevisionTypeTest.php Remove legacy integration test.
old_tests/integration/API/V1/NewWithPrivateTMKeyTest.php Remove legacy integration test.
old_tests/integration/API/V1/NewWithOwnershipTest.php Remove legacy integration test.
old_tests/README.md Remove legacy test documentation.
old_tests/.htaccess Remove legacy directory access restriction file.
migrations/20260326190000_alter_table_segment_metadata_add_unique_index.php Add migration for unique segment metadata key per segment.
lib/View/templates/_context_preview.html Add new context preview view template.
lib/View/fileupload/index.php Instantiate upload handler with injected files array.
lib/View/fileupload/UploadHandler.php Decouple upload handler from superglobals by injecting files.
lib/View/API/V2/Json/Job.php Use typed hook events for outsource info + project URLs.
lib/View/API/V2/Json/Activity.php Use typed hook event for activity log entry filtering.
lib/View/API/App/Json/Analysis/AnalysisFile.php Guard/encode non-string metadata keys/values.
lib/Utils/TMS/TMSService.php Accept request files array instead of using $_FILES.
lib/Utils/LQA/SizeRestriction/SizeRestriction.php Use typed event for character length counting.
lib/Utils/LQA/QA/TagChecker.php Use typed events for tag mismatch/position checks.
lib/Utils/LQA/QA/SizeRestrictionChecker.php Use metadata marshaller constant for size restriction key.
lib/Utils/LQA/QA/DomHandler.php Use typed event for excluded tags injection.
lib/Utils/LQA/QA.php Remove legacy constant alias for size restriction.
lib/Utils/Engines/MyMemory.php Use typed event for MyMemory parameter filtering.
lib/Utils/AsyncTasks/Workers/Analysis/TMAnalysisWorker.php Use typed event for analysis-before-MT hook; remove after-close-project hook.
lib/Utils/AsyncTasks/Workers/Analysis/FastAnalysis.php Use typed run event; remove fast-analysis completion hook.
lib/Utils/AsyncTasks/Workers/AIAssistantWorker.php Clean up comment and blank line.
lib/Utils/AIAssistant/GeminiClient.php Remove unused AppConfig import.
lib/Routes/view_routes.php Add route for context preview view.
lib/Routes/api_v3_routes.php Add API v3 endpoints for context-url configuration.
lib/Plugins/Features/TranslationVersions/VersionHandlerInterface.php Clarify return array shape in docblock.
lib/Plugins/Features/ReviewExtended/ReviewedWordCountModel.php Use typed event for revision-change email filtering.
lib/Plugins/Features/ReviewExtended/ChunkReviewModel.php Use typed run event for chunk review updates.
lib/Plugins/Features/ProjectCompletion/Model/ProjectCompletionStatusModel.php Use typed event for review password mapping.
lib/Plugins/Features/ProjectCompletion/Model/EventModel.php Use typed run event on completion event save.
lib/Plugins/Features/ProjectCompletion.php Switch to typed hook event methods for job password + translation events.
lib/Model/matecat.sql Make segment_metadata index unique in schema.
lib/Model/Users/UserDao.php Add result type checks; remove legacy escaping.
lib/Model/Translators/TranslatorsModel.php Dispatch typed job-password-changed run event.
lib/Model/Segments/SegmentUIStruct.php Add context_url to UI struct.
lib/Model/Segments/SegmentMetadataMarshaller.php New enum for allowed segment metadata keys and marshalling rules.
lib/Model/Segments/SegmentMetadataMapper.php Map trans-unit attributes into metadata collection.
lib/Model/Segments/SegmentMetadataCollection.php New collection helper for segment metadata.
lib/Model/Segments/ContextUrlResolver.php Resolve segment/file/project-level context URL precedence.
lib/Model/Segments/ContextResType.php Add enum for context resource type.
lib/Model/Projects/ProjectsMetadataMarshaller.php Add project-level context-url metadata.
lib/Model/ProjectCreation/SegmentStorageService.php Store segment metadata via collection; replace hooks with typed events.
lib/Model/ProjectCreation/ProjectManagerModel.php Use SegmentMetadataMarshaller allowlist for segment metadata detection.
lib/Model/ProjectCreation/JobCreationService.php Use typed events for payable rates and job creation validation.
lib/Model/OwnerFeatures/OwnerFeatureDao.php Tighten types; remove null coalescing defaults; adjust signatures.
lib/Model/JobSplitMerge/JobSplitMergeService.php Dispatch typed run events post-split and post-merge.
lib/Model/Files/MetadataStruct.php Allow mixed values for file metadata.
lib/Model/Files/FilesMetadataMarshaller.php New enum for allowed file metadata keys and marshalling.
lib/Model/FeaturesBase/Hook/RunEvent.php New base class for typed run hook events.
lib/Model/FeaturesBase/Hook/FilterEvent.php New base class for typed filter hook events.
lib/Model/FeaturesBase/Hook/Event/Run/ValidateProjectCreationEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/ValidateJobCreationEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/TmAnalysisDisabledEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/SetTranslationCommittedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/ReviewPasswordChangedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/ProjectCompletionEventSavedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/PostProjectCreateEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/PostJobSplittedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/PostJobMergedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/PostAddSegmentTranslationEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/JobPasswordChangedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/FilterProjectNameModifiedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/ChunkReviewUpdatedEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/BeforeProjectCreationEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Run/AlterChunkReviewStructEvent.php New run hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/WordCountEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/SanitizeOriginalDataMapEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/RewriteContributionContextsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/ProjectUrlsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/PrepareNotesForRenderingEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/PopulatePreTranslationsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/OutsourceAvailableInfoEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/IsAnInternalUserEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/InjectExcludedTagsInQaEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/HandleJsonNotesBeforeInsertEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FromLayer0ToLayer1Event.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterRevisionChangeNotificationListEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterPayableRatesEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterMyMemoryGetParametersEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterJobPasswordToReviewPasswordEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterGetSegmentsResultEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterCreateProjectFeaturesEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterContributionStructOnSetTranslationEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterContributionStructOnMTSetEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/FilterActivityLogEntryEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/EncodeInstructionsEvent New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/DecodeInstructionsEvent New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/CorrectTagErrorsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/CheckTagPositionsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/CheckTagMismatchEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/CharacterLengthCountEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/AppendInitialTemplateVarsEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/AppendFieldToAnalysisObjectEvent.php New filter hook event class.
lib/Model/FeaturesBase/Hook/Event/Filter/AnalysisBeforeMTGetContributionEvent.php New filter hook event class.
lib/Model/FeaturesBase/BasicFeatureStruct.php Tighten options docblock and return type.
lib/Model/DataAccess/XFetchEnvelope.php Add value object for XFetch cache metadata.
lib/Model/DataAccess/IDatabase.php Remove legacy escape() method.
lib/Model/DataAccess/Database.php Fix ON DUPLICATE KEY UPDATE binding + remove escape().
lib/Model/DataAccess/AbstractDao.php Track query compute delta and propagate duplicate bind values.
lib/Model/Conversion/Upload.php Update docs to use request files array.
lib/Model/Analysis/AbstractStatus.php Use typed outsource-available event.
lib/Controller/Views/OutsourceTo/AbstractController.php Avoid logging raw superglobals; log request params.
lib/Controller/Views/ContextReviewController.php New view controller for context preview route.
lib/Controller/Views/CattoolController.php Use typed event for template vars hook.
lib/Controller/Views/AnalyzeController.php Use typed event for template vars hook.
lib/Controller/Abstracts/BaseKleinViewController.php Use typed internal-user filter hook event.
lib/Controller/Abstracts/Authentication/SessionTokenStoreHandler.php Disable XFetch semantics for session tokens.
lib/Controller/API/V3/FileInfoController.php Use typed decode-instructions event.
lib/Controller/API/V3/DeepLGlossaryController.php Avoid $_POST/$_FILES; use request object.
lib/Controller/API/V2/UrlsController.php Use typed project URLs event.
lib/Controller/API/V2/SplitJobController.php Simplify split access check signature; remove feature hook call.
lib/Controller/API/V2/ProjectCreationStatusController.php Remove plugin filtering of creation status result.
lib/Controller/API/V2/GlossaryFilesController.php Pass request files array into uploadFile().
lib/Controller/API/V2/DownloadOriginalController.php Avoid logging raw $_POST; log request post params.
lib/Controller/API/V2/DownloadController.php Remove conversion override hook and zip preview hook; avoid logging $_REQUEST.
lib/Controller/API/V2/ChangeProjectNameController.php Dispatch typed run event instead of filter.
lib/Controller/API/V2/ChangePasswordController.php Dispatch typed job/review password changed run events; remove project password hook.
lib/Controller/API/V1/NewController.php Replace several hooks with typed events; adjust metadata validation.
lib/Controller/API/Commons/Validators/IsOwnerInternalUserValidator.php Use typed internal-user event; improve readability.
lib/Controller/API/Commons/Validators/InternalUserValidator.php Use typed internal-user event; improve readability.
lib/Controller/API/App/XliffToTargetConverterController.php Use request files array instead of $_FILES.
lib/Controller/API/App/TMXFileController.php Pass request files into uploadFile().
lib/Controller/API/App/GetWarningController.php Remove feature-driven warning filters and size restriction lookup change.
lib/Controller/API/App/GetTagProjectionController.php Avoid logging raw $_POST.
lib/Controller/API/App/GetSearchController.php Dispatch typed set-translation-committed event; normalize queryParams type.
lib/Controller/API/App/GetContributionController.php Convert rewriteContributionContexts hook to typed event.
lib/Controller/API/App/FilesController.php Avoid $_POST; use controller params.
lib/Controller/API/App/CreateProjectController.php Convert filterCreateProjectFeatures hook to typed event.
lib/Controller/API/App/CompletionEventController.php Load features from chunk project; dispatch typed alter-chunk-review run event.
lib/Controller/API/App/ChangeJobsStatusController.php Avoid logging raw $_POST.
lib/Bootstrap.php Remove bootstrapCompleted plugin hook invocation.
composer.json Add Gemini client; bump matecat/subfiltering major version.
INSTALL/matecat.sql Make segment_metadata index unique in installer schema.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +38
const handleMouseUp = () => {
if (!isDraggingRef.current) return
isDraggingRef.current = false
setIsDragging(false)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

If the component unmounts while dragging, the cleanup removes listeners but never resets document.body.style.cursor / userSelect, leaving the whole app in row-resize / non-selectable state. In the cleanup function, also set isDraggingRef.current = false, setIsDragging(false) (if needed), and restore the body styles (optionally guarding so you only reset if you set them).

Copilot uses AI. Check for mistakes.

return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

If the component unmounts while dragging, the cleanup removes listeners but never resets document.body.style.cursor / userSelect, leaving the whole app in row-resize / non-selectable state. In the cleanup function, also set isDraggingRef.current = false, setIsDragging(false) (if needed), and restore the body styles (optionally guarding so you only reset if you set them).

Suggested change
window.removeEventListener('mouseup', handleMouseUp)
window.removeEventListener('mouseup', handleMouseUp)
if (isDraggingRef.current) {
isDraggingRef.current = false
setIsDragging(false)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to 71

if(!is_string($metadatum->key) or !is_string($metadatum->value)) {
$metadatum->value = json_encode($metadatum->value);
$metadatum->key = json_encode($metadatum->key);
}

$this->metadata[] = new AnalysisFileMetadata($metadatum->key, $metadatum->value);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The condition uses or, so if either key or value is non-string you JSON-encode both. That corrupts valid string keys (e.g. \"instructions\") when only the value is non-string. Encode/cast key and value independently (e.g., only JSON-encode value when it’s non-scalar) and avoid JSON-encoding keys unless you explicitly support non-string key types.

Suggested change
if(!is_string($metadatum->key) or !is_string($metadatum->value)) {
$metadatum->value = json_encode($metadatum->value);
$metadatum->key = json_encode($metadatum->key);
}
$this->metadata[] = new AnalysisFileMetadata($metadatum->key, $metadatum->value);
$key = is_string($metadatum->key) ? $metadatum->key : (string)$metadatum->key;
if (is_string($metadatum->value)) {
$value = $metadatum->value;
} elseif (is_scalar($metadatum->value) || $metadatum->value === null) {
$value = (string)$metadatum->value;
} else {
$value = json_encode($metadatum->value);
}
$this->metadata[] = new AnalysisFileMetadata($key, $value);

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +27
public array $sql_down = [
'
ALTER TABLE `segment_metadata` REMOVE PARTITIONING;
ALTER TABLE `segment_metadata`
DROP INDEX `idx_id_segment_meta_key`,
ADD INDEX `idx_id_segment_meta_key` (`id_segment`, `meta_key`);
'
];
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The down migration is not a safe inverse of the up migration: REMOVE PARTITIONING is unrelated to the index change and will fail on non-partitioned tables (or change table structure unexpectedly). Also, multiple statements in a single string may not be supported by the migration runner/DB driver. Down should strictly revert the unique index back to a non-unique index (and omit partition operations).

Copilot uses AI. Check for mistakes.
Comment on lines 67 to +69
return $thisDao->setCacheTTL($ttl)->_fetchObjectMap($stmt, OwnerFeatureStruct::class, [
'id_customer' => $id_customer
]) ?? [];
]);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Both methods declare a return type of array, but they now directly return _fetchObjectMap(...) without the previous ?? [] fallback. If _fetchObjectMap can return null (as the old code implied), this will raise a TypeError. Restore the ?? [] fallback or ensure _fetchObjectMap always returns an array (including when there are no results).

Copilot uses AI. Check for mistakes.
Comment on lines +452 to +471
// const rawMetadata = seg.get('metadata')
// const metadataArr = rawMetadata
// ? typeof rawMetadata.toJS === 'function'
// ? rawMetadata.toJS()
// : rawMetadata
// : []
// const findMeta = (key) =>
// metadataArr.find((m) => m.meta_key === key)?.meta_value ?? null
// segmentsList.push({
// sid: seg.get('sid'),
// source: seg.get('segment'),
// target: seg.get('translation'),
// context_url: seg.get('context_url') ?? null,
// resname: findMeta('resname'),
// restype: findMeta('restype'),
// })
// }
// ContextPreviewChannel.sendMessage({type: 'segments', segments: segmentsList})
// }, [segments])

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Large commented-out logic in production code makes the intent unclear and is easy to let drift out-of-date. If this is intentionally deferred, prefer removing it and tracking via an issue/PR, or reintroduce it behind a feature flag/config so it stays executable and testable.

Suggested change
// const rawMetadata = seg.get('metadata')
// const metadataArr = rawMetadata
// ? typeof rawMetadata.toJS === 'function'
// ? rawMetadata.toJS()
// : rawMetadata
// : []
// const findMeta = (key) =>
// metadataArr.find((m) => m.meta_key === key)?.meta_value ?? null
// segmentsList.push({
// sid: seg.get('sid'),
// source: seg.get('segment'),
// target: seg.get('translation'),
// context_url: seg.get('context_url') ?? null,
// resname: findMeta('resname'),
// restype: findMeta('restype'),
// })
// }
// ContextPreviewChannel.sendMessage({type: 'segments', segments: segmentsList})
// }, [segments])

Copilot uses AI. Check for mistakes.
Comment on lines +96 to 98
const {placeholderRegex, decodeNeeded, encodedPlaceholder, regex, type} =
tagSignatures[key]
const shouldExcludeTag = excludeTags.some((value) => value === type)
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This changes the destructured property from placeholder to encodedPlaceholder. If tagSignatures[...] entries still provide placeholder (and not encodedPlaceholder), the transform will stop substituting placeholders and will fall back to match. To make this robust, either update the tagSignatures definition consistently or support both keys (e.g., prefer encodedPlaceholder but fallback to placeholder).

Copilot uses AI. Check for mistakes.
!shouldExcludeTag
? (match) => {
return placeholder ? placeholder : match
return encodedPlaceholder ? encodedPlaceholder : match
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This changes the destructured property from placeholder to encodedPlaceholder. If tagSignatures[...] entries still provide placeholder (and not encodedPlaceholder), the transform will stop substituting placeholders and will fall back to match. To make this robust, either update the tagSignatures definition consistently or support both keys (e.g., prefer encodedPlaceholder but fallback to placeholder).

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
public function __construct(
private mixed $segmentsList,
private readonly array $requestData,
) {
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The event exposes requestData as readonly with only a getter. In the previous hook usage (filter('rewriteContributionContexts', $segmentsList, $request)), plugins could influence both the segments list and the request context. With this design, plugins can only alter segmentsList (via a setter) but cannot update request data at all. If request mutation is still required, add setRequestData(array $requestData): void (and drop readonly) or clarify by removing the unused request reassignment at the call site.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +37
public function getRequestData(): array
{
return $this->requestData;
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

The event exposes requestData as readonly with only a getter. In the previous hook usage (filter('rewriteContributionContexts', $segmentsList, $request)), plugins could influence both the segments list and the request context. With this design, plugins can only alter segmentsList (via a setter) but cannot update request data at all. If request mutation is still required, add setRequestData(array $requestData): void (and drop readonly) or clarify by removing the unused request reassignment at the call site.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 21, 2026 10:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 268 out of 328 changed files in this pull request and generated 1 comment.

{
$request = $this->validateTheRequest();

$this->setView('context_preview.html', [
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The controller renders context_preview.html, but the added template file is lib/View/templates/_context_preview.html. If the view loader expects an exact filename, this will cause a template-not-found error. Rename the template to context_preview.html or update setView() to reference _context_preview.html (whichever matches the project’s template naming conventions).

Suggested change
$this->setView('context_preview.html', [
$this->setView('_context_preview.html', [

Copilot uses AI. Check for mistakes.
- replace OPEN_SEGMENT listener with RENDER_SEGMENTS listener
  wrapped by a segment-change guard (lastOpenedSid)
- OPEN_SEGMENT only fires for keyboard/action paths
  (SegmentActions.openSegment), missing click-based
  SET_OPEN_SEGMENT dispatched by Segment.js onClickEvent
Comment on lines +236 to +291
private function buildNewTranslation(string $translation, string $errJson, QA $check): array
{
$old_translation = $this->getOldTranslation();

$result['stats'] = $job_stats;
$result['file_stats'] = $file_stats;
$result['code'] = 1;
$result['data'] = "OK";
$result['version'] = date_create($new_translation['translation_date'])->getTimestamp();
$result['translation'] = $this->getTranslationObject($new_translation);
$client_suggestion_array = json_decode($this->data['suggestion_array'] ?? '[]', true);
$chosenIndex = $this->data['chosen_suggestion_index'] !== null ? (int)$this->data['chosen_suggestion_index'] : null;
$client_chosen_suggestion_params = ($chosenIndex !== null && isset($client_suggestion_array[$chosenIndex - 1])) ? $client_suggestion_array[$chosenIndex - 1] : [];
$client_chosen_suggestion = new ShapelessConcreteStruct($client_chosen_suggestion_params);

/* FIXME: added for code compatibility with front-end. Remove. */
$_warn = $check->getWarnings();
$warning = $_warn[0];
/* */
$new_translation = new SegmentTranslationStruct();
$new_translation->id_segment = (int)$this->data['id_segment'];
$new_translation->id_job = (int)$this->data['id_job'];
$new_translation->status = $this->data['status'];

$result['warning']['cod'] = $warning->outcome;
if ($warning->outcome > 0) {
$result['warning']['id'] = $this->data['id_segment'];
if ($this->data['segment'] === null) {
throw new RuntimeException('Segment must not be null in buildNewTranslation');
}
$new_translation->segment_hash = $this->data['segment']->segment_hash;
$new_translation->translation = $translation;
$new_translation->serialized_errors_list = $errJson;
$new_translation->suggestions_array = ($chosenIndex !== null ? $this->data['suggestion_array'] : $old_translation->suggestions_array);
$new_translation->suggestion_position = ($chosenIndex !== null ? $chosenIndex : $old_translation->suggestion_position);
$new_translation->warning = $check->thereAreWarnings();
$new_translation->translation_date = date("Y-m-d H:i:s");
$new_translation->suggestion = $old_translation->suggestion; //IMPORTANT: raw_translation is in layer 0 and suggestion too
$new_translation->suggestion_source = $old_translation->suggestion_source;
$new_translation->suggestion_match = $old_translation->suggestion_match;

// update suggestion
if ($this->canUpdateSuggestion($new_translation, $client_chosen_suggestion)) {
$new_translation->suggestion = !empty($client_chosen_suggestion->raw_translation) ? $client_chosen_suggestion->raw_translation : $old_translation->suggestion; //IMPORTANT: raw_translation is in layer 0 and suggestion too

// update suggestion match
if ($client_chosen_suggestion->match == EngineConstants::MT) {
/** @var ProjectStruct $project */
$project = $this->data['project'];
// case 1. is MT
$new_translation->suggestion_match = (string)($project->getMetadataValue(ProjectsMetadataMarshaller::MT_QUALITY_VALUE_IN_EDITOR->value) ?? 85);
$new_translation->suggestion_source = EngineConstants::MT;
} elseif ($client_chosen_suggestion->match == InternalMatchesConstants::NO_MATCH) {
// case 2. no match
$new_translation->suggestion_source = InternalMatchesConstants::NO_MATCH;
} else {
$result['warning']['id'] = 0;
// case 3. otherwise is TM
$new_translation->suggestion_match = (string)(int)$client_chosen_suggestion->match; // cast '71%' to int 71
$new_translation->suggestion_source = EngineConstants::TM;
}
}

$new_translation->time_to_edit = (int)$this->data['time_to_edit'];

return [
'new' => $new_translation,
'old' => $old_translation,
];
}
Comment on lines +236 to +291
private function buildNewTranslation(string $translation, string $errJson, QA $check): array
{
$old_translation = $this->getOldTranslation();

$result['stats'] = $job_stats;
$result['file_stats'] = $file_stats;
$result['code'] = 1;
$result['data'] = "OK";
$result['version'] = date_create($new_translation['translation_date'])->getTimestamp();
$result['translation'] = $this->getTranslationObject($new_translation);
$client_suggestion_array = json_decode($this->data['suggestion_array'] ?? '[]', true);
$chosenIndex = $this->data['chosen_suggestion_index'] !== null ? (int)$this->data['chosen_suggestion_index'] : null;
$client_chosen_suggestion_params = ($chosenIndex !== null && isset($client_suggestion_array[$chosenIndex - 1])) ? $client_suggestion_array[$chosenIndex - 1] : [];
$client_chosen_suggestion = new ShapelessConcreteStruct($client_chosen_suggestion_params);

/* FIXME: added for code compatibility with front-end. Remove. */
$_warn = $check->getWarnings();
$warning = $_warn[0];
/* */
$new_translation = new SegmentTranslationStruct();
$new_translation->id_segment = (int)$this->data['id_segment'];
$new_translation->id_job = (int)$this->data['id_job'];
$new_translation->status = $this->data['status'];

$result['warning']['cod'] = $warning->outcome;
if ($warning->outcome > 0) {
$result['warning']['id'] = $this->data['id_segment'];
if ($this->data['segment'] === null) {
throw new RuntimeException('Segment must not be null in buildNewTranslation');
}
$new_translation->segment_hash = $this->data['segment']->segment_hash;
$new_translation->translation = $translation;
$new_translation->serialized_errors_list = $errJson;
$new_translation->suggestions_array = ($chosenIndex !== null ? $this->data['suggestion_array'] : $old_translation->suggestions_array);
$new_translation->suggestion_position = ($chosenIndex !== null ? $chosenIndex : $old_translation->suggestion_position);
$new_translation->warning = $check->thereAreWarnings();
$new_translation->translation_date = date("Y-m-d H:i:s");
$new_translation->suggestion = $old_translation->suggestion; //IMPORTANT: raw_translation is in layer 0 and suggestion too
$new_translation->suggestion_source = $old_translation->suggestion_source;
$new_translation->suggestion_match = $old_translation->suggestion_match;

// update suggestion
if ($this->canUpdateSuggestion($new_translation, $client_chosen_suggestion)) {
$new_translation->suggestion = !empty($client_chosen_suggestion->raw_translation) ? $client_chosen_suggestion->raw_translation : $old_translation->suggestion; //IMPORTANT: raw_translation is in layer 0 and suggestion too

// update suggestion match
if ($client_chosen_suggestion->match == EngineConstants::MT) {
/** @var ProjectStruct $project */
$project = $this->data['project'];
// case 1. is MT
$new_translation->suggestion_match = (string)($project->getMetadataValue(ProjectsMetadataMarshaller::MT_QUALITY_VALUE_IN_EDITOR->value) ?? 85);
$new_translation->suggestion_source = EngineConstants::MT;
} elseif ($client_chosen_suggestion->match == InternalMatchesConstants::NO_MATCH) {
// case 2. no match
$new_translation->suggestion_source = InternalMatchesConstants::NO_MATCH;
} else {
$result['warning']['id'] = 0;
// case 3. otherwise is TM
$new_translation->suggestion_match = (string)(int)$client_chosen_suggestion->match; // cast '71%' to int 71
$new_translation->suggestion_source = EngineConstants::TM;
}
}

$new_translation->time_to_edit = (int)$this->data['time_to_edit'];

return [
'new' => $new_translation,
'old' => $old_translation,
];
}
Comment on lines +305 to +422
private function persistTranslation(
SegmentTranslationStruct $newTranslation,
SegmentTranslationStruct $oldTranslation,
string $translation,
string $errJson,
QA $check
): array {
/**
* Update Time to Edit and
*
* Evaluate new Avg post-editing effort for the job:
* - get old translation
* - get suggestion
* - evaluate $_seg_oldPEE and normalize it on the number of words for this segment
*
* - Get a new translation
* - Evaluate $_seg_newPEE and normalize it on the number of words for this segment
*
* - Get $_jobTotalPEE
* - Evaluate $_jobTotalPEE - $_seg_oldPEE + $_seg_newPEE and save it into the job's row
*/
$this->updateJobPEE($oldTranslation->toArray(), $newTranslation->toArray());

// if saveVersionAndIncrement() return true it means that it was persisted a new version of the parent segment
/** @var VersionHandlerInterface $versionsHandler */
$versionsHandler = $this->VersionsHandler;
$versionsHandler->saveVersionAndIncrement($newTranslation, $oldTranslation);

/**
* when the status of the translation changes, the auto propagation flag
* must be removed
*/
if ($newTranslation->translation != $oldTranslation->translation or
$this->data['status'] == TranslationStatus::STATUS_TRANSLATED or
$this->data['status'] == TranslationStatus::STATUS_APPROVED or
$this->data['status'] == TranslationStatus::STATUS_APPROVED2
) {
$newTranslation->autopropagated_from = null;
}

/**
* Translation is inserted here.
*/
CatUtils::addSegmentTranslation($newTranslation, (bool)$this->isRevision());

/**
* @see ProjectCompletion
*/
$this->getFeatureSet()->dispatchRun(new PostAddSegmentTranslationEvent([
'chunk' => $this->data['chunk'],
'is_review' => (bool)$this->isRevision(),
'logged_user' => $this->user
]));

$propagationTotal = [
'totals' => [],
'propagated_ids' => [],
'segments_for_propagation' => []
];

if ($this->data['propagate'] && in_array($this->data['status'], [
TranslationStatus::STATUS_TRANSLATED,
TranslationStatus::STATUS_APPROVED,
TranslationStatus::STATUS_APPROVED2,
TranslationStatus::STATUS_REJECTED
])
) {
//propagate translations
$TPropagation = new SegmentTranslationStruct();
$TPropagation['status'] = $this->data['status'];
$TPropagation['id_job'] = $this->data['id_job'];
$TPropagation['translation'] = $translation;
$TPropagation['autopropagated_from'] = (int)$this->data['id_segment'];
$TPropagation['serialized_errors_list'] = $errJson;
$TPropagation['warning'] = $check->thereAreWarnings();
$TPropagation['segment_hash'] = $oldTranslation['segment_hash'];
$TPropagation['translation_date'] = Utils::mysqlTimestamp(time());
$TPropagation['match_type'] = $oldTranslation['match_type'];
$TPropagation['locked'] = $oldTranslation['locked'];

$propagationTotal = $versionsHandler->propagateTranslation($TPropagation);
}

$this->featureSet->run('setTranslationCommitted', [
'translation' => $new_translation,
'old_translation' => $old_translation,
'propagated_ids' => $propagationTotal['segments_for_propagation']['propagated_ids'] ?? null,
'chunk' => $this->data['chunk'],
'segment' => $this->data['segment'],
'user' => $this->user,
'source_page_code' => ReviewUtils::revisionNumberToSourcePage($this->data['revisionNumber'])
]);

$result = $this->featureSet->filter('filterSetTranslationResult', $result, [
'translation' => $new_translation,
'old_translation' => $old_translation,
'propagated_ids' => $propagationTotal['segments_for_propagation']['propagated_ids'] ?? null,
'chunk' => $this->data['chunk'],
'segment' => $this->data['segment']
]);


//EVERY time a user changes a row in his job when the job is completed,
// a query to do the update is executed...
// Avoid this by setting a key on redis with a reasonable TTL
$redisHandler = new RedisHandler();
$job_status = $redisHandler->getConnection()->get('job_completeness:' . $this->data['id_job']);
if (
if ($this->isSplittedSegment()) {
/* put the split inside the transaction if they are present */
$translationStruct = SegmentSplitStruct::getStruct();
$translationStruct->id_segment = (int)$this->data['id_segment'];
$translationStruct->id_job = (int)$this->data['id_job'];

$translationStruct->target_chunk_lengths = [
'len' => $this->data['split_chunk_lengths'],
'statuses' => $this->data['split_statuses']
];

$translationDao = new SplitDAO(Database::obtain());
$translationDao->atomicUpdate($translationStruct);
}

//COMMIT THE TRANSACTION
/*
* Hooked by TranslationVersions, which manage translation versions
*
* This is also the init handler of all R1/R2 handling and Qr score calculation by
* by TranslationEventsHandler and BatchReviewProcessor
*/
$versionsHandler->storeTranslationEvent([
'translation' => $newTranslation,
'old_translation' => $oldTranslation,
'propagation' => $propagationTotal,
'chunk' => $this->chunk,
'user' => $this->user,
'source_page_code' => ReviewUtils::revisionNumberToSourcePage($this->data['revisionNumber']),
'features' => $this->featureSet,
'project' => $this->data['project']
]);

return $propagationTotal;
}
- when closed, show a minimal "Visual context" tab (Figma spec)
  instead of the full header bar
- tab floats over content via fixed positioning, centered above footer
- slide-up + fade-in entrance (250ms ease-out), slide-down + fade-out
  exit (200ms ease-in) when navigating between segments with/without
  context
- add explicit white background to CattolFooter
Copilot AI review requested due to automatic review settings April 21, 2026 13:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 269 out of 329 changed files in this pull request and generated 4 comments.

Comment on lines +22 to +24
ALTER TABLE `segment_metadata` REMOVE PARTITIONING;
ALTER TABLE `segment_metadata`
DROP INDEX `idx_id_segment_meta_key`,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The down migration includes ALTER TABLE ... REMOVE PARTITIONING;, which is unrelated to the unique-index change and will fail on non-partitioned tables. Also, if your migration runner executes each $sql_down entry as a single statement, embedding two ALTER statements in one string (with semicolons) can break execution. Remove the partitioning clause, and split into two separate SQL entries if multi-statement strings aren’t supported.

Suggested change
ALTER TABLE `segment_metadata` REMOVE PARTITIONING;
ALTER TABLE `segment_metadata`
DROP INDEX `idx_id_segment_meta_key`,
ALTER TABLE `segment_metadata`
DROP INDEX `idx_id_segment_meta_key`;
',
'
ALTER TABLE `segment_metadata`

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +53
$filterActivityLogEntryEvent = new FilterActivityLogEntryEvent($record->toArray());
$featureSet->dispatchFilter($filterActivityLogEntryEvent);
$filteredRecord = new ActivityLogStruct($filterActivityLogEntryEvent->getRecord());
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The hook now operates on arrays and then rehydrates ActivityLogStruct. If a plugin drops required keys or changes types, rehydration can produce partially-initialized structs (or break expectations) and the subsequent reads may become unreliable. Consider validating the returned array (required keys/types) and falling back to the original $record when invalid, or allow the hook to return an ActivityLogStruct directly.

Copilot uses AI. Check for mistakes.
{
public static function hookName(): string
{
return 'jobPasswordChanged';
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The hook name here is camelCase (jobPasswordChanged), but the previous hook invoked in the codebase was snake_case (job_password_changed). Similar renames appear across multiple events (e.g., alter_chunk_review_structalterChunkReviewStruct, filter_job_password_to_review_passwordfilterJobPasswordToReviewPassword). If third-party/internal plugins depend on the old hook names, this is a breaking change. Consider keeping hookName() identical to the legacy string keys (or adding an alias/compat layer that dispatches both) to preserve plugin compatibility.

Suggested change
return 'jobPasswordChanged';
return 'job_password_changed';

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 78
* This method means to allow project_completion to work alone, the undo feature belongs to AbstractRevisionFeature
*/
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

This used to call a filter hook named alter_chunk_review_struct and now dispatches a run event whose hookName() is alterChunkReviewStruct. Besides the hook-name breaking change, this also changes the hook type (filter → run), so plugins that previously relied on filter semantics may no longer work. If the hook is still meant to be a filter, consider implementing it as a FilterEvent and dispatching via dispatchFilter, or explicitly support the legacy filter hook name/type during the transition.

Suggested change
* This method means to allow project_completion to work alone, the undo feature belongs to AbstractRevisionFeature
*/
* This method means to allow project_completion to work alone, the undo feature belongs to AbstractRevisionFeature
*
* Keep backward compatibility with listeners still bound to the legacy hook name.
*/
$this->featureSet->dispatchRun(new class($this->event) extends AlterChunkReviewStructEvent {
public function hookName(): string
{
return 'alter_chunk_review_struct';
}
});

Copilot uses AI. Check for mistakes.
- center collapsed wrapper via left:0/right:0 + flex instead of
  transform: translateX(-50%) which was being animated
- transform now only handles vertical slide (translateY)
- add pointer-events: none on full-width wrapper, auto on tab
# Conflicts:
#	composer.lock
#	phpstan.neon
@riccio82 riccio82 changed the title Context review ✨ feat(context-preview): in-context review panel with segment-aware highlighting Apr 28, 2026
Copilot AI review requested due to automatic review settings April 28, 2026 13:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants