mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Restored context parameter to i18n, and added to a “comment” key
Rolled pluralisation functionality into the i18n::_t() method Warnings on missing default can now be turned off
This commit is contained in:
parent
8a07c56bdf
commit
de02a3f733
@ -172,8 +172,12 @@ It can be used to translate strings in both PHP files and template files. The us
|
||||
elsewhere in your code.
|
||||
* **$default:** The original language string to be translated. This should be declared
|
||||
whenever used, and will get picked up the [text collector](#collecting-text).
|
||||
* **$string:** (optional) Natural language comment (particularly short phrases and individual words)
|
||||
are very context dependent. This parameter allows the developer to convey this information
|
||||
to the translator.
|
||||
* **$injection::** (optional) An array of injecting variables into the second parameter
|
||||
|
||||
|
||||
## Pluralisation
|
||||
|
||||
i18n also supports locale-respective pluralisation rules. Many languages have more than two plural forms,
|
||||
@ -182,8 +186,11 @@ unlike English which has two only; One for the singular, and another for any oth
|
||||
More information on what forms these plurals can take for various locales can be found on the
|
||||
[CLDR documentation](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html)
|
||||
|
||||
The ability to pluralise strings is provided through the `i18n::pluaralise` method, which is similar to the
|
||||
`i18n::_t` method, other than that it takes an additional `$count` argument.
|
||||
The ability to pluralise strings is provided through the `i18n::_t` method when supplied with a
|
||||
`{count}` argument and `|` pipe-delimiter provided with the default string.
|
||||
|
||||
Plural forms can also be explicitly declared via the i18nEntityProvider interface in array-format
|
||||
with both a 'one' and 'other' key (as per the CLDR for the default `en` language).
|
||||
|
||||
For instance, this is an example of how to correctly declare pluralisations for an object
|
||||
|
||||
@ -235,17 +242,9 @@ Please ensure that any required plurals are exposed via provideI18nEntities.
|
||||
array('value' => $itemRestored)
|
||||
);
|
||||
|
||||
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
|
||||
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
|
||||
|
||||
You can invoke plurals for any object using the new `i18n::pluralise` method.
|
||||
In addition to array form, you can also pass in a pipe-delimited string as a default
|
||||
argument for brevity.
|
||||
|
||||
|
||||
:::php
|
||||
public function pluralise($count)
|
||||
{
|
||||
return i18n::pluralise('MyObject.PLURALS', 'An object|{count} objects', $count);
|
||||
}
|
||||
|
||||
#### Usage in Template Files
|
||||
|
||||
@ -267,11 +266,8 @@ the PHP version of the function.
|
||||
// Using injection to add variables into the translated strings (note that $Name and $Greeting must be available in the current template scope).
|
||||
<%t Header.Greeting "Hello {name} {greeting}" name=$Name greeting=$Greeting %>
|
||||
|
||||
Pluralisation in templates is available via the global `$pluralise` method.
|
||||
|
||||
|
||||
:::ss
|
||||
You have $pluralise('i18nTestModule.PLURALS', 'An item|{count} items', $Count) in your cart
|
||||
// Plurals follow the same convention, required a `|` and `{count}` in the default string
|
||||
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
|
||||
|
||||
|
||||
#### Caching in Template Files with locale switching
|
||||
|
@ -219,10 +219,10 @@ Where your code once used SQLQuery you should now use SQLSelect in all cases, as
|
||||
In many cases, localisation strings which worked in 3.x will continue to work in 4.0, however certain patterns
|
||||
have been deprecated and will be removed in 5.0. These include:
|
||||
|
||||
- _t calls with a $context parameter, which is ignored.
|
||||
- _t calls with sprintf-style placeholders (`%s`). Replace with named placeholders instead.
|
||||
- _t calls with non-associative injection arguments. Please use an associative array for all arguments.
|
||||
- _t calls which do not include a default value.
|
||||
- _t calls which do not include a default value will now raise a warning. This can be disabled by setting
|
||||
the `i18n.missing_default_warning` config to false.
|
||||
|
||||
Note: If you attempt to use non-associative injection arguments with named placeholders, the result will
|
||||
now trigger an exception.
|
||||
@ -232,7 +232,7 @@ The non-associative array return type is deprecated. If returning a default stri
|
||||
other than itself, it should return an array with the `default` and `module` keys respectively.
|
||||
|
||||
Full locale-rule respecting localisation for plural forms is now supported. The default
|
||||
key for an object plural form is `<fqn>.PLURALS`, and follows CLDR array form for each
|
||||
key for an object plural form is `<Namespaced\ClassName>.PLURALS`, and follows CLDR array form for each
|
||||
pluralisation. See [the CLDR chart](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html)
|
||||
for reference.
|
||||
|
||||
@ -285,33 +285,23 @@ In YML format this will be expressed as the below:
|
||||
DESCRIPTION: 'This is the description for this section'
|
||||
|
||||
|
||||
You can invoke plurals for any object using the new `i18n::pluralise` method.
|
||||
In addition to array form, you can also pass in a pipe-delimited string as a default
|
||||
argument for brevity.
|
||||
Usage of these pluralised strings is through the existing _t() method,
|
||||
and require a `|` pipe-delimeter with a {count} argument.
|
||||
|
||||
|
||||
:::php
|
||||
public function pluralise($count)
|
||||
{
|
||||
return i18n::pluralise('MyObject.PLURALS', 'An object|{count} objects', $count);
|
||||
return _t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => $count ]);
|
||||
}
|
||||
|
||||
|
||||
Which is equivalent to the below:
|
||||
In templates this can also be invoked as below:
|
||||
|
||||
|
||||
:::php
|
||||
public function pluralise($count)
|
||||
{
|
||||
return i18n::pluralise('MyObject.PLURALS', [
|
||||
'one' => 'An object',
|
||||
'other' => '{count} objects',
|
||||
], $count);
|
||||
}
|
||||
:::ss
|
||||
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
|
||||
|
||||
|
||||
Note: Template syntax for pluralisation is not yet available.
|
||||
|
||||
#### New asset storage mechanism
|
||||
|
||||
File system has been abstracted into an abstract interface. By default, the out of the box filesystem
|
||||
@ -1322,20 +1312,18 @@ handle field-level and form-level messages. This has the following properties:
|
||||
|
||||
* Upgrade of i18n to symfony/translation
|
||||
* Localisation based on language-only (without any specific locale) is now supported
|
||||
* i18nEntityProvider::provideI18nEntities() Now is expected to return only a single array
|
||||
* `i18nEntityProvider::provideI18nEntities()` Now is expected to return only a single array
|
||||
map of key to default values.
|
||||
* i18n keys for '.PLURAL_NAME' and '.SINGULAR_NAME' have been changed back to FQN class names
|
||||
for all DataObject subclasses.
|
||||
* i18n keys for '.PLURAL_NAME' and '.SINGULAR_NAME' have been changed back to use the namespaced class names
|
||||
for all DataObject subclasses, rather than just the basename without namespace.
|
||||
* i18n key for locale-respective pluralisation rules added as '.PLURALS'. These can be configured
|
||||
within yaml in array format as per [ruby i18n pluralization rules](http://guides.rubyonrails.org/i18n.html#pluralization).
|
||||
|
||||
#### <a name="overview-i18n-removed"></a>i18n API Removed API
|
||||
|
||||
* Zend_Translate removed
|
||||
* i18n::_t $context parameter deprecated
|
||||
* i18n::_t Support for sprintf-style `%s` arguments deprecated
|
||||
* i18n::_t Using non-associative injection with named parameters is now an error
|
||||
* i18nEntityProvider no longer can collect strings for other modules.
|
||||
* `Zend_Translate` removed
|
||||
* `i18n::_t` Support for sprintf-style `%s` arguments deprecated
|
||||
* `i18n::_t` Using non-associative injection with named parameters is now an error
|
||||
|
||||
### <a name="overview-mailer"></a>Email and Mailer
|
||||
|
||||
|
@ -744,11 +744,11 @@ en:
|
||||
other: '{count} Permission Role Codes'
|
||||
SINGULARNAME: 'Permission Role Code'
|
||||
SilverStripe\Security\RememberLoginHash:
|
||||
PLURALNAME: 'Remember Login Hashs'
|
||||
PLURALNAME: 'Login Hashes'
|
||||
PLURALS:
|
||||
one: 'A Remember Login Hash'
|
||||
other: '{count} Remember Login Hashs'
|
||||
SINGULARNAME: 'Remember Login Hash'
|
||||
one: 'A Login Hash'
|
||||
other: '{count} Login Hashes'
|
||||
SINGULARNAME: 'Login Hash'
|
||||
SiteTree:
|
||||
TABMAIN: Main
|
||||
TableListField:
|
||||
|
@ -143,14 +143,24 @@ function project()
|
||||
}
|
||||
|
||||
/**
|
||||
* @see i18n::_t()
|
||||
* This is the main translator function. Returns the string defined by $entity according to the
|
||||
* currently set locale.
|
||||
*
|
||||
* @param string $entity
|
||||
* @param string $string
|
||||
* @param array $injection
|
||||
* Also supports pluralisation of strings. Pass in a `count` argument, as well as a
|
||||
* default value with `|` pipe-delimited options for each plural form.
|
||||
*
|
||||
* @param string $entity Entity that identifies the string. It must be in the form
|
||||
* "Namespace.Entity" where Namespace will be usually the class name where this
|
||||
* string is used and Entity identifies the string inside the namespace.
|
||||
* @param mixed $arg,... Additional arguments are parsed as such:
|
||||
* - Next string argument is a default. Pass in a `|` pipe-delimeted value with `{count}`
|
||||
* to do pluralisation.
|
||||
* - Any other string argument after default is context for i18nTextCollector
|
||||
* - Any array argument in any order is an injection parameter list. Pass in a `count`
|
||||
* injection parameter to pluralise.
|
||||
* @return string
|
||||
*/
|
||||
function _t($entity, $string = "", $injection = [])
|
||||
function _t($entity, $arg = null)
|
||||
{
|
||||
// Pass args directly to handle deprecation
|
||||
return call_user_func_array([i18n::class, '_t'], func_get_args());
|
||||
|
@ -21,6 +21,9 @@ use DateInterval;
|
||||
*/
|
||||
class RememberLoginHash extends DataObject
|
||||
{
|
||||
private static $singular_name = 'Login Hash';
|
||||
|
||||
private static $plural_name = 'Login Hashes';
|
||||
|
||||
private static $db = array (
|
||||
'DeviceID' => 'Varchar(40)',
|
||||
@ -29,7 +32,7 @@ class RememberLoginHash extends DataObject
|
||||
);
|
||||
|
||||
private static $has_one = array (
|
||||
'Member' => 'SilverStripe\\Security\\Member',
|
||||
'Member' => Member::class,
|
||||
);
|
||||
|
||||
private static $indexes = array(
|
||||
|
@ -22,9 +22,9 @@ interface MessageProvider
|
||||
*
|
||||
* @param string $entity Identifier for this message in Namespace.key format
|
||||
* @param array|string $default Default message with pipe-separated delimiters, or array
|
||||
* @param int $count Number to pluralise against
|
||||
* @param array $injection List of injection variables
|
||||
* @param int $count Number to pluralise against
|
||||
* @return string Localised string
|
||||
*/
|
||||
public function pluralise($entity, $default, $count, $injection);
|
||||
public function pluralise($entity, $default, $injection, $count);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace SilverStripe\i18n\Messages\Symfony;
|
||||
|
||||
use SilverStripe\Core\Config\Configurable;
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\i18n\Messages\Reader;
|
||||
use Symfony\Component\Translation\Loader\ArrayLoader;
|
||||
use Symfony\Component\Translation\PluralizationRules;
|
||||
@ -15,24 +16,6 @@ use Symfony\Component\Translation\PluralizationRules;
|
||||
*/
|
||||
class ModuleYamlLoader extends ArrayLoader
|
||||
{
|
||||
use Configurable;
|
||||
|
||||
/**
|
||||
* Map of rails plurals into symfony standard order
|
||||
*
|
||||
* @see PluralizationRules For symfony's implementation of this logic
|
||||
* @config
|
||||
* @var array
|
||||
*/
|
||||
private static $plurals = [
|
||||
'zero',
|
||||
'one',
|
||||
'two',
|
||||
'few',
|
||||
'many',
|
||||
'other',
|
||||
];
|
||||
|
||||
/**
|
||||
* Message reader
|
||||
*
|
||||
@ -123,7 +106,7 @@ class ModuleYamlLoader extends ArrayLoader
|
||||
protected function normalisePlurals($key, $map, $locale)
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($this->config()->get('plurals') as $form) {
|
||||
foreach (i18n::config()->get('plurals') as $form) {
|
||||
if (isset($map[$form])) {
|
||||
$parts[] = $map[$form];
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ class SymfonyMessageProvider implements MessageProvider
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function pluralise($entity, $default, $count, $injection)
|
||||
public function pluralise($entity, $default, $injection, $count)
|
||||
{
|
||||
if (is_array($default)) {
|
||||
$default = $this->normalisePlurals($default);
|
||||
|
@ -67,6 +67,7 @@ class YamlReader implements Reader
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($messages);
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace SilverStripe\i18n\Messages;
|
||||
|
||||
use SilverStripe\Assets\Filesystem;
|
||||
use SilverStripe\i18n\i18n;
|
||||
use Symfony\Component\Yaml\Dumper;
|
||||
use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader;
|
||||
use LogicException;
|
||||
@ -10,6 +11,9 @@ use LogicException;
|
||||
/**
|
||||
* Write yml files compatible with ModuleYamlLoader
|
||||
*
|
||||
* Note: YamlWriter may not correctly denormalise plural strings if writing outside of the
|
||||
* default locale (en).
|
||||
*
|
||||
* @see ModuleYamlLoader
|
||||
*/
|
||||
class YamlWriter implements Writer
|
||||
@ -69,30 +73,78 @@ class YamlWriter implements Writer
|
||||
*/
|
||||
protected function denormaliseMessages($messages)
|
||||
{
|
||||
// Sort prior to denormalisation
|
||||
ksort($messages);
|
||||
$entities = [];
|
||||
foreach ($messages as $entity => $value) {
|
||||
// Skip un-namespaced keys
|
||||
$value = $this->denormaliseValue($value);
|
||||
|
||||
// Non-nested key
|
||||
if (strstr($entity, '.') === false) {
|
||||
$entities[$entity] = $value;
|
||||
continue;
|
||||
}
|
||||
$parts = explode('.', $entity);
|
||||
$class = array_shift($parts);
|
||||
|
||||
// Ensure the `.ss` suffix gets added to the top level class rather than the key
|
||||
if (count($parts) > 1 && reset($parts) === 'ss') {
|
||||
$class .= '.ss';
|
||||
array_shift($parts);
|
||||
}
|
||||
$key = implode('.', $parts);
|
||||
// Get key nested within class
|
||||
list($class, $key) = $this->getClassKey($entity);
|
||||
if (!isset($entities[$class])) {
|
||||
$entities[$class] = [];
|
||||
}
|
||||
|
||||
$entities[$class][$key] = $value;
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entities array format into yml-ready string / array
|
||||
*
|
||||
* @param array|string $value Input value
|
||||
* @return array|string denormalised value
|
||||
*/
|
||||
protected function denormaliseValue($value)
|
||||
{
|
||||
// Check plural form
|
||||
$plurals = $this->getPluralForm($value);
|
||||
if ($plurals) {
|
||||
return $plurals;
|
||||
}
|
||||
|
||||
// Non-plural non-array is already denormalised
|
||||
if (!is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Denormalise from default key
|
||||
if (!empty($value['default'])) {
|
||||
return $this->denormaliseValue($value['default']);
|
||||
}
|
||||
|
||||
// No value
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array-plural form for any value
|
||||
*
|
||||
* @param array|string $value
|
||||
* @return array List of plural forms, or empty array if not plural
|
||||
*/
|
||||
protected function getPluralForm($value)
|
||||
{
|
||||
// Strip non-plural keys away
|
||||
if (is_array($value)) {
|
||||
$forms = i18n::config()->get('plurals');
|
||||
$forms = array_combine($forms, $forms);
|
||||
return array_intersect_key($value, $forms);
|
||||
}
|
||||
|
||||
// Parse from string
|
||||
// Note: Risky outside of en locale.
|
||||
return i18n::parse_plurals($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert messages to yml ready to write
|
||||
*
|
||||
@ -108,4 +160,24 @@ class YamlWriter implements Writer
|
||||
], 99);
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine class and key for a localisation entity
|
||||
*
|
||||
* @param string $entity
|
||||
* @return array Two-length array with class and key as elements
|
||||
*/
|
||||
protected function getClassKey($entity)
|
||||
{
|
||||
$parts = explode('.', $entity);
|
||||
$class = array_shift($parts);
|
||||
|
||||
// Ensure the `.ss` suffix gets added to the top level class rather than the key
|
||||
if (count($parts) > 1 && reset($parts) === 'ss') {
|
||||
$class .= '.ss';
|
||||
array_shift($parts);
|
||||
}
|
||||
$key = implode('.', $parts);
|
||||
return array($class, $key);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace SilverStripe\i18n\TextCollection;
|
||||
|
||||
use SilverStripe\i18n\i18n;
|
||||
use SilverStripe\View\SSTemplateParser;
|
||||
|
||||
/**
|
||||
@ -10,64 +11,105 @@ use SilverStripe\View\SSTemplateParser;
|
||||
class Parser extends SSTemplateParser
|
||||
{
|
||||
/**
|
||||
* Current entity
|
||||
* List of all entities
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $entities = [];
|
||||
|
||||
/**
|
||||
* List of all entities
|
||||
* Current entity
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $currentEntity = [];
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* Key of current entity
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($string)
|
||||
protected $currentEntityKey = null;
|
||||
|
||||
/*
|
||||
* Show warning if default omitted
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $warnIfEmpty = true;
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param bool $warnIfEmpty
|
||||
*/
|
||||
public function __construct($string, $warnIfEmpty = true)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->string = $string;
|
||||
$this->pos = 0;
|
||||
$this->depth = 0;
|
||||
$this->regexps = array();
|
||||
$this->warnIfEmpty = $warnIfEmpty;
|
||||
}
|
||||
|
||||
public function Translate__construct(&$res)
|
||||
{
|
||||
$this->currentEntity = [null, null];
|
||||
$this->currentEntity = [];
|
||||
$this->currentEntityKey = null;
|
||||
}
|
||||
|
||||
public function Translate_Entity(&$res, $sub)
|
||||
{
|
||||
$this->currentEntity[0] = $sub['text']; // key
|
||||
$this->currentEntityKey = $sub['text']; // key
|
||||
}
|
||||
|
||||
public function Translate_Default(&$res, $sub)
|
||||
{
|
||||
$this->currentEntity[1] = $sub['String']['text']; // default
|
||||
$this->currentEntity['default'] = $sub['String']['text']; // default
|
||||
}
|
||||
|
||||
public function Translate_Context(&$res, $sub)
|
||||
{
|
||||
$this->currentEntity['comment'] = $sub['String']['text']; //comment
|
||||
}
|
||||
|
||||
public function Translate__finalise(&$res)
|
||||
{
|
||||
// Capture entity if, and only if, a default vaule is provided
|
||||
if ($this->currentEntity[1]) {
|
||||
$this->entities[$this->currentEntity[0]] = $this->currentEntity[1];
|
||||
// Validate entity
|
||||
$entity = $this->currentEntity;
|
||||
if (empty($entity['default'])) {
|
||||
if ($this->warnIfEmpty) {
|
||||
trigger_error("Missing localisation default for key " . $this->currentEntityKey, E_USER_NOTICE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect plural forms
|
||||
$plurals = i18n::parse_plurals($entity['default']);
|
||||
if ($plurals) {
|
||||
unset($entity['default']);
|
||||
$entity = array_merge($entity, $plurals);
|
||||
}
|
||||
|
||||
// If only default is set, simplify
|
||||
if (count($entity) === 1 && !empty($entity['default'])) {
|
||||
$entity = $entity['default'];
|
||||
}
|
||||
|
||||
$this->entities[$this->currentEntityKey] = $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a template and returns any translatable entities
|
||||
*
|
||||
* @param string $template String to parse for translations
|
||||
* @param bool $warnIfEmpty Show warnings if default omitted
|
||||
* @return array Map of keys -> values
|
||||
*/
|
||||
public static function getTranslatables($template)
|
||||
public static function getTranslatables($template, $warnIfEmpty = true)
|
||||
{
|
||||
// Run the parser and throw away the result
|
||||
$parser = new Parser($template);
|
||||
$parser = new Parser($template, $warnIfEmpty);
|
||||
if (substr($template, 0, 3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
|
||||
$parser->pos = 3;
|
||||
}
|
||||
|
@ -49,6 +49,13 @@ class i18nTextCollector
|
||||
*/
|
||||
protected $defaultLocale;
|
||||
|
||||
/**
|
||||
* Trigger if warnings should be shown if default is omitted
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $warnOnEmptyDefault = false;
|
||||
|
||||
/**
|
||||
* The directory base on which the collector should act.
|
||||
* Usually the webroot set through {@link Director::baseFolder()}.
|
||||
@ -95,6 +102,7 @@ class i18nTextCollector
|
||||
: i18n::get_lang_from_locale(i18n::config()->get('default_locale'));
|
||||
$this->basePath = Director::baseFolder();
|
||||
$this->baseSavePath = Director::baseFolder();
|
||||
$this->setWarnOnEmptyDefault(i18n::config()->get('missing_default_warning'));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -418,20 +426,28 @@ class i18nTextCollector
|
||||
// @see CMSMenu::provideI18nEntities for an example usage
|
||||
foreach ($entitiesByModule[$module] as $fullName => $spec) {
|
||||
$specModule = $module;
|
||||
$specDefault = $spec;
|
||||
|
||||
// Rewrite spec if module is specified
|
||||
if (is_array($spec) && isset($spec['module'])) {
|
||||
$specModule = $spec['module'];
|
||||
$specDefault = $spec['default'];
|
||||
unset($spec['module']);
|
||||
|
||||
// If only element is defalt, simplify
|
||||
if (count($spec) === 1 && !empty($spec['default'])) {
|
||||
$spec = $spec['default'];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from source module
|
||||
if ($specModule !== $module) {
|
||||
unset($entitiesByModule[$module][$fullName]);
|
||||
}
|
||||
|
||||
// Write to target module
|
||||
if (!isset($entitiesByModule[$specModule])) {
|
||||
$entitiesByModule[$specModule] = [];
|
||||
}
|
||||
$entitiesByModule[$specModule][$fullName] = $specDefault;
|
||||
$entitiesByModule[$specModule][$fullName] = $spec;
|
||||
}
|
||||
}
|
||||
return $entitiesByModule;
|
||||
@ -532,30 +548,43 @@ class i18nTextCollector
|
||||
$tokens = token_get_all("<?php\n" . $content);
|
||||
$inTransFn = false;
|
||||
$inConcat = false;
|
||||
$finalTokenDueToArray = false;
|
||||
$inArrayClosedBy = false; // Set to the expected closing token, or false if not in array
|
||||
$currentEntity = array();
|
||||
foreach ($tokens as $token) {
|
||||
if (is_array($token)) {
|
||||
list($id, $text) = $token;
|
||||
|
||||
if ($inTransFn && $id == T_ARRAY) {
|
||||
//raw 'array' token found in _t function, stop processing the tokens for this _t now
|
||||
$finalTokenDueToArray = true;
|
||||
// Suppress tokenisation within array
|
||||
if ($inTransFn && !$inArrayClosedBy && $id == T_ARRAY) {
|
||||
$inArrayClosedBy = ')'; // Array will close with this element
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start definition
|
||||
if ($id == T_STRING && $text == '_t') {
|
||||
// start definition
|
||||
$inTransFn = true;
|
||||
} elseif ($inTransFn && (
|
||||
in_array($id, [T_VARIABLE, T_STATIC, T_CLASS_C]) ||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip rest of processing unless we are in a translation, and not inside a nested array
|
||||
if (!$inTransFn || $inArrayClosedBy) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If inside this translation, some elements might be unreachable
|
||||
if (in_array($id, [T_VARIABLE, T_STATIC, T_CLASS_C]) ||
|
||||
($id === T_STRING && in_array($text, ['self', 'static', 'parent']))
|
||||
)) {
|
||||
) {
|
||||
// Un-collectable strings such as _t(static::class.'.KEY').
|
||||
// Should be provided by i18nEntityProvider instead
|
||||
$inTransFn = false;
|
||||
$inArrayClosedBy = false;
|
||||
$inConcat = false;
|
||||
$currentEntity = array();
|
||||
} elseif ($inTransFn && $id == T_CONSTANT_ENCAPSED_STRING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($id == T_CONSTANT_ENCAPSED_STRING) {
|
||||
// Fixed quoting escapes, and remove leading/trailing quotes
|
||||
if (preg_match('/^\'/', $text)) {
|
||||
$text = str_replace("\\'", "'", $text);
|
||||
@ -578,30 +607,75 @@ class i18nTextCollector
|
||||
$currentEntity[] = $text;
|
||||
}
|
||||
}
|
||||
} elseif ($inTransFn && $token == '.') {
|
||||
continue; // is_array
|
||||
}
|
||||
|
||||
// Test we can close this array
|
||||
if ($inTransFn && $inArrayClosedBy && ($token === $inArrayClosedBy)) {
|
||||
$inArrayClosedBy = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continue only if in translation and not in array
|
||||
if (!$inTransFn || $inArrayClosedBy) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($token) {
|
||||
case '.':
|
||||
$inConcat = true;
|
||||
} elseif ($inTransFn && $token == ',') {
|
||||
break;
|
||||
case ',':
|
||||
$inConcat = false;
|
||||
} elseif ($inTransFn && ($token == ')' || $finalTokenDueToArray || $token == '[')) {
|
||||
break;
|
||||
case '[':
|
||||
// Enter array
|
||||
$inArrayClosedBy = ']';
|
||||
break;
|
||||
case ')':
|
||||
// finalize definition
|
||||
$inTransFn = false;
|
||||
$inConcat = false;
|
||||
// Only collect translations with default values provided
|
||||
// Ensure key is valid before saving
|
||||
if (!empty($currentEntity[0])) {
|
||||
$key = $currentEntity[0];
|
||||
$default = '';
|
||||
$comment = '';
|
||||
if (!empty($currentEntity[1])) {
|
||||
$entities[$currentEntity[0]] = $currentEntity[1];
|
||||
} elseif (!empty($currentEntity[0])) {
|
||||
// Add minor notice
|
||||
trigger_error("Missing localisation default for key ".$currentEntity[0], E_USER_NOTICE);
|
||||
$default = $currentEntity[1];
|
||||
if (!empty($currentEntity[2])) {
|
||||
$comment = $currentEntity[2];
|
||||
}
|
||||
}
|
||||
// Save in appropriate format
|
||||
if ($default) {
|
||||
$plurals = i18n::parse_plurals($default);
|
||||
// Use array form if either plural or metadata is provided
|
||||
if ($plurals) {
|
||||
$entity = $plurals;
|
||||
} elseif ($comment) {
|
||||
$entity = ['default' => $default];
|
||||
} else {
|
||||
$entity = $default;
|
||||
}
|
||||
if ($comment) {
|
||||
$entity['comment'] = $comment;
|
||||
}
|
||||
$entities[$key] = $entity;
|
||||
} elseif ($this->getWarnOnEmptyDefault()) {
|
||||
trigger_error("Missing localisation default for key " . $currentEntity[0], E_USER_NOTICE);
|
||||
}
|
||||
}
|
||||
$currentEntity = array();
|
||||
$finalTokenDueToArray = false;
|
||||
$inArrayClosedBy = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise all keys
|
||||
foreach ($entities as $key => $default) {
|
||||
foreach ($entities as $key => $entity) {
|
||||
unset($entities[$key]);
|
||||
$entities[$this->normalizeEntity($key, $module)] = $default;
|
||||
$entities[$this->normalizeEntity($key, $module)] = $entity;
|
||||
}
|
||||
ksort($entities);
|
||||
|
||||
@ -620,7 +694,7 @@ class i18nTextCollector
|
||||
public function collectFromTemplate($content, $fileName, $module, &$parsedFiles = array())
|
||||
{
|
||||
// use parser to extract <%t style translatable entities
|
||||
$entities = Parser::getTranslatables($content);
|
||||
$entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault());
|
||||
|
||||
// use the old method of getting _t() style translatable entities
|
||||
// Collect in actual template
|
||||
@ -675,15 +749,18 @@ class i18nTextCollector
|
||||
// Detect non-associative result for any key
|
||||
if (is_array($value) && $value === array_values($value)) {
|
||||
Deprecation::notice('5.0', 'Non-associative translations from providei18nEntities is deprecated');
|
||||
if (!empty($value[2])) {
|
||||
$provided[$key] = [
|
||||
$entity = array_filter([
|
||||
'default' => $value[0],
|
||||
'module' => $value[2],
|
||||
];
|
||||
} else {
|
||||
'comment' => isset($value[1]) ? $value[1] : null,
|
||||
'module' => isset($value[2]) ? $value[2] : null,
|
||||
]);
|
||||
if (count($entity) === 1) {
|
||||
$provided[$key] = $value[0];
|
||||
} elseif ($entity) {
|
||||
$provided[$key] = $entity;
|
||||
} else {
|
||||
unset($provided[$key]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
$entities = array_merge($entities, $provided);
|
||||
@ -777,4 +854,22 @@ class i18nTextCollector
|
||||
{
|
||||
$this->defaultLocale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getWarnOnEmptyDefault()
|
||||
{
|
||||
return $this->warnOnEmptyDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $warnOnEmptyDefault
|
||||
* @return $this
|
||||
*/
|
||||
public function setWarnOnEmptyDefault($warnOnEmptyDefault)
|
||||
{
|
||||
$this->warnOnEmptyDefault = $warnOnEmptyDefault;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -1956,56 +1956,80 @@ class i18n implements TemplateGlobalProvider
|
||||
);
|
||||
|
||||
/**
|
||||
* This is the main translator function. Returns the string defined by $class and $entity according to the
|
||||
* Map of rails plurals into standard order (fewest to most)
|
||||
* Note: Default locale only supplies one|other, but non-default locales
|
||||
* can specify custom plurals.
|
||||
*
|
||||
* @config
|
||||
* @var array
|
||||
*/
|
||||
private static $plurals = [
|
||||
'zero',
|
||||
'one',
|
||||
'two',
|
||||
'few',
|
||||
'many',
|
||||
'other',
|
||||
];
|
||||
|
||||
/**
|
||||
* Plural forms in default (en) locale
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $default_plurals = [
|
||||
'one',
|
||||
'other',
|
||||
];
|
||||
|
||||
/**
|
||||
* Warn if _t() invoked without a default.
|
||||
*
|
||||
* @config
|
||||
* @var bool
|
||||
*/
|
||||
private static $missing_default_warning = true;
|
||||
|
||||
/**
|
||||
* This is the main translator function. Returns the string defined by $entity according to the
|
||||
* currently set locale.
|
||||
*
|
||||
* Also supports pluralisation of strings. Pass in a `count` argument, as well as a
|
||||
* default value with `|` pipe-delimited options for each plural form.
|
||||
*
|
||||
* @param string $entity Entity that identifies the string. It must be in the form
|
||||
* "Namespace.Entity" where Namespace will be usually the class name where this
|
||||
* string is used and Entity identifies the string inside the namespace.
|
||||
* @param string $default The original string itself. In a usual call this is a
|
||||
* mandatory parameter, but if you are reusing a string which has already been
|
||||
* "declared" (using another call to this function, with the same class and entity),
|
||||
* you can omit it.
|
||||
* @param array $injection (optional) array of key value pairs that are used
|
||||
* to replace corresponding expressions in {curly brackets} in the $string.
|
||||
* The injection array can also be used as the their argument to the _t() function
|
||||
* @return string The translated string, according to the currently set locale {@link i18n::set_locale()}
|
||||
* @param mixed $arg,... Additional arguments are parsed as such:
|
||||
* - Next string argument is a default. Pass in a `|` pipe-delimited value with `{count}`
|
||||
* to do pluralisation.
|
||||
* - Any other string argument after default is context for i18nTextCollector
|
||||
* - Any array argument in any order is an injection parameter list. Pass in a `count`
|
||||
* injection parameter to pluralise.
|
||||
* @return string
|
||||
*/
|
||||
public static function _t($entity, $default = '', $injection = [])
|
||||
public static function _t($entity, $arg = null)
|
||||
{
|
||||
// Deprecate passing in injection as second param
|
||||
if (is_array($default)) {
|
||||
Deprecation::notice('5.0', 'Passing in $injection as second parameter is deprecated');
|
||||
$injection = $default;
|
||||
$default = '';
|
||||
// Detect args
|
||||
$default = null;
|
||||
$injection = [];
|
||||
foreach (array_slice(func_get_args(), 1) as $arg) {
|
||||
if (is_array($arg)) {
|
||||
$injection = $arg;
|
||||
} elseif (!isset($default)) {
|
||||
$default = $arg ?: '';
|
||||
}
|
||||
}
|
||||
|
||||
// Encourage the provision of default values so that text collector can discover new strings
|
||||
if (!$default) {
|
||||
user_error(
|
||||
"Localisation without a default is deprecated (key: $entity) and will be an exception in 5.0",
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
// Deprecate old $context param
|
||||
if (!is_array($injection)) {
|
||||
// Don't need to show warning if only mistake is passing in null instead of empty array
|
||||
if ($injection || func_num_args() > 3) {
|
||||
Deprecation::notice('5.0', '$context parameter is deprecated');
|
||||
}
|
||||
// Find best injection array
|
||||
if (func_num_args() > 3 && is_array(func_get_arg(3))) {
|
||||
$injection = func_get_arg(3);
|
||||
} else {
|
||||
$injection = [];
|
||||
}
|
||||
if (!$default && static::config()->get('missing_default_warning')) {
|
||||
user_error("Missing default for localisation key $entity", E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Deprecate legacy injection format (`string %s, %d`)
|
||||
// inject the variables from injectionArray (if present)
|
||||
$sprintfArgs = [];
|
||||
if ($default && $injection && !preg_match('/\{[\w\d]*\}/i', $default) && preg_match('/%[s,d]/', $default)) {
|
||||
if ($default && !preg_match('/\{[\w\d]*\}/i', $default) && preg_match('/%[s,d]/', $default)) {
|
||||
Deprecation::notice('5.0', 'sprintf style localisation variables are deprecated');
|
||||
$sprintfArgs = array_values($injection);
|
||||
$injection = [];
|
||||
@ -2019,8 +2043,20 @@ class i18n implements TemplateGlobalProvider
|
||||
$injection = [];
|
||||
}
|
||||
|
||||
// Detect plurals: Has a {count} argument as well as a `|` pipe delimited string (if provided)
|
||||
$isPlural = isset($injection['count']);
|
||||
$count = $isPlural ? $injection['count'] : null;
|
||||
// Refine check against default
|
||||
if ($isPlural && $default && !static::parse_plurals($default)) {
|
||||
$isPlural = false;
|
||||
}
|
||||
|
||||
// Pass back to translation backend
|
||||
if ($isPlural) {
|
||||
$result = static::getMessageProvider()->pluralise($entity, $default, $injection, $count);
|
||||
} else {
|
||||
$result = static::getMessageProvider()->translate($entity, $default, $injection);
|
||||
}
|
||||
|
||||
// Sometimes default is omitted, so we don't know we have %s injection format until after translation
|
||||
if (!$default && !preg_match('/\{[\w\d]*\}/i', $result) && preg_match('/%[s,d]/', $result)) {
|
||||
@ -2043,37 +2079,44 @@ class i18n implements TemplateGlobalProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluralise an item or items.
|
||||
* Split plural string into standard CLDR array form.
|
||||
* A string is considered a pluralised form if it has a {count} argument, and
|
||||
* a single `|` pipe-delimiting character.
|
||||
*
|
||||
* Yaml form of these strings should be set via the rails i18n standard format
|
||||
* http://guides.rubyonrails.org/i18n.html#pluralization. For example:
|
||||
* Note: Only splits in the default (en) locale as the string form contains limited metadata.
|
||||
*
|
||||
* <code>
|
||||
* en:
|
||||
* ChangeSet:
|
||||
* DESCRIPTION_ITEM_PLURALS:
|
||||
* one: 'one item'
|
||||
* other: '{count} items'
|
||||
* </code>
|
||||
*
|
||||
* Some languages support up to 6 plural forms:
|
||||
* @link http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
||||
*
|
||||
* @todo text collection support for pluralised strings
|
||||
*
|
||||
* @param string $entity Entity that identifies the string. It must be in the form
|
||||
* "Namespace.Entity" where Namespace will be usually the class name where this
|
||||
* string is used and Entity identifies the string inside the namespace.
|
||||
* Standard convention is to have a `Class.<FIELD>_PLURALS` key for a <Field> on a class
|
||||
* @param string|array $default If passed as a string, treated as a symfony format specifier.
|
||||
* If passed as an array, treated as a ruby i18n pluralised form.
|
||||
* @param int $count Number to pluralise against
|
||||
* @param array $injection Additional parameters
|
||||
* @return string Localised number
|
||||
* @param string $string Input string
|
||||
* @return array List of plural forms, or empty array if not plural
|
||||
*/
|
||||
public static function pluralise($entity, $default = '', $count = 0, $injection = [])
|
||||
public static function parse_plurals($string)
|
||||
{
|
||||
return static::getMessageProvider()->pluralise($entity, $default, $count, $injection);
|
||||
if (strstr($string, '|') && strstr($string, '{count}')) {
|
||||
$keys = i18n::config()->get('default_plurals');
|
||||
$values = explode('|', $string);
|
||||
if (count($keys) == count($values)) {
|
||||
return array_combine($keys, $values);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CLDR array plural form to `|` pipe-delimited string.
|
||||
* Unlike parse_plurals, this supports all locale forms (not just en)
|
||||
*
|
||||
* @param array $plurals
|
||||
* @return string Delimited string, or null if not plurals
|
||||
*/
|
||||
public static function encode_plurals($plurals)
|
||||
{
|
||||
// Validate against global plural list
|
||||
$forms = i18n::config()->get('plurals');
|
||||
$forms = array_combine($forms, $forms);
|
||||
$intersect = array_intersect_key($plurals, $forms);
|
||||
if ($intersect) {
|
||||
return implode('|', $intersect);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2452,7 +2495,6 @@ class i18n implements TemplateGlobalProvider
|
||||
'i18nLocale' => 'get_locale',
|
||||
'get_locale',
|
||||
'i18nScriptDirection' => 'get_script_direction',
|
||||
'pluralise',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,10 @@ class MemberAuthenticatorTest extends SapphireTest
|
||||
);
|
||||
$form->restoreFormState();
|
||||
$this->assertEmpty($result);
|
||||
$this->assertEquals(_t('Member.ERRORWRONGCRED'), $form->getMessage());
|
||||
$this->assertEquals(
|
||||
_t('Member.ERRORWRONGCRED', 'The provided details don\'t seem to be correct. Please try again.'),
|
||||
$form->getMessage()
|
||||
);
|
||||
$this->assertEquals(ValidationResult::TYPE_ERROR, $form->getMessageType());
|
||||
$this->assertEquals(ValidationResult::CAST_TEXT, $form->getMessageCast());
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace SilverStripe\i18n\Tests;
|
||||
|
||||
use SilverStripe\Dev\Debug;
|
||||
use SilverStripe\Dev\SapphireTest;
|
||||
use SilverStripe\i18n\Messages\YamlReader;
|
||||
|
||||
@ -27,11 +26,15 @@ class YamlReaderTest extends SapphireTest
|
||||
'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace',
|
||||
'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template',
|
||||
'i18nTestModule.SPRINTFNAMESPACE' => 'My replacement: %s',
|
||||
'i18nTestModule.PLURAL' => [
|
||||
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
|
||||
'i18nTestModule.PLURALS' => [
|
||||
'one' => 'A test',
|
||||
'other' => '{count} tests',
|
||||
],
|
||||
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
|
||||
'Month.PLURALS' => [
|
||||
'one' => 'A month',
|
||||
'other' => '{count} months',
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $output);
|
||||
}
|
||||
|
@ -15,25 +15,47 @@ class YamlWriterTest extends SapphireTest
|
||||
'Level1.Level2.EntityName' => 'Text',
|
||||
'Level1.OtherEntityName' => 'Other Text',
|
||||
'Level1.Plurals' => [
|
||||
'context' => 'Some ignored context',
|
||||
'one' => 'An item',
|
||||
'other' => '{count} items',
|
||||
],
|
||||
'Level1.PluralString1' => 'An item|{count} items',
|
||||
'Level1.PluralString2' => [
|
||||
'context' => 'Another ignored context',
|
||||
'default' => 'An item|{count} items',
|
||||
],
|
||||
// Some near-false-positives for plurals
|
||||
'Level1.NotPlural1' => 'Not a plural|string', // no count
|
||||
'Level1.NotPlural2' => 'Not|a|plural|string{count}', // unexpected number
|
||||
'Level1.NotPlural3' => 'Not a plural string {count}', // no pipe
|
||||
'Level1.BoolTest' => 'True',
|
||||
'Level1.FlagTest' => 'No',
|
||||
'Level1.TextTest' => 'Maybe',
|
||||
'Template.ss.Key' => 'Template var',
|
||||
'TopLevel' => 'The Top',
|
||||
];
|
||||
$yaml = <<<YAML
|
||||
de:
|
||||
Level1:
|
||||
BoolTest: 'True'
|
||||
FlagTest: 'No'
|
||||
Level2.EntityName: Text
|
||||
NotPlural1: 'Not a plural|string'
|
||||
NotPlural2: 'Not|a|plural|string{count}'
|
||||
NotPlural3: 'Not a plural string {count}'
|
||||
OtherEntityName: 'Other Text'
|
||||
PluralString1:
|
||||
one: 'An item'
|
||||
other: '{count} items'
|
||||
PluralString2:
|
||||
one: 'An item'
|
||||
other: '{count} items'
|
||||
Plurals:
|
||||
one: 'An item'
|
||||
other: '{count} items'
|
||||
BoolTest: 'True'
|
||||
FlagTest: 'No'
|
||||
TextTest: Maybe
|
||||
Template.ss:
|
||||
Key: 'Template var'
|
||||
TopLevel: 'The Top'
|
||||
|
||||
YAML;
|
||||
|
@ -49,6 +49,8 @@ class i18nTest extends SapphireTest
|
||||
'fr_FR',
|
||||
'de_AT',
|
||||
'de_DE',
|
||||
'ja_JP',
|
||||
'pl_PL',
|
||||
'es_AR',
|
||||
'es_ES',
|
||||
'mi_NZ',
|
||||
@ -147,6 +149,7 @@ class i18nTest extends SapphireTest
|
||||
public function testTemplateTranslation()
|
||||
{
|
||||
$oldLocale = i18n::get_locale();
|
||||
i18n::config()->update('missing_default_warning', false);
|
||||
|
||||
/** @var SymfonyMessageProvider $provider */
|
||||
$provider = Injector::inst()->get(MessageProvider::class);
|
||||
@ -322,6 +325,8 @@ class i18nTest extends SapphireTest
|
||||
* */
|
||||
public function testNewTemplateTranslation()
|
||||
{
|
||||
i18n::config()->update('missing_default_warning', false);
|
||||
|
||||
/** @var SymfonyMessageProvider $provider */
|
||||
$provider = Injector::inst()->get(MessageProvider::class);
|
||||
$provider->getTranslator()->addResource(
|
||||
@ -432,6 +437,46 @@ class i18nTest extends SapphireTest
|
||||
);
|
||||
}
|
||||
|
||||
public function pluralisationDataProvider()
|
||||
{
|
||||
return [
|
||||
// English - 2 plural forms
|
||||
['en_NZ', 0, '0 months'],
|
||||
['en_NZ', 1, 'A month'],
|
||||
['en_NZ', 2, '2 months'],
|
||||
['en_NZ', 5, '5 months'],
|
||||
['en_NZ', 10, '10 months'],
|
||||
// Polish - 4 plural forms
|
||||
['pl_PL', 0, '0 miesięcy'],
|
||||
['pl_PL', 1, '1 miesiąc'],
|
||||
['pl_PL', 2, '2 miesiące'],
|
||||
['pl_PL', 5, '5 miesięcy'],
|
||||
['pl_PL', 10, '10 miesięcy'],
|
||||
// Japanese - 1 plural form
|
||||
['ja_JP', 0, '0日'],
|
||||
['ja_JP', 1, '1日'],
|
||||
['ja_JP', 2, '2日'],
|
||||
['ja_JP', 5, '5日'],
|
||||
['ja_JP', 10, '10日'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider pluralisationDataProvider()
|
||||
* @param string $locale
|
||||
* @param int $count
|
||||
* @param string $expected
|
||||
*/
|
||||
public function testPluralisation($locale, $count, $expected)
|
||||
{
|
||||
i18n::set_locale($locale);
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
_t('Month.PLURALS', 'A month|{count} months', ['count' => $count]),
|
||||
"Plural form in locale $locale with count $count should be $expected"
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetLanguageName()
|
||||
{
|
||||
i18n::config()->update(
|
||||
|
@ -9,10 +9,12 @@ class i18nProviderClass implements i18nEntityProvider
|
||||
return [
|
||||
'i18nProviderClass.TITLE' => 'My Provider Class',
|
||||
'i18nProviderClass.PLURALS' => [
|
||||
'comment' => 'Plural forms for the test class',
|
||||
'one' => 'A class',
|
||||
'other' => '{count} classes',
|
||||
],
|
||||
'i18nProviderClass.OTHER_MODULE' => [
|
||||
'comment' => 'Test string in another module',
|
||||
'default' => 'i18ntestmodule string defined in i18nothermodule',
|
||||
'module' => 'i18ntestmodule'
|
||||
],
|
||||
|
@ -11,8 +11,12 @@ en:
|
||||
WITHNAMESPACE: Include Entity with Namespace
|
||||
LAYOUTTEMPLATE: Layout Template
|
||||
SPRINTFNAMESPACE: My replacement: %s
|
||||
PLURAL:
|
||||
PLURALS:
|
||||
one: 'A test'
|
||||
other: '{count} tests'
|
||||
i18nTestModuleInclude.ss:
|
||||
SPRINTFINCLUDENAMESPACE: My include replacement: %s
|
||||
Month:
|
||||
PLURALS:
|
||||
one: 'A month'
|
||||
other: '{count} months'
|
||||
|
@ -0,0 +1,4 @@
|
||||
ja:
|
||||
Month:
|
||||
PLURALS:
|
||||
other: '{count}日'
|
@ -0,0 +1,7 @@
|
||||
pl:
|
||||
Month:
|
||||
PLURALS:
|
||||
one: '1 miesiąc'
|
||||
few: '{count} miesiące'
|
||||
many: '{count} miesięcy'
|
||||
other: '{count} miesiąca'
|
@ -8,4 +8,5 @@
|
||||
<%t i18nTestModule.INJECTIONS_DOES_NOT_EXIST "Hello {name} {greeting}. But it is late, {goodbye}" name="Mark" greeting="welcome" goodbye="bye" %>
|
||||
<%t i18nTestModule.INJECTIONS "Hello {name} {greeting}. But it is late, {goodbye}" name="Paul" greeting="good you are here" goodbye="see you" %>
|
||||
<%t i18nTestModule.INJECTIONS "Hello {name} {greeting}. But it is late, {goodbye}" is "New context (this should be ignored)" name="Steffen" greeting="willkommen" goodbye="wiedersehen" %>
|
||||
<%t i18nTestModule.INJECTIONS "Hello {name} {greeting}. But it is late, {goodbye}" name=$absoluteBaseURL greeting=$get_locale goodbye="global calls" %>
|
||||
<%t i18nTestModule.INJECTIONS name="Cat" greeting='meow' goodbye="meow" %>
|
||||
<%t i18nTestModule.INJECTIONS name=$absoluteBaseURL greeting=$get_locale goodbye="global calls" %>
|
||||
|
@ -2,6 +2,6 @@
|
||||
$Layout
|
||||
lonely _t() call that should be ignored
|
||||
<% _t('i18nTestModule.NEWENTITY',"Not stored in master file yet") %>
|
||||
Single: $pluralise('i18nTestModule.PLURALS', 'An item|{count} items', 1)
|
||||
Multiple: $pluralise('i18nTestModule.PLURALS', 'An item|{count} items', 4)
|
||||
None: $pluralise('i18nTestModule.PLURALS', 'An item|{count} items', 0)
|
||||
Single: <%t i18nTestModule.PLURALS 'An item|{count} items' count=1 %>
|
||||
Multiple: <%t i18nTestModule.PLURALS 'An item|{count} items' count=4 %>
|
||||
None: <%t i18nTestModule.PLURALS 'An item|{count} items' count=0 %>
|
||||
|
@ -60,7 +60,10 @@ _t(
|
||||
PHP;
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'Test.CONCATENATED' => "Line 1 and Line '2' and Line \"3\"",
|
||||
'Test.CONCATENATED' => [
|
||||
'default' => "Line 1 and Line '2' and Line \"3\"",
|
||||
'comment' => 'Comment'
|
||||
],
|
||||
'Test.CONCATENATED2' => "Line \"4\" and Line 5"
|
||||
),
|
||||
$c->collectFromCode($php, 'mymodule')
|
||||
@ -70,6 +73,8 @@ PHP;
|
||||
public function testCollectFromNewTemplateSyntaxUsingParserSubclass()
|
||||
{
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false);
|
||||
|
||||
$html = <<<SS
|
||||
<% _t('Test.SINGLEQUOTE','Single Quote'); %>
|
||||
<%t i18nTestModule.NEWMETHODSIG "New _t method signature test" %>
|
||||
@ -78,6 +83,7 @@ PHP;
|
||||
<%t i18nTestModule.INJECTIONS_2 "Hello {name} {greeting}" is "context (ignored)" name="Steffen" greeting="Wilkommen" %>
|
||||
<%t i18nTestModule.INJECTIONS_3 name="Cat" greeting='meow' goodbye="meow" %>
|
||||
<%t i18nTestModule.INJECTIONS_4 name=\$absoluteBaseURL greeting=\$get_locale goodbye="global calls" %>
|
||||
<%t i18nTestModule.INJECTIONS_9 "An item|{count} items" is "Test Pluralisation" count=4 %>
|
||||
SS;
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test');
|
||||
|
||||
@ -87,10 +93,26 @@ SS;
|
||||
'i18nTestModule.NEWMETHODSIG' => "New _t method signature test",
|
||||
'i18nTestModule.INJECTIONS_0' => "Hello {name} {greeting}, and {goodbye}",
|
||||
'i18nTestModule.INJECTIONS_1' => "Hello {name} {greeting}, and {goodbye}",
|
||||
'i18nTestModule.INJECTIONS_2' => "Hello {name} {greeting}",
|
||||
'i18nTestModule.INJECTIONS_2' => [
|
||||
'default' => "Hello {name} {greeting}",
|
||||
'comment' => 'context (ignored)',
|
||||
],
|
||||
'i18nTestModule.INJECTIONS_9' => [
|
||||
'one' => 'An item',
|
||||
'other' => '{count} items',
|
||||
'comment' => 'Test Pluralisation'
|
||||
],
|
||||
],
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test')
|
||||
);
|
||||
|
||||
// Test warning is raised on empty default
|
||||
$c->setWarnOnEmptyDefault(true);
|
||||
$this->setExpectedException(
|
||||
PHPUnit_Framework_Error_Notice::class,
|
||||
'Missing localisation default for key i18nTestModule.INJECTIONS_3'
|
||||
);
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test');
|
||||
}
|
||||
|
||||
public function testCollectFromTemplateSimple()
|
||||
@ -125,6 +147,7 @@ SS;
|
||||
public function testCollectFromTemplateAdvanced()
|
||||
{
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false);
|
||||
|
||||
$html = <<<SS
|
||||
<% _t(
|
||||
@ -145,7 +168,10 @@ SS;
|
||||
) %>
|
||||
SS;
|
||||
$this->assertEquals(
|
||||
[ 'Test.PRIOANDCOMMENT' => ' Prio and Value with "Double Quotes"' ],
|
||||
[ 'Test.PRIOANDCOMMENT' => [
|
||||
'default' => ' Prio and Value with "Double Quotes"',
|
||||
'comment' => 'Comment with "Double Quotes"',
|
||||
]],
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test')
|
||||
);
|
||||
|
||||
@ -158,9 +184,29 @@ SS;
|
||||
) %>
|
||||
SS;
|
||||
$this->assertEquals(
|
||||
[ 'Test.PRIOANDCOMMENT' => " Prio and Value with 'Single Quotes'" ],
|
||||
[ 'Test.PRIOANDCOMMENT' => [
|
||||
'default' => " Prio and Value with 'Single Quotes'",
|
||||
'comment' => "Comment with 'Single Quotes'",
|
||||
]],
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test')
|
||||
);
|
||||
|
||||
// Test empty
|
||||
$html = <<<SS
|
||||
<% _t('Test.PRIOANDCOMMENT') %>
|
||||
SS;
|
||||
$this->assertEquals(
|
||||
[],
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test')
|
||||
);
|
||||
|
||||
// Test warning is raised on empty default
|
||||
$c->setWarnOnEmptyDefault(true);
|
||||
$this->setExpectedException(
|
||||
PHPUnit_Framework_Error_Notice::class,
|
||||
'Missing localisation default for key Test.PRIOANDCOMMENT'
|
||||
);
|
||||
$c->collectFromTemplate($html, 'mymodule', 'Test');
|
||||
}
|
||||
|
||||
|
||||
@ -209,7 +255,12 @@ _t(
|
||||
);
|
||||
PHP;
|
||||
$this->assertEquals(
|
||||
[ 'Test.PRIOANDCOMMENT' => ' Value with "Double Quotes"' ],
|
||||
[
|
||||
'Test.PRIOANDCOMMENT' => [
|
||||
'default' => ' Value with "Double Quotes"',
|
||||
'comment' => 'Comment with "Double Quotes"',
|
||||
]
|
||||
],
|
||||
$c->collectFromCode($php, 'mymodule')
|
||||
);
|
||||
|
||||
@ -222,7 +273,10 @@ _t(
|
||||
);
|
||||
PHP;
|
||||
$this->assertEquals(
|
||||
[ 'Test.PRIOANDCOMMENT' => " Value with 'Single Quotes'" ],
|
||||
[ 'Test.PRIOANDCOMMENT' => [
|
||||
'default' => " Value with 'Single Quotes'",
|
||||
'comment' => "Comment with 'Single Quotes'"
|
||||
] ],
|
||||
$c->collectFromCode($php, 'mymodule')
|
||||
);
|
||||
|
||||
@ -241,6 +295,8 @@ PHP;
|
||||
_t(
|
||||
'Test.PRIOANDCOMMENT',
|
||||
"Doublequoted Value with 'Unescaped Single Quotes'"
|
||||
|
||||
|
||||
);
|
||||
PHP;
|
||||
$this->assertEquals(
|
||||
@ -287,6 +343,7 @@ PHP;
|
||||
public function testCollectFromCodeNewSignature()
|
||||
{
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests
|
||||
|
||||
$php = <<<PHP
|
||||
_t('i18nTestModule.NEWMETHODSIG',"New _t method signature test");
|
||||
@ -295,25 +352,39 @@ _t('i18nTestModule.INJECTIONS2', "Hello {name} {greeting}. But it is late, {good
|
||||
_t("i18nTestModule.INJECTIONS3", "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
"New context (this should be ignored)",
|
||||
array("name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"));
|
||||
_t('i18nTestModule.INJECTIONS4', array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"));
|
||||
_t('i18nTestModule.INJECTIONS6', "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
["name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"]);
|
||||
_t("i18nTestModule.INJECTIONS7", "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
"New context (this should be ignored)",
|
||||
["name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"]);
|
||||
_t('i18nTestModule.INJECTIONS8', ["name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"]);
|
||||
_t('i18nTestModule.INJECTIONS9', "An item|{count} items", ['count' => 4], "Test Pluralisation");
|
||||
PHP;
|
||||
|
||||
$collectedTranslatables = $c->collectFromCode($php, 'mymodule');
|
||||
|
||||
$expectedArray = [
|
||||
'i18nTestModule.INJECTIONS2' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'i18nTestModule.INJECTIONS3' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'i18nTestModule.INJECTIONS3' => [
|
||||
'default' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'comment' => 'New context (this should be ignored)'
|
||||
],
|
||||
'i18nTestModule.INJECTIONS6' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'i18nTestModule.INJECTIONS7' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'i18nTestModule.INJECTIONS7' => [
|
||||
'default' => "Hello {name} {greeting}. But it is late, {goodbye}",
|
||||
'comment' => "New context (this should be ignored)",
|
||||
],
|
||||
'i18nTestModule.INJECTIONS9' => [
|
||||
'one' => 'An item',
|
||||
'other' => '{count} items',
|
||||
'comment' => 'Test Pluralisation',
|
||||
],
|
||||
'i18nTestModule.NEWMETHODSIG' => "New _t method signature test",
|
||||
];
|
||||
$this->assertEquals($expectedArray, $collectedTranslatables);
|
||||
|
||||
// Test warning is raised
|
||||
// Test warning is raised on empty default
|
||||
$this->setExpectedException(
|
||||
PHPUnit_Framework_Error_Notice::class,
|
||||
'Missing localisation default for key i18nTestModule.INJECTIONS4'
|
||||
@ -321,6 +392,7 @@ PHP;
|
||||
$php = <<<PHP
|
||||
_t('i18nTestModule.INJECTIONS4', array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"));
|
||||
PHP;
|
||||
$c->setWarnOnEmptyDefault(true);
|
||||
$c->collectFromCode($php, 'mymodule');
|
||||
}
|
||||
|
||||
@ -345,6 +417,7 @@ PHP;
|
||||
public function testCollectFromIncludedTemplates()
|
||||
{
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests
|
||||
|
||||
$templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss';
|
||||
$html = file_get_contents($templateFilePath);
|
||||
@ -419,6 +492,7 @@ PHP;
|
||||
public function testCollectMergesWithExisting()
|
||||
{
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false);
|
||||
$c->setWriter(new YamlWriter());
|
||||
$c->basePath = $this->alternateBasePath;
|
||||
$c->baseSavePath = $this->alternateBaseSavePath;
|
||||
@ -441,18 +515,22 @@ PHP;
|
||||
$entitiesByModule['i18ntestmodule']
|
||||
);
|
||||
$this->assertEquals(
|
||||
'i18ntestmodule string defined in i18nothermodule',
|
||||
[
|
||||
'comment' => 'Test string in another module',
|
||||
'default' => 'i18ntestmodule string defined in i18nothermodule',
|
||||
],
|
||||
$entitiesByModule['i18ntestmodule']['i18nProviderClass.OTHER_MODULE']
|
||||
);
|
||||
}
|
||||
|
||||
public function testCollectFromFilesystemAndWriteMasterTables()
|
||||
{
|
||||
$local = i18n::get_locale();
|
||||
i18n::set_locale('en_US'); //set the locale to the US locale expected in the asserts
|
||||
i18n::config()->update('default_locale', 'en_US');
|
||||
i18n::config()->update('missing_default_warning', false);
|
||||
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false);
|
||||
$c->setWriter(new YamlWriter());
|
||||
$c->basePath = $this->alternateBasePath;
|
||||
$c->baseSavePath = $this->alternateBaseSavePath;
|
||||
@ -564,8 +642,6 @@ PHP;
|
||||
" MAINTEMPLATE: 'Theme2 Main Template'\n",
|
||||
$theme2LangFileContent
|
||||
);
|
||||
|
||||
i18n::set_locale($local); //set the locale to the US locale expected in the asserts
|
||||
}
|
||||
|
||||
public function testCollectFromEntityProvidersInCustomObject()
|
||||
@ -574,6 +650,8 @@ PHP;
|
||||
$this->popManifests();
|
||||
|
||||
$c = i18nTextCollector::create();
|
||||
|
||||
// Collect from MyObject.php
|
||||
$filePath = __DIR__ . '/i18nTest/MyObject.php';
|
||||
$matches = $c->collectFromEntityProviders($filePath);
|
||||
$this->assertEquals(
|
||||
@ -593,6 +671,36 @@ PHP;
|
||||
);
|
||||
}
|
||||
|
||||
public function testCollectFromEntityProvidersInWebRoot()
|
||||
{
|
||||
// Collect from i18nProviderClass
|
||||
$c = i18nTextCollector::create();
|
||||
$c->setWarnOnEmptyDefault(false);
|
||||
$c->setWriter(new YamlWriter());
|
||||
$c->basePath = $this->alternateBasePath;
|
||||
$c->baseSavePath = $this->alternateBaseSavePath;
|
||||
$entitiesByModule = $c->collect(null, false);
|
||||
$this->assertEquals(
|
||||
[
|
||||
'comment' => 'Plural forms for the test class',
|
||||
'one' => 'A class',
|
||||
'other' => '{count} classes',
|
||||
],
|
||||
$entitiesByModule['i18nothermodule']['i18nProviderClass.PLURALS']
|
||||
);
|
||||
$this->assertEquals(
|
||||
'My Provider Class',
|
||||
$entitiesByModule['i18nothermodule']['i18nProviderClass.TITLE']
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
'comment' => 'Test string in another module',
|
||||
'default' => 'i18ntestmodule string defined in i18nothermodule',
|
||||
],
|
||||
$entitiesByModule['i18ntestmodule']['i18nProviderClass.OTHER_MODULE']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that duplicate keys are resolved to the appropriate modules
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user