✨ feat(context-preview): in-context review panel with segment-aware highlighting#4429
✨ feat(context-preview): in-context review panel with segment-aware highlighting#4429
Conversation
… 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.
… returns collection, API serves typed values
…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)
…aStruct value as mixed
…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.
- 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.
There was a problem hiding this comment.
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 typeddispatchFilter()/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.
| const handleMouseUp = () => { | ||
| if (!isDraggingRef.current) return | ||
| isDraggingRef.current = false | ||
| setIsDragging(false) | ||
| document.body.style.cursor = '' | ||
| document.body.style.userSelect = '' | ||
| } |
There was a problem hiding this comment.
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).
|
|
||
| return () => { | ||
| window.removeEventListener('mousemove', handleMouseMove) | ||
| window.removeEventListener('mouseup', handleMouseUp) |
There was a problem hiding this comment.
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).
| window.removeEventListener('mouseup', handleMouseUp) | |
| window.removeEventListener('mouseup', handleMouseUp) | |
| if (isDraggingRef.current) { | |
| isDraggingRef.current = false | |
| setIsDragging(false) | |
| document.body.style.cursor = '' | |
| document.body.style.userSelect = '' | |
| } |
|
|
||
| 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); |
There was a problem hiding this comment.
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.
| 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); |
| 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`); | ||
| ' | ||
| ]; |
There was a problem hiding this comment.
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).
| return $thisDao->setCacheTTL($ttl)->_fetchObjectMap($stmt, OwnerFeatureStruct::class, [ | ||
| 'id_customer' => $id_customer | ||
| ]) ?? []; | ||
| ]); |
There was a problem hiding this comment.
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).
| // 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]) | ||
|
|
There was a problem hiding this comment.
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.
| // 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]) |
| const {placeholderRegex, decodeNeeded, encodedPlaceholder, regex, type} = | ||
| tagSignatures[key] | ||
| const shouldExcludeTag = excludeTags.some((value) => value === type) |
There was a problem hiding this comment.
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).
| !shouldExcludeTag | ||
| ? (match) => { | ||
| return placeholder ? placeholder : match | ||
| return encodedPlaceholder ? encodedPlaceholder : match |
There was a problem hiding this comment.
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).
| public function __construct( | ||
| private mixed $segmentsList, | ||
| private readonly array $requestData, | ||
| ) { | ||
| } |
There was a problem hiding this comment.
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.
| public function getRequestData(): array | ||
| { | ||
| return $this->requestData; | ||
| } |
There was a problem hiding this comment.
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.
| { | ||
| $request = $this->validateTheRequest(); | ||
|
|
||
| $this->setView('context_preview.html', [ |
There was a problem hiding this comment.
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).
| $this->setView('context_preview.html', [ | |
| $this->setView('_context_preview.html', [ |
- 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
| 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, | ||
| ]; | ||
| } |
| 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, | ||
| ]; | ||
| } |
| 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
| ALTER TABLE `segment_metadata` REMOVE PARTITIONING; | ||
| ALTER TABLE `segment_metadata` | ||
| DROP INDEX `idx_id_segment_meta_key`, |
There was a problem hiding this comment.
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.
| 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` |
| $filterActivityLogEntryEvent = new FilterActivityLogEntryEvent($record->toArray()); | ||
| $featureSet->dispatchFilter($filterActivityLogEntryEvent); | ||
| $filteredRecord = new ActivityLogStruct($filterActivityLogEntryEvent->getRecord()); |
There was a problem hiding this comment.
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.
| { | ||
| public static function hookName(): string | ||
| { | ||
| return 'jobPasswordChanged'; |
There was a problem hiding this comment.
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_struct → alterChunkReviewStruct, filter_job_password_to_review_password → filterJobPasswordToReviewPassword). 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.
| return 'jobPasswordChanged'; | |
| return 'job_password_changed'; |
| * This method means to allow project_completion to work alone, the undo feature belongs to AbstractRevisionFeature | ||
| */ |
There was a problem hiding this comment.
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.
| * 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'; | |
| } | |
| }); |
- 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
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 featurefix— bug fixrefactor— restructure without behavior changechore— build, deps, config, docsperf— performance improvementtest— test coverageChanges
lib/Controller/API/App/ContextUrlController.phppublic/js/components/context-preview/lib/Model/FeatureSet.phplib/Controller/API/App/SetTranslationController.phplib/Controller/API/(multiple)lib/Model/Search/SearchModel.phplib/Model/DataAccess/Database.phpplugins/airbnb/,plugins/translated/phpstan.neondocs/tests/unit/TestDatabase/tests/unit/Search/SearchModelTest.phpTesting
vendor/bin/phpunit --exclude-group=ExternalServices --no-coveragepasses./vendor/bin/phpstanpasses (0 errors, with baseline)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
Claude Code (claude-opus-4-6) — refactoring (controllers, hooks, FeatureSet typing), documentation, tests, PR description
Notes