Skip to content

Commit

Permalink
Merge pull request #24174 from totten/master-lang
Browse files Browse the repository at this point in the history
Message Templates - Allow rendering & previewing of translated messages
  • Loading branch information
eileenmcnaughton authored Sep 2, 2022
2 parents 22a3e45 + 669dcbf commit d1f11af
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 11 deletions.
15 changes: 10 additions & 5 deletions CRM/Core/BAO/MessageTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,10 @@ protected static function renderTemplateRaw($params) {

self::synchronizeLegacyParameters($params);
$params = array_merge($modelDefaults, $viewDefaults, $envelopeDefaults, $params);

$language = $params['language'] ?? (!empty($params['contactId']) ? Civi\Api4\Contact::get(FALSE)->addWhere('id', '=', $params['contactId'])->addSelect('preferred_language')->execute()->first()['preferred_language'] : NULL);
CRM_Utils_Hook::alterMailParams($params, 'messageTemplate');
$mailContent = self::loadTemplate((string) $params['workflow'], $params['isTest'], $params['messageTemplateID'] ?? NULL, $params['groupName'] ?? '', $params['messageTemplate'], $params['subject'] ?? NULL);
[$mailContent, $translatedLanguage] = self::loadTemplate((string) $params['workflow'], $params['isTest'], $params['messageTemplateID'] ?? NULL, $params['groupName'] ?? '', $params['messageTemplate'], $params['subject'] ?? NULL, $language);
$params['tokenContext']['locale'] = $translatedLanguage ?? $params['language'] ?? NULL;

self::synchronizeLegacyParameters($params);
$rendered = CRM_Core_TokenSmarty::render(CRM_Utils_Array::subset($mailContent, ['text', 'html', 'subject']), $params['tokenContext'], $params['tplParams']);
Expand Down Expand Up @@ -459,18 +460,21 @@ protected static function getWorkflowNameIdMap() {
* If omitted, the record will be loaded from workflowName/messageTemplateID.
* @param string|null $subjectOverride
* This option is the older, wonkier version of $messageTemplate['msg_subject']...
* @param string|null $language
*
* @return array
* @throws \API_Exception
* @throws \CRM_Core_Exception
*/
protected static function loadTemplate(string $workflowName, bool $isTest, int $messageTemplateID = NULL, $groupName = NULL, ?array $messageTemplateOverride = NULL, ?string $subjectOverride = NULL): array {
protected static function loadTemplate(string $workflowName, bool $isTest, int $messageTemplateID = NULL, $groupName = NULL, ?array $messageTemplateOverride = NULL, ?string $subjectOverride = NULL, ?string $language = NULL): array {
$base = ['msg_subject' => NULL, 'msg_text' => NULL, 'msg_html' => NULL, 'pdf_format_id' => NULL];
if (!$workflowName && !$messageTemplateID) {
throw new CRM_Core_Exception(ts("Message template's option value or ID missing."));
}

$apiCall = MessageTemplate::get(FALSE)
->setLanguage($language)
->setTranslationMode('fuzzy')
->addSelect('msg_subject', 'msg_text', 'msg_html', 'pdf_format_id', 'id')
->addWhere('is_default', '=', 1);

Expand All @@ -480,7 +484,8 @@ protected static function loadTemplate(string $workflowName, bool $isTest, int $
else {
$apiCall->addWhere('workflow_name', '=', $workflowName);
}
$messageTemplate = array_merge($base, $apiCall->execute()->first() ?: [], $messageTemplateOverride ?: []);
$result = $apiCall->execute();
$messageTemplate = array_merge($base, $result->first() ?: [], $messageTemplateOverride ?: []);
if (empty($messageTemplate['id']) && empty($messageTemplateOverride)) {
if ($messageTemplateID) {
throw new CRM_Core_Exception(ts('No such message template: id=%1.', [1 => $messageTemplateID]));
Expand Down Expand Up @@ -527,7 +532,7 @@ protected static function loadTemplate(string $workflowName, bool $isTest, int $
$mailContent['subject'] = $subjectOverride;
}

return $mailContent;
return [$mailContent, $messageTemplate['actual_language'] ?? NULL];
}

/**
Expand Down
45 changes: 45 additions & 0 deletions CRM/Core/BAO/TranslateGetWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* Wrapper to swap in translated text.
*/
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;
}

}
103 changes: 101 additions & 2 deletions CRM/Core/BAO/Translation.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
+--------------------------------------------------------------------+
*/

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

/**
*
* @package CRM
* @copyright CiviCRM LLC https://civicrm.org/licensing
*/
class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements \Civi\Core\HookInterface {
class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements HookInterface {

use CRM_Core_DynamicFKAccessTrait;

Expand All @@ -23,7 +27,7 @@ class CRM_Core_BAO_Translation extends CRM_Core_DAO_Translation implements \Civi
*
* @return array[]
*/
public static function getStatuses() {
public static function getStatuses(): array {
return [
['id' => 1, 'name' => 'active', 'label' => ts('Active')],
['id' => 2, 'name' => 'draft', 'label' => ts('Draft')],
Expand Down Expand Up @@ -144,4 +148,99 @@ 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 {
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]
: [];
}

}
3 changes: 1 addition & 2 deletions Civi/Api4/Action/WorkflowMessage/Render.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,12 @@ class Render extends \Civi\Api4\Generic\AbstractAction {

public function _run(\Civi\Api4\Generic\Result $result) {
$this->validateValues();

$r = \CRM_Core_BAO_MessageTemplate::renderTemplate([
'model' => $this->_model,
'messageTemplate' => $this->getMessageTemplate(),
'messageTemplateId' => $this->getMessageTemplateId(),
'language' => $this->getLanguage(),
]);

$result[] = \CRM_Utils_Array::subset($r, ['subject', 'html', 'text']);
}

Expand Down
11 changes: 11 additions & 0 deletions Civi/Api4/Generic/DAOGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
*
* @method $this setHaving(array $clauses)
* @method array getHaving()
* @method $this setTranslationMode(string|null $mode)
* @method string|null getTranslationMode()
*/
class DAOGetAction extends AbstractGetAction {
use Traits\DAOActionTrait;
Expand Down Expand Up @@ -80,6 +82,15 @@ class DAOGetAction extends AbstractGetAction {
*/
protected $having = [];

/**
* Should we automatically overload the result with translated data?
* How do we pick the suitable translation?
*
* @var string|null
* @options fuzzy,strict
*/
protected $translationMode;

/**
* @throws \API_Exception
* @throws \CRM_Core_Exception
Expand Down
2 changes: 1 addition & 1 deletion ext/message_admin/ang/crmMsgadm/Edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@
crmApi4({
examples: ['ExampleData', 'get', {
// FIXME: workflow name
language: $ctrl.lang,
where: [["tags", "CONTAINS", "preview"], ["name", "LIKE", "workflow/" + $ctrl.records.main.workflow_name + "/%"]],
select: ['name', 'title', 'data']
}],
Expand All @@ -279,7 +280,6 @@
format: 'example'
}]
}).then(function(resp) {
console.log('resp',resp);
if ((!resp.examples || resp.examples.length === 0) && resp.adhoc) {
// In the future, if Preview dialog allows editing adhoc examples, then we can show the dialog. But for now, it won't work without explicit examples.
crmUiAlert({
Expand Down
9 changes: 8 additions & 1 deletion ext/message_admin/ang/crmMsgadm/Preview.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
(function(angular, $, _) {

angular.module('crmMsgadm').controller('MsgtpluiPreviewCtrl', function($scope, crmUiHelp, crmStatus, crmApi4, crmUiAlert, $timeout, $q, dialogService) {
angular.module('crmMsgadm').controller('MsgtpluiPreviewCtrl', function($scope, crmUiHelp, crmStatus, crmApi4, crmUiAlert, $timeout, $q, dialogService, $location) {
var ts = $scope.ts = CRM.ts('crmMsgadm');
var hs = $scope.hs = crmUiHelp({file: 'CRM/MessageAdmin/crmMsgadm'}); // See: templates/CRM/MessageAdmin/crmMsgadm.hlp

var $ctrl = this, model = $scope.model;
var args = $location.search();
if (args.lang) {
$ctrl.lang = args.lang;
}

$ctrl.exampleId = parseInt(_.findKey(model.examples, {name: model.exampleName}));
$ctrl.revisionId = parseInt(_.findKey(model.revisions, {name: model.revisionName}));
Expand Down Expand Up @@ -35,6 +39,7 @@
dlgModel.refresh = function(){
return crmApi4('ExampleData', 'get', {
where: [["name", "=", model.examples[$ctrl.exampleId].name]],
language: $ctrl.lang,
select: ['name', 'file', 'title', 'data']
}).then(function(response){
dlgModel.title = ts('Example: %1', {1: response[0].title || response[0].name});
Expand Down Expand Up @@ -64,6 +69,7 @@
function requestStoredExample() {
return crmApi4('ExampleData', 'get', {
where: [["name", "=", model.examples[$ctrl.exampleId].name]],
language: $ctrl.lang,
select: ['data']
}).then(function(response) {
return response[0].data;
Expand All @@ -82,6 +88,7 @@
rendering.then(function(exampleData) {
var filteredData = model.filterData ? model.filterData(exampleData) : exampleData;
return crmApi4('WorkflowMessage', 'render', {
language: $ctrl.lang,
workflow: filteredData.workflow,
values: filteredData.modelProps,
messageTemplate: model.revisions[$ctrl.revisionId].rec
Expand Down
3 changes: 3 additions & 0 deletions tests/events/hook_civicrm_alterMailParams.evch.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
'Precedence' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer'], 'regex' => '/(bulk|first-class|list)/'],
'job_id' => ['type' => 'int|NULL', 'for' => ['civimail', 'flexmailer']],

// ## Language
'language' => ['type' => 'string|NULL', 'for' => ['messageTemplate']],

// ## Content

'subject' => ['for' => ['messageTemplate', 'singleEmail'], 'type' => 'string'],
Expand Down
Loading

0 comments on commit d1f11af

Please sign in to comment.