diff --git a/composer.json b/composer.json index 053a9094f..185e092cd 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,8 @@ "symfony/yaml": "~2.7", "embed/embed": "^2.6", "swiftmailer/swiftmailer": "~5.4", - "symfony/config": "^2.8|^3", - "symfony/translation": "^2.8|^3" + "symfony/config": "^2.8|^3", + "symfony/translation": "^2.8|^3" }, "require-dev": { "phpunit/PHPUnit": "~4.8", diff --git a/docs/en/02_Developer_Guides/13_i18n/index.md b/docs/en/02_Developer_Guides/13_i18n/index.md index a51b86605..66e8c934c 100644 --- a/docs/en/02_Developer_Guides/13_i18n/index.md +++ b/docs/en/02_Developer_Guides/13_i18n/index.md @@ -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 @@ -234,19 +241,11 @@ Please ensure that any required plurals are exposed via provideI18nEntities. "Restored {value} successfully", 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 diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 7bb5648f3..2e78b427c 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -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 `.PLURALS`, and follows CLDR array form for each +key for an object plural form is `.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). #### 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 ### Email and Mailer diff --git a/lang/en.yml b/lang/en.yml index 2ab21a75b..a03ecf877 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -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: diff --git a/src/Core/Core.php b/src/Core/Core.php index 225177ecf..93105fc8e 100644 --- a/src/Core/Core.php +++ b/src/Core/Core.php @@ -143,14 +143,24 @@ function project() } /** - * @see i18n::_t() - * - * @param string $entity - * @param string $string - * @param array $injection - * @return string - */ -function _t($entity, $string = "", $injection = []) + * 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 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, $arg = null) { // Pass args directly to handle deprecation return call_user_func_array([i18n::class, '_t'], func_get_args()); diff --git a/src/Security/RememberLoginHash.php b/src/Security/RememberLoginHash.php index 35fe2fe31..e98deb71c 100644 --- a/src/Security/RememberLoginHash.php +++ b/src/Security/RememberLoginHash.php @@ -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( diff --git a/src/i18n/Messages/MessageProvider.php b/src/i18n/Messages/MessageProvider.php index c18369a60..a32a1a213 100644 --- a/src/i18n/Messages/MessageProvider.php +++ b/src/i18n/Messages/MessageProvider.php @@ -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); } diff --git a/src/i18n/Messages/Symfony/ModuleYamlLoader.php b/src/i18n/Messages/Symfony/ModuleYamlLoader.php index ecd1e7000..e16fd1d10 100644 --- a/src/i18n/Messages/Symfony/ModuleYamlLoader.php +++ b/src/i18n/Messages/Symfony/ModuleYamlLoader.php @@ -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]; } diff --git a/src/i18n/Messages/Symfony/SymfonyMessageProvider.php b/src/i18n/Messages/Symfony/SymfonyMessageProvider.php index 25c044ace..5a76841ae 100644 --- a/src/i18n/Messages/Symfony/SymfonyMessageProvider.php +++ b/src/i18n/Messages/Symfony/SymfonyMessageProvider.php @@ -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); diff --git a/src/i18n/Messages/YamlReader.php b/src/i18n/Messages/YamlReader.php index 2cbc952d2..c633aca53 100644 --- a/src/i18n/Messages/YamlReader.php +++ b/src/i18n/Messages/YamlReader.php @@ -67,6 +67,7 @@ class YamlReader implements Reader } } } + ksort($messages); return $messages; } } diff --git a/src/i18n/Messages/YamlWriter.php b/src/i18n/Messages/YamlWriter.php index 322cf034a..47648b7a4 100644 --- a/src/i18n/Messages/YamlWriter.php +++ b/src/i18n/Messages/YamlWriter.php @@ -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); + } } diff --git a/src/i18n/TextCollection/Parser.php b/src/i18n/TextCollection/Parser.php index d2febae90..43b9df3a9 100644 --- a/src/i18n/TextCollection/Parser.php +++ b/src/i18n/TextCollection/Parser.php @@ -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; } diff --git a/src/i18n/TextCollection/i18nTextCollector.php b/src/i18n/TextCollection/i18nTextCollector.php index 34a8d04e3..b272501ea 100644 --- a/src/i18n/TextCollection/i18nTextCollector.php +++ b/src/i18n/TextCollection/i18nTextCollector.php @@ -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(" $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(); + $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] = [ - 'default' => $value[0], - 'module' => $value[2], - ]; - } else { + $entity = array_filter([ + 'default' => $value[0], + '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; + } } diff --git a/src/i18n/i18n.php b/src/i18n/i18n.php index aeaaa83cf..f2935ff59 100644 --- a/src/i18n/i18n.php +++ b/src/i18n/i18n.php @@ -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 - $result = static::getMessageProvider()->translate($entity, $default, $injection); + 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. * - * - * en: - * ChangeSet: - * DESCRIPTION_ITEM_PLURALS: - * one: 'one item' - * other: '{count} items' - * - * - * 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._PLURALS` key for a 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', ); } diff --git a/tests/php/Security/MemberAuthenticatorTest.php b/tests/php/Security/MemberAuthenticatorTest.php index 50d19dbf8..893a3f670 100644 --- a/tests/php/Security/MemberAuthenticatorTest.php +++ b/tests/php/Security/MemberAuthenticatorTest.php @@ -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()); } diff --git a/tests/php/i18n/YamlReaderTest.php b/tests/php/i18n/YamlReaderTest.php index 55c748ca0..2f285723a 100644 --- a/tests/php/i18n/YamlReaderTest.php +++ b/tests/php/i18n/YamlReaderTest.php @@ -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); } diff --git a/tests/php/i18n/YamlWriterTest.php b/tests/php/i18n/YamlWriterTest.php index 7da982ebf..c8927336f 100644 --- a/tests/php/i18n/YamlWriterTest.php +++ b/tests/php/i18n/YamlWriterTest.php @@ -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 = <<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( diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18nothermodule/code/i18nProviderClass.php b/tests/php/i18n/i18nTest/_fakewebroot/i18nothermodule/code/i18nProviderClass.php index a978f91da..ef1bd581d 100644 --- a/tests/php/i18n/i18nTest/_fakewebroot/i18nothermodule/code/i18nProviderClass.php +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18nothermodule/code/i18nProviderClass.php @@ -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' ], diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/en.yml b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/en.yml index 66c49b332..f3ea1a627 100644 --- a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/en.yml +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/en.yml @@ -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' diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/ja.yml b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/ja.yml new file mode 100644 index 000000000..f778958a4 --- /dev/null +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/ja.yml @@ -0,0 +1,4 @@ +ja: + Month: + PLURALS: + other: '{count}日' diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/pl.yml b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/pl.yml new file mode 100644 index 000000000..399de32cc --- /dev/null +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/lang/pl.yml @@ -0,0 +1,7 @@ +pl: + Month: + PLURALS: + one: '1 miesiąc' + few: '{count} miesiące' + many: '{count} miesięcy' + other: '{count} miesiąca' diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss index aec117959..5e46088ce 100644 --- a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/Layout/i18nTestModule.ss @@ -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" %> diff --git a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss index 27ef419f2..e14ba28b8 100644 --- a/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss +++ b/tests/php/i18n/i18nTest/_fakewebroot/i18ntestmodule/templates/i18nTestModule.ss @@ -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 %> diff --git a/tests/php/i18n/i18nTextCollectorTest.php b/tests/php/i18n/i18nTextCollectorTest.php index 3d84f79bd..1a25b2a4b 100644 --- a/tests/php/i18n/i18nTextCollectorTest.php +++ b/tests/php/i18n/i18nTextCollectorTest.php @@ -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 = << <%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; $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; + $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 = <<"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 = <<"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 */