Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Message Templates - Allow rendering & previewing of translated messages #24174

Merged
merged 10 commits into from
Sep 2, 2022
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really stumped me - I couldn't figure out what Biz Locale is - 'Business Locale' 'Bees Locale' 'Random Prefix Locale'?

I feel like maybe this is the ResolvedLocale?

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