From d7b6cbac79359a55a5b1c02f9aab702fefd9d423 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 15:44:22 +0200 Subject: [PATCH 01/10] remove target locale that duplicates a newly changed source locale (WP-1016) ConfigurationProfileFormController::save() set the source locale and built the target-locale list independently, and validateTargetLocales() only checked for duplicate Smartling locale codes among targets - never against the new source blogId. The only place that ever excluded the source locale from targets was a render-time loop in the profile edit view, which only affects what's displayed on the next page load, not what gets persisted when the source locale itself is changed in the same submission. Now save() skips any submitted target-locale entry whose blogId matches the profile's (possibly just-changed) source locale before persisting, so a stale or duplicate row can no longer be saved as both source and target. --- .../ConfigurationProfileFormController.php | 7 + ...ConfigurationProfileFormControllerTest.php | 133 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index 81835595..f2676c09 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -230,11 +230,18 @@ public function save(): void $profile->setSourceLocale($locale); } + $sourceBlogId = $profile->getSourceLocale()->getBlogId(); + $usedTargetLocales = []; if (array_key_exists('targetLocales', $settings)) { $locales = []; foreach ($settings['targetLocales'] as $blogId => $settings) { + if ((int)$blogId === $sourceBlogId) { + // Never persist the source locale as a target locale, even if a stale + // form submission still includes it after the source locale was changed. + continue; + } try { $tLocale = new TargetLocale(); $tLocale->setBlogId($blogId); diff --git a/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php new file mode 100644 index 00000000..d1be5634 --- /dev/null +++ b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php @@ -0,0 +1,133 @@ +storedRequest = $_REQUEST; + } + + protected function tearDown(): void + { + parent::tearDown(); + $_REQUEST = $this->storedRequest; + } + + private function createController(SettingsManager $settingsManager, SiteHelper $siteHelper): ConfigurationProfileFormController + { + return new ConfigurationProfileFormController( + $this->createMock(ApiWrapperInterface::class), + $this->createMock(LocalizationPluginProxyInterface::class), + $this->createMock(PluginInfo::class), + $settingsManager, + $siteHelper, + $this->createMock(SubmissionManager::class), + $this->createMock(Cache::class), + ); + } + + public function testSaveRemovesTargetLocaleMatchingNewSourceLocale(): void + { + $profile = new ConfigurationProfileEntity(); + $profile->setId(1); + + $siteHelper = $this->createMock(SiteHelper::class); + $siteHelper->method('getBlogLabelById')->willReturnCallback( + static fn(LocalizationPluginProxyInterface $proxy, int $blogId) => 'blog-' . $blogId + ); + + $settingsManager = $this->createMock(SettingsManager::class); + $settingsManager->method('getEntityById')->with(1)->willReturn([$profile]); + $settingsManager->expects($this->once()) + ->method('storeEntity') + ->with($this->callback(function (ConfigurationProfileEntity $savedProfile) { + $targetBlogIds = array_map(static fn($locale) => $locale->getBlogId(), $savedProfile->getTargetLocales()); + // Target locale for blogId 3 (== new source locale) must be dropped, + // while the unrelated target locale for blogId 4 must survive. + return $savedProfile->getSourceLocale()->getBlogId() === 3 + && !in_array(3, $targetBlogIds, true) + && in_array(4, $targetBlogIds, true) + && count($targetBlogIds) === 1; + })) + ->willReturnArgument(0); + + $controller = $this->createController($settingsManager, $siteHelper); + + $_REQUEST['smartling_settings'] = [ + 'id' => 1, + 'defaultLocale' => '3', + 'targetLocales' => [ + 3 => ['enabled' => 'on', 'target' => 'fr-FR'], + 4 => ['enabled' => 'on', 'target' => 'de-DE'], + ], + ]; + + $controller->save(); + } + + public function testSaveKeepsNonCollidingTargetLocalesAndStillDetectsDuplicateSmartlingLocales(): void + { + $profile = new ConfigurationProfileEntity(); + $profile->setId(1); + + $siteHelper = $this->createMock(SiteHelper::class); + $siteHelper->method('getBlogLabelById')->willReturnCallback( + static fn(LocalizationPluginProxyInterface $proxy, int $blogId) => 'blog-' . $blogId + ); + + $settingsManager = $this->createMock(SettingsManager::class); + $settingsManager->method('getEntityById')->with(1)->willReturn([$profile]); + // Duplicate Smartling locale codes among the remaining (non-source) targets + // must still block the save, same as before this change. + $settingsManager->expects($this->never())->method('storeEntity'); + + $controller = $this->createController($settingsManager, $siteHelper); + + $_REQUEST['smartling_settings'] = [ + 'id' => 1, + 'defaultLocale' => '3', + 'targetLocales' => [ + 3 => ['enabled' => 'on', 'target' => 'fr-FR'], + 4 => ['enabled' => 'on', 'target' => 'de-DE'], + 5 => ['enabled' => 'on', 'target' => 'de-DE'], + ], + ]; + + $controller->save(); + } + } +} From fe0e7931b4902791abafdecd3aafe29e6478fe88 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 15:54:13 +0200 Subject: [PATCH 02/10] allow live target-locale toggling when the source locale changes (WP-1016) Target-locale rows were previously skipped entirely from the DOM for whatever blog was the currently persisted source locale, so switching the source-locale select client-side (without reloading the page) never made the previous source available as a target, nor hid the newly picked source from the target list until the page was reloaded post-save. Every target-locale row is now always rendered, tagged with its blogId, and hidden/disabled by default only if it matches the profile's current source locale. A change handler on the source-locale selects keeps this in sync live: picking a new source hides and disables its row (so it can never be submitted as a target) and re-enables/reveals whatever was previously hidden - all without a page reload, on top of the save()-time filtering already in place as the source of truth. --- .../ConfigurationProfileFormController.php | 24 +++++++++++----- .../WP/View/ConfigurationProfileForm.php | 16 +++++------ js/configuration-profile-form.js | 20 +++++++++++++ ...ConfigurationProfileFormControllerTest.php | 28 +++++++++++++++++++ 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index f2676c09..5cc463e2 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -277,6 +277,7 @@ protected function renderLocales( int $blogId, string $smartlingName, bool $enabled, + bool $disabled = false, ): string { $parts = []; @@ -290,20 +291,30 @@ protected function renderLocales( $checkboxProperties['checked'] = 'checked'; } + if (true === $disabled) { + $checkboxProperties['disabled'] = 'disabled'; + } + $parts[] = HtmlTagGeneratorHelper::tag('input', '', $checkboxProperties); $parts[] = HtmlTagGeneratorHelper::tag('span', htmlspecialchars($displayName)); $parts = [ HtmlTagGeneratorHelper::tag('label', implode('', $parts), ['class' => 'radio-label']), ]; + $targetLocaleProperties = [ + 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId), + ]; + + if (true === $disabled) { + $targetLocaleProperties['disabled'] = 'disabled'; + } + if (0 === count($locales)) { $sLocale = HtmlTagGeneratorHelper::tag( 'input', '', - [ - 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId), - 'type' => 'text', - ]); + $targetLocaleProperties + ['type' => 'text'], + ); } else { $sLocale = HtmlTagGeneratorHelper::tag( 'select', @@ -311,9 +322,8 @@ protected function renderLocales( $smartlingName, $locales ), - [ - 'name' => sprintf('smartling_settings[targetLocales][%s][target]', $blogId), - ]); + $targetLocaleProperties, + ); } $parts = [ diff --git a/inc/Smartling/WP/View/ConfigurationProfileForm.php b/inc/Smartling/WP/View/ConfigurationProfileForm.php index 542b4cee..60059aed 100644 --- a/inc/Smartling/WP/View/ConfigurationProfileForm.php +++ b/inc/Smartling/WP/View/ConfigurationProfileForm.php @@ -473,13 +473,8 @@ getTargetLocales(); $supportedLocales = $this->api->getSupportedLocales($profile); + $currentSourceBlogId = $profile->getSourceLocale()->getBlogId(); foreach ($locales as $blogId => $label) { - if ($blogId === $profile->getSourceLocale() - ->getBlogId() - ) { - continue; - } - $smartlingLocale = ''; $enabled = false; @@ -490,10 +485,15 @@ break; } } + + // The current source locale is never a selectable target: its row + // stays in the DOM (hidden/disabled) so JS can reveal it again if the + // source locale is changed to a different blog before the form is saved. + $isSourceLocaleRow = $blogId === $currentSourceBlogId; ?> - - renderLocales($supportedLocales, $label, $blogId, $smartlingLocale, $enabled) ?> + + renderLocales($supportedLocales, $label, $blogId, $smartlingLocale, $enabled, $isSourceLocaleRow) ?> save(); } + + public function testRenderLocalesDisablesInputsForSourceLocaleRow(): void + { + $controller = $this->createController( + $this->createMock(SettingsManager::class), + $this->createMock(SiteHelper::class), + ); + + $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales'); + + $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, true); + + $this->assertStringContainsString('disabled="disabled"', $html); + } + + public function testRenderLocalesDoesNotDisableInputsForRegularRow(): void + { + $controller = $this->createController( + $this->createMock(SettingsManager::class), + $this->createMock(SiteHelper::class), + ); + + $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales'); + + $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, false); + + $this->assertStringNotContainsString('disabled="disabled"', $html); + } } } From 9c0399fb0eefa5988e647de26204cc0586bb73f1 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 15:58:36 +0200 Subject: [PATCH 03/10] restore ReflectionMethod::setAccessible() for PHP 8.0 compatibility (WP-1016) setAccessible() only became a no-op starting PHP 8.1; this project targets PHP 8.0, where it's still required to invoke a protected method via reflection. --- .../WP/Controller/ConfigurationProfileFormControllerTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php index 989854cd..35040051 100644 --- a/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php +++ b/tests/Smartling/WP/Controller/ConfigurationProfileFormControllerTest.php @@ -138,6 +138,9 @@ public function testRenderLocalesDisablesInputsForSourceLocaleRow(): void ); $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales'); + // PHP 8.0 (this project's target) still requires setAccessible() to invoke a + // protected method via reflection; it only became a no-op starting PHP 8.1. + $method->setAccessible(true); $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, true); @@ -152,6 +155,7 @@ public function testRenderLocalesDoesNotDisableInputsForRegularRow(): void ); $method = new \ReflectionMethod(ConfigurationProfileFormController::class, 'renderLocales'); + $method->setAccessible(true); $html = $method->invoke($controller, ['en-US' => 'English'], 'French', 3, 'fr-FR', true, false); From 31a980462f1c45a8ea593feb20ae9389e2a1cadd Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 18:36:21 +0200 Subject: [PATCH 04/10] add source and target content edit links to Translation Progress screen (WP-1016) The Submissions Board previously showed the Title and Locale columns as plain text, with no way to jump to the actual WordPress content being translated. This links Title to the source content's edit screen and Locale to the target content's edit screen, both multisite-aware via get_admin_url($blogId, ...). - WordpressContentTypeHelper: extracted buildEditUrl() shared by the existing getEditUrl() (target) and new getSourceEditUrl() (source). getEditUrl()'s behavior for existing callers is unchanged. - SubmissionTableWidget: new buildSourceTitleCell()/buildTargetLocaleCell() helpers wrap the title/locale text in a link when one is available, falling back to plain text when the id is 0 (not yet translated) or the content type is unsupported. - Attachments resolve through post.php?post=ID&action=edit, same as regular posts. Built with TDD; full unit suite (625 tests) passes. --- .../Helpers/WordpressContentTypeHelper.php | 19 ++- .../WP/Table/SubmissionTableWidget.php | 38 ++++- .../WordpressContentTypeHelperTest.php | 145 ++++++++++++++++++ .../WP/Table/SubmissionTableWidgetTest.php | 42 +++++ 4 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 tests/Smartling/Helpers/WordpressContentTypeHelperTest.php diff --git a/inc/Smartling/Helpers/WordpressContentTypeHelper.php b/inc/Smartling/Helpers/WordpressContentTypeHelper.php index b1eaff96..2e4f7613 100644 --- a/inc/Smartling/Helpers/WordpressContentTypeHelper.php +++ b/inc/Smartling/Helpers/WordpressContentTypeHelper.php @@ -145,6 +145,19 @@ public static function getBaseTypeByContentType($contentType) } public static function getEditUrl(SubmissionEntity $submission) + { + return static::buildEditUrl($submission, $submission->getTargetBlogId(), $submission->getTargetId()); + } + + /** + * Same as getEditUrl(), but builds a link to the source content instead of the target. + */ + public static function getSourceEditUrl(SubmissionEntity $submission): string + { + return static::buildEditUrl($submission, $submission->getSourceBlogId(), $submission->getSourceId()); + } + + private static function buildEditUrl(SubmissionEntity $submission, int $blogId, int $contentId): string { /** * @var ContentTypeAbstract $ctHandler @@ -155,16 +168,16 @@ public static function getEditUrl(SubmissionEntity $submission) $tail = ''; switch ($ctHandler->getBaseType()) { case 'post': - $tail = vsprintf('/post.php?post=%s&action=edit', [$submission->getTargetId()]); + $tail = vsprintf('/post.php?post=%s&action=edit', [$contentId]); break; case 'taxonomy': - $tail = sprintf('/term.php?taxonomy=%s&tag_ID=%s', $submission->getContentType(), $submission->getTargetId()); + $tail = sprintf('/term.php?taxonomy=%s&tag_ID=%s', $submission->getContentType(), $contentId); break; default: return ''; } - return get_admin_url($submission->getTargetBlogId(), $tail); + return get_admin_url($blogId, $tail); } else { Bootstrap::getLogger()->warning( vsprintf( diff --git a/inc/Smartling/WP/Table/SubmissionTableWidget.php b/inc/Smartling/WP/Table/SubmissionTableWidget.php index f9354669..605bc703 100644 --- a/inc/Smartling/WP/Table/SubmissionTableWidget.php +++ b/inc/Smartling/WP/Table/SubmissionTableWidget.php @@ -135,6 +135,34 @@ public function column_cb($item): string ); } + /** + * Wraps an already HTML-escaped source title in a link to the source content's WP edit + * screen. Falls back to plain text when no source edit URL could be built (e.g. an + * unsupported content type). + */ + public static function buildSourceTitleCell(string $escapedTitle, string $sourceEditUrl): string + { + if ($sourceEditUrl === '') { + return $escapedTitle; + } + + return HtmlTagGeneratorHelper::tag('a', $escapedTitle, ['href' => $sourceEditUrl]); + } + + /** + * Wraps the target blog label in a link to the target content's WP edit screen. Falls + * back to plain text when no target edit URL could be built (e.g. translation not yet + * applied, or an unsupported content type). + */ + public static function buildTargetLocaleCell(string $blogLabel, string $targetEditUrl): string + { + if ($targetEditUrl === '') { + return $blogLabel; + } + + return HtmlTagGeneratorHelper::tag('a', $blogLabel, ['href' => $targetEditUrl]); + } + public function get_columns(): array { $columns = $this->submissionManager->getColumnsLabels(); @@ -398,7 +426,10 @@ public function prepare_items(): void $fileName = htmlentities($row[SubmissionEntity::FIELD_FILE_URI]); $row[SubmissionEntity::FIELD_FILE_URI] = $fileName; - $row[SubmissionEntity::FIELD_SOURCE_TITLE] = htmlentities($row[SubmissionEntity::FIELD_SOURCE_TITLE]); + $row[SubmissionEntity::FIELD_SOURCE_TITLE] = static::buildSourceTitleCell( + htmlentities($row[SubmissionEntity::FIELD_SOURCE_TITLE]), + 0 !== $element->getSourceId() ? WordpressContentTypeHelper::getSourceEditUrl($element) : '' + ); $row[SubmissionEntity::FIELD_CONTENT_TYPE] = WordpressContentTypeHelper::getLocalizedContentType($row[SubmissionEntity::FIELD_CONTENT_TYPE]); $row[SubmissionEntity::FIELD_SUBMISSION_DATE] = $this->sqlToReadableDate($row[SubmissionEntity::FIELD_SUBMISSION_DATE]); $row[SubmissionEntity::FIELD_APPLIED_DATE] = $this->sqlToReadableDate($row[SubmissionEntity::FIELD_APPLIED_DATE]); @@ -407,7 +438,10 @@ public function prepare_items(): void } catch (BlogNotFoundException $e) { $blogLabel = "*blog id {$row[SubmissionEntity::FIELD_TARGET_BLOG_ID]} not found*"; } - $row[SubmissionEntity::FIELD_TARGET_LOCALE] = $blogLabel; + $row[SubmissionEntity::FIELD_TARGET_LOCALE] = static::buildTargetLocaleCell( + $blogLabel, + 0 !== $element->getTargetId() ? WordpressContentTypeHelper::getEditUrl($element) : '' + ); $row[SubmissionEntity::VIRTUAL_FIELD_JOB_LINK] = $jobInfo->getJobName() === '' ? '' : "getProjectUid()}/account-jobs/?filename=$fileName\">" . esc_html($jobInfo->getJobName()) . ''; $flagBlockParts = []; diff --git a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php new file mode 100644 index 00000000..0cc1f146 --- /dev/null +++ b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php @@ -0,0 +1,145 @@ +originalManager = Bootstrap::getContainer()->get('content-type-descriptor-manager'); + } + + protected function tearDown(): void + { + Bootstrap::getContainer()->set('content-type-descriptor-manager', $this->originalManager); + parent::tearDown(); + } + + private function registerContentTypeHandler(?string $baseType): void + { + $manager = $this->createMock(ContentTypeManager::class); + + if ($baseType === null) { + // A handler that implements the interface but not ContentTypeAbstract, + // matching the "unknown/unregistered content type" branch in production code. + $manager->method('getHandler')->willReturn($this->createMock(ContentTypeInterface::class)); + } else { + $handler = $this->createMock(ContentTypeAbstract::class); + $handler->method('getBaseType')->willReturn($baseType); + $manager->method('getHandler')->willReturn($handler); + } + + Bootstrap::getContainer()->set('content-type-descriptor-manager', $manager); + } + + public function testGetSourceEditUrlBuildsPostEditLink(): void + { + $this->registerContentTypeHandler('post'); + + $submission = (new SubmissionEntity()) + ->setContentType('post') + ->setSourceBlogId(5) + ->setSourceId(42); + + $this->assertSame( + 'https://blog-5.test/post.php?post=42&action=edit', + WordpressContentTypeHelper::getSourceEditUrl($submission) + ); + } + + public function testGetSourceEditUrlBuildsAttachmentEditLinkUsingPostPhp(): void + { + // Attachments share the 'post' base type, so they must resolve through the + // same post.php edit screen as regular posts, not upload.php. + $this->registerContentTypeHandler('post'); + + $submission = (new SubmissionEntity()) + ->setContentType('attachment') + ->setSourceBlogId(5) + ->setSourceId(99); + + $this->assertSame( + 'https://blog-5.test/post.php?post=99&action=edit', + WordpressContentTypeHelper::getSourceEditUrl($submission) + ); + } + + public function testGetSourceEditUrlBuildsTaxonomyEditLink(): void + { + $this->registerContentTypeHandler('taxonomy'); + + $submission = (new SubmissionEntity()) + ->setContentType('category') + ->setSourceBlogId(7) + ->setSourceId(13); + + $this->assertSame( + 'https://blog-7.test/term.php?taxonomy=category&tag_ID=13', + WordpressContentTypeHelper::getSourceEditUrl($submission) + ); + } + + public function testGetSourceEditUrlReturnsEmptyStringForUnsupportedBaseType(): void + { + $this->registerContentTypeHandler('virtual'); + + $submission = (new SubmissionEntity()) + ->setContentType('menu') + ->setSourceBlogId(1) + ->setSourceId(1); + + $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission)); + } + + public function testGetSourceEditUrlReturnsEmptyStringForUnknownContentType(): void + { + $this->registerContentTypeHandler(null); + + $submission = (new SubmissionEntity()) + ->setContentType('does-not-exist') + ->setSourceBlogId(1) + ->setSourceId(1); + + $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission)); + } + + public function testGetEditUrlStillBuildsTargetEditLink(): void + { + // Regression check: refactoring getEditUrl() to share code with + // getSourceEditUrl() must not change its existing target-link behavior. + $this->registerContentTypeHandler('post'); + + $submission = (new SubmissionEntity()) + ->setContentType('post') + ->setTargetBlogId(9) + ->setTargetId(101); + + $this->assertSame( + 'https://blog-9.test/post.php?post=101&action=edit', + WordpressContentTypeHelper::getEditUrl($submission) + ); + } + } +} diff --git a/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php b/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php index 67cc2ced..6503d2c7 100644 --- a/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php +++ b/tests/Smartling/WP/Table/SubmissionTableWidgetTest.php @@ -113,6 +113,48 @@ public function testProcessBulkActionIgnoresNonArraySubmissionPayload(): void $x->processBulkAction(); } + /** + * The Title column must link to the source content's WP edit screen when a source + * edit URL is available. + */ + public function testBuildSourceTitleCellWrapsTitleInLinkWhenUrlAvailable(): void + { + $this->assertSame( + 'My Title', + SubmissionTableWidget::buildSourceTitleCell('My Title', 'https://example.com/wp-admin/post.php?post=1&action=edit') + ); + } + + /** + * With no source edit URL (e.g. unsupported content type), the Title column must + * fall back to plain text instead of rendering a dead/empty link. + */ + public function testBuildSourceTitleCellReturnsPlainTextWhenUrlMissing(): void + { + $this->assertSame('My Title', SubmissionTableWidget::buildSourceTitleCell('My Title', '')); + } + + /** + * The Locale column must link to the target content's WP edit screen when a target + * edit URL is available (e.g. translation already applied). + */ + public function testBuildTargetLocaleCellWrapsLabelInLinkWhenUrlAvailable(): void + { + $this->assertSame( + 'German', + SubmissionTableWidget::buildTargetLocaleCell('German', 'https://de.example.com/wp-admin/post.php?post=2&action=edit') + ); + } + + /** + * With no target edit URL (e.g. translation not yet applied, target_id === 0), the + * Locale column must fall back to plain text instead of rendering a dead link. + */ + public function testBuildTargetLocaleCellReturnsPlainTextWhenUrlMissing(): void + { + $this->assertSame('German', SubmissionTableWidget::buildTargetLocaleCell('German', '')); + } + private function buildWidget( ApiWrapperInterface $apiWrapper, SettingsManager $settingsManager, From 3bfbf0646458ee8126980a023686454e9b7f8222 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 18:41:31 +0200 Subject: [PATCH 05/10] unstable release (WP-1016) --- composer.json | 2 +- readme.txt | 4 ++++ smartling-connector.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2467c479..db666a05 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "smartling/wordpress-connector", "license": "GPL-2.0-or-later", - "version": "5.7.0", + "version": "5.7.1", "description": "", "type": "wordpress-plugin", "repositories": [ diff --git a/readme.txt b/readme.txt index 87132a16..6a269367 100755 --- a/readme.txt +++ b/readme.txt @@ -62,6 +62,10 @@ Additional information on the Smartling Connector for WordPress can be found [he 3. Track translation status within WordPress from the Submissions Board. View overall progress of submitted translation requests as well as resend updated content. == Changelog == += 5.7.1 = +* Added source and target content links to the Translation Progress screen +* Fixed possible misconfiguration where target locales were being saved as a duplicate of a newly changed source locale + = 5.7.0 = * Reworked upload queue, added a live-refreshing upload queue count with visual feedback on change diff --git a/smartling-connector.php b/smartling-connector.php index bddaadd5..5afd7487 100755 --- a/smartling-connector.php +++ b/smartling-connector.php @@ -11,7 +11,7 @@ * Plugin Name: Smartling Connector * Plugin URI: https://www.smartling.com/products/automate/integrations/wordpress/ * Description: Integrate your WordPress site with Smartling to upload your content and download translations. - * Version: 5.7.0 + * Version: 5.7.1 * Author: Smartling * Author URI: https://www.smartling.com * License: GPL-2.0+ From 0c69bf946fdcf7dab4c2e49763f94276a67c934a Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 22:07:58 +0200 Subject: [PATCH 06/10] fix fatal error on Submissions Board for historical unregistered content types (WP-1016) buildEditUrl() called ContentTypeManager::getHandler($contentType) with no exception handling. getHandler() throws SmartlingInvalidFactoryArgumentException for any content type that isn't currently registered as a descriptor - e.g. a historical submission row whose custom-post-type integration has since been removed or deactivated (observed in production for 'sovos_product'). SubmissionTableWidget::prepare_items() calls getEditUrl()/getSourceEditUrl() for every row on the Submissions Board regardless of content type, so an unregistered historical content type crashed the entire page with an uncaught fatal error. buildEditUrl() now catches the exception, logs a warning (including the submission id for debugging), and returns '' - the same "no link available" fallback already used for unsupported/unknown content types, matching the graceful-degradation pattern already used by getLocalizedContentType() in this same class. Reproduced with a failing test first (mocking getHandler() to throw the same exception/message seen in production), then fixed. Full unit suite (627 tests) passes. --- .../Helpers/WordpressContentTypeHelper.php | 18 ++++++-- .../WordpressContentTypeHelperTest.php | 41 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/inc/Smartling/Helpers/WordpressContentTypeHelper.php b/inc/Smartling/Helpers/WordpressContentTypeHelper.php index 2e4f7613..513dcada 100644 --- a/inc/Smartling/Helpers/WordpressContentTypeHelper.php +++ b/inc/Smartling/Helpers/WordpressContentTypeHelper.php @@ -159,10 +159,20 @@ public static function getSourceEditUrl(SubmissionEntity $submission): string private static function buildEditUrl(SubmissionEntity $submission, int $blogId, int $contentId): string { - /** - * @var ContentTypeAbstract $ctHandler - */ - $ctHandler = static::getContentTypeManager()->getHandler($submission->getContentType()); + try { + /** + * @var ContentTypeAbstract $ctHandler + */ + $ctHandler = static::getContentTypeManager()->getHandler($submission->getContentType()); + } catch (\Exception $e) { + // getHandler() throws for a content type that isn't currently registered as a + // descriptor - e.g. a historical submission whose custom-post-type integration + // has since been removed or deactivated. That must degrade to "no link", not + // crash the whole page (this is rendered for every row on the Submissions Board). + Bootstrap::getLogger()->warning(sprintf('%s (submissionId=%d)', $e->getMessage(), $submission->getId())); + + return ''; + } if ($ctHandler instanceof ContentTypeAbstract) { $tail = ''; diff --git a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php index 0cc1f146..ee7b96b4 100644 --- a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php +++ b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php @@ -18,6 +18,7 @@ function get_admin_url($blogId = null, $path = '') use Smartling\ContentTypes\ContentTypeAbstract; use Smartling\ContentTypes\ContentTypeInterface; use Smartling\ContentTypes\ContentTypeManager; + use Smartling\Exception\SmartlingInvalidFactoryArgumentException; use Smartling\Helpers\WordpressContentTypeHelper; use Smartling\Submissions\SubmissionEntity; @@ -54,6 +55,46 @@ private function registerContentTypeHandler(?string $baseType): void Bootstrap::getContainer()->set('content-type-descriptor-manager', $manager); } + /** + * ContentTypeManager::getHandler() throws (rather than returning some sentinel + * value) when the requested content type was never registered as a descriptor - + * e.g. a historical submission row whose custom-post-type integration has since + * been removed/deactivated. buildEditUrl() must not let that exception escape. + */ + private function registerThrowingContentTypeManager(): void + { + $manager = $this->createMock(ContentTypeManager::class); + $manager->method('getHandler')->willThrowException( + new SmartlingInvalidFactoryArgumentException("Requested descriptor for 'sovos_product' that doesn't exists.") + ); + + Bootstrap::getContainer()->set('content-type-descriptor-manager', $manager); + } + + public function testGetSourceEditUrlReturnsEmptyStringWhenContentTypeManagerThrows(): void + { + $this->registerThrowingContentTypeManager(); + + $submission = (new SubmissionEntity()) + ->setContentType('sovos_product') + ->setSourceBlogId(1) + ->setSourceId(1); + + $this->assertSame('', WordpressContentTypeHelper::getSourceEditUrl($submission)); + } + + public function testGetEditUrlReturnsEmptyStringWhenContentTypeManagerThrows(): void + { + $this->registerThrowingContentTypeManager(); + + $submission = (new SubmissionEntity()) + ->setContentType('sovos_product') + ->setTargetBlogId(1) + ->setTargetId(1); + + $this->assertSame('', WordpressContentTypeHelper::getEditUrl($submission)); + } + public function testGetSourceEditUrlBuildsPostEditLink(): void { $this->registerContentTypeHandler('post'); From 0bdf96d16db83ec391473f9ebfbcadcbf6a94618 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Mon, 7 Sep 2026 22:08:25 +0200 Subject: [PATCH 07/10] unstable release (WP-1016) --- composer.json | 2 +- readme.txt | 2 +- smartling-connector.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index db666a05..c60b3fba 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "smartling/wordpress-connector", "license": "GPL-2.0-or-later", - "version": "5.7.1", + "version": "5.7.2", "description": "", "type": "wordpress-plugin", "repositories": [ diff --git a/readme.txt b/readme.txt index 6a269367..516fe2ce 100755 --- a/readme.txt +++ b/readme.txt @@ -62,7 +62,7 @@ Additional information on the Smartling Connector for WordPress can be found [he 3. Track translation status within WordPress from the Submissions Board. View overall progress of submitted translation requests as well as resend updated content. == Changelog == -= 5.7.1 = += 5.7.2 = * Added source and target content links to the Translation Progress screen * Fixed possible misconfiguration where target locales were being saved as a duplicate of a newly changed source locale diff --git a/smartling-connector.php b/smartling-connector.php index 5afd7487..65bd5b7e 100755 --- a/smartling-connector.php +++ b/smartling-connector.php @@ -11,7 +11,7 @@ * Plugin Name: Smartling Connector * Plugin URI: https://www.smartling.com/products/automate/integrations/wordpress/ * Description: Integrate your WordPress site with Smartling to upload your content and download translations. - * Version: 5.7.1 + * Version: 5.7.2 * Author: Smartling * Author URI: https://www.smartling.com * License: GPL-2.0+ From 620ceaf3972affc9c4bcc56bf42c321b954faf9f Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 9 Sep 2026 11:24:39 +0200 Subject: [PATCH 08/10] change version, rename function (WP-1016) --- composer.json | 2 +- .../Helpers/WordpressContentTypeHelper.php | 39 ++++++------------- .../ConfigurationProfileFormController.php | 8 ++-- .../WP/Table/SubmissionTableWidget.php | 12 +----- .../WP/View/ConfigurationProfileForm.php | 3 -- .../WP/View/post-based-content-type.php | 2 +- .../WP/View/taxonomy-based-content-type.php | 2 +- js/configuration-profile-form.js | 4 -- readme.txt | 5 +-- smartling-connector.php | 2 +- .../WordpressContentTypeHelperTest.php | 4 +- 11 files changed, 24 insertions(+), 59 deletions(-) diff --git a/composer.json b/composer.json index c60b3fba..0469cf58 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "smartling/wordpress-connector", "license": "GPL-2.0-or-later", - "version": "5.7.2", + "version": "5.7.3", "description": "", "type": "wordpress-plugin", "repositories": [ diff --git a/inc/Smartling/Helpers/WordpressContentTypeHelper.php b/inc/Smartling/Helpers/WordpressContentTypeHelper.php index 513dcada..4b3838b8 100644 --- a/inc/Smartling/Helpers/WordpressContentTypeHelper.php +++ b/inc/Smartling/Helpers/WordpressContentTypeHelper.php @@ -9,10 +9,6 @@ use Smartling\Exception\SmartlingDirectRunRuntimeException; use Smartling\Submissions\SubmissionEntity; -/** - * Class WordpressContentTypeHelper - * @package Smartling\Helpers - */ class WordpressContentTypeHelper { /** @@ -144,14 +140,11 @@ public static function getBaseTypeByContentType($contentType) return $ctHandler->getBaseType(); } - public static function getEditUrl(SubmissionEntity $submission) + public static function getTargetEditUrl(SubmissionEntity $submission) { return static::buildEditUrl($submission, $submission->getTargetBlogId(), $submission->getTargetId()); } - /** - * Same as getEditUrl(), but builds a link to the source content instead of the target. - */ public static function getSourceEditUrl(SubmissionEntity $submission): string { return static::buildEditUrl($submission, $submission->getSourceBlogId(), $submission->getSourceId()); @@ -160,22 +153,14 @@ public static function getSourceEditUrl(SubmissionEntity $submission): string private static function buildEditUrl(SubmissionEntity $submission, int $blogId, int $contentId): string { try { - /** - * @var ContentTypeAbstract $ctHandler - */ $ctHandler = static::getContentTypeManager()->getHandler($submission->getContentType()); } catch (\Exception $e) { - // getHandler() throws for a content type that isn't currently registered as a - // descriptor - e.g. a historical submission whose custom-post-type integration - // has since been removed or deactivated. That must degrade to "no link", not - // crash the whole page (this is rendered for every row on the Submissions Board). Bootstrap::getLogger()->warning(sprintf('%s (submissionId=%d)', $e->getMessage(), $submission->getId())); return ''; } if ($ctHandler instanceof ContentTypeAbstract) { - $tail = ''; switch ($ctHandler->getBaseType()) { case 'post': $tail = vsprintf('/post.php?post=%s&action=edit', [$contentId]); @@ -188,18 +173,18 @@ private static function buildEditUrl(SubmissionEntity $submission, int $blogId, } return get_admin_url($blogId, $tail); - } else { - Bootstrap::getLogger()->warning( - vsprintf( - 'Requested edit URI for unknown content-type \'%s\'', - [ - $submission->getContentType(), - ] - ) - ); - - return ''; } + Bootstrap::getLogger()->warning( + vsprintf( + 'Requested edit URI for unknown content-type \'%s\'', + [ + $submission->getContentType(), + ] + ) + ); + + return ''; + } } \ No newline at end of file diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index 5cc463e2..b943691f 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -238,8 +238,6 @@ public function save(): void foreach ($settings['targetLocales'] as $blogId => $settings) { if ((int)$blogId === $sourceBlogId) { - // Never persist the source locale as a target locale, even if a stale - // form submission still includes it after the source locale was changed. continue; } try { @@ -276,8 +274,8 @@ protected function renderLocales( string $displayName, int $blogId, string $smartlingName, - bool $enabled, - bool $disabled = false, + bool $checked, + bool $disabled, ): string { $parts = []; @@ -287,7 +285,7 @@ protected function renderLocales( 'name' => sprintf('smartling_settings[targetLocales][%s][enabled]', $blogId), ]; - if (true === $enabled) { + if (true === $checked) { $checkboxProperties['checked'] = 'checked'; } diff --git a/inc/Smartling/WP/Table/SubmissionTableWidget.php b/inc/Smartling/WP/Table/SubmissionTableWidget.php index 605bc703..47749690 100644 --- a/inc/Smartling/WP/Table/SubmissionTableWidget.php +++ b/inc/Smartling/WP/Table/SubmissionTableWidget.php @@ -135,11 +135,6 @@ public function column_cb($item): string ); } - /** - * Wraps an already HTML-escaped source title in a link to the source content's WP edit - * screen. Falls back to plain text when no source edit URL could be built (e.g. an - * unsupported content type). - */ public static function buildSourceTitleCell(string $escapedTitle, string $sourceEditUrl): string { if ($sourceEditUrl === '') { @@ -149,11 +144,6 @@ public static function buildSourceTitleCell(string $escapedTitle, string $source return HtmlTagGeneratorHelper::tag('a', $escapedTitle, ['href' => $sourceEditUrl]); } - /** - * Wraps the target blog label in a link to the target content's WP edit screen. Falls - * back to plain text when no target edit URL could be built (e.g. translation not yet - * applied, or an unsupported content type). - */ public static function buildTargetLocaleCell(string $blogLabel, string $targetEditUrl): string { if ($targetEditUrl === '') { @@ -440,7 +430,7 @@ public function prepare_items(): void } $row[SubmissionEntity::FIELD_TARGET_LOCALE] = static::buildTargetLocaleCell( $blogLabel, - 0 !== $element->getTargetId() ? WordpressContentTypeHelper::getEditUrl($element) : '' + 0 !== $element->getTargetId() ? WordpressContentTypeHelper::getTargetEditUrl($element) : '' ); $row[SubmissionEntity::VIRTUAL_FIELD_JOB_LINK] = $jobInfo->getJobName() === '' ? '' : "getProjectUid()}/account-jobs/?filename=$fileName\">" . esc_html($jobInfo->getJobName()) . ''; diff --git a/inc/Smartling/WP/View/ConfigurationProfileForm.php b/inc/Smartling/WP/View/ConfigurationProfileForm.php index 60059aed..ae1ac4e7 100644 --- a/inc/Smartling/WP/View/ConfigurationProfileForm.php +++ b/inc/Smartling/WP/View/ConfigurationProfileForm.php @@ -486,9 +486,6 @@ } } - // The current source locale is never a selectable target: its row - // stays in the DOM (hidden/disabled) so JS can reveal it again if the - // source locale is changed to a different blog before the form is saved. $isSourceLocaleRow = $blogId === $currentSourceBlogId; ?> diff --git a/inc/Smartling/WP/View/post-based-content-type.php b/inc/Smartling/WP/View/post-based-content-type.php index 319271cf..d426a8dd 100644 --- a/inc/Smartling/WP/View/post-based-content-type.php +++ b/inc/Smartling/WP/View/post-based-content-type.php @@ -85,7 +85,7 @@ $enabled = !(1 === $item->getIsCloned() || 1 === $item->hasLocks()); if (0 !== (int) $item->getTargetId()) { - $editUrl = WordpressContentTypeHelper::getEditUrl($item); + $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item); } /** diff --git a/inc/Smartling/WP/View/taxonomy-based-content-type.php b/inc/Smartling/WP/View/taxonomy-based-content-type.php index 081800c5..98060dfc 100644 --- a/inc/Smartling/WP/View/taxonomy-based-content-type.php +++ b/inc/Smartling/WP/View/taxonomy-based-content-type.php @@ -97,7 +97,7 @@ $percent = $item->getCompletionPercentage(); $status = $item->getStatusColor(); $statusFlags = $item->getStatusFlags(); - $editUrl = WordpressContentTypeHelper::getEditUrl($item); + $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item); $enabled = !(1 === $item->getIsCloned() || 1 === $item->getIsLocked()); break; } diff --git a/js/configuration-profile-form.js b/js/configuration-profile-form.js index 75521277..6305cd5e 100644 --- a/js/configuration-profile-form.js +++ b/js/configuration-profile-form.js @@ -12,10 +12,6 @@ $('#smartling-configuration-profile-form').validate() } - // Keep the target-locale list in sync with the chosen source locale: the row matching - // the current source is hidden and disabled (so it can never be submitted as a target), - // while any previously selected source becomes available again as soon as it stops - // being the source - all without a page reload. const syncTargetLocaleRows = function (sourceLocaleSelect) { const sourceBlogId = String(sourceLocaleSelect.value); $('#target-locale-block tr.target-locale-row').each(function () { diff --git a/readme.txt b/readme.txt index 516fe2ce..4f8dd5e9 100755 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: translation, localization, multilingual, internationalization, smartling Requires at least: 5.5 Tested up to: 7.0 Requires PHP: 8.0 -Stable tag: 5.7.0 +Stable tag: 5.7.3 License: GPLv2 or later Translate content in WordPress quickly and seamlessly with Smartling, the industry-leading Translation Management System. @@ -62,8 +62,7 @@ Additional information on the Smartling Connector for WordPress can be found [he 3. Track translation status within WordPress from the Submissions Board. View overall progress of submitted translation requests as well as resend updated content. == Changelog == -= 5.7.2 = -* Added source and target content links to the Translation Progress screen += 5.7.3 = * Fixed possible misconfiguration where target locales were being saved as a duplicate of a newly changed source locale = 5.7.0 = diff --git a/smartling-connector.php b/smartling-connector.php index 65bd5b7e..81d70c2f 100755 --- a/smartling-connector.php +++ b/smartling-connector.php @@ -11,7 +11,7 @@ * Plugin Name: Smartling Connector * Plugin URI: https://www.smartling.com/products/automate/integrations/wordpress/ * Description: Integrate your WordPress site with Smartling to upload your content and download translations. - * Version: 5.7.2 + * Version: 5.7.3 * Author: Smartling * Author URI: https://www.smartling.com * License: GPL-2.0+ diff --git a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php index ee7b96b4..8ae74483 100644 --- a/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php +++ b/tests/Smartling/Helpers/WordpressContentTypeHelperTest.php @@ -92,7 +92,7 @@ public function testGetEditUrlReturnsEmptyStringWhenContentTypeManagerThrows(): ->setTargetBlogId(1) ->setTargetId(1); - $this->assertSame('', WordpressContentTypeHelper::getEditUrl($submission)); + $this->assertSame('', WordpressContentTypeHelper::getTargetEditUrl($submission)); } public function testGetSourceEditUrlBuildsPostEditLink(): void @@ -179,7 +179,7 @@ public function testGetEditUrlStillBuildsTargetEditLink(): void $this->assertSame( 'https://blog-9.test/post.php?post=101&action=edit', - WordpressContentTypeHelper::getEditUrl($submission) + WordpressContentTypeHelper::getTargetEditUrl($submission) ); } } From 7b67f0e3e49d33c63a4fdec98b206086ed67a6bb Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Wed, 9 Sep 2026 11:27:28 +0200 Subject: [PATCH 09/10] use sprintf instead of vsprintf (WP-1016) --- inc/Smartling/Helpers/WordpressContentTypeHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/Smartling/Helpers/WordpressContentTypeHelper.php b/inc/Smartling/Helpers/WordpressContentTypeHelper.php index 4b3838b8..5f2ee768 100644 --- a/inc/Smartling/Helpers/WordpressContentTypeHelper.php +++ b/inc/Smartling/Helpers/WordpressContentTypeHelper.php @@ -163,7 +163,7 @@ private static function buildEditUrl(SubmissionEntity $submission, int $blogId, if ($ctHandler instanceof ContentTypeAbstract) { switch ($ctHandler->getBaseType()) { case 'post': - $tail = vsprintf('/post.php?post=%s&action=edit', [$contentId]); + $tail = sprintf('/post.php?post=%s&action=edit', $contentId); break; case 'taxonomy': $tail = sprintf('/term.php?taxonomy=%s&tag_ID=%s', $submission->getContentType(), $contentId); @@ -187,4 +187,4 @@ private static function buildEditUrl(SubmissionEntity $submission, int $blogId, return ''; } -} \ No newline at end of file +} From 5e03bcc3cbd839ee5596b2e0ff78e64414d311d7 Mon Sep 17 00:00:00 2001 From: Vitalii Solovei Date: Fri, 11 Sep 2026 14:36:09 +0200 Subject: [PATCH 10/10] address PR #631 review comments (WP-1016) - guard getTargetEditUrl() call with getTargetId() check in taxonomy-based-content-type.php, matching post-based-content-type.php - htmlentities() the target blog label in SubmissionTableWidget, matching the source title cell's escaping - rename shadowed loop variable in ConfigurationProfileFormController::save() targetLocales loop - add changelog entries for the edit-links and fatal-error fixes Co-Authored-By: Claude Sonnet 5 --- .../WP/Controller/ConfigurationProfileFormController.php | 8 ++++---- inc/Smartling/WP/Table/SubmissionTableWidget.php | 2 +- inc/Smartling/WP/View/taxonomy-based-content-type.php | 4 +++- readme.txt | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php index b943691f..511755a7 100644 --- a/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php +++ b/inc/Smartling/WP/Controller/ConfigurationProfileFormController.php @@ -236,7 +236,7 @@ public function save(): void if (array_key_exists('targetLocales', $settings)) { $locales = []; - foreach ($settings['targetLocales'] as $blogId => $settings) { + foreach ($settings['targetLocales'] as $blogId => $targetLocaleSettings) { if ((int)$blogId === $sourceBlogId) { continue; } @@ -244,9 +244,9 @@ public function save(): void $tLocale = new TargetLocale(); $tLocale->setBlogId($blogId); $tLocale->setLabel($this->siteHelper->getBlogLabelById($this->localizationPluginProxy, $blogId)); - $enabled = 'on' === $settings['enabled']; - $tLocale->setEnabled(array_key_exists('enabled', $settings) && $enabled); - $smartlingLocale = array_key_exists('target', $settings) ? $settings['target'] : -1; + $enabled = 'on' === $targetLocaleSettings['enabled']; + $tLocale->setEnabled(array_key_exists('enabled', $targetLocaleSettings) && $enabled); + $smartlingLocale = array_key_exists('target', $targetLocaleSettings) ? $targetLocaleSettings['target'] : -1; $tLocale->setSmartlingLocale($smartlingLocale); if ($smartlingLocale !== -1 && $enabled) { $usedTargetLocales[] = $smartlingLocale; diff --git a/inc/Smartling/WP/Table/SubmissionTableWidget.php b/inc/Smartling/WP/Table/SubmissionTableWidget.php index 47749690..a8175b51 100644 --- a/inc/Smartling/WP/Table/SubmissionTableWidget.php +++ b/inc/Smartling/WP/Table/SubmissionTableWidget.php @@ -429,7 +429,7 @@ public function prepare_items(): void $blogLabel = "*blog id {$row[SubmissionEntity::FIELD_TARGET_BLOG_ID]} not found*"; } $row[SubmissionEntity::FIELD_TARGET_LOCALE] = static::buildTargetLocaleCell( - $blogLabel, + htmlentities($blogLabel), 0 !== $element->getTargetId() ? WordpressContentTypeHelper::getTargetEditUrl($element) : '' ); $row[SubmissionEntity::VIRTUAL_FIELD_JOB_LINK] = $jobInfo->getJobName() === '' ? '' : "getProjectUid()}/account-jobs/?filename=$fileName\">" . esc_html($jobInfo->getJobName()) . ''; diff --git a/inc/Smartling/WP/View/taxonomy-based-content-type.php b/inc/Smartling/WP/View/taxonomy-based-content-type.php index 98060dfc..91eead4b 100644 --- a/inc/Smartling/WP/View/taxonomy-based-content-type.php +++ b/inc/Smartling/WP/View/taxonomy-based-content-type.php @@ -97,7 +97,9 @@ $percent = $item->getCompletionPercentage(); $status = $item->getStatusColor(); $statusFlags = $item->getStatusFlags(); - $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item); + if (0 !== $item->getTargetId()) { + $editUrl = WordpressContentTypeHelper::getTargetEditUrl($item); + } $enabled = !(1 === $item->getIsCloned() || 1 === $item->getIsLocked()); break; } diff --git a/readme.txt b/readme.txt index 4f8dd5e9..7975f286 100755 --- a/readme.txt +++ b/readme.txt @@ -64,6 +64,7 @@ Additional information on the Smartling Connector for WordPress can be found [he == Changelog == = 5.7.3 = * Fixed possible misconfiguration where target locales were being saved as a duplicate of a newly changed source locale +* Added links to the source and target content (if available) on the Translation Progress screen = 5.7.0 = * Reworked upload queue, added a live-refreshing upload queue count with visual feedback on change