From 9048f45f537f402067307cff1e37ef81389cc034 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Fri, 5 Aug 2022 03:31:12 -0700 Subject: [PATCH] APIv4 - Implement option` $translationMode` via TranslationGetWrapper --- CRM/Core/BAO/TranslateGetWrapper.php | 45 +++++++++ CRM/Core/BAO/Translation.php | 98 +++++++++++++++++++ .../CRM/Core/BAO/MessageTemplateTest.php | 85 ++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 CRM/Core/BAO/TranslateGetWrapper.php diff --git a/CRM/Core/BAO/TranslateGetWrapper.php b/CRM/Core/BAO/TranslateGetWrapper.php new file mode 100644 index 0000000000..67214b2426 --- /dev/null +++ b/CRM/Core/BAO/TranslateGetWrapper.php @@ -0,0 +1,45 @@ +fields = $translated['fields']; + $this->translatedLanguage = $translated['language']; + } + + /** + * @inheritdoc + */ + public function fromApiInput($apiRequest) { + return $apiRequest; + } + + /** + * @inheritdoc + */ + public function toApiOutput($apiRequest, $result) { + foreach ($result as &$value) { + if (!isset($value['id'], $this->fields[$value['id']])) { + continue; + } + $toSet = array_intersect_key($this->fields[$value['id']], $value); + $value = array_merge($value, $toSet); + $value['actual_language'] = $this->translatedLanguage; + } + return $result; + } + +} diff --git a/CRM/Core/BAO/Translation.php b/CRM/Core/BAO/Translation.php index c7ab686cce..f508281c9d 100644 --- a/CRM/Core/BAO/Translation.php +++ b/CRM/Core/BAO/Translation.php @@ -9,6 +9,9 @@ +--------------------------------------------------------------------+ */ +use Civi\Api4\Generic\AbstractAction; +use Civi\Api4\Translation; + /** * * @package CRM @@ -144,4 +147,99 @@ class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements \Civi } } + /** + * Callback for hook_civicrm_post(). + * + * Flush out cached values. + * + * @param \Civi\Core\Event\PostEvent $event + */ + public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event): void { + unset(Civi::$statics[__CLASS__]); + } + + /** + * Implements hook_civicrm_apiWrappers(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_apiWrappers/ + * + * @see \CRM_Utils_Hook::apiWrappers() + * @throws \CRM_Core_Exception + */ + public static function hook_civicrm_apiWrappers(&$wrappers, $apiRequest): void { + if (!($apiRequest instanceof \Civi\Api4\Generic\DAOGetAction)) { + return; + } + + $mode = $apiRequest->getTranslationMode(); + if ($mode !== 'fuzzy') { + return; + } + + $communicationLanguage = \Civi\Core\Locale::detect()->nominal; + if ($communicationLanguage === Civi::settings()->get('lcMessages')) { + return; + } + + if ($apiRequest['action'] === 'get') { + if (!isset(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$communicationLanguage])) { + $translated = self::getTranslatedFieldsForRequest($apiRequest); + // @todo - once https://github.com/civicrm/civicrm-core/pull/24063 is merged + // this could set any defined translation fields that don't have a translation + // for one or more fields in the set to '' - ie 'if any are defined for + // an entity/language then all must be' - it seems like being strict on this + // now will make it easier later.... + //n No, this doesn't work - 'fields' array doesn't look like that. + //n if (!empty($translated['fields']['msg_html']) && !isset($translated['fields']['msg_text'])) { + //n $translated['fields']['msg_text'] = ''; + //n } + foreach ($translated['fields'] ?? [] as $field) { + \Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$communicationLanguage]['fields'][$field['entity_id']][$field['entity_field']] = $field['string']; + \Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$communicationLanguage]['language'] = $translated['language']; + } + } + if (!empty(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$communicationLanguage])) { + $wrappers[] = new CRM_Core_BAO_TranslateGetWrapper(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$communicationLanguage]); + } + } + } + + /** + * @param \Civi\Api4\Generic\AbstractAction $apiRequest + * @return array translated fields. + * + * @throws \CRM_Core_Exception + */ + protected static function getTranslatedFieldsForRequest(AbstractAction $apiRequest): array { + $userLocale = \Civi\Core\Locale::detect(); + + $translations = Translation::get() + ->addWhere('entity_table', '=', CRM_Core_DAO_AllCoreTables::getTableForEntityName($apiRequest['entity'])) + ->setCheckPermissions(FALSE) + ->setSelect(['entity_field', 'entity_id', 'string', 'language']); + if ((substr($userLocale->nominal, '-3', '3') !== '_NO')) { + // Generally we want to check for any translations of the base language + // and prefer, for example, French French over US English for French Canadians. + // Sites that genuinely want to cater to both will add translations for both + // and we work through preferences below. + $translations->addWhere('language', 'LIKE', substr($userLocale->nominal, 0, 2) . '%'); + } + else { + // And here we have ... the Norwegians. They have three main variants which + // share the same country suffix but not language prefix. As with other languages + // any Norwegian is better than no Norwegian and sites that care will do multiple + $translations->addWhere('language', 'LIKE', '%_NO'); + } + $fields = $translations->execute(); + $languages = []; + foreach ($fields as $index => $field) { + $languages[$field['language']][$index] = $field; + } + + $bizLocale = $userLocale->renegotiate(array_keys($languages)); + return $bizLocale + ? ['fields' => $languages[$bizLocale->nominal], 'language' => $bizLocale->nominal] + : []; + } + } diff --git a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php index 7b87397f88..431c471013 100644 --- a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php +++ b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php @@ -3,6 +3,7 @@ use Civi\Api4\Address; use Civi\Api4\Contact; use Civi\Api4\MessageTemplate; +use Civi\Api4\Translation; use Civi\Token\TokenProcessor; /** @@ -201,6 +202,56 @@ class CRM_Core_BAO_MessageTemplateTest extends CiviUnitTestCase { $this->assertStringContainsString('Case ID : 1234', $message); } + public function getTranslationSettings(): array { + $es = []; + $es['fr_FR-full'] = [ + ['partial_locales' => FALSE, 'uiLanguages' => ['en_US', 'fr_FR', 'fr_CA']], + ]; + $es['fr_FR-partial'] = [ + ['partial_locales' => TRUE, 'uiLanguages' => ['en_US']], + ]; + return $es; + } + + /** + * Test that translated strings are rendered for templates where they exist. + * + * @dataProvider getTranslationSettings + * @throws \API_Exception|\CRM_Core_Exception + */ + public function testGetTranslatedTemplate($translationSettings): void { + $cleanup = \CRM_Utils_AutoClean::swapSettings($translationSettings); + + $this->individualCreate(['preferred_language' => 'fr_FR']); + $this->contributionCreate(['contact_id' => $this->ids['Contact']['individual_0']]); + $this->addTranslation(); + + $messageTemplate = MessageTemplate::get() + ->addWhere('is_default', '=', 1) + ->addWhere('workflow_name', 'IN', ['contribution_online_receipt', 'contribution_offline_receipt']) + ->addSelect('id', 'msg_subject', 'msg_html', 'workflow_name') + ->setLanguage('fr_FR') + ->setTranslationMode('fuzzy') + ->execute()->indexBy('workflow_name'); + + $this->assertFrenchTranslationRetrieved($messageTemplate['contribution_online_receipt']); + + $this->assertStringContainsString('{ts}Contribution Receipt{/ts}', $messageTemplate['contribution_offline_receipt']['msg_subject']); + $this->assertStringContainsString('Below you will find a receipt', $messageTemplate['contribution_offline_receipt']['msg_html']); + $this->assertArrayNotHasKey('actual_language', $messageTemplate['contribution_offline_receipt']); + + $messageTemplate = MessageTemplate::get() + ->addWhere('is_default', '=', 1) + ->addWhere('workflow_name', 'IN', ['contribution_online_receipt', 'contribution_offline_receipt']) + ->addSelect('id', 'msg_subject', 'msg_html', 'workflow_name') + ->setLanguage('fr_CA') + ->setTranslationMode('fuzzy') + ->execute()->indexBy('workflow_name'); + + $this->assertFrenchTranslationRetrieved($messageTemplate['contribution_online_receipt']); + + } + /** * Test rendering of domain tokens. * @@ -920,4 +971,38 @@ t_stuff.favourite_emoticon | return $expected; } + /** + * @return mixed + * @throws \API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + private function addTranslation() { + $messageTemplateID = MessageTemplate::get() + ->addWhere('is_default', '=', 1) + ->addWhere('workflow_name', '=', 'contribution_online_receipt') + ->addSelect('id') + ->execute()->first()['id']; + + Translation::save()->setRecords([ + ['entity_field' => 'msg_subject', 'string' => 'Bonjour'], + ['entity_field' => 'msg_html', 'string' => 'Voila!'], + ['entity_field' => 'msg_text', 'string' => '{contribution.total_amount}'], + ])->setDefaults([ + 'entity_table' => 'civicrm_msg_template', + 'entity_id' => $messageTemplateID, + 'status_id:name' => 'active', + 'language' => 'fr_FR', + ])->execute(); + return $messageTemplateID; + } + + /** + * @param $contribution_online_receipt + */ + private function assertFrenchTranslationRetrieved($contribution_online_receipt): void { + $this->assertEquals('Bonjour', $contribution_online_receipt['msg_subject']); + $this->assertEquals('Voila!', $contribution_online_receipt['msg_html']); + $this->assertEquals('fr_FR', $contribution_online_receipt['actual_language']); + } + } -- 2.25.1