Skip to content

Commit

Permalink
APIv4 - Add generic 'setPreferredLanguage', to all APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
eileenmcnaughton committed Aug 1, 2022
1 parent 64964b6 commit 8ba78f3
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
45 changes: 45 additions & 0 deletions CRM/Core/BAO/TranslateGetWrapper.php
@@ -0,0 +1,45 @@
<?php

/**
* Collection of upgrade steps.
*/
class CRM_Core_BAO_TranslateGetWrapper {

protected $fields;
protected $translatedLanguage;

/**
* CRM_Core_BAO_TranslateGetWrapper constructor.
*
* This wrapper replaces values with configured translated values, if any exist.
*
* @param array $translated
*/
public function __construct($translated) {
$this->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;
}

}
121 changes: 121 additions & 0 deletions CRM/Core/BAO/Translation.php
Expand Up @@ -9,6 +9,9 @@
+--------------------------------------------------------------------+
*/

use Civi\Api4\Generic\AbstractAction;
use Civi\Api4\Translation;

/**
*
* @package CRM
Expand Down Expand Up @@ -144,4 +147,122 @@ public static function self_civi_api4_validate(\Civi\Api4\Event\ValidateValuesEv
}
}

/**
* 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 {
// Only implement for apiv4 & not in a circular way.
if ($apiRequest['entity'] === 'Translation'
|| $apiRequest['entity'] === 'Entity'
|| !$apiRequest instanceof AbstractAction
// Only intervene in 'get'. Code handling save type actions left out of scope.
|| $apiRequest['action'] !== 'get'
) {
return;
}

$apiLanguage = $apiRequest->getPreferredLanguage();
if (!$apiLanguage || $apiRequest->getPreferredLanguage() === Civi::settings()->get('lcMessages')) {
return;
}

if ($apiRequest['action'] === 'get') {
if (!isset(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$apiRequest->getPreferredLanguage()])) {
$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....
if (!empty($translated['fields']['msg_html']) && !isset($translated['fields']['msg_text'])) {
$translated['fields']['msg_text'] = '';
}
foreach ($translated['fields'] as $field) {
\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$apiRequest->getPreferredLanguage()]['fields'][$field['entity_id']][$field['entity_field']] = $field['string'];
\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$apiRequest->getPreferredLanguage()]['language'] = $translated['language'];
}
}
if (!empty(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$apiRequest->getPreferredLanguage()])) {
$wrappers[] = new CRM_Core_BAO_TranslateGetWrapper(\Civi::$statics[__CLASS__]['translate_fields'][$apiRequest['entity']][$apiRequest->getPreferredLanguage()]);
}

}
}

/**
* @param \Civi\Api4\Generic\AbstractAction $apiRequest
* @return array translated fields.
*
* @throws \CRM_Core_Exception
*/
protected static function getTranslatedFieldsForRequest(AbstractAction $apiRequest): array {
$translations = Translation::get()
->addWhere('entity_table', '=', CRM_Core_DAO_AllCoreTables::getTableForEntityName($apiRequest['entity']))
->setCheckPermissions(FALSE)
->setSelect(['entity_field', 'entity_id', 'string', 'language']);
if ((substr($apiRequest->getPreferredLanguage(), '-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($apiRequest->getPreferredLanguage(), 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;
}
if (isset($languages[$apiRequest->getPreferredLanguage()])) {
return ['fields' => $languages[$apiRequest->getPreferredLanguage()()], 'language' => $apiRequest->getPreferredLanguage()];
}
if (count($languages) === 1) {
return ['fields' => reset($languages), 'language' => key($languages)];
}
if (count($languages) > 1) {
// In this situation we have multiple language options but no exact match.
// This might be, for example, a case where we have, for example, a US English and
// a British English, but no Kiwi English. In that case the best is arguable
// but I think we all agree that we want to avoid Aussie English here.
$defaultLanguages = [
'de' => 'de_DE',
'en' => 'en_US',
'fr' => 'fr_FR',
'es' => 'es_ES',
'nl' => 'nl_NL',
'pt' => 'pt_PT',
'zh' => 'zh_TW',
];
$defaultLanguage = $defaultLanguages[substr($apiRequest->getPreferredLanguage()(), 0, 2)] ?? NULL;
if (isset($languages[$defaultLanguage])) {
return ['fields' => $languages[$defaultLanguage], 'language' => $defaultLanguage];
}
// We have no way to determine which is best from the available variants.
// If the site wants more control they can translate to the variants.
return ['fields' => reset($languages), 'language' => key($languages)];
}
return [];
}

}
12 changes: 12 additions & 0 deletions Civi/Api4/Generic/AbstractAction.php
Expand Up @@ -32,6 +32,8 @@
* @method bool getDebug()
* @method $this setChain(array $chain)
* @method array getChain()
* @method $this setPreferredLanguage(string|null $language)
* @method string|null getPreferredLanguage()
*/
abstract class AbstractAction implements \ArrayAccess {

Expand All @@ -44,6 +46,16 @@ abstract class AbstractAction implements \ArrayAccess {
*/
protected $version = 4;

/**
* Preferred Language (optional).
*
* If set then listeners such as the Translation subsystem may alter
* the output.
*
* @var string
*/
protected $preferredLanguage;

/**
* Additional api requests - will be called once per result.
*
Expand Down
69 changes: 69 additions & 0 deletions tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php
Expand Up @@ -3,6 +3,7 @@
use Civi\Api4\Address;
use Civi\Api4\Contact;
use Civi\Api4\MessageTemplate;
use Civi\Api4\Translation;
use Civi\Token\TokenProcessor;

/**
Expand Down Expand Up @@ -200,6 +201,40 @@ public function testCaseActivityCopyTemplate():void {
$this->assertStringContainsString('Case ID : 1234', $message);
}

/**
* Test that translated strings are rendered for templates where they exist.
*
* @throws \API_Exception|\CRM_Core_Exception
*/
public function testGetTranslatedTemplate(): void {
$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')
->setPreferredLanguage('fr_FR')
->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')
->setPreferredLanguage('ca_FR')
->execute()->indexBy('workflow_name');

$this->assertFrenchTranslationRetrieved($messageTemplate['contribution_online_receipt']);

}

/**
* Test rendering of domain tokens.
*
Expand Down Expand Up @@ -919,4 +954,38 @@ protected function getExpectedContactOutputNewStyle($id, array $tokenData, strin
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']);
}

}

0 comments on commit 8ba78f3

Please sign in to comment.