Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CRM/Custom/Form/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public function preProcess() {
}

// Add crm-options-repeat web component. FIXME: need an autoloader for web components.
\Civi::resources()->addScriptFile('civicrm', 'elements/civi-ts.js');
\Civi::resources()->addScriptFile('civicrm', 'js/CrmOptionsRepeat.js');

if ($this->_gid) {
Expand Down
39 changes: 39 additions & 0 deletions Civi/Api4/Action/System/Translate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

namespace Civi\Api4\Action\System;

use Civi\Api4\Generic\AbstractAction;
use Civi\Api4\Generic\Result;

/**
* Get translated strings
*/
class Translate extends AbstractAction {

/**
* @var array
* @required
*/
protected array $strings = [];

public function _run(Result $result) {
$dictionary = [];

foreach ($this->strings as $string) {
$dictionary[$string] = ts($string);
}

$result['dictionary'] = $dictionary;
}

}
106 changes: 106 additions & 0 deletions elements/civi-ts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* TODO:
* - integrate with/reuse CRM.ts and CRM.strings
* - handle multiple domains
* - handle parameters?
* - handle attributes
* - use a bespoke endpoint for fetching strings
*/

CRM = CRM || {};

CRM.babel = {

toFetch: [],

nextFetch: null,

get: (src) => localStorage.getItem('civi-babel-' + src),

// TODO: add cache key?
set: (src, translation) => localStorage.setItem('civi-babel-' + src, translation),

ts: (string) => {
const translation = CRM.babel.get(string);
if (!translation) {
CRM.babel.queueFetch(string);
return null;
}

return translation;
},

queueFetch: (string) => {
if (CRM.babel.toFetch.includes(string)) {
return;
}

CRM.babel.toFetch.push(string);

// if we have 50 strings backed up, fetch immediately
if (CRM.babel.toFetch.length > 100) {
clearTimeout(CRM.babel.nextFetch);
CRM.babel.doFetch();
}
// otherwise debounce the fetch for 1s
else {
clearTimeout(CRM.babel.nextFetch);
CRM.babel.nextFetch = setTimeout(() => CRM.babel.doFetch(), 1000);
}
},

doFetch: () => {
// get the batch
const thisBatch = CRM.babel.toFetch;
CRM.babel.toFetch = [];

// prep the batch for best cacheability
thisBatch.sort();
// const batchHash = md5(thisBatch);

// TODO: use a bespoke endpoint to allow caching and/or rate handling
CRM.api4('System', 'translate', {
strings: thisBatch,
// hash: batchHash
})
// add
.then((response) => {
if (response.is_error) {
// fetch failed - requeue
thisBatch.forEach((string) => CRM.babel.queueFetch(string));
return;
}

// add returned strings to local storage
Object.entries(response.dictionary).forEach((e) => CRM.babel.set(e[0], e[1]));

CRM.babel.propogate(thisBatch);
})
},


propogate: (strings) => {
strings.forEach((string) => {
const translation = CRM.babel.get(string);
document.querySelectorAll(`civi-ts[src="${string}"]`).forEach((e) => e.innerText = translation);
});
}

}

class CiviTs extends HTMLElement {

constructor() {
super();
}

connectedCallback() {
// if a translation exists it will be added immediately
// if not babel will fetch and propogate
this.innerText = CRM.babel.ts(this.getAttribute('src'));
}
}

// Register the custom element
customElements.define('civi-ts', CiviTs);

18 changes: 9 additions & 9 deletions js/CrmOptionsRepeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ class CrmOptionsRepeat extends HTMLElement {
<thead>
<tr>
<th></th>
<th>{ts}Default{/ts}</th>
<th><civi-ts src="Default"></civi-ts></th>
<th>
{ts}Label{/ts}
<civi-ts src="label"></civi-ts>
<a class="crm-hover-button crm-options-repeat-sort" title="{ts escape='html'}Sort by label{/ts}">
<i class="crm-i fa-sort-alpha-down" aria-hidden="true" role="img"></i>
<span class="sr-only">{ts}Sort by label{/ts}</span>
<span class="sr-only"><civi-ts src="Sort by label"></civi-ts></span>
</a>
</th>

<th>
{ts}Value{/ts}
<civi-ts src="Value"></civi-ts>
<a class="crm-hover-button crm-options-repeat-sort" title="{ts escape='html'}Sort by value{/ts}">
<i class="crm-i fa-sort-numeric-down" aria-hidden="true" role="img"></i>
<span class="sr-only">{ts}Sort by value{/ts}</span>
<span class="sr-only"><civi-ts src="Sort by value"></civi-ts></span>
</a>
</th>
<th>{ts}Enabled{/ts}</th>
<th><civi-ts src="Enabled"></civi-ts></th>
<th></th>
</tr>
</thead>
Expand All @@ -30,7 +30,7 @@ class CrmOptionsRepeat extends HTMLElement {
<td>
<a class="crm-draggable">
<i class="crm-i fa-arrows-up-down" role="img" aria-hidden="true"></i>
<span class="sr-only">{ts}Change order{/ts}</span>
<span class="sr-only"><civi-ts src="Change order"></civi-ts></span>
</a>
</td>
<td><input type="radio" name="is_default" class="crm-form-radio"></td>
Expand All @@ -40,7 +40,7 @@ class CrmOptionsRepeat extends HTMLElement {
<td>
<a class="crm-hover-button crm-options-repeat-remove" title="{ts escape='html'}Delete{/ts}">
<i class="crm-i fa-trash" role="img" aria-hidden="true"></i>
<span class="sr-only">{ts}Delete{/ts}</span>
<span class="sr-only"><civi-ts src="Delete"></civi-ts></span>
</a>
</td>
</tr>
Expand All @@ -50,7 +50,7 @@ class CrmOptionsRepeat extends HTMLElement {
<td colspan="6">
<button type="button" class="crm-options-repeat-add">
<i class="crm-i fa-plus" role="img" aria-hidden="true"></i>
{ts}Add Option{/ts}
<civi-ts src="Add Option"></civi-ts>
</button>
</td>
</tr>
Expand Down