Skip to content

Commit

Permalink
chore(i18n): Introduce Locale, Message, and MessageBundle
Browse files Browse the repository at this point in the history
These concepts will help us clean up the i18n code significantly.
  • Loading branch information
ewinslow committed Mar 2, 2015
1 parent 286e718 commit 4ad86f6
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 6 deletions.
39 changes: 39 additions & 0 deletions engine/classes/Elgg/I18n/ArrayMessageBundle.php
@@ -0,0 +1,39 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* Uses an array as a source for the message bundle.
*
* This is mostly useful for testing so we can configure translators
* in-memory instead of going to the file system.
*
* @since 1.11
*
* @access private
*/
final class ArrayMessageBundle implements MessageBundle {

/** @var array */
private $messages;

/**
* Constructor
*
* @param string $messages Map of language keys to message templates
*/
public function __construct(array $messages) {
$this->messages = $messages;
}

/** @inheritDoc */
public function get($key) {
if (!is_string($key) || !isset($this->messages[$key]) || !is_string($this->messages[$key])) {
// TODO(ewinslow): Throw an exception in strict mode?
return null;
}

return new SprintfMessage($this->messages[$key]);
}
}
49 changes: 49 additions & 0 deletions engine/classes/Elgg/I18n/Locale.php
@@ -0,0 +1,49 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* Language class to ensure only valid languages are used.
*
* @since 1.11
*
* @access private
*/
final class Locale {

/** @var string */
private $locale;

/**
* Use Locale::parse to construct
*
* @param string $locale A string representation of the locale
*/
private function __construct($locale) {
$this->locale = $locale;
}

/** @inheritDoc */
public function __toString() {
return $this->locale;
}

/**
* Create a language, asserting that the language code is valid.
*
* @param string $locale Language code
*
* @return Locale
*
* @throws InvalidLocaleException
*/
public static function parse($locale) {
// TODO(evan): Better sanitizing of locales using \Locale perhaps
if (strlen($locale) < 2 || strlen($locale) > 5) {
throw new InvalidLocaleException("Unrecognized locale: $locale");
}

return new Locale($locale);
}
}
32 changes: 32 additions & 0 deletions engine/classes/Elgg/I18n/Message.php
@@ -0,0 +1,32 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* A single localizable message template.
*
* We introduced this class because we want to have the flexibility of
* easily switching our message template language from sprintf to ICU...
*
* @since 1.11
*
* @access private
*/
interface Message {
/**
* Applies the inputs to the message template and returns the result.
*
* @param array $args The inputs to this message
*
* @return string The rendered including all the interpolated inputs
*/
public function format(array $args = []);

/**
* Get the string template this message uses for translation.
*
* @return string
*/
public function getTemplate();
}
23 changes: 23 additions & 0 deletions engine/classes/Elgg/I18n/MessageBundle.php
@@ -0,0 +1,23 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* TODO(ewinslow): Replace this with Map<string,Message> when PHP supports generics?
*
* @since 1.11
*
* @access private
*/
interface MessageBundle {

/**
* Fetch the translatable message associated with the given key
*
* @param string $key
*
* @return Message?
*/
public function get($key);
}
29 changes: 29 additions & 0 deletions engine/classes/Elgg/I18n/NullMessage.php
@@ -0,0 +1,29 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* A message that always ignores all parameters and just returns the template.
*
* @since 1.11
*
* @access private
*/
final class NullMessage {
/** @var string */
private $template;

private function __construct($template) {
$this->template = $template;
}

/** @inheritDoc */
public function format(array $args) {
return $this->template;
}

public function getTemplate() {
return $this->template;
}
}
8 changes: 3 additions & 5 deletions engine/classes/Elgg/I18n/NullTranslator.php
Expand Up @@ -5,13 +5,11 @@
/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* @access private
* @since 1.10.0
*
* @package Elgg.Core
* @subpackage I18n
* @since 1.10.0
* @access private
*/
class NullTranslator extends Translator {
final class NullTranslator extends Translator {
/** @inheritDoc */
public function translate($key, $args = array(), $lang = '') {
return $key;
Expand Down
33 changes: 33 additions & 0 deletions engine/classes/Elgg/I18n/SprintfMessage.php
@@ -0,0 +1,33 @@
<?php
namespace Elgg\I18n;

/**
* WARNING: API IN FLUX. DO NOT USE DIRECTLY.
*
* A single localizable message template.
*
* We introduced this class because we want to have the flexibility of
* moving away from sprintf-based formatting eventually...
*
* @since 1.11
*
* @access private
*/
final class SprintfMessage implements Message {
/** @var string */
private $template;

public function __construct($template) {
$this->template = $template;
}

/** @inheritDoc */
public function format(array $args = []) {
return vsprintf($this->template, $args);
}

/** @inheritDoc */
public function getTemplate() {
return $this->template;
}
}
29 changes: 28 additions & 1 deletion engine/tests/phpunit/Elgg/I18n/TranslatorTest.php
Expand Up @@ -12,7 +12,34 @@ public function testSetLanguageFromGetParameter() {
_elgg_services()->input->set('hl', $input_lang);

$lang = $translator->getLanguage();
$this->assertEquals($lang, $input_lang);
$this->assertEquals($lang, $input_lang);
}

public function testKeyIsReturnedIfNoTranslationCanBeFound() {
$translator = new Translator();

$this->assertEquals('foobar', $translator->translate('foobar'));
}

public function testTranslateReturnsTranslationForSpecifiedLanguageIfAvailable() {
$this->markTestIncomplete();
}

public function testTranslateReturnsTranslationForUserLanguageIfNoLanguageWasSpecified() {
$this->markTestIncomplete();
}

public function testFallsBackToUserLanguageIfTranslationForSpecifiedLanguageIsNotAvailable() {
$this->markTestIncomplete();
}

public function testDoesNotPerformSprintfFormattingIfArgsNotProvided() {
$this->markTestSkipped('Translator requires refactoring before this can work');

$messages = new ArrayMessageBundle(['foo' => '%s']);
$translator = new Translator(['en' => $messages]);

$this->assertEquals('%s', $translator->translate('foo'));
$this->assertEquals('%s', $translator->translate('foo', 'es'));
}
}

0 comments on commit 4ad86f6

Please sign in to comment.