Skip to content

Instantly share code, notes, and snippets.

@totten
Last active August 2, 2022 05:50
Show Gist options
  • Save totten/ae7db562432e5feeb94d0fe3651d4c5a to your computer and use it in GitHub Desktop.
Save totten/ae7db562432e5feeb94d0fe3651d4c5a to your computer and use it in GitHub Desktop.
Ways to extend an API call to get translations

Approach: Add a general field on all API-actions which activates the translation behavior.

class AbstractAction {
  public function setLanguage(string $locale): static;
}

For example:

$msgs = MessageTemplate::get()
  ->setLimit(2)
  ->setLanguage('en_NZ')
  ->execute();

// Ex: User preferred 'en_NZ'. For $msgs[0], there was a prefect match.
assert $msgs[0]['msg_title'] === 'Gumboots: Are they too skux or too egg? Vote now!';
assert $msgs[0]['api.language'] === ['preferred' => 'en_NZ', 'matched' => 'en_NZ', ...];

// Ex: User preferred `en_NZ`. For $msgs[1], the closest match was `en_US`. 
assert $msgs[1]['msg_title'] === 'Religion in the workplace: Is it holy or blasphemous? Vote now!';
assert $msgs[1]['api.language'] === ['preferred' => 'en_NZ', 'matched' => 'en_US', ...];

A couple fidgety things about this sketch:

  • Which name to give the setter-method (eg setLanguage() or setPreferredLanguage() or setLocale())? setLanguage() is nice and pithy, but it already exists and does something different, and it sounds more matter-of-fact than it is. (The behavior is fuzzy/magical).
  • Which name to give the extra result field (eg actual_language or api.language or #locale). There's no convention of outputting this kind of extra field in APIv4.

Approach: Add a generic translate() action that is available for chaining.

class AbstractEntity {
  public function translate(string $locale): \Civi\Api4\Action\TranslateAction;
}

For example:

$msgs = MessageTemplate::get()
  ->setLimit(2)
  ->addChain('en_NZ', MessageTemplate::translate()->setLanguage('en_NZ')->setId('$id'))
  ->execute();

// Ex: User preferred 'en_NZ'. For $msgs[0], there was a prefect match.
assert $msgs[0]['en_NZ']['data'] === ['msg_title'=> 'Gumboots: Are they too skux or too egg? Vote now!', ...];
assert $msgs[0]['en_NZ']['locale'] === ['preferred' => 'en_NZ', 'matched' => 'en_NZ'];

// Ex: User preferred `en_NZ`. For $msgs[1], the closest match was `en_US`. 
assert $msgs[1]['en_NZ']['data' = ['msg_title' => 'Religion in the workplace: Is it holy or blasphemous? Vote now!', ...];
assert $msgs[1]['en_NZ']['locale'] === ['preferred' => 'en_NZ', 'matched' => 'en_US', ...];

Approach: Add a general opt-in "filter" mechanism. Generalizes the idea of 1-SetLang.md and could be used for other things (timezone, date format, currency-format, meters/feet, etc).

class AbstractAction {
  public function setFilter(string $name, mixed $options): static;
}

For example:

$msgs = MessageTemplate::get()
  ->setLimit(2)
  ->setFilter('locale', 'en_NZ')
  ->execute();

// Ex: User preferred 'en_NZ'. For $msgs[0], there was a prefect match.
assert $msgs[0]['msg_title'] =  'Gumboots: Are they too skux or too egg? Vote now!';
assert $msgs[0]['filter.locale'] === ['preferred' => 'en_NZ', 'matched' => 'en_NZ', ...];

// Ex: User preferred `en_NZ`. For $msgs[1], the closest match was `en_US`. 
assert $msgs[1]['msg_title'] =  'Religion in the workplace: Is it holy or blasphemous? Vote now!';
assert $msgs[1]['filter.locale'] === ['preferred' => 'en_NZ', 'matched' => 'en_US', ...];

The locale filter could be implemented with some interface or base-class or event -- which would have its own docblocks, eg:

/**
 * Filter the API output, substituting available fields with data from the `Translation` table.
 *
 * Note: This also outputs a pseudo-field 'filter.locale' with information about how the record was
 * (or was not) translated.
 */
class LocaleFilter extends AbstractFilter {
  public function filter($request, $result) { ... }
}

Approach: Add a magic join for translated entities.

For example:

$msgs = MessageTemplate::get()
  ->setLimit(2)
  ->addJoin('Translation as ts', 'LEFT', ['locale', 'SORTALIKE', 'en_NZ'])
  ->addSelect('ts.*')
  ->execute();

// Ex: User preferred 'en_NZ'. For $msgs[0], there was a prefect match.
assert $msgs[0]['ts.msg_title'] === 'Gumboots: Are they too skux or too egg? Vote now!';
assert $msgs[0]['ts.locale'] === 'en_NZ';

// Ex: User preferred `en_NZ`. For $msgs[1], the closest match was `en_US`. 
assert $msgs[1]['ts.msg_title'] === 'Religion in the workplace: Is it holy or blasphemous? Vote now!';
assert $msgs[1]['ts.locale'] === 'en_US';

(Note: I'm not certain how to get this amount of magic into a 'JOIN'...)

These have trade-offs with regard to:

  • How well they fit into existing API conventions
    • eg Do you expect more fields to be inserted, notwithstanding $select? How do you learn about the filtering that happend?
  • Whether IDE autocompletion works (or how close IDE completion gets)
  • How much boilerplate you have to add to enable lookups
  • How easy/hard it is to read the "effective" (merged) record
  • How easy/hard it is to distinguish the "canonical" and "translated" data
  • Whether they imply a basic add-in ("Do one thing") or a full-facade ("All actions/options should adapt to the flag").
    • eg What happens if you take some result data and use it for $where or update() or save()? Do you expect it to do extra translation-related work?
  • What impact they have on existing callers who do (or do not) set the $language option.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment