Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(i18n): Introduce Locale, Message, and MessageBundle
These concepts will help us clean up the i18n code significantly.
- Loading branch information
Showing
12 changed files
with
355 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?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 array $messages Map of locales to maps of keys to message-templates | ||
*/ | ||
public function __construct(array $messages) { | ||
$this->messages = $messages; | ||
} | ||
|
||
/** @inheritDoc */ | ||
public function get($key, Locale $locale) { | ||
assert(is_string($key), '$key must be a string'); | ||
|
||
if (!isset($this->messages["$locale"]) || !is_array($this->messages["$locale"])) { | ||
return null; | ||
} | ||
|
||
$messages = $this->messages["$locale"]; | ||
if (!is_string($key) || !isset($messages[$key]) || !is_string($messages[$key])) { | ||
return null; | ||
} | ||
|
||
return new SprintfMessageTemplate($messages[$key]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?php | ||
namespace Elgg\I18n; | ||
|
||
/** | ||
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. | ||
* | ||
* TODO(ewinslow): Have this extend Table(row=string, column=Locale, value=?MessageTemplate) | ||
* if we ever support a Table data structure. | ||
* | ||
* @since 1.11 | ||
* | ||
* @access private | ||
*/ | ||
interface MessageBundle { | ||
|
||
/** | ||
* Fetches the translatable message associated with the given key | ||
* | ||
* @param string $key String identifier for the message | ||
* @param Locale $locale Locale in which the message is written | ||
* | ||
* @return ?MessageTemplate The message object or null if not found. | ||
*/ | ||
public function get($key, Locale $locale); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?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... | ||
* | ||
* Example messages: | ||
* - "{subject} spent {num_nights,number,integer} nights camping in {location}." (ICU) | ||
* - "%s spent %d nights camping in %s" (sprintf) | ||
* | ||
* @since 1.11 | ||
* | ||
* @access private | ||
*/ | ||
abstract class MessageTemplate { | ||
/** @var string */ | ||
protected $template; | ||
|
||
/** | ||
* Constructor | ||
* | ||
* @param string $template The message template | ||
*/ | ||
public function __construct($template) { | ||
$this->template = $template; | ||
} | ||
|
||
/** | ||
* 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 abstract function format(array $args); | ||
|
||
/** | ||
* Get the string template this message uses for translation. | ||
* | ||
* @return string | ||
*/ | ||
public function __toString() { | ||
return $this->template; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
namespace Elgg\I18n; | ||
|
||
/** | ||
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. | ||
* | ||
* @access private | ||
* | ||
* @since 1.11 | ||
*/ | ||
class MessageTranslator implements TranslatorInterface { | ||
|
||
// TODO Maybe this should be an array of locales to provide multiple fallbacks? | ||
/** @var Locale */ | ||
private $defaultLocale; | ||
|
||
/** @var MessageBundle */ | ||
private $messages; | ||
|
||
/** | ||
* Constructor | ||
* | ||
* @param Locale $defaultLocale The fallback locale | ||
* @param MessageBundle $messages Messages that this translator is aware of | ||
*/ | ||
public function __construct(Locale $defaultLocale, MessageBundle $messages) { | ||
$this->defaultLocale = $defaultLocale; | ||
$this->messages = $messages; | ||
} | ||
|
||
/** @inheritDoc */ | ||
public function translate($key, array $args = [], Locale $locale = null) { | ||
$locales = [ | ||
$locale, | ||
$this->defaultLocale, | ||
Locale::parse('en'), | ||
]; | ||
|
||
foreach ($locales as $locale) { | ||
if (!$locale) { | ||
continue; | ||
} | ||
|
||
$message = $this->messages->get($key, $locale); | ||
|
||
if ($message) { | ||
return $message->format($args); | ||
} | ||
} | ||
|
||
return $key; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?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 extends Message { | ||
/** @inheritDoc */ | ||
public function format(array $args) { | ||
return "$this"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
namespace Elgg\I18n; | ||
|
||
/** | ||
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. | ||
* | ||
* A message that uses vsprintf to insert arguments into the template. | ||
* | ||
* @since 1.11 | ||
* | ||
* @access private | ||
*/ | ||
final class SprintfMessageTemplate extends MessageTemplate { | ||
/** @inheritDoc */ | ||
public function format(array $args) { | ||
return \vsprintf($this->template, $args); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
namespace Elgg\I18n; | ||
|
||
/** | ||
* WARNING: API IN FLUX. DO NOT USE DIRECTLY. | ||
* | ||
* Can "translate" language keys into various human-readable, localized strings. | ||
* | ||
* TODO(ewinslow): Remove the "Interface" suffix | ||
* | ||
* @since 1.11 | ||
* | ||
* @access private | ||
*/ | ||
interface TranslatorInterface { | ||
/** | ||
* Given a message key, returns a best-effort translated string. | ||
* | ||
* If the translator doesn't know how to translate into the specified locale, | ||
* it can try translating into a related or similar locale (e.g. en-US => en). | ||
* | ||
* If no locale is specified, or if no translation can be found for the specified | ||
* locale, the translator may choose to fall back to some other language(s). | ||
* | ||
* It should never throw exceptions, since lack of translation should never be | ||
* cause to bring down an app or cancel a request. However, implementations may | ||
* log warnings to alert admins that requested language strings are missing. | ||
* | ||
* @param string $key A key identifying the message to translate. | ||
* @param array $args An array of arguments with which to format the message. | ||
* @param Locale $locale Optionally, the standard language code | ||
* (defaults to site/user default, then English) | ||
* | ||
* @return string The final, best-effort translation. | ||
*/ | ||
function translate($key, array $args = [], Locale $locale = null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<?php | ||
namespace Elgg\I18n; | ||
|
||
use PHPUnit_Framework_TestCase as TestCase; | ||
|
||
class MessageTranslatorTest extends TestCase { | ||
public function setUp() { | ||
$this->english = Locale::parse('en'); | ||
$this->spanish = Locale::parse('es'); | ||
} | ||
|
||
public function testKeyIsReturnedIfNoTranslationCanBeFound() { | ||
$messages = new ArrayMessageBundle([]); | ||
$translator = new MessageTranslator(Locale::parse('en'), $messages); | ||
|
||
$this->assertEquals('foobar', $translator->translate('foobar')); | ||
} | ||
|
||
public function testTranslateReturnsTranslationForSpecifiedLocaleIfAvailable() { | ||
$messages = new ArrayMessageBundle([ | ||
'en' => ['one' => 'one'], | ||
'es' => ['one' => 'uno'], | ||
]); | ||
$translator = new MessageTranslator(Locale::parse('en'), $messages); | ||
|
||
$this->assertEquals('uno', $translator->translate('one', [], Locale::parse('es'))); | ||
} | ||
|
||
public function testTranslateReturnsTranslationForDefaultLocaleIfNoLocaleWasSpecified() { | ||
$messages = new ArrayMessageBundle([ | ||
'en' => ['one' => 'one'], | ||
'es' => ['one' => 'uno'], | ||
]); | ||
$translator = new MessageTranslator(Locale::parse('en'), $messages); | ||
|
||
$this->assertEquals('one', $translator->translate('one', [])); | ||
} | ||
|
||
public function testFallsBackToLanguageIfTranslationForSpecifiedLanguageIsNotAvailable() { | ||
$messages = new ArrayMessageBundle([ | ||
'en' => ['one' => 'one'], | ||
]); | ||
$translator = new MessageTranslator(Locale::parse('en'), $messages); | ||
|
||
$this->assertEquals('one', $translator->translate('one', [], Locale::parse('es'))); | ||
} | ||
} |
Oops, something went wrong.