Merge pull request #6558 from open-sausages/pulls/4.0/i18n-symfony

API Replace i18n message localisation with symfony/translation
This commit is contained in:
Daniel Hensby 2017-01-26 13:11:59 +00:00 committed by GitHub
commit c81959cce0
73 changed files with 2581 additions and 4421 deletions

View File

@ -454,11 +454,10 @@ mappings:
i18nRailsYamlAdapter: SilverStripe\i18n\i18nRailsYamlAdapter
i18nSSLegacyAdapter: SilverStripe\i18n\i18nSSLegacyAdapter
i18nSSLegacyAdapter_Iterator: SilverStripe\i18n\i18nSSLegacyAdapter_Iterator
i18nTextCollector: SilverStripe\i18n\i18nTextCollector
i18nTextCollector_Writer: SilverStripe\i18n\i18nTextCollector_Writer
i18nTextCollector_Writer_Php: SilverStripe\i18n\i18nTextCollector_Writer_Php
i18nTextCollector_Writer_RailsYaml: SilverStripe\i18n\i18nTextCollector_Writer_RailsYaml
i18nTextCollector_Parser: SilverStripe\i18n\i18nTextCollector_Parser
i18nTextCollector: SilverStripe\i18n\TextCollection\i18nTextCollector
i18nTextCollector_Writer: SilverStripe\i18n\Messages\Writer
i18nTextCollector_Writer_RailsYaml: SilverStripe\i18n\Messages\YamlWriter
i18nTextCollector_Parser: SilverStripe\i18n\TextCollection\Parser
i18nTranslateAdapterInterface: SilverStripe\i18n\i18nTranslateAdapterInterface
SilverStripe\Framework\Logging\DebugViewFriendlyErrorFormatter: SilverStripe\Logging\DebugViewFriendlyErrorFormatter
SilverStripe\Framework\Logging\DetailedErrorFormatter: SilverStripe\Logging\DetailedErrorFormatter

View File

@ -14,7 +14,47 @@ SilverStripe\i18n\i18n:
module_priority:
- other_modules
---
name: i18nMessages
---
SilverStripe\Core\Injector\Injector:
# Custom yml loader for localisation messages
SilverStripe\i18n\Messages\Reader:
class: SilverStripe\i18n\Messages\YamlReader
SilverStripe\i18n\Messages\Writer:
class: SilverStripe\i18n\Messages\YamlWriter
Symfony\Component\Translation\Loader\LoaderInterface:
class: SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader
properties:
Reader: %$SilverStripe\i18n\Messages\Reader
# Ensure our cache respects ModuleYamlLoader's self-invalidation
# @see DirectoryListResource::isFresh()
# Note: This could be replaced with a more aggressive cache if necessary on a live environment
Symfony\Component\Config\ConfigCacheFactoryInterface:
class: Symfony\Component\Config\ResourceCheckerConfigCacheFactory
constructor:
0: [ %$Symfony\Component\Config\Resource\SelfCheckingResourceChecker ]
# Create default translator with standard cache path and our custom loader
Symfony\Component\Translation\TranslatorInterface:
class: Symfony\Component\Translation\Translator
constructor:
0: 'en'
1: null
2: `TEMP_FOLDER`
properties:
ConfigCacheFactory: %$Symfony\Component\Config\ConfigCacheFactoryInterface
calls:
FallbackLocales: [ setFallbackLocales, [['en']]]
Loader: [ addLoader, ['ss', %$Symfony\Component\Translation\Loader\LoaderInterface ]]
# Set this translator as our message provider for silverstripe's i18n
SilverStripe\i18n\Messages\MessageProvider:
class: SilverStripe\i18n\Messages\Symfony\SymfonyMessageProvider
properties:
Translator: %$Symfony\Component\Translation\TranslatorInterface
---
Name: textcollector
---
SilverStripe\Core\Injector\Injector:
SilverStripe\i18n\i18nTextCollector_Writer: SilverStripe\i18n\i18nTextCollector_Writer_RailsYaml
SilverStripe\i18n\TextCollection\i18nTextCollector:
properties:
Reader: %$SilverStripe\i18n\Messages\Reader
Writer: %$SilverStripe\i18n\Messages\Writer

View File

@ -428,7 +428,10 @@ class CMSMenu extends Object implements IteratorAggregate, i18nEntityProvider
foreach ($cmsClasses as $cmsClass) {
$defaultTitle = LeftAndMain::menu_title($cmsClass, false);
$ownerModule = i18n::get_owner_module($cmsClass);
$entities["{$cmsClass}.MENUTITLE"] = array($defaultTitle, 'Menu title', $ownerModule);
$entities["{$cmsClass}.MENUTITLE"] = [
'default' => $defaultTitle,
'module' => $ownerModule
];
}
return $entities;
}

View File

@ -464,7 +464,7 @@ abstract class ModelAdmin extends LeftAndMain
}
$fields = new FieldList(
new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $this->modelClass),
new HiddenField('ClassName', false, $this->modelClass),
new FileField('_CsvFile', false)
);

View File

@ -22,7 +22,9 @@
"league/flysystem": "~1.0.12",
"symfony/yaml": "~2.7",
"embed/embed": "^2.6",
"swiftmailer/swiftmailer": "~5.4"
"swiftmailer/swiftmailer": "~5.4",
"symfony/config": "^2.8|^3",
"symfony/translation": "^2.8|^3"
},
"require-dev": {
"phpunit/PHPUnit": "~4.8",

View File

@ -121,6 +121,21 @@ As well as properties, method calls can also be specified:
- [ pushHandler, [ %$DefaultHandler ] ]
## Using constants as variables
Any of the core constants can be used as a service argument by quoting with back ticks "`".
:::yaml
CachingService:
class: SilverStripe\Cache\CacheProvider
properties:
CacheDir: `TEMP_DIR`
Note: undefined variables will be replaced with null
## Factories
Some services require non-trivial construction which means they must be created by a factory class. To do this, create

View File

@ -166,12 +166,68 @@ All strings passed through the `_t()` function will be collected in a separate l
The `_t()` function is the main gateway to localized text, and takes four parameters, all but the first being optional.
It can be used to translate strings in both PHP files and template files. The usage for each case is described below.
* **$entity:** Unique identifier, composed by a namespace and an entity name, with a dot separating them. Both are arbitrary names, although by convention we use the name of the containing class or template. Use this identifier to reference the same translation elsewhere in your code.
* **$string:** (optional) The original language string to be translated. Only needs to be declared once, and gets 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.
* **$array::** (optional) An array of injecting variables into the second parameter
* **$entity:** Unique identifier, composed by a namespace and an entity name, with a dot
separating them. Both are arbitrary names, although by convention we use the name of
the containing class or template. Use this identifier to reference the same translation
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,
unlike English which has two only; One for the singular, and another for any other number.
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::_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
:::php
class MyObject extends DataObject, implements i18nEntityProvider
{
public function provideI18nEntities()
{
return [
'MyObject.SINGULAR_NAME' => 'object',
'MyObject.PLURAL_NAME' => 'objects',
'MyObject.PLURALS' => [
'one' => 'An object',
'other' => '{count} objects',
],
];
}
}
In YML format this will be expressed as the below. This follows the
[ruby i18n convention](guides.rubyonrails.org/i18n.html#pluralization) for plural forms.
:::yaml
en:
MyObject:
SINGULAR_NAME: 'object'
PLURAL_NAME: 'objects'
PLURALS:
one: 'An object',
other: '{count} objects'
Note: i18nTextCollector support for pluralisation is not yet available.
Please ensure that any required plurals are exposed via provideI18nEntities.
#### Usage in PHP Files
@ -180,16 +236,16 @@ to the translator.
// Simple string translation
_t('LeftAndMain.FILESIMAGES','Files & Images');
// Using the natural languate comment parameter to supply additional context information to translators
_t('LeftAndMain.HELLO','Site content','Menu title');
// Using injection to add variables into the translated strings.
_t('CMSMain.RESTORED',
"Restored {value} successfully",
'This is a message when restoring a broken part of the CMS',
array('value' => $itemRestored)
);
// Plurals are invoked via a `|` pipe-delimeter with a {count} argument
_t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => '$count ]);
#### Usage in Template Files
<div class="hint" markdown='1'>
@ -207,12 +263,13 @@ the PHP version of the function.
// Simple string translation
<%t Namespace.Entity "String to translate" %>
// Using the natural languate comment parameter to supply additional context information to translators
<%t SearchResults.NoResult "There are no results matching your query." is "A message displayed to users when the search produces no results." %>
// 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 %>
// 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
When caching a `<% loop %>` or `<% with %>` with `<%t params %>`. It is important to add the Locale to the cache key
@ -279,13 +336,14 @@ Each module can have one language table per locale, stored by convention in the
The translation is powered by [Zend_Translate](http://framework.zend.com/manual/current/en/modules/zend.i18n.translating.html),
which supports different translation adapters, dealing with different storage formats.
By default, SilverStripe 3.x uses a YAML format (through the [Zend_Translate_RailsYAML adapter](https://github.com/chillu/zend_translate_railsyaml)).
By default, SilverStripe uses a YAML format which is loaded via the
[symfony/translate](http://symfony.com/doc/current/translation.html) library.
Example: framework/lang/en.yml (extract)
en:
ImageUploader:
Attach: 'Attach %s'
Attach: 'Attach {title}'
UploadField:
NOTEADDFILES: 'You can add files once you have saved for the first time.'
@ -293,7 +351,7 @@ Translation table: framework/lang/de.yml (extract)
de:
ImageUploader:
ATTACH: '%s anhängen'
ATTACH: '{title} anhängen'
UploadField:
NOTEADDFILES: 'Sie können Dateien hinzufügen sobald Sie das erste mal gespeichert haben'
@ -301,24 +359,6 @@ Note that translations are cached across requests.
The cache can be cleared through the `?flush=1` query parameter,
or explicitly through `Zend_Translate::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL)`.
<div class="hint" markdown='1'>
The format of language definitions has changed significantly in since version 2.x.
</div>
In order to enable usage of [version 2.x style language definitions](http://doc.silverstripe.org/framework/en/2.4/topics/i18n#language-tables-in-php) in 3.x, you need to register a legacy adapter
in your `mysite/_config.php`:
:::php
i18n::register_translator(
new Zend_Translate(array(
'adapter' => 'i18nSSLegacyAdapter',
'locale' => i18n::default_locale(),
'disableNotices' => true,
)),
'legacy',
9 // priority lower than standard translator
);
## Javascript Usage
The i18n system in JavaScript is similar to its PHP equivalent.

View File

@ -15,6 +15,7 @@ guide developers in preparing existing 3.x code for compatibility with 4.0
* [ORM API](#overview-orm)
* [Filesystem API](#overview-filesystem)
* [Template and Form API](#overview-template)
* [i18n](#overview-i18n)
* [Email and Mailer](#overview-mailer)
* [Commit History](#commit-history)
@ -47,6 +48,7 @@ guide developers in preparing existing 3.x code for compatibility with 4.0
unless explicitly opted out.
* Themes are now configured to cascade, where you can specify a list of themes, and have the template engine
search programatically through a prioritised list when resolving template and CSS file paths.
* i18n Updated to use symfony/translation over zend Framework 1. Zend_Translate has been removed.
## <a name="upgrading"></a>Upgrading
@ -212,6 +214,94 @@ instead, or if used in an actual XML file use `.CDATA` (see [template casting](/
Where your code once used SQLQuery you should now use SQLSelect in all cases, as this has been removed (check the [3.2.0](3.2.0) upgrading notes).
#### Upgrade code that uses i18n
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 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 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.
Implementors of i18nEntityProvider should note that the return type for provideI18nEntities() has changed as well.
The non-associative array return type is deprecated. If returning a default string for a module
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 `<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.
The below demonstrates how you can provide new localisation strings for an object,
including both plurals and cross-module localisations.
:::php
class MyObject extends DataObject, implements i18nEntityProvider
{
public function provideI18nEntities()
{
return [
'MyObject.SINGULAR_NAME' => 'object',
'MyObject.PLURAL_NAME' => 'objects',
'MyObject.PLURALS' => [
'one' => 'An object',
'other' => '{count} objects',
],
'AnotherSection.DESCRIPTION' => [
'default' => 'This is the description for this section',
'module' => 'extendedmodule',
],
];
}
}
In YML format this will be expressed as the below:
`mymodule/lang/en.yml`:
:::yaml
en:
MyObject:
SINGULAR_NAME: 'object'
PLURAL_NAME: 'objects'
PLURALS:
one: 'An object',
other: '{count} objects'
`extendedmodule/lang/en.yml`:
:::yaml
en:
AnotherSection:
DESCRIPTION: 'This is the description for this section'
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 _t('MyObject.PLURALS', 'An object|{count} objects', [ 'count' => $count ]);
}
In templates this can also be invoked as below:
:::ss
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
#### New asset storage mechanism
File system has been abstracted into an abstract interface. By default, the out of the box filesystem
@ -1216,15 +1306,32 @@ handle field-level and form-level messages. This has the following properties:
* `Requirements::delete_combined_files()` and `Requirements::delete_combined_files()` methods have been removed
as they are obsolete.
### <a name="overview-i18n"></a>i18n API
#### <a name="overview-i18n-api"></a>i18n API Additions / Changes
* 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
map of key to default values.
* 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` 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
#### <a name="overview-orm-api"></a>Email Additions / Changes
* `Mailer` converted to an interface
* `SwfitMailer` added as new default mailer
* `Email` re-written to be powered by [SwiftMailer](https://github.com/swiftmailer/swiftmailer)
#### <a name="overview-orm-api"></a>Email Additions / Changes
* Default template body variable renamed from `$Body` to `$EmailContent`
* `$email->setTemplate()` renamed to `$email->setHTMLTemplate()`
* Added `$email->setPlainTemplate` for rendering plain versions of email

View File

@ -120,13 +120,15 @@ en:
PASSWORD: Password
ChangeSet:
DESCRIPTION_AND: '{first} and {second}'
DESCRIPTION_ITEM: item
DESCRIPTION_ITEMS: items
DESCRIPTION_ITEM_PLURALS:
one: 'one item'
other: '{count} items'
DESCRIPTION_LIST_FIRST: '{item}'
DESCRIPTION_LIST_LAST: '{list}, and {item}'
DESCRIPTION_LIST_MID: '{list}, {item}'
DESCRIPTION_OTHER_ITEM: 'other item'
DESCRIPTION_OTHER_ITEMS: 'other items'
DESCRIPTION_OTHER_ITEM_PLURALS:
one: 'one other item'
other: '{count} other items'
NAME: Name
PLURALNAME: Campaigns
SINGULARNAME: Campaign
@ -328,7 +330,7 @@ en:
CSSCLASSRIGHT: 'On the right, with text wrapping around.'
DETAILS: Details
EMAIL: 'Email address'
FILE: SilverStripe\\Assets\\File
FILE: File
FOLDER: Folder
IMAGEALT: 'Alternative text (alt)'
IMAGEALTTEXT: 'Alternative text (alt) - shown if image can''t be displayed'
@ -665,46 +667,88 @@ en:
MENUTITLE: Security
SilverStripe\Assets\File:
PLURALNAME: Files
PLURALS:
one: 'A File'
other: '{count} Files'
SINGULARNAME: File
SilverStripe\Assets\Folder:
PLURALNAME: Folders
PLURALS:
one: 'A Folder'
other: '{count} Folders'
SINGULARNAME: Folder
SilverStripe\Assets\Image:
PLURALNAME: Images
PLURALS:
one: 'An Image'
other: '{count} Images'
SINGULARNAME: Image
SilverStripe\ORM\DataObject:
PLURALNAME: 'Data Objects'
PLURALS:
one: 'A Data Object'
other: '{count} Data Objects'
SINGULARNAME: 'Data Object'
SilverStripe\ORM\Versioning\ChangeSet:
PLURALNAME: Campaigns
PLURALS:
one: 'A Campaign'
other: '{count} Campaigns'
SINGULARNAME: Campaign
SilverStripe\ORM\Versioning\ChangeSetItem:
PLURALNAME: 'Change Set Items'
PLURALS:
one: 'A Change Set Item'
other: '{count} Change Set Items'
SINGULARNAME: 'Change Set Item'
SilverStripe\Security\Group:
PLURALNAME: Groups
PLURALS:
one: 'A Group'
other: '{count} Groups'
SINGULARNAME: Group
SilverStripe\Security\LoginAttempt:
PLURALNAME: 'Login Attempts'
PLURALS:
one: 'A Login Attempt'
other: '{count} Login Attempts'
SINGULARNAME: 'Login Attempt'
SilverStripe\Security\Member:
PLURALNAME: Members
PLURALS:
one: 'A Member'
other: '{count} Members'
SINGULARNAME: Member
SilverStripe\Security\MemberPassword:
PLURALNAME: 'Member Passwords'
PLURALS:
one: 'A Member Password'
other: '{count} Member Passwords'
SINGULARNAME: 'Member Password'
SilverStripe\Security\Permission:
PLURALNAME: Permissions
PLURALS:
one: 'A Permission'
other: '{count} Permissions'
SINGULARNAME: Permission
SilverStripe\Security\PermissionRole:
PLURALNAME: Roles
PLURALS:
one: 'A Role'
other: '{count} Roles'
SINGULARNAME: Role
SilverStripe\Security\PermissionRoleCode:
PLURALNAME: 'Permission Role Codes'
PLURALS:
one: 'A Permission Role Code'
other: '{count} Permission Role Codes'
SINGULARNAME: 'Permission Role Code'
SilverStripe\Security\RememberLoginHash:
PLURALNAME: 'Remember Login Hashs'
SINGULARNAME: 'Remember Login Hash'
PLURALNAME: 'Login Hashes'
PLURALS:
one: 'A Login Hash'
other: '{count} Login Hashes'
SINGULARNAME: 'Login Hash'
SiteTree:
TABMAIN: Main
TableListField:

View File

@ -7,6 +7,7 @@ use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ConfigStaticManifest;
use SilverStripe\Core\Manifest\ConfigManifest;
use SilverStripe\Control\Director;
use SilverStripe\Dev\Deprecation;
use SilverStripe\i18n\i18n;
/**
@ -121,7 +122,7 @@ $errorHandler->start();
*/
function singleton($className)
{
if ($className === 'SilverStripe\\Core\\Config\\Config') {
if ($className === Config::class) {
throw new InvalidArgumentException("Don't pass Config to singleton()");
}
if (!isset($className)) {
@ -142,17 +143,27 @@ function project()
}
/**
* @see i18n::_t()
*
* @param string $entity
* @param string $string
* @param string $context
* @param array $injection
* @return string
*/
function _t($entity, $string = "", $context = "", $injection = null)
* 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)
{
return i18n::_t($entity, $string, $context, $injection);
// Pass args directly to handle deprecation
return call_user_func_array([i18n::class, '_t'], func_get_args());
}
/**

View File

@ -519,10 +519,17 @@ class Injector
return $newVal;
}
// Evaluate service references
if (is_string($value) && strpos($value, '%$') === 0) {
$id = substr($value, 2);
return $this->get($id);
}
// Evaluate constants surrounded by back ticks
if (preg_match('/^`(?<name>[^`]+)`$/', $value, $matches)) {
$value = defined($matches['name']) ? constant($matches['name']) : null;
}
return $value;
}

View File

@ -2,7 +2,6 @@
namespace SilverStripe\Dev;
use SilverStripe\Core\Config\Config;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Configurable;

View File

@ -6,7 +6,7 @@ use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\BuildTask;
use SilverStripe\i18n\i18nTextCollector;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
/**
* Collects i18n strings

View File

@ -560,7 +560,7 @@ class ConfirmedPasswordField extends FormField
{
/** @var ReadonlyField $field */
$field = $this->castedCopy('SilverStripe\\Forms\\ReadonlyField')
->setTitle($this->title ? $this->title : _t('Member.PASSWORD'))
->setTitle($this->title ? $this->title : _t('Member.PASSWORD', 'Password'))
->setValue('*****');
return $field;

View File

@ -212,7 +212,7 @@ class GridFieldFilterHeader implements GridField_HTMLProvider, GridField_DataMan
$fields->push(
GridField_FormAction::create($gridField, 'filter', false, 'filter', null)
->addExtraClass('btn font-icon-search btn--no-text btn--icon-large grid-field__filter-submit ss-gridfield-button-filter')
->setAttribute('title', _t('GridField.Filter', "Filter"))
->setAttribute('title', _t('GridField.Filter', 'Filter'))
->setAttribute('id', 'action_filter_' . $gridField->getModelClass() . '_' . $columnField)
);
$fields->push(

View File

@ -154,7 +154,7 @@ class HTMLEditorField extends TextareaField
*/
public function performReadonlyTransformation()
{
return $this->castedCopy('SilverStripe\\Forms\\HTMLEditor\\HTMLEditorField_Readonly');
return $this->castedCopy(HTMLEditorField_Readonly::class);
}
public function performDisabledTransformation()

View File

@ -104,6 +104,7 @@ class HTMLEditorField_Toolbar extends RequestHandler
* Return a {@link Form} instance allowing a user to
* add links in the TinyMCE content editor.
*
* @skipUpgrade
* @return Form
*/
public function LinkForm()
@ -111,7 +112,7 @@ class HTMLEditorField_Toolbar extends RequestHandler
$siteTree = TreeDropdownField::create(
'internal',
_t('HTMLEditorField.PAGE', "Page"),
'SilverStripe\\CMS\\Model\\SiteTree',
SiteTree::class,
'ID',
'MenuTitle',
true
@ -158,7 +159,7 @@ class HTMLEditorField_Toolbar extends RequestHandler
$siteTree,
TextField::create('external', _t('HTMLEditorField.URL', 'URL'), 'http://'),
EmailField::create('email', _t('HTMLEditorField.EMAIL', 'Email address')),
$fileField = UploadField::create('file', _t('HTMLEditorField.FILE', 'SilverStripe\\Assets\\File')),
$fileField = UploadField::create('file', _t('HTMLEditorField.FILE', 'File')),
TextField::create('Anchor', _t('HTMLEditorField.ANCHORVALUE', 'Anchor')),
TextField::create('Subject', _t('HTMLEditorField.SUBJECT', 'Email subject')),
TextField::create('Description', _t('HTMLEditorField.LINKDESCR', 'Link description')),
@ -229,7 +230,7 @@ class HTMLEditorField_Toolbar extends RequestHandler
$columns = $fileField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
$columns->setDisplayFields(array(
'StripThumbnail' => false,
'Title' => _t('File.Title'),
'Title' => _t('File.Title', 'Title'),
'Created' => File::singleton()->fieldLabel('Created'),
));
$columns->setFieldCasting(array(

View File

@ -674,16 +674,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* E.g. "0 Pages", "1 File", "3 Images"
*
* @param string $count
* @param bool $prependNumber Include number in result. Defaults to true.
* @return string
*/
public function i18n_pluralise($count, $prependNumber = true)
public function i18n_pluralise($count)
{
$default = 'one ' . $this->i18n_singular_name() . '|{count} ' . $this->i18n_plural_name();
return i18n::pluralise(
$this->i18n_singular_name(),
$this->i18n_plural_name(),
$count,
$prependNumber
static::class.'.PLURALS',
$default,
$count
);
}
@ -696,12 +695,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function singular_name()
{
if (!$name = $this->stat('singular_name')) {
$reflection = new \ReflectionClass($this);
$name = ucwords(trim(strtolower(preg_replace('/_?([A-Z])/', ' $1', $reflection->getShortName()))));
$name = $this->stat('singular_name');
if ($name) {
return $name;
}
return $name;
return ucwords(trim(strtolower(preg_replace(
'/_?([A-Z])/',
' $1',
ClassInfo::shortName($this)
))));
}
/**
@ -717,9 +719,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function i18n_singular_name()
{
// @todo Update localisation to FQN for all classes
$reflection = new \ReflectionClass($this);
return _t($reflection->getShortName().'.SINGULARNAME', $this->singular_name());
return _t(static::class.'.SINGULARNAME', $this->singular_name());
}
/**
@ -733,14 +733,13 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
{
if ($name = $this->stat('plural_name')) {
return $name;
} else {
$name = $this->singular_name();
//if the penultimate character is not a vowel, replace "y" with "ies"
if (preg_match('/[^aeiou]y$/i', $name)) {
$name = substr($name, 0, -1) . 'ie';
}
return ucfirst($name . 's');
}
$name = $this->singular_name();
//if the penultimate character is not a vowel, replace "y" with "ies"
if (preg_match('/[^aeiou]y$/i', $name)) {
$name = substr($name, 0, -1) . 'ie';
}
return ucfirst($name . 's');
}
/**
@ -755,10 +754,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
public function i18n_plural_name()
{
// @todo Update localisation to FQN for all classes
$name = $this->plural_name();
$reflection = new \ReflectionClass($this);
return _t($reflection->getShortName().'.PLURALNAME', $name);
return _t(static::class.'.PLURALNAME', $this->plural_name());
}
/**
@ -3740,33 +3736,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
*/
private static $summary_fields = null;
/**
* Collect all static properties on the object
* which contain natural language, and need to be translated.
* The full entity name is composed from the class name and a custom identifier.
*
* @return array A numerical array which contains one or more entities in array-form.
* Each numeric entity array contains the "arguments" for a _t() call as array values:
* $entity, $string, $priority, $context.
*/
public function provideI18nEntities()
{
$entities = array();
$entities["{$this->class}.SINGULARNAME"] = array(
$this->singular_name(),
'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
);
$entities["{$this->class}.PLURALNAME"] = array(
$this->plural_name(),
'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the'
. ' interface'
);
return $entities;
// Note: see http://guides.rubyonrails.org/i18n.html#pluralization for rules
// Best guess for a/an rule. Better guesses require overriding in subclasses
$pluralName = $this->plural_name();
$singularName = $this->singular_name();
$conjunction = preg_match('/^[aeiou]/i', $singularName) ? 'An ' : 'A ';
return [
static::class.'.SINGULARNAME' => $this->singular_name(),
static::class.'.PLURALNAME' => $pluralName,
static::class.'.PLURALS' => [
'one' => $conjunction . $singularName,
'other' => '{count} ' . $pluralName
]
];
}
/**

View File

@ -494,14 +494,14 @@ class ChangeSet extends DataObject
if ($countedOther) {
if ($counted) {
$parts[] = i18n::pluralise(
_t('ChangeSet.DESCRIPTION_OTHER_ITEM', 'other item'),
_t('ChangeSet.DESCRIPTION_OTHER_ITEMS', 'other items'),
'ChangeSet.DESCRIPTION_OTHER_ITEM_PLURALS',
'one other item|{count} other items',
$countedOther
);
} else {
$parts[] = i18n::pluralise(
_t('ChangeSet.DESCRIPTION_ITEM', 'item'),
_t('ChangeSet.DESCRIPTION_ITEMS', 'items'),
'ChangeSet.DESCRIPTION_ITEM_PLURALS',
'one item|{count} items',
$countedOther
);
}

View File

@ -83,7 +83,10 @@ class MemberAuthenticator extends Authenticator
$result = $member->checkPassword($data['Password']);
$success = $result->isValid();
} else {
$result = ValidationResult::create()->addError(_t('Member.ERRORWRONGCRED'));
$result = ValidationResult::create()->addError(_t(
'Member.ERRORWRONGCRED',
'The provided details don\'t seem to be correct. Please try again.'
));
}
// Emit failure to member and form (if available)

View File

@ -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(

View File

@ -43,7 +43,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
// Get all the exposed variables from all classes that implement the TemplateGlobalProvider interface
$this->createCallableArray(
self::$globalProperties,
"SilverStripe\\View\\TemplateGlobalProvider",
TemplateGlobalProvider::class,
"get_template_global_variables"
);
}
@ -55,7 +55,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
// //call non-statically
$this->createCallableArray(
self::$iteratorProperties,
"SilverStripe\\View\\TemplateIteratorProvider",
TemplateIteratorProvider::class,
"get_template_iterator_variables",
true
);
@ -82,11 +82,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
if (!is_array($details)) {
$details = array(
'method' => $details,
'casting' => Config::inst()->get(
'SilverStripe\\View\\ViewableData',
'default_cast',
Config::FIRST_SET
)
'casting' => ViewableData::config()->get('default_cast', Config::FIRST_SET)
);
}
@ -189,11 +185,7 @@ class SSViewer_DataPresenter extends SSViewer_Scope
// If not provided, use default
if (!$casting) {
$casting = Config::inst()->get(
'SilverStripe\\View\\ViewableData',
'default_cast',
Config::FIRST_SET
);
$casting = ViewableData::config()->get('default_cast', Config::FIRST_SET);
}
$obj = Injector::inst()->get($casting, false, array($property));

View File

@ -0,0 +1,30 @@
<?php
namespace SilverStripe\i18n\Messages;
/**
* Provides localisation of messages
*/
interface MessageProvider
{
/**
* Localise this message
*
* @param string $entity Identifier for this message in Namespace.key format
* @param string $default Default message
* @param array $injection List of injection variables
* @return string Localised string
*/
public function translate($entity, $default, $injection);
/**
* Pluralise a message
*
* @param string $entity Identifier for this message in Namespace.key format
* @param array|string $default Default message with pipe-separated delimiters, or array
* @param array $injection List of injection variables
* @param int $count Number to pluralise against
* @return string Localised string
*/
public function pluralise($entity, $default, $injection, $count);
}

View File

@ -0,0 +1,18 @@
<?php
namespace SilverStripe\i18n\Messages;
/**
* Message reader. Inverse of Writer
*/
interface Reader
{
/**
* Get messages from this locale
*
* @param string $locale
* @param string $path Filename (or other identifier)
* @return array messages Flat array of localisation keys to values.
*/
public function read($locale, $path);
}

View File

@ -0,0 +1,76 @@
<?php
namespace SilverStripe\i18n\Messages\Symfony;
use SilverStripe\Core\Flushable;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\SelfCheckingResourceInterface;
/**
* Some arbitrary resource which expires when flush is invoked.
* Uses a canary file to mark future freshness requests as stale.
*
* @link https://media.giphy.com/media/fRRD3T37DeY6Y/giphy.gif for use case
* @see DirectoryResource
*/
class FlushInvalidatedResource implements SelfCheckingResourceInterface, \Serializable, Flushable
{
public function __toString()
{
return md5(__CLASS__);
}
public function getResource()
{
// @deprecated at 3.0, do nothing
return null;
}
public function isFresh($timestamp)
{
// Check mtime of canary
$canary = static::canary();
if (file_exists($canary)) {
return filemtime($canary) < $timestamp;
}
// Rebuild canary
static::touch();
return false;
}
public function serialize()
{
return '';
}
public function unserialize($serialized)
{
// no-op
}
public static function flush()
{
// Mark canary as dirty
static::touch();
}
/**
* Path to i18n canary
*
* @return string
*/
protected static function canary()
{
return TEMP_FOLDER . '/catalog.i18n_canary';
}
/**
* Touch the canary
*/
protected static function touch()
{
touch(static::canary());
}
}

View File

@ -0,0 +1,126 @@
<?php
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;
/**
* Loads yaml localisations across all modules simultaneously.
* Note: This will also convert rails yml plurals into symfony standard format.
* Acts as a YamlFileLoader, but across a list of modules
*/
class ModuleYamlLoader extends ArrayLoader
{
/**
* Message reader
*
* @var Reader
*/
protected $reader = null;
public function load($resource, $locale, $domain = 'messages')
{
$messages = [];
foreach ($resource as $path) {
// Note: already-loaded messages have higher priority
$messages = array_merge(
$this->loadMessages($path, $locale),
$messages
);
}
ksort($messages);
$catalog = parent::load($messages, $locale, $domain);
// Ensure this catalog is invalidated on flush
$catalog->addResource(new FlushInvalidatedResource());
return $catalog;
}
/**
* @return Reader
*/
public function getReader()
{
return $this->reader;
}
/**
* @param Reader $reader
* @return $this
*/
public function setReader(Reader $reader)
{
$this->reader = $reader;
return $this;
}
/**
* Load messages
*
* @param string $path
* @param string $locale
* @return array
*/
protected function loadMessages($path, $locale)
{
$filePath = $path . $locale . '.yml';
$messages = $this->getReader()->read($locale, $filePath);
return $this->normaliseMessages($messages, $locale);
}
/**
* Normalises plurals in messages from rails-yaml format to symfony.
*
* @param array $messages List of messages
* @param string $locale
* @return array Normalised messages
*/
protected function normaliseMessages($messages, $locale)
{
foreach ($messages as $key => $value) {
if (is_array($value)) {
$messages[$key] = $this->normalisePlurals($key, $value, $locale);
}
}
return $messages;
}
/**
* Normalise rails-yaml plurals into pipe-separated rules
*
* @link http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
* @link http://guides.rubyonrails.org/i18n.html#pluralization
* @link http://symfony.com/doc/current/components/translation/usage.html#component-translation-pluralization
*
* @param string $key
* @param array $map
* @param string $locale
* @return string
*/
protected function normalisePlurals($key, $map, $locale)
{
$parts = [];
foreach (i18n::config()->get('plurals') as $form) {
if (isset($map[$form])) {
$parts[] = $map[$form];
}
}
// Non-associative plural, just keep in same order
if (empty($parts)) {
return $parts = $map;
}
// Warn if mismatched plural forms
if (count($map) !== count($parts)) {
trigger_error("Plural form {$locale}.{$key} has invalid plural keys", E_USER_WARNING);
}
return implode('|', $parts);
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace SilverStripe\i18n\Messages\Symfony;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\Messages\MessageProvider;
use Symfony\Component\Translation\Translator;
/**
* Implement message localisation using a symfony/translate backend
*/
class SymfonyMessageProvider implements MessageProvider
{
use Injectable;
use Configurable;
/**
* List of locales initialised
*
* @var array
*/
protected $loadedLocales = [];
/**
* @var Translator
*/
protected $translator = null;
/**
* List of source folder dirs to load yml localisations from
*
* @var array
*/
protected $sourceDirs = [];
/**
* @return Translator
*/
public function getTranslator()
{
return $this->translator;
}
/**
* @param Translator $translator
* @return $this
*/
public function setTranslator($translator)
{
$this->translator = $translator;
foreach ($translator->getFallbackLocales() as $locale) {
$this->load($locale);
}
return $this;
}
/**
* Load resources for the given locale
*
* @param string $locale
*/
protected function load($locale)
{
if (isset($this->loadedLocales[$locale])) {
return;
}
// Add full locale file. E.g. 'en_NZ'
$this
->getTranslator()
->addResource('ss', $this->getSourceDirs(), $locale);
// Add lang-only file. E.g. 'en'
$lang = i18n::get_lang_from_locale($locale);
if ($lang !== $locale) {
$this
->getTranslator()
->addResource('ss', $this->getSourceDirs(), $lang);
}
$this->loadedLocales[$locale] = true;
}
public function translate($entity, $default, $injection)
{
// Ensure localisation is ready
$locale = i18n::get_locale();
$this->load($locale);
// Prepare arguments
$arguments = $this->templateInjection($injection);
// Pass to symfony translator
$result = $this->getTranslator()->trans($entity, $arguments, 'messages', $locale);
// Manually inject default if no translation found
if ($entity === $result) {
$result = $this->getTranslator()->trans($default, $arguments, 'messages', $locale);
}
return $result;
}
public function pluralise($entity, $default, $injection, $count)
{
if (is_array($default)) {
$default = $this->normalisePlurals($default);
}
// Ensure localisation is ready
$locale = i18n::get_locale();
$this->load($locale);
// Prepare arguments
$arguments = $this->templateInjection(array_merge(
$injection,
[ 'count' => $count ]
));
// Pass to symfony translator
$result = $this->getTranslator()->transChoice($entity, $count, $arguments, 'messages', $locale);
// Manually inject default if no translation found
if ($entity === $result) {
$result = $this->getTranslator()->transChoice($default, $count, $arguments, 'messages', $locale);
}
return $result;
}
/**
* Get the list of /lang dirs to load localisations from
*
* @return array
*/
public function getSourceDirs()
{
if (!$this->sourceDirs) {
$this->setSourceDirs(i18n::get_lang_dirs());
}
return $this->sourceDirs;
}
/**
* Set the list of /lang dirs to load localisations from
*
* @param array $sourceDirs
* @return $this
*/
public function setSourceDirs($sourceDirs)
{
$this->sourceDirs = $sourceDirs;
return $this;
}
/**
* Generate template safe injection parameters
*
* @param array $injection
* @return array Injection array with all keys surrounded with {} placeholders
*/
protected function templateInjection($injection)
{
$injection = $injection ?: [];
// Rewrite injection to {} surrounded placeholders
$arguments = array_combine(
array_map(function ($val) {
return '{' . $val . '}';
}, array_keys($injection)),
$injection
);
return $arguments;
}
/**
* Convert ruby i18n plural form to symfony pipe-delimited form.
*
* @param array $parts
* @return array|string
*/
protected function normalisePlurals($parts)
{
return implode('|', $parts);
}
}

View File

@ -1,21 +1,20 @@
<?php
namespace SilverStripe\i18n;
namespace SilverStripe\i18n\Messages;
/**
* Allows serialization of entity definitions collected through {@link i18nTextCollector}
* into a persistent format, usually on the filesystem.
*/
interface i18nTextCollector_Writer
interface Writer
{
/**
* @param array $entities Map of entity names (incl. namespace) to an numeric array, with at
* least one element, the original string, and an optional second element, the context.
* @param array $messages Map of entity names (incl. namespace) to default values. Values
* may be array format for pluralised values, or strings for normal localisations.
* @param string $locale
* @param string $path The directory base on which the collector should create new lang folders
* and files. Usually the webroot set through {@link Director::baseFolder()}. Can be overwritten
* for testing or export purposes.
* @return bool success
*/
public function write($entities, $locale, $path);
public function write($messages, $locale, $path);
}

View File

@ -0,0 +1,73 @@
<?php
namespace SilverStripe\i18n\Messages;
use SilverStripe\Dev\Debug;
use Symfony\Component\Translation\Exception\InvalidResourceException;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
class YamlReader implements Reader
{
/**
* @var Parser
*/
protected $parser = null;
/**
* @return Parser
*/
protected function getParser()
{
if (!$this->parser) {
$this->parser = new Parser();
}
return $this->parser;
}
public function read($locale, $path)
{
try {
if (!file_exists($path)) {
return [];
}
// Load
$yaml = $this->getParser()->parse(file_get_contents($path));
if (empty($yaml[$locale])) {
return [];
}
// Normalise messages
return $this->normaliseMessages($yaml[$locale]);
} catch (ParseException $exception) {
var_dump($exception);
throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s"', $path), 0, $exception);
}
}
/**
* Flatten [class => [ key1 => value1, key2 => value2]] into [class.key1 => value1, class.key2 => value2]
*
* Inverse of YamlWriter::denormaliseMessages()
*
* @param array $entities
* @return mixed
*/
protected function normaliseMessages($entities)
{
$messages = [];
// Squash second and third levels together (class.key)
foreach ($entities as $class => $keys) {
// Check if namespace omits class
if (!is_array($keys)) {
$messages[$class] = $keys;
} else {
foreach ($keys as $key => $value) {
$fullKey = "{$class}.{$key}";
$messages[$fullKey] = $value;
}
}
}
ksort($messages);
return $messages;
}
}

View File

@ -0,0 +1,183 @@
<?php
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;
/**
* 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
{
/**
* @var Dumper
*/
protected $dumper = null;
/**
* @return Dumper
*/
protected function getDumper()
{
if (!$this->dumper) {
$this->dumper = new Dumper();
$this->dumper->setIndentation(2);
}
return $this->dumper;
}
public function write($messages, $locale, $path)
{
// Skip empty entities
if (empty($messages)) {
return;
}
// Create folder for lang files
$langFolder = $path . '/lang';
if (!file_exists($langFolder)) {
Filesystem::makeFolder($langFolder);
touch($langFolder . '/_manifest_exclude');
}
// De-normalise messages and convert to yml
$content = $this->getYaml($messages, $locale);
// Open the English file and write the Master String Table
$langFile = $langFolder . '/' . $locale . '.yml';
if ($fh = fopen($langFile, "w")) {
fwrite($fh, $content);
fclose($fh);
} else {
throw new LogicException("Cannot write language file! Please check permissions of $langFile");
}
}
/**
* Explodes [class.key1 => value1, class.key2 => value2] into [class => [ key1 => value1, key2 => value2]]
*
* Inverse of YamlReader::normaliseMessages()
*
* @param array $messages
* @return array
*/
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;
}
// 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
*
* @param array $messages
* @param string $locale
* @return string
*/
public function getYaml($messages, $locale)
{
$entities = $this->denormaliseMessages($messages);
$content = $this->getDumper()->dump([
$locale => $entities
], 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);
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace SilverStripe\i18n\TextCollection;
use SilverStripe\i18n\i18n;
use SilverStripe\View\SSTemplateParser;
/**
* Parser that scans through a template and extracts the parameters to the _t and <%t calls
*/
class Parser extends SSTemplateParser
{
/**
* List of all entities
*
* @var array
*/
protected $entities = [];
/**
* Current entity
*
* @var array
*/
protected $currentEntity = [];
/**
* Key of current entity
*
* @var 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 = [];
$this->currentEntityKey = null;
}
public function Translate_Entity(&$res, $sub)
{
$this->currentEntityKey = $sub['text']; // key
}
public function Translate_Default(&$res, $sub)
{
$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)
{
// 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, $warnIfEmpty = true)
{
// Run the parser and throw away the result
$parser = new Parser($template, $warnIfEmpty);
if (substr($template, 0, 3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
$parser->pos = 3;
}
$parser->match_TopTemplate();
return $parser->getEntities();
}
/**
* @return array
*/
public function getEntities()
{
return $this->entities;
}
}

View File

@ -1,14 +1,18 @@
<?php
namespace SilverStripe\i18n;
namespace SilverStripe\i18n\TextCollection;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Object;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Dev\Debug;
use SilverStripe\Control\Director;
use ReflectionClass;
use SilverStripe\Dev\Deprecation;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nEntityProvider;
use SilverStripe\i18n\Messages\Reader;
use SilverStripe\i18n\Messages\Writer;
/**
* SilverStripe-variant of the "gettext" tool:
@ -34,8 +38,9 @@ use ReflectionClass;
* @uses i18nEntityProvider
* @uses i18n
*/
class i18nTextCollector extends Object
class i18nTextCollector
{
use Injectable;
/**
* Default (master) locale
@ -44,6 +49,13 @@ class i18nTextCollector extends Object
*/
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()}.
@ -62,10 +74,17 @@ class i18nTextCollector extends Object
public $baseSavePath;
/**
* @var i18nTextCollector_Writer
* @var Writer
*/
protected $writer;
/**
* Translation reader
*
* @var Reader
*/
protected $reader;
/**
* List of file extensions to parse
*
@ -83,33 +102,53 @@ class i18nTextCollector extends Object
: i18n::get_lang_from_locale(i18n::config()->get('default_locale'));
$this->basePath = Director::baseFolder();
$this->baseSavePath = Director::baseFolder();
parent::__construct();
$this->setWarnOnEmptyDefault(i18n::config()->get('missing_default_warning'));
}
/**
* Assign a writer
*
* @param i18nTextCollector_Writer $writer
* @param Writer $writer
* @return $this
*/
public function setWriter($writer)
{
$this->writer = $writer;
return $this;
}
/**
* Gets the currently assigned writer, or the default if none is specified.
*
* @return i18nTextCollector_Writer
* @return Writer
*/
public function getWriter()
{
if (!$this->writer) {
$this->setWriter(Injector::inst()->get('SilverStripe\\i18n\\i18nTextCollector_Writer'));
}
return $this->writer;
}
/**
* Get reader
*
* @return Reader
*/
public function getReader()
{
return $this->reader;
}
/**
* Set reader
*
* @param Reader $reader
* @return $this
*/
public function setReader(Reader $reader)
{
$this->reader = $reader;
return $this;
}
/**
* This is the main method to build the master string tables with the
* original strings. It will search for existent modules that use the
@ -347,31 +386,19 @@ class i18nTextCollector extends Object
*/
protected function mergeWithExisting($entitiesByModule)
{
// TODO Support all defined source formats through i18n::get_translators().
// Currently not possible because adapter instances can't be fully reset through the Zend API,
// meaning master strings accumulate across modules
foreach ($entitiesByModule as $module => $entities) {
$adapter = Injector::inst()->create('SilverStripe\\i18n\\i18nRailsYamlAdapter');
$fileName = $adapter->getFilenameForLocale($this->defaultLocale);
$masterFile = "{$this->basePath}/{$module}/lang/{$fileName}";
if (!file_exists($masterFile)) {
continue;
}
// For each module do a simple merge of the default yml with these strings
foreach ($entitiesByModule as $module => $messages) {
// Load existing localisations
$masterFile = "{$this->basePath}/{$module}/lang/{$this->defaultLocale}.yml";
$existingMessages = $this->getReader()->read($this->defaultLocale, $masterFile);
$adapter->addTranslation(array(
'content' => $masterFile,
'locale' => $this->defaultLocale
));
$entitiesByModule[$module] = array_merge(
array_map(
// Transform each master string from scalar value to array of strings
function ($v) {
return array($v);
},
$adapter->getMessages($this->defaultLocale)
),
$entities
);
// Merge
if ($existingMessages) {
$entitiesByModule[$module] = array_merge(
$existingMessages,
$messages
);
}
}
return $entitiesByModule;
}
@ -395,18 +422,32 @@ class i18nTextCollector extends Object
$entitiesByModule[$module] = $processedEntities;
}
// extract all entities for "foreign" modules (fourth argument)
// Extract all entities for "foreign" modules ('module' key in array form)
// @see CMSMenu::provideI18nEntities for an example usage
foreach ($entitiesByModule[$module] as $fullName => $spec) {
if (!empty($spec[2]) && $spec[2] !== $module) {
$othermodule = $spec[2];
if (!isset($entitiesByModule[$othermodule])) {
$entitiesByModule[$othermodule] = array();
$specModule = $module;
// Rewrite spec if module is specified
if (is_array($spec) && isset($spec['module'])) {
$specModule = $spec['module'];
unset($spec['module']);
// If only element is defalt, simplify
if (count($spec) === 1 && !empty($spec['default'])) {
$spec = $spec['default'];
}
unset($spec[2]);
$entitiesByModule[$othermodule][$fullName] = $spec;
}
// Remove from source module
if ($specModule !== $module) {
unset($entitiesByModule[$module][$fullName]);
}
// Write to target module
if (!isset($entitiesByModule[$specModule])) {
$entitiesByModule[$specModule] = [];
}
$entitiesByModule[$specModule][$fullName] = $spec;
}
}
return $entitiesByModule;
@ -493,11 +534,12 @@ class i18nTextCollector extends Object
/**
* Extracts translatables from .php files.
* Note: Translations without default values are omitted.
*
* @param string $content The text content of a parsed template-file
* @param string $module Module's name or 'themes'. Could also be a namespace
* Generated by templates includes. E.g. 'UploadField.ss'
* @return array $entities An array of entities representing the extracted translation function calls in code
* @return array Map of localised keys to default values provided for this code
*/
public function collectFromCode($content, $module)
{
@ -506,29 +548,46 @@ class i18nTextCollector extends Object
$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 && $id == T_VARIABLE) {
// Dynamic definition from provideEntities - skip
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);
$text = str_replace("\\'", "'", $text);
$text = preg_replace('/^\'/', '', $text);
$text = preg_replace('/\'$/', '', $text);
} else {
@ -538,35 +597,85 @@ class i18nTextCollector extends Object
}
if ($inConcat) {
$currentEntity[count($currentEntity)-1] .= $text;
// Parser error
if (empty($currentEntity)) {
user_error('Error concatenating localisation key', E_USER_WARNING);
} else {
$currentEntity[count($currentEntity) - 1] .= $text;
}
} else {
$currentEntity[] = $text;
}
}
} elseif ($inTransFn && $token == '.') {
$inConcat = true;
} elseif ($inTransFn && $token == ',') {
$inConcat = false;
} elseif ($inTransFn && ($token == ')' || $finalTokenDueToArray || $token == '[')) {
// finalize definition
$inTransFn = false;
$inConcat = false;
$entity = array_shift($currentEntity);
$entities[$entity] = $currentEntity;
$currentEntity = array();
$finalTokenDueToArray = false;
continue; // is_array
}
}
foreach ($entities as $entity => $spec) {
// call without master language definition
if (!$spec) {
unset($entities[$entity]);
// Test we can close this array
if ($inTransFn && $inArrayClosedBy && ($token === $inArrayClosedBy)) {
$inArrayClosedBy = false;
continue;
}
unset($entities[$entity]);
$entities[$this->normalizeEntity($entity, $module)] = $spec;
// Continue only if in translation and not in array
if (!$inTransFn || $inArrayClosedBy) {
continue;
}
switch ($token) {
case '.':
$inConcat = true;
break;
case ',':
$inConcat = false;
break;
case '[':
// Enter array
$inArrayClosedBy = ']';
break;
case ')':
// finalize definition
$inTransFn = false;
$inConcat = false;
// Ensure key is valid before saving
if (!empty($currentEntity[0])) {
$key = $currentEntity[0];
$default = '';
$comment = '';
if (!empty($currentEntity[1])) {
$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();
$inArrayClosedBy = false;
break;
}
}
// Normalise all keys
foreach ($entities as $key => $entity) {
unset($entities[$key]);
$entities[$this->normalizeEntity($key, $module)] = $entity;
}
ksort($entities);
@ -585,7 +694,7 @@ class i18nTextCollector extends Object
public function collectFromTemplate($content, $fileName, $module, &$parsedFiles = array())
{
// use parser to extract <%t style translatable entities
$entities = i18nTextCollector_Parser::GetTranslatables($content);
$entities = Parser::getTranslatables($content, $this->getWarnOnEmptyDefault());
// use the old method of getting _t() style translatable entities
// Collect in actual template
@ -622,7 +731,7 @@ class i18nTextCollector extends Object
$classes = ClassInfo::classes_for_file($filePath);
foreach ($classes as $class) {
// Skip non-implementing classes
if (!class_exists($class) || !is_a($class, 'SilverStripe\\i18n\\i18nEntityProvider', true)) {
if (!class_exists($class) || !is_a($class, i18nEntityProvider::class, true)) {
continue;
}
@ -632,8 +741,29 @@ class i18nTextCollector extends Object
continue;
}
/** @var i18nEntityProvider $obj */
$obj = singleton($class);
$entities = array_merge($entities, (array)$obj->provideI18nEntities());
$provided = $obj->provideI18nEntities();
// Handle deprecated return syntax
foreach ($provided as $key => $value) {
// 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');
$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);
}
ksort($entities);
@ -724,4 +854,22 @@ class i18nTextCollector extends Object
{
$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;
}
}

View File

@ -3,25 +3,17 @@
namespace SilverStripe\i18n;
use SilverStripe\Control\Director;
use SilverStripe\Core\Cache;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Object;
use SilverStripe\Core\Flushable;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\Dev\Deprecation;
use SilverStripe\i18n\Messages\MessageProvider;
use SilverStripe\View\SSViewer;
use SilverStripe\View\TemplateGlobalProvider;
use Zend_Cache_Backend_ExtendedInterface;
use Zend_Cache;
use Zend_Cache_Core;
use Zend_Translate_Adapter;
use Zend_Translate;
use Zend_Locale_Data;
use Zend_Locale_Exception;
use SilverStripe\View\ThemeResourceLoader;
use InvalidArgumentException;
require_once 'Zend/Translate.php';
/**
* Base-class for storage and retrieval of translated entities.
*
@ -79,11 +71,15 @@ require_once 'Zend/Translate.php';
*
* @author Bernat Foj Capell <bernat@silverstripe.com>
*/
class i18n extends Object implements TemplateGlobalProvider, Flushable
class i18n implements TemplateGlobalProvider
{
use Injectable;
use Configurable;
/**
* This static variable is used to store the current defined locale.
*
* @var string
*/
protected static $current_locale = '';
@ -93,12 +89,6 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
*/
private static $default_locale = 'en_US';
/**
* @config
* @var boolean
*/
private static $js_i18n = true;
/**
* @config
* @var string
@ -112,38 +102,34 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
private static $time_format = 'H:mm';
/**
* @var array Array of priority keys to instances of Zend_Translate, mapped by name.
*/
protected static $translators;
/**
* Triggered early in the request when someone requests a flush.
*/
public static function flush()
{
$cache = self::get_cache();
$backend = $cache->getBackend();
if ($backend instanceof Zend_Cache_Backend_ExtendedInterface
&& ($capabilities = $backend->getCapabilities())
&& $capabilities['tags']
) {
$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $cache->getTags());
} else {
$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
}
}
/**
* Return an instance of the cache used for i18n data.
* List of prioritised modules, in lowest to highest priority.
*
* @skipUpgrade
* @return Zend_Cache_Core
* @config
* @var array
*/
public static function get_cache()
{
return Cache::factory('i18n', 'Output', array('lifetime' => null, 'automatic_serialization' => true));
}
private static $module_priority = [];
/**
* Config for ltr/rtr of specific locales.
* Will default to ltr.
*
* @config
* @var array
*/
private static $text_direction = [
'ar' => 'rtl',
'dv' => 'rtl',
'fa' => 'rtl',
'ha_Arab' => 'rtl',
'he' => 'rtl',
'ku' => 'rtl',
'pa_Arab' => 'rtl',
'ps' => 'rtl',
'syr' => 'rtl',
'ug' => 'rtl',
'ur' => 'rtl',
'uz_Arab' => 'rtl',
];
/**
* An exhaustive list of possible locales (code => language and country)
@ -1970,258 +1956,169 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
);
/**
* 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.
*
* @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 $string 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 string $context (optional) If the string can be difficult to translate by any reason, you can help
* translators with some more info using this param
* @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()}
* 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-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, $string = "", $context = "", $injection = null)
public static function _t($entity, $arg = null)
{
//fetch the injection array out of the parameters (if it is present)
$argList = func_get_args();
$argNum = func_num_args();
//_t($entity, $string = "", $context (optional), $injectionArray (optional))
$injectionArray = null;
for ($i = 0; $i < $argNum; $i++) {
if (is_array($argList[$i])) { //we have reached the injectionArray
$injectionArray = $argList[$i]; //any array in the args will be the injection array
// 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 ?: '';
}
}
// Find best translation
$locale = i18n::get_locale();
$returnValue = static::with_translators(function (Zend_Translate_Adapter $adapter) use ($entity, $locale) {
// Return translation only if we found a match thats not the entity itself (Zend fallback)
$translation = $adapter->translate($entity, $locale);
if ($translation && $translation != $entity) {
return $translation;
}
return null;
});
// Fall back to default string argument
if ($returnValue === null) {
$returnValue = (is_string($string)) ? $string : '';
// Encourage the provision of default values so that text collector can discover new strings
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)
if ($injectionArray) {
$regex = '/\{[\w\d]*\}/i';
if (!preg_match($regex, $returnValue)) {
// Legacy mode: If no injection placeholders are found,
// replace sprintf placeholders in fixed order.
// Fail silently in case the translation is outdated
preg_match_all('/%[s,d]/', $returnValue, $returnValueArgs);
if ($returnValueArgs) {
foreach ($returnValueArgs[0] as $i => $returnValueArg) {
if ($i >= count($injectionArray)) {
$injectionArray[] = '';
}
}
}
$replaced = vsprintf($returnValue, array_values($injectionArray));
if ($replaced) {
$returnValue = $replaced;
}
} elseif (!ArrayLib::is_associative($injectionArray)) {
// Legacy mode: If injection placeholders are found,
// but parameters are passed without names, replace them in fixed order.
$returnValue = preg_replace_callback(
$regex,
function () use (&$injectionArray) {
return $injectionArray ? array_shift($injectionArray) : '';
},
$returnValue
);
} else {
// Standard placeholder replacement with named injections and variable order.
foreach ($injectionArray as $variable => $injection) {
$placeholder = '{'.$variable.'}';
$returnValue = str_replace($placeholder, $injection, $returnValue, $count);
if (!$count) {
Injector::inst()->get('Logger')->log('notice', sprintf(
"Couldn't find placeholder '%s' in translation string '%s' (id: '%s')",
$placeholder,
$returnValue,
$entity
));
}
}
}
$sprintfArgs = [];
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 = [];
}
return $returnValue;
}
// If injection isn't associative, assume legacy injection format
$failUnlessSprintf = false;
if ($injection && array_values($injection) === $injection) {
$failUnlessSprintf = true; // Note: Will trigger either a deprecation error or exception below
$sprintfArgs = array_values($injection);
$injection = [];
}
/**
* Pluralise an item or items.
*
* @param string $singular Singular form
* @param string $plural Plural form
* @param int $number Number of items (natural number only)
* @param bool $prependNumber Include number in result
* @return string Result with the number and pluralised form appended. E.g. '1 page'
*/
public static function pluralise($singular, $plural, $number, $prependNumber = true)
{
$locale = static::get_locale();
$form = static::with_translators(
function (Zend_Translate_Adapter $adapter) use ($singular, $plural, $number, $locale) {
// Return translation only if we found a match thats not the entity itself (Zend fallback)
$result = $adapter->plural($singular, $plural, $number, $locale);
if ($result) {
return $result;
}
return null;
}
);
if ($prependNumber) {
return _t('i18n.PLURAL', '{number} {form}', [
'number' => $number,
'form' => $form
]);
// 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 {
return $form;
$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)) {
Deprecation::notice('5.0', 'sprintf style localisation is deprecated');
if ($injection) {
$sprintfArgs = array_values($injection);
}
} elseif ($failUnlessSprintf) {
// Note: After removing deprecated code, you can move this error up into the is-associative check
// Neither default nor translated strings were %s substituted, and our array isn't associative
throw new InvalidArgumentException('Injection must be an associative array');
}
// @deprecated (see above)
if ($sprintfArgs) {
return vsprintf($result, $sprintfArgs);
}
return $result;
}
/**
* Loop over all translators in order of precedence, and return the first non-null value
* returned via $callback
* 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.
*
* @param callable $callback Callback which is given the translator
* @return mixed First non-null result from $callback, or null if none matched
* Note: Only splits in the default (en) locale as the string form contains limited metadata.
*
* @param string $string Input string
* @return array List of plural forms, or empty array if not plural
*/
protected static function with_translators($callback)
public static function parse_plurals($string)
{
// get current locale (either default or user preference)
$locale = i18n::get_locale();
$lang = i18n::get_lang_from_locale($locale);
// Only call getter if static isn't already defined (for performance reasons)
$translatorsByPrio = self::$translators ?: self::get_translators();
foreach ($translatorsByPrio as $priority => $translators) {
/** @var Zend_Translate $translator */
foreach ($translators as $name => $translator) {
$adapter = $translator->getAdapter();
// at this point, we need to ensure the language and locale are loaded
// as include_by_locale() doesn't load a fallback.
// TODO Remove reliance on global state, by refactoring into an i18nTranslatorManager
// which is instanciated by core with a $clean instance variable.
if (!$adapter->isAvailable($lang)) {
i18n::include_by_locale($lang);
}
if (!$adapter->isAvailable($locale)) {
i18n::include_by_locale($locale);
}
$result = call_user_func($callback, $adapter);
if ($result !== null) {
return $result;
}
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);
}
}
// Nothing matched
return null;
}
/**
* @return array Array of priority keys to instances of Zend_Translate, mapped by name.
*/
public static function get_translators()
{
if (!Zend_Translate::getCache()) {
Zend_Translate::setCache(self::get_cache());
}
if (!self::$translators) {
$defaultPriority = 10;
self::$translators[$defaultPriority] = array(
'core' => new Zend_Translate(array(
'adapter' => 'SilverStripe\\i18n\\i18nRailsYamlAdapter',
'locale' => i18n::config()->get('default_locale'),
'disableNotices' => true,
))
);
i18n::include_by_locale('en');
i18n::include_by_locale('en_US');
}
return self::$translators;
return [];
}
/**
* @param String
* @return Zend_Translate
* 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 get_translator($name)
public static function encode_plurals($plurals)
{
foreach (self::get_translators() as $priority => $translators) {
if (isset($translators[$name])) {
return $translators[$name];
}
// 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;
}
/**
* @param Zend_Translate $translator Needs to implement {@link i18nTranslateAdapterInterface}
* @param string $name If left blank will override the default translator.
* @param int $priority
*/
public static function register_translator($translator, $name, $priority = 10)
{
if (!is_int($priority)) {
throw new InvalidArgumentException("register_translator expects an int priority");
}
// Ensure it's not there. If it is, we're replacing it. It may exist in a different priority.
self::unregister_translator($name);
// Add our new translator
if (!isset(self::$translators[$priority])) {
self::$translators[$priority] = array();
}
self::$translators[$priority][$name] = $translator;
// Resort array, ensuring highest priority comes first
krsort(self::$translators);
i18n::include_by_locale('en_US');
i18n::include_by_locale('en');
}
/**
* @param String
*/
public static function unregister_translator($name)
{
foreach (self::get_translators() as $priority => $translators) {
if (isset($translators[$name])) {
unset(self::$translators[$priority][$name]);
}
}
}
/**
* Get a list of commonly used languages
*
@ -2285,25 +2182,14 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
public static function get_existing_translations()
{
$locales = array();
// TODO Inspect themes
$modules = ClassLoader::instance()->getManifest()->getModules();
foreach ($modules as $module) {
if (!file_exists("{$module}/lang/")) {
continue;
}
foreach (static::get_lang_dirs() as $langPath) {
$allLocales = i18n::config()->get('all_locales');
$moduleLocales = scandir("{$module}/lang/");
foreach ($moduleLocales as $moduleLocale) {
$locale = pathinfo($moduleLocale, PATHINFO_FILENAME);
$ext = pathinfo($moduleLocale, PATHINFO_EXTENSION);
if ($locale && in_array($ext, array('php','yml'))) {
$langFiles = scandir($langPath);
foreach ($langFiles as $langFile) {
$locale = pathinfo($langFile, PATHINFO_FILENAME);
$ext = pathinfo($langFile, PATHINFO_EXTENSION);
if ($locale && $ext === 'yml') {
// Normalize locale to include likely region tag, avoid repetition in locale labels
// TODO Replace with CLDR list of actually available languages/regions
// Only allow explicitly registered locales, otherwise we'll get into trouble
// if the locale doesn't exist in Zend's CLDR data
$fullLocale = self::get_locale_from_lang($locale);
if (isset($allLocales[$fullLocale])) {
$locales[$fullLocale] = $allLocales[$fullLocale];
@ -2505,36 +2391,32 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
* Returns the script direction in format compatible with the HTML "dir" attribute.
*
* @see http://www.w3.org/International/tutorials/bidi-xhtml/
* @param String $locale Optional locale incl. region (underscored)
* @return String "rtl" or "ltr"
* @param string $locale Optional locale incl. region (underscored)
* @return string "rtl" or "ltr"
*/
public static function get_script_direction($locale = null)
{
require_once 'Zend/Locale/Data.php';
$dirs = static::config()->get('text_direction');
if (!$locale) {
$locale = i18n::get_locale();
}
try {
$dir = Zend_Locale_Data::getList($locale, 'layout');
} catch (Zend_Locale_Exception $e) {
$dir = Zend_Locale_Data::getList(i18n::get_lang_from_locale($locale), 'layout');
if (isset($dirs[$locale])) {
return $dirs[$locale];
}
return ($dir && $dir['characters'] == 'right-to-left') ? 'rtl' : 'ltr';
$lang = static::get_lang_from_locale($locale);
if (isset($dirs[$lang])) {
return $dirs[$lang];
}
return 'ltr';
}
/**
* Includes all available language files for a certain defined locale.
* Get sorted modules
*
* @param string $locale All resources from any module in locale $locale will be loaded
* @param Boolean $clean Clean old caches?
* @return array Array of module names -> path
*/
public static function include_by_locale($locale, $clean = false)
public static function get_sorted_modules()
{
if ($clean) {
self::flush();
}
// Get list of module => path pairs, and then just the names
$modules = ClassLoader::instance()->getManifest()->getModules();
$moduleNames = array_keys($modules);
@ -2545,7 +2427,7 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
array_splice($moduleNames, $idx, 1);
}
// Get the order from the config syste,
// Get the order from the config syste (lowest to highest)
$order = i18n::config()->get('module_priority');
// Find all modules that don't have their order specified by the config system
@ -2559,9 +2441,9 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
array_splice($order, 0, 0, $unspecified);
}
// Put the project module back in at the begining if it wasn't specified by the config system
// Put the project at end (highest priority)
if (!in_array($project, $order)) {
array_unshift($order, $project);
$order[] = $project;
}
$sortedModules = array();
@ -2571,90 +2453,40 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
}
}
$sortedModules = array_reverse($sortedModules, true);
// Loop in reverse order, meaning the translator with the highest priority goes first
$translatorsByPrio = array_reverse(self::get_translators(), true);
foreach ($translatorsByPrio as $priority => $translators) {
/** @var Zend_Translate $translator */
foreach ($translators as $name => $translator) {
/** @var i18nTranslateAdapterInterface|Zend_Translate_Adapter $adapter */
$adapter = $translator->getAdapter();
// Load translations from modules
foreach ($sortedModules as $module) {
$filename = $adapter->getFilenameForLocale($locale);
$filepath = "{$module}/lang/" . $filename;
if ($filename && !file_exists($filepath)) {
continue;
}
$adapter->addTranslation(
array('content' => $filepath, 'locale' => $locale)
);
}
// Load translations from themes
// TODO Replace with theme listing once implemented in TemplateManifest
$themesBase = Director::baseFolder() . '/themes';
if (is_dir($themesBase)) {
foreach (scandir($themesBase) as $theme) {
if (strpos($theme, Config::inst()->get('SilverStripe\\View\\SSViewer', 'theme')) === 0
&& file_exists("{$themesBase}/{$theme}/lang/")
) {
$filename = $adapter->getFilenameForLocale($locale);
$filepath = "{$themesBase}/{$theme}/lang/" . $filename;
if ($filename && !file_exists($filepath)) {
continue;
}
$adapter->addTranslation(
array('content' => $filepath, 'locale' => $locale)
);
}
}
}
// Add empty translations to ensure the locales are "registered" with isAvailable(),
// and the next invocation of include_by_locale() doesn't cause a new reparse.
$adapter->addTranslation(
array(
// Cached by content hash, so needs to be locale dependent
'content' => array($locale => $locale),
'locale' => $locale,
'usetranslateadapter' => true
)
);
}
}
return $sortedModules;
}
/**
* Given a class name (a "locale namespace"), will search for its module and, if available,
* will load the resources for the currently defined locale.
* If not available, the original English resource will be loaded instead (to avoid blanks)
* Find the list of prioritised /lang folders in this application
*
* @param string $class Resources for this class will be included, according to the set locale.
* @return array
*/
public static function include_by_class($class)
public static function get_lang_dirs()
{
$module = self::get_owner_module($class);
$paths = [];
$translatorsByPrior = array_reverse(self::get_translators(), true);
foreach ($translatorsByPrior as $priority => $translators) {
/** @var Zend_Translate $translator */
foreach ($translators as $name => $translator) {
/** @var i18nTranslateAdapterInterface|Zend_Translate_Adapter $adapter */
$adapter = $translator->getAdapter();
$filename = $adapter->getFilenameForLocale(self::get_locale());
$filepath = "{$module}/lang/" . $filename;
if ($filename && !file_exists($filepath)) {
continue;
}
$adapter->addTranslation(array(
'content' => $filepath,
'locale' => self::get_locale()
));
// Search sorted modules
foreach (static::get_sorted_modules() as $module => $path) {
$langPath = "{$path}/lang/";
if (is_dir($langPath)) {
$paths[] = $langPath;
}
}
// Search theme dirs
$locator = ThemeResourceLoader::instance();
foreach (SSViewer::get_themes() as $theme) {
if ($locator->getSet($theme)) {
continue;
}
$path = $locator->getPath($theme);
$langPath = "{$path}/lang/";
if (is_dir($langPath)) {
$paths[] = $langPath;
}
}
return $paths;
}
public static function get_template_global_variables()
@ -2665,4 +2497,12 @@ class i18n extends Object implements TemplateGlobalProvider, Flushable
'i18nScriptDirection' => 'get_script_direction',
);
}
/**
* @return MessageProvider
*/
public static function getMessageProvider()
{
return Injector::inst()->get(MessageProvider::class);
}
}

View File

@ -2,6 +2,8 @@
namespace SilverStripe\i18n;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
/**
* Dynamically provide translatable entites for the {@link i18n} logic.
* This is particularly handy for natural language strings in static variables
@ -22,53 +24,53 @@ interface i18nEntityProvider
{
/**
* Returns the list of provided translations for this object.
*
* Note: Pluralised forms are always returned in array format.
*
* Example usage:
* <code>
* class MyTestClass implements i18nEntityProvider {
* function provideI18nEntities() {
* $entities = array();
* foreach($this->stat('my_static_array) as $key => $value) {
* $entities["MyTestClass.my_static_array_{$key}"] = array(
* $value,
*
* 'My context description'
* );
* }
* return $entities;
* }
*
* public static function my_static_array() {
* $t_my_static_array = array();
* foreach(self::$my_static_array as $k => $v) {
* $t_my_static_array[$k] = _t("MyTestClass.my_static_array_{$key}", $v);
* }
* return $t_my_static_array;
* }
* class MyTestClass implements i18nEntityProvider
* {
* public function provideI18nEntities()
* {
* $entities = [];
* foreach($this->stat('my_static_array) as $key => $value) {
* $entities["MyTestClass.my_static_array_{$key}"] = $value;
* }
* $entities["MyTestClass.PLURALS"] = [
* 'one' => 'A test class',
* 'other' => '{count} test classes',
* ]
* return $entities;
* }
* }
* </code>
*
* Example usage in {@link DataObject->provideI18nEntities()}.
*
* You can ask textcollector to add the provided entity to a different module
* than the class is contained in by adding a 4th argument to the array:
* <code>
* class MyTestClass implements i18nEntityProvider {
* function provideI18nEntities() {
* $entities = array();
* $entities["MyOtherModuleClass.MYENTITY"] = array(
* $value,
* You can ask textcollector to add the provided entity to a different module.
* Simply wrap the returned value for any item in an array with the format:
* [ 'default' => $defaultValue, 'module' => $module ]
*
* 'My context description',
* 'myothermodule'
* );
* }
* return $entities;
* <code>
* class MyTestClass implements i18nEntityProvider
* {
* public function provideI18nEntities()
* {
* $entities = [
* 'MyOtherModuleClass.MYENTITY' => [
* 'default' => $value,
* 'module' => 'myothermodule',
* ]
* ];
* }
* return $entities;
* }
* </code>
*
* @return array All entites in an associative array, with
* entity name as the key, and a numerical array of pseudo-arguments
* for _t() as a value.
* @return array Map of keys to default values, which are strings in the default case,
* and array-form for pluralisations.
*/
public function provideI18nEntities();
}

View File

@ -1,21 +0,0 @@
<?php
namespace SilverStripe\i18n;
use Translate_Adapter_RailsYaml;
require_once 'Zend/Translate.php';
require_once 'zend_translate_railsyaml/library/Translate/Adapter/RailsYAML.php';
class i18nRailsYamlAdapter extends Translate_Adapter_RailsYaml implements i18nTranslateAdapterInterface
{
/**
* @param String
* @return String
*/
public function getFilenameForLocale($locale)
{
return "$locale.yml";
}
}

View File

@ -1,69 +0,0 @@
<?php
namespace SilverStripe\i18n;
use SilverStripe\View\SSTemplateParser;
/**
* Parser that scans through a template and extracts the parameters to the _t and <%t calls
*/
class i18nTextCollector_Parser extends SSTemplateParser
{
private static $entities = array();
private static $currentEntity = array();
public function __construct($string)
{
parent::__construct();
$this->string = $string;
$this->pos = 0;
$this->depth = 0;
$this->regexps = array();
}
public function Translate__construct(&$res)
{
self::$currentEntity = array(null, null, null); //start with empty array
}
public function Translate_Entity(&$res, $sub)
{
self::$currentEntity[0] = $sub['text']; //entity
}
public function Translate_Default(&$res, $sub)
{
self::$currentEntity[1] = $sub['String']['text']; //value
}
public function Translate_Context(&$res, $sub)
{
self::$currentEntity[2] = $sub['String']['text']; //comment
}
public function Translate__finalise(&$res)
{
// set the entity name and the value (default), as well as the context (comment)
// priority is no longer used, so that is blank
self::$entities[self::$currentEntity[0]] = array(self::$currentEntity[1], null, self::$currentEntity[2]);
}
/**
* Parses a template and returns any translatable entities
*/
public static function GetTranslatables($template)
{
self::$entities = array();
// Run the parser and throw away the result
$parser = new i18nTextCollector_Parser($template);
if (substr($template, 0, 3) == pack("CCC", 0xef, 0xbb, 0xbf)) {
$parser->pos = 3;
}
$parser->match_TopTemplate();
return self::$entities;
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace SilverStripe\i18n;
use SilverStripe\Assets\Filesystem;
use Symfony\Component\Yaml\Dumper;
use LogicException;
/**
* Writes files compatible with {@link i18nRailsYamlAdapter}.
*/
class i18nTextCollector_Writer_RailsYaml implements i18nTextCollector_Writer
{
public function write($entities, $locale, $path)
{
// Create folder for lang files
$langFolder = $path . '/lang';
if (!file_exists($langFolder)) {
Filesystem::makeFolder($langFolder);
touch($langFolder . '/_manifest_exclude');
}
// Open the English file and write the Master String Table
$langFile = $langFolder . '/' . $locale . '.yml';
if ($fh = fopen($langFile, "w")) {
fwrite($fh, $this->getYaml($entities, $locale));
fclose($fh);
} else {
throw new LogicException("Cannot write language file! Please check permissions of $langFile");
}
return true;
}
public function getYaml($entities, $locale)
{
// Unflatten array
$entitiesNested = array();
foreach ($entities as $entity => $spec) {
// Legacy support: Don't count *.ss as namespace
$entity = preg_replace('/\.ss\./', '___ss.', $entity);
$parts = explode('.', $entity);
$currLevel = &$entitiesNested;
while ($part = array_shift($parts)) {
$part = str_replace('___ss', '.ss', $part);
if (!isset($currLevel[$part])) {
$currLevel[$part] = array();
}
$currLevel = &$currLevel[$part];
}
$currLevel = $spec[0];
}
// Write YAML
$dumper = new Dumper();
$dumper->setIndentation(2);
// TODO Dumper can't handle YAML comments, so the context information is currently discarded
$result = $dumper->dump(array($locale => $entitiesNested), 99);
return $result;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace SilverStripe\i18n;
/**
* Makes the {@link Zend_Translate_Adapter} base class aware of file naming conventions within SilverStripe.
* Needs to be implemented by all translators used through {@link i18n::register_translator()}.
*
* A bit of context: Zend is file extension agnostic by default, and simply uses the filenames to detect locales
* with the 'scan' option, passing all files to the used adapter. We support multiple formats in the same /lang/
* folder, so need to be more selective about including files to avoid e.g. a YAML adapter trying to parse a PHP file.
*
* @see http://framework.zend.com/manual/en/zend.translate.additional.html#zend.translate.additional.combination
*/
interface i18nTranslateAdapterInterface
{
/**
* @param string $locale
* @return string
*/
public function getFilenameForLocale($locale);
}

View File

@ -208,6 +208,35 @@ class InjectorTest extends SapphireTest
$this->assertEquals('Three', $another->filters[2]);
}
public function testConstantUsage()
{
$injector = new Injector();
$services = array(
AnotherService::class => array(
'properties' => array(
'filters' => array(
'`BASE_PATH`',
'`TEMP_FOLDER`',
'`NOT_DEFINED`',
'THIRDPARTY_DIR' // Not back-tick escaped
)
),
)
);
$injector->load($services);
$another = $injector->get(AnotherService::class);
$this->assertEquals(
[
BASE_PATH,
TEMP_FOLDER,
null,
'THIRDPARTY_DIR',
],
$another->filters
);
}
public function testAutoSetInjector()
{
$injector = new Injector();

View File

@ -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());
}

View File

@ -0,0 +1,41 @@
<?php
namespace SilverStripe\i18n\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\Messages\YamlReader;
class YamlReaderTest extends SapphireTest
{
/**
*
*/
public function testRead()
{
$reader = new YamlReader();
$path = __DIR__ . '/i18nTest/_fakewebroot/i18ntestmodule/lang/en.yml';
$output = $reader->read('en', $path);
$expected = [
'NONAMESPACE' => 'Include Entity without Namespace',
'SPRINTFNONAMESPACE' => 'My replacement no namespace: %s',
'SPRINTFINCLUDENONAMESPACE' => 'My include replacement no namespace: %s',
'LAYOUTTEMPLATENONAMESPACE' => 'Layout Template no namespace',
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes"',
'i18nTestModule.ADDITION' => 'Addition',
'i18nTestModule.MAINTEMPLATE' => 'Main Template',
'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace',
'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template',
'i18nTestModule.SPRINTFNAMESPACE' => 'My replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
'i18nTestModule.PLURALS' => [
'one' => 'A test',
'other' => '{count} tests',
],
'Month.PLURALS' => [
'one' => 'A month',
'other' => '{count} months',
],
];
$this->assertEquals($expected, $output);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace SilverStripe\i18n\Tests;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\Messages\YamlWriter;
class YamlWriterTest extends SapphireTest
{
public function testYamlWriter()
{
$writer = new YamlWriter();
$entities = [
'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'
TextTest: Maybe
Template.ss:
Key: 'Template var'
TopLevel: 'The Top'
YAML;
$this->assertEquals($yaml, Convert::nl2os($writer->getYaml($entities, 'de')));
}
}

View File

@ -2,136 +2,30 @@
namespace SilverStripe\i18n\Tests;
use SilverStripe\Assets\Filesystem;
use InvalidArgumentException;
use SilverStripe\Control\Director;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nRailsYamlAdapter;
use SilverStripe\i18n\Tests\i18nTest\CustomTranslatorAdapter;
use SilverStripe\i18n\Tests\i18nTest\MyObject;
use SilverStripe\i18n\Tests\i18nTest\MySubObject;
use SilverStripe\i18n\Tests\i18nTest\OtherCustomTranslatorAdapter;
use SilverStripe\i18n\Tests\i18nTest\TestDataObject;
use SilverStripe\i18n\Tests\i18nTest\TestObject;
use SilverStripe\i18n\Messages\MessageProvider;
use SilverStripe\i18n\Messages\Symfony\SymfonyMessageProvider;
use SilverStripe\View\ArrayData;
use SilverStripe\View\SSViewer;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest;
use Zend_Translate;
require_once 'Zend/Translate.php';
class i18nTest extends SapphireTest
{
/**
* @var string $tmpBasePath Used to write language files.
* We don't want to store them inside framework (or in any web-accessible place)
* in case something goes wrong with the file parsing.
*/
protected $alternateBaseSavePath;
/**
* @var string $alternateBasePath Fake webroot with a single module
* /i18ntestmodule which contains some files with _t() calls.
*/
protected $alternateBasePath;
protected $extraDataObjects = [
TestDataObject::class
];
protected $preloadClasses = [
OtherCustomTranslatorAdapter::class,
CustomTranslatorAdapter::class,
TestObject::class,
MySubObject::class,
MyObject::class
];
use i18nTestManifest;
public function setUp()
{
parent::setUp();
// Force loading of classes before manifests potentially break autoloading
foreach ($this->preloadClasses as $class) {
if (!class_exists($class)) {
throw new \LogicException("Could not load class $class");
}
}
$s = DIRECTORY_SEPARATOR;
$this->alternateBasePath = __DIR__ . $s . 'i18nTest' . $s . "_fakewebroot";
$this->alternateBaseSavePath = TEMP_FOLDER . $s . 'i18nTextCollectorTest_webroot';
Filesystem::makeFolder($this->alternateBaseSavePath);
Director::config()->update('alternate_base_folder', $this->alternateBasePath);
// Replace old template loader with new one with alternate base path
$this->_oldLoader = ThemeResourceLoader::instance();
ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet(
'$default',
new ThemeManifest(
$this->alternateBasePath,
project(),
false,
true
)
);
SSViewer::config()->update('theme', 'testtheme1');
$this->originalLocale = i18n::get_locale();
// Override default adapter to avoid cached translations between tests.
// Emulates behaviour in i18n::get_translators()
$this->origAdapter = i18n::get_translator('core');
$adapter = new Zend_Translate(
array(
'adapter' => i18nRailsYamlAdapter::class,
'locale' => i18n::config()->get('default_locale'),
'disableNotices' => true,
)
);
i18n::register_translator($adapter, 'core');
$adapter->removeCache();
i18n::include_by_locale('en');
}
/**
* Number of test manifests
*
* @var int
*/
protected $manifests = 0;
/**
* Safely push a new class manifest.
* These will be cleaned up on tearDown()
*
* @param ClassManifest $manifest
*/
protected function pushManifest(ClassManifest $manifest)
{
$this->manifests++;
ClassLoader::instance()->pushManifest($manifest);
$this->setupManifest();
}
public function tearDown()
{
ThemeResourceLoader::set_instance($this->_oldLoader);
i18n::set_locale($this->originalLocale);
i18n::register_translator($this->origAdapter, 'core');
while ($this->manifests > 0) {
ClassLoader::instance()->popManifest();
$this->manifests--;
}
$this->tearDownManifest();
parent::tearDown();
}
@ -145,16 +39,24 @@ class i18nTest extends SapphireTest
public function testGetClosestTranslation()
{
// Validate necessary assumptions for this test
// As per set of locales loaded from _fakewebroot
$translations = i18n::get_existing_translations();
$this->assertTrue(isset($translations['en_US']));
$this->assertTrue(isset($translations['en_GB']));
$this->assertTrue(isset($translations['es_ES']));
$this->assertTrue(isset($translations['es_AR']));
$this->assertFalse(isset($translations['en_ZZ']));
$this->assertFalse(isset($translations['es_ZZ']));
$this->assertFalse(isset($translations['zz_ZZ']));
$this->assertEquals(
[
'en_GB',
'en_US',
'fr_FR',
'de_AT',
'de_DE',
'ja_JP',
'pl_PL',
'es_AR',
'es_ES',
'mi_NZ',
],
array_keys($translations)
);
// Test indeterminate locales
$this->assertEmpty(i18n::get_closest_translation('zz_ZZ'));
@ -172,57 +74,51 @@ class i18nTest extends SapphireTest
public function testDataObjectFieldLabels()
{
$oldLocale = i18n::get_locale();
i18n::set_locale('de_DE');
$obj = new i18nTest\TestDataObject();
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTest_DataObject.MyProperty' => 'MyProperty'
),
// Load into the translator as a literal array data source
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[ 'i18nTest_DataObject.MyProperty' => 'MyProperty' ],
'en_US'
);
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTest_DataObject.MyProperty' => 'Mein Attribut'
),
$provider->getTranslator()->addResource(
'array',
[ 'i18nTest_DataObject.MyProperty' => 'Mein Attribut' ],
'de_DE'
);
$provider->getTranslator()->addResource(
'array',
[ 'i18nTest_DataObject.MyUntranslatedProperty' => 'Mein Attribut' ],
'en_US'
);
// Test field labels
$obj = new i18nTest\TestDataObject();
$this->assertEquals(
$obj->fieldLabel('MyProperty'),
'Mein Attribut'
);
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTest_DataObject.MyUntranslatedProperty' => 'Mein Attribut'
),
'en_US'
);
$this->assertEquals(
$obj->fieldLabel('MyUntranslatedProperty'),
'My Untranslated Property'
);
i18n::set_locale($oldLocale);
}
public function testProvideI18nEntities()
{
$oldLocale = i18n::get_locale();
i18n::set_locale('en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTest_Object.MyProperty' => 'Untranslated'
),
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[ 'i18nTest_Object.MyProperty' => 'Untranslated' ],
'en_US'
);
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTest_Object.my_translatable_property' => 'Übersetzt'
),
$provider->getTranslator()->addResource(
'array',
[ 'i18nTest_Object.my_translatable_property' => 'Übersetzt' ],
'de_DE'
);
@ -253,25 +149,30 @@ class i18nTest extends SapphireTest
public function testTemplateTranslation()
{
$oldLocale = i18n::get_locale();
i18n::config()->update('missing_default_warning', false);
i18n::set_locale('en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.MAINTEMPLATE' => 'Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'My include replacement no namespace: %s'
),
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.MAINTEMPLATE' => 'Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'My include replacement no namespace: %s'
],
'en_US'
);
$viewer = new SSViewer('i18nTestModule');
$parsedHtml = Convert::nl2os($viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue'))));
$parsedHtml = Convert::nl2os($viewer->process(new ArrayData([
'TestProperty' => 'TestPropertyValue'
])));
$this->assertContains(
Convert::nl2os("Layout Template\n"),
$parsedHtml
@ -281,22 +182,24 @@ class i18nTest extends SapphireTest
$parsedHtml
);
i18n::set_locale('de_DE');
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.MAINTEMPLATE' => 'TRANS Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'TRANS My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'TRANS Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'TRANS Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'TRANS My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'TRANS Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'TRANS Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'TRANS My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'TRANS My include replacement no namespace: %s'
),
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.MAINTEMPLATE' => 'TRANS Main Template',
'i18nTestModule.ss.SPRINTFNONAMESPACE' => 'TRANS My replacement no namespace: %s',
'i18nTestModule.LAYOUTTEMPLATE' => 'TRANS Layout Template',
'i18nTestModule.ss.LAYOUTTEMPLATENONAMESPACE' => 'TRANS Layout Template no namespace',
'i18nTestModule.SPRINTFNAMESPACE' => 'TRANS My replacement: %s',
'i18nTestModule.WITHNAMESPACE' => 'TRANS Include Entity with Namespace',
'i18nTestModuleInclude.ss.NONAMESPACE' => 'TRANS Include Entity without Namespace',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENAMESPACE' => 'TRANS My include replacement: %s',
'i18nTestModuleInclude.ss.SPRINTFINCLUDENONAMESPACE' => 'TRANS My include replacement no namespace: %s',
'i18nTestModule.PLURALS' => 'An item|{count} items',
],
'de_DE'
);
i18n::set_locale('de_DE');
$viewer = new SSViewer('i18nTestModule');
$parsedHtml = Convert::nl2os($viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue'))));
$this->assertContains(
@ -331,35 +234,34 @@ class i18nTest extends SapphireTest
Convert::nl2os("TRANS My include replacement no namespace: TestPropertyValue\n"),
$parsedHtml
);
// Check plurals
$this->assertContains('Single: An item', $parsedHtml);
$this->assertContains('Multiple: 4 items', $parsedHtml);
$this->assertContains('None: 0 items', $parsedHtml);
i18n::set_locale($oldLocale);
}
public function testNewTMethodSignature()
{
global $lang;
$oldLocale = i18n::get_locale();
i18n::set_locale('en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test',
'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}',
'i18nTestModule.INJECTIONSLEGACY' => 'TRANS Hello %s %s. But it is late, %s',
),
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test',
'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}',
'i18nTestModule.INJECTIONSLEGACY' => 'TRANS Hello %s %s. But it is late, %s',
],
'en_US'
);
$entity = "i18nTestModule.INJECTIONS";
$default = "Hello {name} {greeting}. But it is late, {goodbye}";
$entityLegacy = 'i18nTestModule.INJECTIONSLEGACY';
$defaultLegacy = 'TRANS Hello %s %s. But it is late, %s';
$translated = i18n::_t('i18nTestModule.NEWMETHODSIG', "New _t method signature test");
$this->assertContains(
"TRANS New _t method signature test",
$translated
);
// Test missing entity key
$translated = i18n::_t(
$entity.'_DOES_NOT_EXIST',
$default,
@ -371,10 +273,11 @@ class i18nTest extends SapphireTest
"Testing fallback to the translation default (but using the injection array)"
);
// Test standard injection
$translated = i18n::_t(
$entity,
$default,
array("name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you")
["name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"]
);
$this->assertContains(
"TRANS Hello Paul good you are here. But it is late, see you",
@ -382,11 +285,12 @@ class i18nTest extends SapphireTest
"Testing entity, default string and injection array"
);
// @deprecated 5.0 Passing in context
$translated = i18n::_t(
$entity,
$default,
"New context (this should be ignored)",
array("name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen")
["name"=>"Steffen", "greeting"=>"willkommen", "goodbye"=>"wiedersehen"]
);
$this->assertContains(
"TRANS Hello Steffen willkommen. But it is late, wiedersehen",
@ -394,16 +298,12 @@ class i18nTest extends SapphireTest
"Full test of translation, using default, context and injection array"
);
$translated = i18n::_t($entity, array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"));
$this->assertContains(
"TRANS Hello Cat meow. But it is late, meow",
$translated,
"Testing a translation with just entity and injection array"
);
// @deprecated 5.0 Passing in % placeholders (detected in default value)
// Note: Missing-placeholder substitution no longer functions
$translated = i18n::_t(
'i18nTestModule.INJECTIONSLEGACY', // has %s placeholders
array("name"=>"Cat", "greeting2"=>"meow", "goodbye"=>"meow")
$entityLegacy, // has %s placeholders
$defaultLegacy,
["name"=>"Cat", "greeting2"=>"meow", "goodbye"=>"meow"]
);
$this->assertContains(
"TRANS Hello Cat meow. But it is late, meow",
@ -411,27 +311,13 @@ class i18nTest extends SapphireTest
"Testing sprintf placeholders with named injections"
);
$translated = i18n::_t(
'i18nTestModule.INJECTIONSLEGACY', // has %s placeholders
array("Cat", "meow"/*, "meow" */) // remove third arg
// Passing in non-associative arrays for placeholders is now an error
$this->setExpectedException(InvalidArgumentException::class, 'Injection must be an associative array');
i18n::_t(
$entity, // has {name} placeholders
$default,
["Cat", "meow", "meow"]
);
$this->assertContains(
"TRANS Hello Cat meow. But it is late, ",
$translated,
"Testing sprintf placeholders with unnamed injections and too few args"
);
$translated = i18n::_t(
'i18nTestModule.INJECTIONS', // has {name} placeholders
array("Cat", "meow", "meow")
);
$this->assertContains(
"TRANS Hello Cat meow. But it is late, meow",
$translated,
"Testing named injection placeholders with unnamed injections"
);
i18n::set_locale($oldLocale);
}
/**
@ -439,20 +325,21 @@ class i18nTest extends SapphireTest
* */
public function testNewTemplateTranslation()
{
global $lang;
$oldLocale = i18n::get_locale();
i18n::config()->update('missing_default_warning', false);
i18n::set_locale('en_US');
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test',
'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}'
),
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.NEWMETHODSIG' => 'TRANS New _t method signature test',
'i18nTestModule.INJECTIONS' => 'TRANS Hello {name} {greeting}. But it is late, {goodbye}'
],
'en_US'
);
$viewer = new SSViewer('i18nTestModule');
$parsedHtml = Convert::nl2os($viewer->process(new ArrayData(array('TestProperty' => 'TestPropertyValue'))));
$parsedHtml = Convert::nl2os($viewer->process(new ArrayData(['TestProperty' => 'TestPropertyValue'])));
$this->assertContains(
Convert::nl2os("Hello Mark welcome. But it is late, bye\n"),
$parsedHtml,
@ -465,12 +352,6 @@ class i18nTest extends SapphireTest
"Testing entity, default string and injection array"
);
$this->assertContains(
Convert::nl2os("TRANS Hello Cat meow. But it is late, meow\n"),
$parsedHtml,
"Testing a translation with just entity and injection array"
);
//test injected calls
$this->assertContains(
Convert::nl2os(
@ -479,8 +360,6 @@ class i18nTest extends SapphireTest
$parsedHtml,
"Testing a translation with just entity and injection array, but with global variables injected in"
);
i18n::set_locale($oldLocale);
}
public function testGetLocaleFromLang()
@ -501,233 +380,101 @@ class i18nTest extends SapphireTest
public function testTranslate()
{
$oldLocale = i18n::get_locale();
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes"',
),
/** @var SymfonyMessageProvider $provider */
$provider = Injector::inst()->get(MessageProvider::class);
$provider->getTranslator()->addResource(
'array',
[ 'i18nTestModule.ENTITY' => 'Entity with "Double Quotes"' ],
'en_US'
);
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de)',
'i18nTestModule.ADDITION' => 'Addition (de)',
),
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de)',
'i18nTestModule.ADDITION' => 'Addition (de)',
],
'de'
);
i18n::get_translator('core')->getAdapter()->addTranslation(
array(
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de_AT)',
),
$provider->getTranslator()->addResource(
'array',
[
'i18nTestModule.ENTITY' => 'Entity with "Double Quotes" (de_AT)',
],
'de_AT'
);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'Entity with "Double Quotes"',
i18n::_t('i18nTestModule.ENTITY', 'Ignored default'),
'Returns translation in default language'
);
i18n::set_locale('de');
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'Entity with "Double Quotes" (de)',
i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'),
'Returns translation according to current locale'
);
i18n::set_locale('de_AT');
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'Entity with "Double Quotes" (de_AT)',
i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'),
'Returns specific regional translation if available'
);
$this->assertEquals(
i18n::_t('i18nTestModule.ADDITION'),
'Addition (de)',
i18n::_t('i18nTestModule.ADDITION', 'Addition'),
'Returns fallback non-regional translation if regional is not available'
);
i18n::set_locale('fr');
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'',
'Returns empty translation without default string if locale is not found'
);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY', 'default'),
'default',
'Returns default string if locale is not found'
);
i18n::set_locale($oldLocale);
}
public function testIncludeByLocale()
{
// Looping through modules, so we can test the translation autoloading
// Load non-exclusive to retain core class autoloading
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false);
$this->pushManifest($classManifest);
$adapter = i18n::get_translator('core')->getAdapter();
$this->assertTrue($adapter->isAvailable('en'));
$this->assertFalse($adapter->isAvailable('de'));
$this->assertFalse(
$adapter->isTranslated('i18nTestModule.ENTITY', 'de'),
'Existing unloaded entity not available before call'
);
$this->assertFalse(
$adapter->isTranslated('i18nTestModule.ENTITY', 'af'),
'Non-existing unloaded entity not available before call'
);
// set _fakewebroot module priority
i18n::config()->update('module_priority', array('subfolder','i18ntestmodule'));
i18n::include_by_locale('de');
$this->assertTrue($adapter->isAvailable('en'));
$this->assertTrue($adapter->isAvailable('de'));
$this->assertTrue($adapter->isTranslated('i18nTestModule.ENTITY', null, 'de'), 'Includes module files');
$this->assertTrue($adapter->isTranslated('i18nTestTheme1.LAYOUTTEMPLATE', null, 'de'), 'Includes theme files');
$this->assertTrue($adapter->isTranslated('i18nTestModule.OTHERENTITY', null, 'de'), 'Includes submodule files');
// check module priority
$this->assertEquals(
$adapter->translate('i18nTestModule.PRIORITYNOTICE', 'de'),
'High Module Priority (de)'
'Entity with "Double Quotes" (fr)',
i18n::_t('i18nTestModule.ENTITY', 'Entity with "Double Quotes"'),
'Non-specific locales fall back to language-only localisations'
);
}
public function testIncludeByLocaleWithoutFallbackLanguage()
public function pluralisationDataProvider()
{
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false);
$this->pushManifest($classManifest);
$adapter = i18n::get_translator('core')->getAdapter();
$this->assertTrue($adapter->isAvailable('en'));
$this->assertFalse($adapter->isAvailable('mi')); // not defined at all
$this->assertFalse($adapter->isAvailable('mi_NZ')); // defined, but not loaded yet
$this->assertFalse(
$adapter->isTranslated('i18nTestModule.ENTITY', 'mi'),
'Existing unloaded entity not available before call'
);
$this->assertFalse(
$adapter->isTranslated('i18nTestModule.ENTITY', 'mi_NZ'),
'Non-existing unloaded entity not available before call'
);
i18n::include_by_locale('mi_NZ');
$this->assertFalse($adapter->isAvailable('mi'));
$this->assertTrue($adapter->isAvailable('mi_NZ'));
$this->assertTrue($adapter->isTranslated('i18nTestModule.ENTITY', null, 'mi_NZ'), 'Includes module files');
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日'],
];
}
public function testRegisterTranslator()
/**
* @dataProvider pluralisationDataProvider()
* @param string $locale
* @param int $count
* @param string $expected
*/
public function testPluralisation($locale, $count, $expected)
{
$translator = new Zend_Translate(
array(
'adapter' => CustomTranslatorAdapter::class,
'disableNotices' => true,
)
);
i18n::register_translator($translator, 'custom', 10);
$translators = i18n::get_translators();
$this->assertArrayHasKey('custom', $translators[10]);
$this->assertInstanceOf('Zend_Translate', $translators[10]['custom']);
$this->assertInstanceOf(CustomTranslatorAdapter::class, $translators[10]['custom']->getAdapter());
i18n::unregister_translator('custom');
$translators = i18n::get_translators();
$this->assertArrayNotHasKey('custom', $translators[10]);
}
public function testMultipleTranslators()
{
// Looping through modules, so we can test the translation autoloading
// Load non-exclusive to retain core class autoloading
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false);
$this->pushManifest($classManifest);
// Changed manifest, so we also need to unset all previously collected messages.
// The easiest way to do this it to register a new adapter.
$adapter = new Zend_Translate(
array(
'adapter' => i18nRailsYamlAdapter::class,
'locale' => i18n::config()->get('default_locale'),
'disableNotices' => true,
)
);
i18n::register_translator($adapter, 'core');
i18n::set_locale('en_US');
i18n::set_locale($locale);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'Entity with "Double Quotes"'
$expected,
_t('Month.PLURALS', 'A month|{count} months', ['count' => $count]),
"Plural form in locale $locale with count $count should be $expected"
);
$this->assertEquals(
i18n::_t('AdapterEntity1', 'AdapterEntity1'),
'AdapterEntity1',
'Falls back to default string if not found'
);
// Add a new translator
$translator = new Zend_Translate(
array(
'adapter' => CustomTranslatorAdapter::class,
'disableNotices' => true,
)
);
i18n::register_translator($translator, 'custom', 11);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY CustomAdapter (en_US)',
'Existing entities overruled by adapter with higher priority'
);
$this->assertEquals(
i18n::_t('AdapterEntity1', 'AdapterEntity1'),
'AdapterEntity1 CustomAdapter (en_US)',
'New entities only defined in new adapter are detected'
);
// Add a second new translator to test priorities
$translator = new Zend_Translate(
array(
'adapter' => OtherCustomTranslatorAdapter::class,
'disableNotices' => true,
)
);
i18n::register_translator($translator, 'othercustom_lower_prio', 5);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY CustomAdapter (en_US)',
'Adapter with lower priority loses'
);
// Add a third new translator to test priorities
$translator = new Zend_Translate(
array(
'adapter' => OtherCustomTranslatorAdapter::class,
'disableNotices' => true,
)
);
i18n::register_translator($translator, 'othercustom_higher_prio', 15);
$this->assertEquals(
i18n::_t('i18nTestModule.ENTITY'),
'i18nTestModule.ENTITY OtherCustomAdapter (en_US)',
'Adapter with higher priority wins'
);
i18n::unregister_translator('custom');
i18n::unregister_translator('othercustom_lower_prio');
i18n::unregister_translator('othercustom_higher_prio');
}
public function testGetLanguageName()

View File

@ -1,30 +0,0 @@
<?php
namespace SilverStripe\i18n\Tests\i18nTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\i18nTranslateAdapterInterface;
use Zend_Translate_Adapter;
class CustomTranslatorAdapter extends Zend_Translate_Adapter implements TestOnly, i18nTranslateAdapterInterface
{
protected function _loadTranslationData($filename, $locale, array $options = array())
{
return array(
$locale => array(
'AdapterEntity1' => 'AdapterEntity1 CustomAdapter (' . $locale . ')',
'i18nTestModule.ENTITY' => 'i18nTestModule.ENTITY CustomAdapter (' . $locale . ')',
)
);
}
public function toString()
{
return 'i18nTest_CustomTranslatorAdapter';
}
public function getFilenameForLocale($locale)
{
return false; // not file based
}
}

View File

@ -2,6 +2,7 @@
namespace SilverStripe\i18n\Tests\i18nTest;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Security\Group;
@ -22,4 +23,15 @@ class MyObject extends DataObject implements TestOnly
private static $singular_name = "My Object";
private static $plural_name = "My Objects";
public function provideI18nEntities()
{
$entities = parent::provideI18nEntities();
return array_merge($entities, [
LeftAndMain::class.'.OTHER_TITLE' => [
'default' => 'Other title',
'module' => 'admin',
],
]);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace SilverStripe\i18n\Tests\i18nTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\i18nTranslateAdapterInterface;
use Zend_Translate_Adapter;
class OtherCustomTranslatorAdapter extends Zend_Translate_Adapter implements TestOnly, i18nTranslateAdapterInterface
{
protected function _loadTranslationData($filename, $locale, array $options = array())
{
return array(
$locale => array(
'i18nTestModule.ENTITY' => 'i18nTestModule.ENTITY OtherCustomAdapter (' . $locale . ')',
)
);
}
public function toString()
{
return 'i18nTest_OtherCustomTranslatorAdapter';
}
public function getFilenameForLocale($locale)
{
return false; // not file based
}
}

View File

@ -8,6 +8,7 @@ use SilverStripe\Security\Member;
class TestDataObject extends DataObject implements TestOnly
{
private static $table_name = 'i18nTest_TestDataObject';
private static $db = array(
'MyProperty' => 'Varchar',

View File

@ -2,11 +2,10 @@
namespace SilverStripe\i18n\Tests\i18nTest;
use SilverStripe\Core\Object;
use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\i18nEntityProvider;
class TestObject extends Object implements TestOnly, i18nEntityProvider
class TestObject implements TestOnly, i18nEntityProvider
{
static $my_translatable_property = "Untranslated";
@ -17,10 +16,8 @@ class TestObject extends Object implements TestOnly, i18nEntityProvider
public function provideI18nEntities()
{
return array(
"i18nTest_Object.my_translatable_property" => array(
self::$my_translatable_property
)
);
return [
"i18nTest_Object.my_translatable_property" => self::$my_translatable_property,
];
}
}

View File

@ -0,0 +1,23 @@
<?php
use SilverStripe\i18n\i18nEntityProvider;
class i18nProviderClass implements i18nEntityProvider
{
public function provideI18nEntities()
{
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'
],
];
}
}

View File

@ -11,5 +11,12 @@ en:
WITHNAMESPACE: Include Entity with Namespace
LAYOUTTEMPLATE: Layout Template
SPRINTFNAMESPACE: My replacement: %s
PLURALS:
one: 'A test'
other: '{count} tests'
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s
Month:
PLURALS:
one: 'A month'
other: '{count} months'

View File

@ -0,0 +1,15 @@
en:
NONAMESPACE: Include Entity without Namespace
SPRINTFNONAMESPACE: My replacement no namespace: %s
SPRINTFINCLUDENONAMESPACE: My include replacement no namespace: %s
LAYOUTTEMPLATENONAMESPACE: Layout Template no namespace
i18nTestModule:
# Comment for entity
ENTITY: Entity with "Double Quotes"
ADDITION: Addition
MAINTEMPLATE: Main Template
WITHNAMESPACE: Include Entity with Namespace
LAYOUTTEMPLATE: Layout Template
SPRINTFNAMESPACE: My replacement: %s
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s

View File

@ -0,0 +1,15 @@
en:
NONAMESPACE: Include Entity without Namespace
SPRINTFNONAMESPACE: My replacement no namespace: %s
SPRINTFINCLUDENONAMESPACE: My include replacement no namespace: %s
LAYOUTTEMPLATENONAMESPACE: Layout Template no namespace
i18nTestModule:
# Comment for entity
ENTITY: Entity with "Double Quotes"
ADDITION: Addition
MAINTEMPLATE: Main Template
WITHNAMESPACE: Include Entity with Namespace
LAYOUTTEMPLATE: Layout Template
SPRINTFNAMESPACE: My replacement: %s
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s

View File

@ -0,0 +1,15 @@
en:
NONAMESPACE: Include Entity without Namespace
SPRINTFNONAMESPACE: My replacement no namespace: %s
SPRINTFINCLUDENONAMESPACE: My include replacement no namespace: %s
LAYOUTTEMPLATENONAMESPACE: Layout Template no namespace
i18nTestModule:
# Comment for entity
ENTITY: Entity with "Double Quotes"
ADDITION: Addition
MAINTEMPLATE: Main Template
WITHNAMESPACE: Include Entity with Namespace
LAYOUTTEMPLATE: Layout Template
SPRINTFNAMESPACE: My replacement: %s
i18nTestModuleInclude.ss:
SPRINTFINCLUDENAMESPACE: My include replacement: %s

View File

@ -0,0 +1,4 @@
ja:
Month:
PLURALS:
other: '{count}日'

View File

@ -0,0 +1,7 @@
pl:
Month:
PLURALS:
one: '1 miesiąc'
few: '{count} miesiące'
many: '{count} miesięcy'
other: '{count} miesiąca'

View File

@ -2,3 +2,6 @@
$Layout
lonely _t() call that should be ignored
<% _t('i18nTestModule.NEWENTITY',"Not stored in master file yet") %>
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 %>

View File

@ -0,0 +1,145 @@
<?php
namespace SilverStripe\i18n\Tests;
use SilverStripe\Control\Director;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\Messages\MessageProvider;
use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader;
use SilverStripe\i18n\Messages\Symfony\SymfonyMessageProvider;
use SilverStripe\i18n\Messages\YamlReader;
use SilverStripe\i18n\Tests\i18nTest\MyObject;
use SilverStripe\i18n\Tests\i18nTest\MySubObject;
use SilverStripe\i18n\Tests\i18nTest\TestDataObject;
use SilverStripe\View\SSViewer;
use SilverStripe\View\SSViewer_DataPresenter;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ThemeManifest;
use SilverStripe\View\ViewableData;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
/**
* Helper trait for bootstrapping test manifest for i18n tests
*/
trait i18nTestManifest
{
/**
* Fake webroot with a single module /i18ntestmodule which contains some files with _t() calls.
*
* @var string
*/
protected $alternateBasePath;
/**
* Number of test manifests
*
* @var int
*/
protected $manifests = 0;
protected function getExtraDataObjects()
{
return [
TestDataObject::class,
MyObject::class,
MySubObject::class,
];
}
/**
* @var ThemeResourceLoader
*/
protected $oldThemeResourceLoader = null;
/**
* @var string
*/
protected $originalLocale = null;
public function setupManifest()
{
// force SSViewer_DataPresenter to cache global template vars before we switch to the
// test-project class manifest (since it will lose visibility of core classes)
$presenter = new SSViewer_DataPresenter(new ViewableData());
unset($presenter);
// Switch to test manifest
$s = DIRECTORY_SEPARATOR;
$this->alternateBasePath = __DIR__ . $s . 'i18nTest' . $s . "_fakewebroot";
Director::config()->update('alternate_base_folder', $this->alternateBasePath);
// Replace old template loader with new one with alternate base path
$this->oldThemeResourceLoader = ThemeResourceLoader::instance();
ThemeResourceLoader::set_instance($loader = new ThemeResourceLoader($this->alternateBasePath));
$loader->addSet(
'$default',
new ThemeManifest(
$this->alternateBasePath,
project(),
false,
true
)
);
SSViewer::set_themes([
'testtheme1',
'$default',
]);
$this->originalLocale = i18n::get_locale();
i18n::set_locale('en_US');
// Set new manifest against the root
$classManifest = new ClassManifest($this->alternateBasePath, true, true, false);
$this->pushManifest($classManifest);
// Setup uncached translator
// This should pull the module list from the above manifest
$translator = new Translator('en');
$translator->setFallbackLocales(['en']);
$loader = new ModuleYamlLoader();
$loader->setReader(new YamlReader());
$translator->addLoader('ss', $loader); // Standard ss module loader
$translator->addLoader('array', new ArrayLoader()); // Note: array loader isn't added by default
$provider = new SymfonyMessageProvider();
$provider->setTranslator($translator);
Injector::inst()->registerService($provider, MessageProvider::class);
}
public function tearDownManifest()
{
ThemeResourceLoader::set_instance($this->oldThemeResourceLoader);
i18n::set_locale($this->originalLocale);
// Reset any manifests pushed during this test
$this->popManifests();
}
/**
* Safely push a new class manifest.
* These will be cleaned up on tearDown()
*
* @param ClassManifest $manifest
*/
protected function pushManifest(ClassManifest $manifest)
{
$this->manifests++;
ClassLoader::instance()->pushManifest($manifest);
}
/**
* Pop off all extra manifests
*/
protected function popManifests()
{
// Reset any manifests pushed during this test
while ($this->manifests > 0) {
ClassLoader::instance()->popManifest();
$this->manifests--;
}
}
}

View File

@ -2,74 +2,46 @@
namespace SilverStripe\i18n\Tests;
use PHPUnit_Framework_Error_Notice;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Manifest\ClassManifest;
use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n;
use SilverStripe\i18n\i18nTextCollector;
use SilverStripe\i18n\i18nTextCollector_Writer_RailsYaml;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
use SilverStripe\i18n\Messages\YamlWriter;
use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\SSViewer;
class i18nTextCollectorTest extends SapphireTest
{
use i18nTestManifest;
/**
* @var string $tmpBasePath Used to write language files.
* We don't want to store them inside framework (or in any web-accessible place)
* in case something goes wrong with the file parsing.
* @var string
*/
protected $alternateBaseSavePath;
/**
* @var string $alternateBasePath Fake webroot with a single module
* /i18ntestmodule which contains some files with _t() calls.
*/
protected $alternateBasePath;
protected $manifest;
protected $alternateBaseSavePath = null;
public function setUp()
{
parent::setUp();
$this->setupManifest();
$this->alternateBasePath = __DIR__ . "/i18nTest/_fakewebroot";
Config::inst()->update(Director::class, 'alternate_base_folder', $this->alternateBasePath);
$this->alternateBaseSavePath = TEMP_FOLDER . '/i18nTextCollectorTest_webroot';
$this->alternateBaseSavePath = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'i18nTextCollectorTest_webroot';
Filesystem::makeFolder($this->alternateBaseSavePath);
// Push a class and template loader running from the fake webroot onto
// the stack.
$this->manifest = new ClassManifest(
$this->alternateBasePath,
false,
true,
false
);
// Replace old template loader with new one with alternate base path
$this->_oldLoader = ThemeResourceLoader::instance();
ThemeResourceLoader::set_instance(new ThemeResourceLoader($this->alternateBasePath));
}
public function tearDown()
{
ThemeResourceLoader::set_instance($this->_oldLoader);
// Pop if added during testing
if (ClassLoader::instance()->getManifest() === $this->manifest) {
ClassLoader::instance()->popManifest();
if (is_dir($this->alternateBaseSavePath)) {
Filesystem::removeFolder($this->alternateBaseSavePath);
}
$this->tearDownManifest();
parent::tearDown();
}
public function testConcatenationInEntityValues()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$php = <<<PHP
_t(
@ -87,81 +59,95 @@ _t(
"Line 5");
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.CONCATENATED' => array("Line 1 and Line '2' and Line \"3\"",'Comment'),
'Test.CONCATENATED2' => array("Line \"4\" and Line 5")
)
'Test.CONCATENATED' => [
'default' => "Line 1 and Line '2' and Line \"3\"",
'comment' => 'Comment'
],
'Test.CONCATENATED2' => "Line \"4\" and Line 5"
),
$c->collectFromCode($php, 'mymodule')
);
}
public function testCollectFromNewTemplateSyntaxUsingParserSubclass()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false);
$html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %>
$html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %>
<%t i18nTestModule.NEWMETHODSIG "New _t method signature test" %>
<%t i18nTestModule.INJECTIONS_0 "Hello {name} {greeting}, and {goodbye}" name="Mark" greeting="welcome" goodbye="bye" %>
<%t i18nTestModule.INJECTIONS_1 "Hello {name} {greeting}, and {goodbye}" name="Paul" greeting="welcome" goodbye="cya" %>
<%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');
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.SINGLEQUOTE' => array('Single Quote'),
'i18nTestModule.NEWMETHODSIG' => array("New _t method signature test",null,null),
'i18nTestModule.INJECTIONS_0' => array("Hello {name} {greeting}, and {goodbye}", null, null),
'i18nTestModule.INJECTIONS_1' => array("Hello {name} {greeting}, and {goodbye}", null, null),
'i18nTestModule.INJECTIONS_2' => array("Hello {name} {greeting}", null, "context (ignored)"),
'i18nTestModule.INJECTIONS_3' => array(null, null, null),
'i18nTestModule.INJECTIONS_4' => array(null, null, null),
)
[
'Test.SINGLEQUOTE' => 'Single Quote',
'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' => [
'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()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$html = <<<SS
<% _t('Test.SINGLEQUOTE','Single Quote'); %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.SINGLEQUOTE' => array('Single Quote')
)
[ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
);
$html = <<<SS
<% _t( "Test.DOUBLEQUOTE", "Double Quote and Spaces" ); %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.DOUBLEQUOTE' => array("Double Quote and Spaces")
)
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
);
$html = <<<SS
<% _t("Test.NOSEMICOLON","No Semicolon") %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.NOSEMICOLON' => array("No Semicolon")
)
[ 'Test.NOSEMICOLON' => "No Semicolon" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
);
}
public function testCollectFromTemplateAdvanced()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false);
$html = <<<SS
<% _t(
@ -170,10 +156,8 @@ SS;
) %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.NEWLINES' => array("New Lines")
)
[ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromTemplate($html, 'mymodule', 'Test')
);
$html = <<<SS
@ -184,10 +168,11 @@ SS;
) %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.PRIOANDCOMMENT' => array(' Prio and Value with "Double Quotes"','Comment with "Double Quotes"')
)
[ 'Test.PRIOANDCOMMENT' => [
'default' => ' Prio and Value with "Double Quotes"',
'comment' => 'Comment with "Double Quotes"',
]],
$c->collectFromTemplate($html, 'mymodule', 'Test')
);
$html = <<<SS
@ -199,42 +184,56 @@ SS;
) %>
SS;
$this->assertEquals(
$c->collectFromTemplate($html, 'mymodule', 'Test'),
array(
'Test.PRIOANDCOMMENT' => array(" Prio and Value with 'Single Quotes'","Comment 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');
}
public function testCollectFromCodeSimple()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$php = <<<PHP
_t('Test.SINGLEQUOTE','Single Quote');
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.SINGLEQUOTE' => array('Single Quote')
)
[ 'Test.SINGLEQUOTE' => 'Single Quote' ],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
_t( "Test.DOUBLEQUOTE", "Double Quote and Spaces" );
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.DOUBLEQUOTE' => array("Double Quote and Spaces")
)
[ 'Test.DOUBLEQUOTE' => "Double Quote and Spaces" ],
$c->collectFromCode($php, 'mymodule')
);
}
public function testCollectFromCodeAdvanced()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$php = <<<PHP
_t(
@ -243,10 +242,8 @@ _t(
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.NEWLINES' => array("New Lines")
)
[ 'Test.NEWLINES' => "New Lines" ],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
@ -258,10 +255,13 @@ _t(
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.PRIOANDCOMMENT' => array(' Value with "Double Quotes"','Comment with "Double Quotes"')
)
[
'Test.PRIOANDCOMMENT' => [
'default' => ' Value with "Double Quotes"',
'comment' => 'Comment with "Double Quotes"',
]
],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
@ -273,10 +273,11 @@ _t(
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.PRIOANDCOMMENT' => array(" Value with 'Single Quotes'","Comment with 'Single Quotes'")
)
[ 'Test.PRIOANDCOMMENT' => [
'default' => " Value with 'Single Quotes'",
'comment' => "Comment with 'Single Quotes'"
] ],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
@ -286,30 +287,28 @@ _t(
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.PRIOANDCOMMENT' => array("Value with 'Escaped Single Quotes'")
)
[ 'Test.PRIOANDCOMMENT' => "Value with 'Escaped Single Quotes'" ],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
_t(
'Test.PRIOANDCOMMENT',
"Doublequoted Value with 'Unescaped Single Quotes'"
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.PRIOANDCOMMENT' => array("Doublequoted Value with 'Unescaped Single Quotes'")
)
[ 'Test.PRIOANDCOMMENT' => "Doublequoted Value with 'Unescaped Single Quotes'"],
$c->collectFromCode($php, 'mymodule')
);
}
public function testNewlinesInEntityValues()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$php = <<<PHP
_t(
@ -321,10 +320,8 @@ PHP;
$eol = PHP_EOL;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.NEWLINESINGLEQUOTE' => array("Line 1{$eol}Line 2")
)
[ 'Test.NEWLINESINGLEQUOTE' => "Line 1{$eol}Line 2" ],
$c->collectFromCode($php, 'mymodule')
);
$php = <<<PHP
@ -335,10 +332,8 @@ Line 2"
);
PHP;
$this->assertEquals(
$c->collectFromCode($php, 'mymodule'),
array(
'Test.NEWLINEDOUBLEQUOTE' => array("Line 1{$eol}Line 2")
)
[ 'Test.NEWLINEDOUBLEQUOTE' => "Line 1{$eol}Line 2" ],
$c->collectFromCode($php, 'mymodule')
);
}
@ -347,79 +342,82 @@ PHP;
*/
public function testCollectFromCodeNewSignature()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests
$php = <<<PHP
_t('i18nTestModule.NEWMETHODSIG',"New _t method signature test");
_t('i18nTestModule.INJECTIONS1','_DOES_NOT_EXIST', "Hello {name} {greeting}. But it is late, {goodbye}",
array("name"=>"Mark", "greeting"=>"welcome", "goodbye"=>"bye"));
_t('i18nTestModule.INJECTIONS2', "Hello {name} {greeting}. But it is late, {goodbye}",
array("name"=>"Paul", "greeting"=>"good you are here", "goodbye"=>"see you"));
_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.INJECTIONS5','_DOES_NOT_EXIST', "Hello {name} {greeting}. But it is late, {goodbye}",
["name"=>"Mark", "greeting"=>"welcome", "goodbye"=>"bye"]);
_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 = (array(
'i18nTestModule.NEWMETHODSIG' => array("New _t method signature test"),
'i18nTestModule.INJECTIONS1' => array("_DOES_NOT_EXIST",
"Hello {name} {greeting}. But it is late, {goodbye}"),
'i18nTestModule.INJECTIONS2' => array("Hello {name} {greeting}. But it is late, {goodbye}"),
'i18nTestModule.INJECTIONS3' => array("Hello {name} {greeting}. But it is late, {goodbye}",
"New context (this should be ignored)"),
'i18nTestModule.INJECTIONS5' => array("_DOES_NOT_EXIST",
"Hello {name} {greeting}. But it is late, {goodbye}"),
'i18nTestModule.INJECTIONS6' => array("Hello {name} {greeting}. But it is late, {goodbye}"),
'i18nTestModule.INJECTIONS7' => array("Hello {name} {greeting}. But it is late, {goodbye}",
"New context (this should be ignored)"),
));
$expectedArray = [
'i18nTestModule.INJECTIONS2' => "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' => [
'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);
ksort($expectedArray);
$this->assertEquals($collectedTranslatables, $expectedArray);
// Test warning is raised on empty default
$this->setExpectedException(
PHPUnit_Framework_Error_Notice::class,
'Missing localisation default for key i18nTestModule.INJECTIONS4'
);
$php = <<<PHP
_t('i18nTestModule.INJECTIONS4', array("name"=>"Cat", "greeting"=>"meow", "goodbye"=>"meow"));
PHP;
$c->setWarnOnEmptyDefault(true);
$c->collectFromCode($php, 'mymodule');
}
/**
* @todo Should be in a separate test suite, but don't want to duplicate setup logic
*/
public function testYamlWriter()
public function testUncollectableCode()
{
$writer = new i18nTextCollector_Writer_RailsYaml();
$entities = array(
'Level1.Level2.EntityName' => array('Text', 'Context'),
'Level1.OtherEntityName' => array('Other Text', 'Other Context'),
'Level1.BoolTest' => array('True'),
'Level1.FlagTest' => array('No'),
'Level1.TextTest' => array('Maybe')
);
$yaml = <<<YAML
de:
Level1:
Level2:
EntityName: Text
OtherEntityName: 'Other Text'
BoolTest: 'True'
FlagTest: 'No'
TextTest: Maybe
$c = i18nTextCollector::create();
YAML;
$this->assertEquals($yaml, Convert::nl2os($writer->getYaml($entities, 'de')));
$php = <<<PHP
_t(static::class.'.KEY1', 'Default');
_t(self::class.'.KEY2', 'Default');
_t(__CLASS__.'.KEY3', 'Default');
_t('Collectable.KEY4', 'Default');
PHP;
$collectedTranslatables = $c->collectFromCode($php, 'mymodule');
// Only one item is collectable
$expectedArray = [ 'Collectable.KEY4' => 'Default' ];
$this->assertEquals($expectedArray, $collectedTranslatables);
}
public function testCollectFromIncludedTemplates()
{
$c = new i18nTextCollector();
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false); // Disable warnings for tests
$templateFilePath = $this->alternateBasePath . '/i18ntestmodule/templates/Layout/i18nTestModule.ss';
$html = file_get_contents($templateFilePath);
@ -427,23 +425,23 @@ YAML;
$this->assertArrayHasKey('RandomNamespace.LAYOUTTEMPLATENONAMESPACE', $matches);
$this->assertEquals(
$matches['RandomNamespace.LAYOUTTEMPLATENONAMESPACE'],
array('Layout Template no namespace')
'Layout Template no namespace',
$matches['RandomNamespace.LAYOUTTEMPLATENONAMESPACE']
);
$this->assertArrayHasKey('RandomNamespace.SPRINTFNONAMESPACE', $matches);
$this->assertEquals(
$matches['RandomNamespace.SPRINTFNONAMESPACE'],
array('My replacement no namespace: %s')
'My replacement no namespace: %s',
$matches['RandomNamespace.SPRINTFNONAMESPACE']
);
$this->assertArrayHasKey('i18nTestModule.LAYOUTTEMPLATE', $matches);
$this->assertEquals(
$matches['i18nTestModule.LAYOUTTEMPLATE'],
array('Layout Template')
'Layout Template',
$matches['i18nTestModule.LAYOUTTEMPLATE']
);
$this->assertArrayHasKey('i18nTestModule.SPRINTFNAMESPACE', $matches);
$this->assertEquals(
$matches['i18nTestModule.SPRINTFNAMESPACE'],
array('My replacement: %s')
'My replacement: %s',
$matches['i18nTestModule.SPRINTFNAMESPACE']
);
// Includes should not automatically inject translations into parent templates
@ -455,8 +453,8 @@ YAML;
public function testCollectFromThemesTemplates()
{
$c = new i18nTextCollector();
Config::inst()->update('SilverStripe\\View\\SSViewer', 'theme', 'testtheme1');
$c = i18nTextCollector::create();
SSViewer::set_themes([ 'testtheme1' ]);
// Collect from layout
$layoutFilePath = $this->alternateBasePath . '/themes/testtheme1/templates/Layout/i18nTestTheme1.ss';
@ -465,16 +463,12 @@ YAML;
// all entities from i18nTestTheme1.ss
$this->assertEquals(
array(
'i18nTestTheme1.LAYOUTTEMPLATE'
=> array('Theme1 Layout Template'),
'i18nTestTheme1.SPRINTFNAMESPACE'
=> array('Theme1 My replacement: %s'),
'i18nTestTheme1.ss.LAYOUTTEMPLATENONAMESPACE'
=> array('Theme1 Layout Template no namespace'),
'i18nTestTheme1.ss.SPRINTFNONAMESPACE'
=> array('Theme1 My replacement no namespace: %s'),
),
[
'i18nTestTheme1.LAYOUTTEMPLATE' => 'Theme1 Layout Template',
'i18nTestTheme1.SPRINTFNAMESPACE' => 'Theme1 My replacement: %s',
'i18nTestTheme1.ss.LAYOUTTEMPLATENONAMESPACE' => 'Theme1 Layout Template no namespace',
'i18nTestTheme1.ss.SPRINTFNONAMESPACE' => 'Theme1 My replacement no namespace: %s',
],
$layoutMatches
);
@ -485,29 +479,21 @@ YAML;
// all entities from i18nTestTheme1Include.ss
$this->assertEquals(
array(
'i18nTestTheme1Include.SPRINTFINCLUDENAMESPACE'
=> array('Theme1 My include replacement: %s'),
'i18nTestTheme1Include.WITHNAMESPACE'
=> array('Theme1 Include Entity with Namespace'),
'i18nTestTheme1Include.ss.NONAMESPACE'
=> array('Theme1 Include Entity without Namespace'),
'i18nTestTheme1Include.ss.SPRINTFINCLUDENONAMESPACE'
=> array('Theme1 My include replacement no namespace: %s')
),
[
'i18nTestTheme1Include.SPRINTFINCLUDENAMESPACE' => 'Theme1 My include replacement: %s',
'i18nTestTheme1Include.WITHNAMESPACE' => 'Theme1 Include Entity with Namespace',
'i18nTestTheme1Include.ss.NONAMESPACE' => 'Theme1 Include Entity without Namespace',
'i18nTestTheme1Include.ss.SPRINTFINCLUDENONAMESPACE' => 'Theme1 My include replacement no namespace: %s'
],
$includeMatches
);
}
public function testCollectMergesWithExisting()
{
i18n::set_locale('en_US');
i18n::config()->update('default_locale', 'en_US');
i18n::include_by_locale('en');
i18n::include_by_locale('en_US');
$c = new i18nTextCollector();
$c->setWriter(new i18nTextCollector_Writer_RailsYaml());
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false);
$c->setWriter(new YamlWriter());
$c->basePath = $this->alternateBasePath;
$c->baseSavePath = $this->alternateBaseSavePath;
@ -522,16 +508,30 @@ YAML;
$entitiesByModule['i18ntestmodule'],
'Adds new entities'
);
// Test cross-module strings are set correctly
$this->assertArrayHasKey(
'i18nProviderClass.OTHER_MODULE',
$entitiesByModule['i18ntestmodule']
);
$this->assertEquals(
[
'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 = new i18nTextCollector();
$c->setWriter(new i18nTextCollector_Writer_RailsYaml());
$c = i18nTextCollector::create();
$c->setWarnOnEmptyDefault(false);
$c->setWriter(new YamlWriter());
$c->basePath = $this->alternateBasePath;
$c->baseSavePath = $this->alternateBaseSavePath;
@ -642,26 +642,62 @@ YAML;
" MAINTEMPLATE: 'Theme2 Main Template'\n",
$theme2LangFileContent
);
i18n::set_locale($local); //set the locale to the US locale expected in the asserts
}
public function testCollectFromEntityProvidersInCustomObject()
{
$c = new i18nTextCollector();
// note: Disable _fakewebroot manifest for this test
$this->popManifests();
$c = i18nTextCollector::create();
// Collect from MyObject.php
$filePath = __DIR__ . '/i18nTest/MyObject.php';
$matches = $c->collectFromEntityProviders($filePath);
$this->assertEquals(
array(
'SilverStripe\i18n\Tests\i18nTest\MyObject.PLURALNAME',
'SilverStripe\i18n\Tests\i18nTest\MyObject.SINGULARNAME',
),
array_keys($matches)
[
'SilverStripe\Admin\LeftAndMain.OTHER_TITLE' => [
'default' => 'Other title',
'module' => 'admin',
],
'SilverStripe\i18n\Tests\i18nTest\MyObject.PLURALNAME' => 'My Objects',
'SilverStripe\i18n\Tests\i18nTest\MyObject.PLURALS' => [
'one' => 'A My Object',
'other' => '{count} My Objects',
],
'SilverStripe\i18n\Tests\i18nTest\MyObject.SINGULARNAME' => 'My Object',
],
$matches
);
}
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 Object',
$matches['SilverStripe\i18n\Tests\i18nTest\MyObject.SINGULARNAME'][0]
'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']
);
}
@ -671,48 +707,47 @@ YAML;
public function testResolveDuplicates()
{
$collector = new Collector();
ClassLoader::instance()->pushManifest($this->manifest);
// Dummy data as collected
$data1 = array(
'i18ntestmodule' => array(
'i18nTestModule.PLURALNAME' => array('Data Objects'),
'i18nTestModule.SINGULARNAME' => array('Data Object')
),
'mymodule' => array(
'i18nTestModule.PLURALNAME' => array('Ignored String'),
'i18nTestModule.STREETNAME' => array('Shortland Street')
)
);
$expected = array(
'i18ntestmodule' => array(
'i18nTestModule.PLURALNAME' => array('Data Objects'),
'i18nTestModule.SINGULARNAME' => array('Data Object')
),
'mymodule' => array(
// Because this key doesn't exist in i18ntestmodule strings
'i18nTestModule.STREETNAME' => array('Shortland Street')
)
);
$data1 = [
'i18ntestmodule' => [
'i18nTestModule.PLURALNAME' => 'Data Objects',
'i18nTestModule.SINGULARNAME' => 'Data Object',
],
'mymodule' => [
'i18nTestModule.PLURALNAME' => 'Ignored String',
'i18nTestModule.STREETNAME' => 'Shortland Street',
],
];
$expected = [
'i18ntestmodule' => [
'i18nTestModule.PLURALNAME' => 'Data Objects',
'i18nTestModule.SINGULARNAME' => 'Data Object',
],
'mymodule' => [
// Removed PLURALNAME because this key doesn't exist in i18ntestmodule strings
'i18nTestModule.STREETNAME' => 'Shortland Street'
]
];
$resolved = $collector->resolveDuplicateConflicts_Test($data1);
$this->assertEquals($expected, $resolved);
// Test getConflicts
$data2 = array(
'module1' => array(
'i18ntestmodule.ONE' => array('One'),
'i18ntestmodule.TWO' => array('Two'),
'i18ntestmodule.THREE' => array('Three'),
),
'module2' => array(
'i18ntestmodule.THREE' => array('Three'),
),
'module3' => array(
'i18ntestmodule.TWO' => array('Two'),
'i18ntestmodule.THREE' => array('Three'),
)
);
$data2 = [
'module1' => [
'i18ntestmodule.ONE' => 'One',
'i18ntestmodule.TWO' => 'Two',
'i18ntestmodule.THREE' => 'Three',
],
'module2' => [
'i18ntestmodule.THREE' => 'Three',
],
'module3' => [
'i18ntestmodule.TWO' => 'Two',
'i18ntestmodule.THREE' => 'Three',
],
];
$conflictsA = $collector->getConflicts_Test($data2);
sort($conflictsA);
$this->assertEquals(
@ -735,7 +770,6 @@ YAML;
public function testModuleDetection()
{
$collector = new Collector();
ClassLoader::instance()->pushManifest($this->manifest);
$modules = $collector->getModules_Test($this->alternateBasePath);
$this->assertEquals(
array(
@ -790,9 +824,10 @@ YAML;
// Standard modules with code in odd places should only have code in those directories detected
$otherFiles = $collector->getFileListForModule_Test('i18nothermodule');
$otherRoot = $this->alternateBasePath . '/i18nothermodule';
$this->assertEquals(3, count($otherFiles));
$this->assertEquals(4, count($otherFiles));
// Only detect well-behaved files
$this->assertArrayHasKey("{$otherRoot}/code/i18nOtherModule.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/code/i18nProviderClass.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/code/i18nTestModuleDecorator.php", $otherFiles);
$this->assertArrayHasKey("{$otherRoot}/templates/i18nOtherModule.ss", $otherFiles);

View File

@ -3,7 +3,7 @@
namespace SilverStripe\i18n\Tests\i18nTextCollectorTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\i18n\i18nTextCollector;
use SilverStripe\i18n\TextCollection\i18nTextCollector;
/**
* Assist with testing of specific protected methods

View File

@ -1,220 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Translate.php 23775 2011-03-01 17:25:24Z ralph $
*/
/**
* @see Zend_Loader
*/
require_once 'Zend/Loader.php';
/**
* @see Zend_Translate_Adapter
*/
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate {
/**
* Adapter names constants
*/
const AN_ARRAY = 'Array';
const AN_CSV = 'Csv';
const AN_GETTEXT = 'Gettext';
const AN_INI = 'Ini';
const AN_QT = 'Qt';
const AN_TBX = 'Tbx';
const AN_TMX = 'Tmx';
const AN_XLIFF = 'Xliff';
const AN_XMLTM = 'XmlTm';
const LOCALE_DIRECTORY = 'directory';
const LOCALE_FILENAME = 'filename';
/**
* Adapter
*
* @var Zend_Translate_Adapter
*/
private $_adapter;
/**
* Generates the standard translation object
*
* @param array|Zend_Config $options Options to use
* @throws Zend_Translate_Exception
*/
public function __construct($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options = array();
$options['adapter'] = array_shift($args);
if (!empty($args)) {
$options['content'] = array_shift($args);
}
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$opt = array_shift($args);
$options = array_merge($opt, $options);
}
} else if (!is_array($options)) {
$options = array('adapter' => $options);
}
$this->setAdapter($options);
}
/**
* Sets a new adapter
*
* @param array|Zend_Config $options Options to use
* @throws Zend_Translate_Exception
*/
public function setAdapter($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options = array();
$options['adapter'] = array_shift($args);
if (!empty($args)) {
$options['content'] = array_shift($args);
}
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$opt = array_shift($args);
$options = array_merge($opt, $options);
}
} else if (!is_array($options)) {
$options = array('adapter' => $options);
}
if (Zend_Loader::isReadable('Zend/Translate/Adapter/' . ucfirst($options['adapter']). '.php')) {
$options['adapter'] = 'Zend_Translate_Adapter_' . ucfirst($options['adapter']);
}
if (!class_exists($options['adapter'])) {
Zend_Loader::loadClass($options['adapter']);
}
if (array_key_exists('cache', $options)) {
Zend_Translate_Adapter::setCache($options['cache']);
}
$adapter = $options['adapter'];
unset($options['adapter']);
$this->_adapter = new $adapter($options);
if (!$this->_adapter instanceof Zend_Translate_Adapter) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("Adapter " . $adapter . " does not extend Zend_Translate_Adapter");
}
}
/**
* Returns the adapters name and it's options
*
* @return Zend_Translate_Adapter
*/
public function getAdapter()
{
return $this->_adapter;
}
/**
* Returns the set cache
*
* @return Zend_Cache_Core The set cache
*/
public static function getCache()
{
return Zend_Translate_Adapter::getCache();
}
/**
* Sets a cache for all instances of Zend_Translate
*
* @param Zend_Cache_Core $cache Cache to store to
* @return void
*/
public static function setCache(Zend_Cache_Core $cache)
{
Zend_Translate_Adapter::setCache($cache);
}
/**
* Returns true when a cache is set
*
* @return boolean
*/
public static function hasCache()
{
return Zend_Translate_Adapter::hasCache();
}
/**
* Removes any set cache
*
* @return void
*/
public static function removeCache()
{
Zend_Translate_Adapter::removeCache();
}
/**
* Clears all set cache data
*
* @param string $tag Tag to clear when the default tag name is not used
* @return void
*/
public static function clearCache($tag = null)
{
Zend_Translate_Adapter::clearCache($tag);
}
/**
* Calls all methods from the adapter
*/
public function __call($method, array $options)
{
if (method_exists($this->_adapter, $method)) {
return call_user_func_array(array($this->_adapter, $method), $options);
}
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("Unknown method '" . $method . "' called!");
}
}

View File

@ -1,998 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @subpackage Zend_Translate_Adapter
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Adapter.php 24268 2011-07-25 14:47:42Z guilhermeblanco $
*/
/**
* @see Zend_Locale
*/
require_once 'Zend/Locale.php';
/**
* @see Zend_Translate_Plural
*/
require_once 'Zend/Translate/Plural.php';
/**
* Basic adapter class for each translation source adapter
*
* @category Zend
* @package Zend_Translate
* @subpackage Zend_Translate_Adapter
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
abstract class Zend_Translate_Adapter {
/**
* Shows if locale detection is in automatic level
* @var boolean
*/
private $_automatic = true;
/**
* Internal value to see already routed languages
* @var array()
*/
private $_routed = array();
/**
* Internal cache for all adapters
* @var Zend_Cache_Core
*/
protected static $_cache = null;
/**
* Internal value to remember if cache supports tags
*
* @var boolean
*/
private static $_cacheTags = false;
/**
* Scans for the locale within the name of the directory
* @constant integer
*/
const LOCALE_DIRECTORY = 'directory';
/**
* Scans for the locale within the name of the file
* @constant integer
*/
const LOCALE_FILENAME = 'filename';
/**
* Array with all options, each adapter can have own additional options
* 'clear' => when true, clears already loaded translations when adding new files
* 'content' => content to translate or file or directory with content
* 'disableNotices' => when true, omits notices from being displayed
* 'ignore' => a prefix for files and directories which are not being added
* 'locale' => the actual set locale to use
* 'log' => a instance of Zend_Log where logs are written to
* 'logMessage' => message to be logged
* 'logPriority' => priority which is used to write the log message
* 'logUntranslated' => when true, untranslated messages are not logged
* 'reload' => reloads the cache by reading the content again
* 'scan' => searches for translation files using the LOCALE constants
* 'tag' => tag to use for the cache
*
* @var array
*/
protected $_options = array(
'clear' => false,
'content' => null,
'disableNotices' => false,
'ignore' => '.',
'locale' => 'auto',
'log' => null,
'logMessage' => "Untranslated message within '%locale%': %message%",
'logPriority' => 5,
'logUntranslated' => false,
'reload' => false,
'route' => null,
'scan' => null,
'tag' => 'Zend_Translate'
);
/**
* Translation table
* @var array
*/
protected $_translate = array();
/**
* Generates the adapter
*
* @param array|Zend_Config $options Translation options for this adapter
* @throws Zend_Translate_Exception
* @return void
*/
public function __construct($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options = array();
$options['content'] = array_shift($args);
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$opt = array_shift($args);
$options = array_merge($opt, $options);
}
} else if (!is_array($options)) {
$options = array('content' => $options);
}
if (array_key_exists('cache', $options)) {
self::setCache($options['cache']);
unset($options['cache']);
}
if (isset(self::$_cache)) {
$id = 'Zend_Translate_' . $this->toString() . '_Options';
$result = self::$_cache->load($id);
if ($result) {
$this->_options = $result;
}
}
if (empty($options['locale']) || ($options['locale'] === "auto")) {
$this->_automatic = true;
} else {
$this->_automatic = false;
}
$locale = null;
if (!empty($options['locale'])) {
$locale = $options['locale'];
unset($options['locale']);
}
$this->setOptions($options);
$options['locale'] = $locale;
if (!empty($options['content'])) {
$this->addTranslation($options);
}
if ($this->getLocale() !== (string) $options['locale']) {
$this->setLocale($options['locale']);
}
}
/**
* Add translations
*
* This may be a new language or additional content for an existing language
* If the key 'clear' is true, then translations for the specified
* language will be replaced and added otherwise
*
* @param array|Zend_Config $options Options and translations to be added
* @throws Zend_Translate_Exception
* @return Zend_Translate_Adapter Provides fluent interface
*/
public function addTranslation($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options = array();
$options['content'] = array_shift($args);
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$opt = array_shift($args);
$options = array_merge($opt, $options);
}
} else if (!is_array($options)) {
$options = array('content' => $options);
}
if (!isset($options['content']) || empty($options['content'])) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("Required option 'content' is missing");
}
$originate = null;
if (!empty($options['locale'])) {
$originate = (string) $options['locale'];
}
if ((array_key_exists('log', $options)) && !($options['log'] instanceof Zend_Log)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Instance of Zend_Log expected for option log');
}
try {
if (!($options['content'] instanceof Zend_Translate) && !($options['content'] instanceof Zend_Translate_Adapter)) {
if (empty($options['locale'])) {
$options['locale'] = null;
}
$options['locale'] = Zend_Locale::findLocale($options['locale']);
}
} catch (Zend_Locale_Exception $e) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("The given Language '{$options['locale']}' does not exist", 0, $e);
}
$options = $options + $this->_options;
if (is_string($options['content']) and is_dir($options['content'])) {
$options['content'] = realpath($options['content']);
$prev = '';
$iterator = new RecursiveIteratorIterator(
new RecursiveRegexIterator(
new RecursiveDirectoryIterator($options['content'], RecursiveDirectoryIterator::KEY_AS_PATHNAME),
'/^(?!.*(\.svn|\.cvs)).*$/', RecursiveRegexIterator::MATCH
),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $directory => $info) {
$file = $info->getFilename();
if (is_array($options['ignore'])) {
foreach ($options['ignore'] as $key => $ignore) {
if (strpos($key, 'regex') !== false) {
if (preg_match($ignore, $directory)) {
// ignore files matching the given regex from option 'ignore' and all files below
continue 2;
}
} else if (strpos($directory, DIRECTORY_SEPARATOR . $ignore) !== false) {
// ignore files matching first characters from option 'ignore' and all files below
continue 2;
}
}
} else {
if (strpos($directory, DIRECTORY_SEPARATOR . $options['ignore']) !== false) {
// ignore files matching first characters from option 'ignore' and all files below
continue;
}
}
if ($info->isDir()) {
// pathname as locale
if (($options['scan'] === self::LOCALE_DIRECTORY) and (Zend_Locale::isLocale($file, true, false))) {
$options['locale'] = $file;
$prev = (string) $options['locale'];
}
} else if ($info->isFile()) {
// filename as locale
if ($options['scan'] === self::LOCALE_FILENAME) {
$filename = explode('.', $file);
array_pop($filename);
$filename = implode('.', $filename);
if (Zend_Locale::isLocale((string) $filename, true, false)) {
$options['locale'] = (string) $filename;
} else {
$parts = explode('.', $file);
$parts2 = array();
foreach($parts as $token) {
$parts2 += explode('_', $token);
}
$parts = array_merge($parts, $parts2);
$parts2 = array();
foreach($parts as $token) {
$parts2 += explode('-', $token);
}
$parts = array_merge($parts, $parts2);
$parts = array_unique($parts);
$prev = '';
foreach($parts as $token) {
if (Zend_Locale::isLocale($token, true, false)) {
if (strlen($prev) <= strlen($token)) {
$options['locale'] = $token;
$prev = $token;
}
}
}
}
}
try {
$options['content'] = $info->getPathname();
$this->_addTranslationData($options);
} catch (Zend_Translate_Exception $e) {
// ignore failed sources while scanning
}
}
}
unset($iterator);
} else {
$this->_addTranslationData($options);
}
if ((isset($this->_translate[$originate]) === true) and (count($this->_translate[$originate]) > 0)) {
$this->setLocale($originate);
}
return $this;
}
/**
* Sets new adapter options
*
* @param array $options Adapter options
* @throws Zend_Translate_Exception
* @return Zend_Translate_Adapter Provides fluent interface
*/
public function setOptions(array $options = array())
{
$change = false;
$locale = null;
foreach ($options as $key => $option) {
if ($key == 'locale') {
$locale = $option;
} else if ((isset($this->_options[$key]) and ($this->_options[$key] != $option)) or
!isset($this->_options[$key])) {
if (($key == 'log') && !($option instanceof Zend_Log)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Instance of Zend_Log expected for option log');
}
if ($key == 'cache') {
self::setCache($option);
continue;
}
$this->_options[$key] = $option;
$change = true;
}
}
if ($locale !== null) {
$this->setLocale($locale);
}
if (isset(self::$_cache) and ($change == true)) {
$id = 'Zend_Translate_' . $this->toString() . '_Options';
if (self::$_cacheTags) {
self::$_cache->save($this->_options, $id, array($this->_options['tag']));
} else {
self::$_cache->save($this->_options, $id);
}
}
return $this;
}
/**
* Returns the adapters name and it's options
*
* @param string|null $optionKey String returns this option
* null returns all options
* @return integer|string|array|null
*/
public function getOptions($optionKey = null)
{
if ($optionKey === null) {
return $this->_options;
}
if (isset($this->_options[$optionKey]) === true) {
return $this->_options[$optionKey];
}
return null;
}
/**
* Gets locale
*
* @return Zend_Locale|string|null
*/
public function getLocale()
{
return $this->_options['locale'];
}
/**
* Sets locale
*
* @param string|Zend_Locale $locale Locale to set
* @throws Zend_Translate_Exception
* @return Zend_Translate_Adapter Provides fluent interface
*/
public function setLocale($locale)
{
if (($locale === "auto") or ($locale === null)) {
$this->_automatic = true;
} else {
$this->_automatic = false;
}
try {
$locale = Zend_Locale::findLocale($locale);
} catch (Zend_Locale_Exception $e) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("The given Language ({$locale}) does not exist", 0, $e);
}
if (!isset($this->_translate[$locale])) {
$temp = explode('_', $locale);
if (!isset($this->_translate[$temp[0]]) and !isset($this->_translate[$locale])) {
if (!$this->_options['disableNotices']) {
if ($this->_options['log']) {
$this->_options['log']->log("The language '{$locale}' has to be added before it can be used.", $this->_options['logPriority']);
} else {
trigger_error("The language '{$locale}' has to be added before it can be used.", E_USER_NOTICE);
}
}
}
$locale = $temp[0];
}
if (empty($this->_translate[$locale])) {
if (!$this->_options['disableNotices']) {
if ($this->_options['log']) {
$this->_options['log']->log("No translation for the language '{$locale}' available.", $this->_options['logPriority']);
} else {
trigger_error("No translation for the language '{$locale}' available.", E_USER_NOTICE);
}
}
}
if ($this->_options['locale'] != $locale) {
$this->_options['locale'] = $locale;
if (isset(self::$_cache)) {
$id = 'Zend_Translate_' . $this->toString() . '_Options';
if (self::$_cacheTags) {
self::$_cache->save($this->_options, $id, array($this->_options['tag']));
} else {
self::$_cache->save($this->_options, $id);
}
}
}
return $this;
}
/**
* Returns the available languages from this adapter
*
* @return array|null
*/
public function getList()
{
$list = array_keys($this->_translate);
$result = null;
foreach($list as $value) {
if (!empty($this->_translate[$value])) {
$result[$value] = $value;
}
}
return $result;
}
/**
* Returns the message id for a given translation
* If no locale is given, the actual language will be used
*
* @param string $message Message to get the key for
* @param string|Zend_Locale $locale (optional) Language to return the message ids from
* @return string|array|false
*/
public function getMessageId($message, $locale = null)
{
if (empty($locale) or !$this->isAvailable($locale)) {
$locale = $this->_options['locale'];
}
return array_search($message, $this->_translate[(string) $locale]);
}
/**
* Returns all available message ids from this adapter
* If no locale is given, the actual language will be used
*
* @param string|Zend_Locale $locale (optional) Language to return the message ids from
* @return array
*/
public function getMessageIds($locale = null)
{
if (empty($locale) or !$this->isAvailable($locale)) {
$locale = $this->_options['locale'];
}
return array_keys($this->_translate[(string) $locale]);
}
/**
* Returns all available translations from this adapter
* If no locale is given, the actual language will be used
* If 'all' is given the complete translation dictionary will be returned
*
* @param string|Zend_Locale $locale (optional) Language to return the messages from
* @return array
*/
public function getMessages($locale = null)
{
if ($locale === 'all') {
return $this->_translate;
}
if ((empty($locale) === true) or ($this->isAvailable($locale) === false)) {
$locale = $this->_options['locale'];
}
return $this->_translate[(string) $locale];
}
/**
* Is the wished language available ?
*
* @see Zend_Locale
* @param string|Zend_Locale $locale Language to search for, identical with locale identifier,
* @see Zend_Locale for more information
* @return boolean
*/
public function isAvailable($locale)
{
$return = isset($this->_translate[(string) $locale]);
return $return;
}
/**
* Load translation data
*
* @param mixed $data
* @param string|Zend_Locale $locale
* @param array $options (optional)
* @return array
*/
abstract protected function _loadTranslationData($data, $locale, array $options = array());
/**
* Internal function for adding translation data
*
* This may be a new language or additional data for an existing language
* If the options 'clear' is true, then the translation data for the specified
* language is replaced and added otherwise
*
* @see Zend_Locale
* @param array|Zend_Config $content Translation data to add
* @throws Zend_Translate_Exception
* @return Zend_Translate_Adapter Provides fluent interface
*/
private function _addTranslationData($options = array())
{
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options['content'] = array_shift($args);
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$options += array_shift($args);
}
}
if (($options['content'] instanceof Zend_Translate) || ($options['content'] instanceof Zend_Translate_Adapter)) {
$options['usetranslateadapter'] = true;
if (!empty($options['locale']) && ($options['locale'] !== 'auto')) {
$options['content'] = $options['content']->getMessages($options['locale']);
} else {
$content = $options['content'];
$locales = $content->getList();
foreach ($locales as $locale) {
$options['locale'] = $locale;
$options['content'] = $content->getMessages($locale);
$this->_addTranslationData($options);
}
return $this;
}
}
try {
$options['locale'] = Zend_Locale::findLocale($options['locale']);
} catch (Zend_Locale_Exception $e) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("The given Language '{$options['locale']}' does not exist", 0, $e);
}
if ($options['clear'] || !isset($this->_translate[$options['locale']])) {
$this->_translate[$options['locale']] = array();
}
$read = true;
if (isset(self::$_cache)) {
$id = 'Zend_Translate_' . md5(serialize($options['content'])) . '_' . $this->toString();
$temp = self::$_cache->load($id);
if ($temp) {
$read = false;
}
}
if ($options['reload']) {
$read = true;
}
if ($read) {
if (!empty($options['usetranslateadapter'])) {
$temp = array($options['locale'] => $options['content']);
} else {
$temp = $this->_loadTranslationData($options['content'], $options['locale'], $options);
}
}
if (empty($temp)) {
$temp = array();
}
$keys = array_keys($temp);
foreach($keys as $key) {
if (!isset($this->_translate[$key])) {
$this->_translate[$key] = array();
}
if (array_key_exists($key, $temp) && is_array($temp[$key])) {
$this->_translate[$key] = $temp[$key] + $this->_translate[$key];
}
}
if ($this->_automatic === true) {
$find = new Zend_Locale($options['locale']);
$browser = $find->getEnvironment() + $find->getBrowser();
arsort($browser);
foreach($browser as $language => $quality) {
if (isset($this->_translate[$language])) {
$this->_options['locale'] = $language;
break;
}
}
}
if (($read) and (isset(self::$_cache))) {
$id = 'Zend_Translate_' . md5(serialize($options['content'])) . '_' . $this->toString();
if (self::$_cacheTags) {
self::$_cache->save($temp, $id, array($this->_options['tag']));
} else {
self::$_cache->save($temp, $id);
}
}
return $this;
}
/**
* Translates the given string
* returns the translation
*
* @see Zend_Locale
* @param string|array $messageId Translation string, or Array for plural translations
* @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with
* locale identifier, @see Zend_Locale for more information
* @return string
*/
public function translate($messageId, $locale = null)
{
if ($locale === null) {
$locale = $this->_options['locale'];
}
$plural = null;
if (is_array($messageId)) {
if (count($messageId) > 2) {
$number = array_pop($messageId);
if (!is_numeric($number)) {
$plocale = $number;
$number = array_pop($messageId);
} else {
$plocale = 'en';
}
$plural = $messageId;
$messageId = $messageId[0];
} else {
$messageId = $messageId[0];
}
}
// CUSTOM ischommer: Skip locale checks, too computationally expensive.
// Assume correct locale value is passed in.
// if (!Zend_Locale::isLocale($locale, true, false)) {
// if (!Zend_Locale::isLocale($locale, false, false)) {
// // language does not exist, return original string
// $this->_log($messageId, $locale);
// // use rerouting when enabled
// if (!empty($this->_options['route'])) {
// if (array_key_exists($locale, $this->_options['route']) &&
// !array_key_exists($locale, $this->_routed)) {
// $this->_routed[$locale] = true;
// return $this->translate($messageId, $this->_options['route'][$locale]);
// }
// }
// $this->_routed = array();
// if ($plural === null) {
// return $messageId;
// }
// $rule = Zend_Translate_Plural::getPlural($number, $plocale);
// if (!isset($plural[$rule])) {
// $rule = 0;
// }
// return $plural[$rule];
// }
// $locale = new Zend_Locale($locale);
// }
// CUSTOM END
$locale = (string) $locale;
if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
// return original translation
if ($plural === null) {
$this->_routed = array();
return $this->_translate[$locale][$messageId];
}
$rule = Zend_Translate_Plural::getPlural($number, $locale);
if (isset($this->_translate[$locale][$plural[0]][$rule])) {
$this->_routed = array();
return $this->_translate[$locale][$plural[0]][$rule];
}
} else if (strlen($locale) != 2) {
// faster than creating a new locale and separate the leading part
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
// return regionless translation (en_US -> en)
if ($plural === null) {
$this->_routed = array();
return $this->_translate[$locale][$messageId];
}
$rule = Zend_Translate_Plural::getPlural($number, $locale);
if (isset($this->_translate[$locale][$plural[0]][$rule])) {
$this->_routed = array();
return $this->_translate[$locale][$plural[0]][$rule];
}
}
}
$this->_log($messageId, $locale);
// use rerouting when enabled
if (!empty($this->_options['route'])) {
if (array_key_exists($locale, $this->_options['route']) &&
!array_key_exists($locale, $this->_routed)) {
$this->_routed[$locale] = true;
return $this->translate($messageId, $this->_options['route'][$locale]);
}
}
$this->_routed = array();
if ($plural === null) {
return $messageId;
}
$rule = Zend_Translate_Plural::getPlural($number, $plocale);
if (!isset($plural[$rule])) {
$rule = 0;
}
return $plural[$rule];
}
/**
* Translates the given string using plural notations
* Returns the translated string
*
* @see Zend_Locale
* @param string $singular Singular translation string
* @param string $plural Plural translation string
* @param integer $number Number for detecting the correct plural
* @param string|Zend_Locale $locale (Optional) Locale/Language to use, identical with
* locale identifier, @see Zend_Locale for more information
* @return string
*/
public function plural($singular, $plural, $number, $locale = null)
{
return $this->translate(array($singular, $plural, $number), $locale);
}
/**
* Logs a message when the log option is set
*
* @param string $message Message to log
* @param String $locale Locale to log
*/
protected function _log($message, $locale) {
if ($this->_options['logUntranslated']) {
$message = str_replace('%message%', $message, $this->_options['logMessage']);
$message = str_replace('%locale%', $locale, $message);
if ($this->_options['log']) {
$this->_options['log']->log($message, $this->_options['logPriority']);
} else {
trigger_error($message, E_USER_NOTICE);
}
}
}
/**
* Translates the given string
* returns the translation
*
* @param string $messageId Translation string
* @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with locale
* identifier, @see Zend_Locale for more information
* @return string
*/
public function _($messageId, $locale = null)
{
return $this->translate($messageId, $locale);
}
/**
* Checks if a string is translated within the source or not
* returns boolean
*
* @param string $messageId Translation string
* @param boolean $original (optional) Allow translation only for original language
* when true, a translation for 'en_US' would give false when it can
* be translated with 'en' only
* @param string|Zend_Locale $locale (optional) Locale/Language to use, identical with locale identifier,
* see Zend_Locale for more information
* @return boolean
*/
public function isTranslated($messageId, $original = false, $locale = null)
{
if (($original !== false) and ($original !== true)) {
$locale = $original;
$original = false;
}
if ($locale === null) {
$locale = $this->_options['locale'];
}
if (!Zend_Locale::isLocale($locale, true, false)) {
if (!Zend_Locale::isLocale($locale, false, false)) {
// language does not exist, return original string
return false;
}
$locale = new Zend_Locale($locale);
}
$locale = (string) $locale;
if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
// return original translation
return true;
} else if ((strlen($locale) != 2) and ($original === false)) {
// faster than creating a new locale and separate the leading part
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
if ((is_string($messageId) || is_int($messageId)) && isset($this->_translate[$locale][$messageId])) {
// return regionless translation (en_US -> en)
return true;
}
}
// No translation found, return original
return false;
}
/**
* Returns the set cache
*
* @return Zend_Cache_Core The set cache
*/
public static function getCache()
{
return self::$_cache;
}
/**
* Sets a cache for all Zend_Translate_Adapters
*
* @param Zend_Cache_Core $cache Cache to store to
*/
public static function setCache(Zend_Cache_Core $cache)
{
self::$_cache = $cache;
self::_getTagSupportForCache();
}
/**
* Returns true when a cache is set
*
* @return boolean
*/
public static function hasCache()
{
if (self::$_cache !== null) {
return true;
}
return false;
}
/**
* Removes any set cache
*
* @return void
*/
public static function removeCache()
{
self::$_cache = null;
}
/**
* Clears all set cache data
*
* @param string $tag Tag to clear when the default tag name is not used
* @return void
*/
public static function clearCache($tag = null)
{
require_once 'Zend/Cache.php';
if (self::$_cacheTags) {
if ($tag == null) {
$tag = 'Zend_Translate';
}
self::$_cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array($tag));
} else {
self::$_cache->clean(Zend_Cache::CLEANING_MODE_ALL);
}
}
/**
* Returns the adapter name
*
* @return string
*/
abstract public function toString();
/**
* Internal method to check if the given cache supports tags
*
* @param Zend_Cache $cache
*/
private static function _getTagSupportForCache()
{
$backend = self::$_cache->getBackend();
if ($backend instanceof Zend_Cache_Backend_ExtendedInterface) {
$cacheOptions = $backend->getCapabilities();
self::$_cacheTags = $cacheOptions['tags'];
} else {
self::$_cacheTags = false;
}
return self::$_cacheTags;
}
}

View File

@ -1,81 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Array.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Array extends Zend_Translate_Adapter
{
private $_data = array();
/**
* Load translation data
*
* @param string|array $data
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param array $options OPTIONAL Options to use
* @return array
*/
protected function _loadTranslationData($data, $locale, array $options = array())
{
$this->_data = array();
if (!is_array($data)) {
if (file_exists($data)) {
ob_start();
$data = include($data);
ob_end_clean();
}
}
if (!is_array($data)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("Error including array or file '".$data."'");
}
if (!isset($this->_data[$locale])) {
$this->_data[$locale] = array();
}
$this->_data[$locale] = $data + $this->_data[$locale];
return $this->_data;
}
/**
* returns the adapters name
*
* @return string
*/
public function toString()
{
return "Array";
}
}

View File

@ -1,121 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Csv.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Csv extends Zend_Translate_Adapter
{
private $_data = array();
/**
* Generates the adapter
*
* @param array|Zend_Config $options Translation content
*/
public function __construct($options = array())
{
$this->_options['delimiter'] = ";";
$this->_options['length'] = 0;
$this->_options['enclosure'] = '"';
if ($options instanceof Zend_Config) {
$options = $options->toArray();
} else if (func_num_args() > 1) {
$args = func_get_args();
$options = array();
$options['content'] = array_shift($args);
if (!empty($args)) {
$options['locale'] = array_shift($args);
}
if (!empty($args)) {
$opt = array_shift($args);
$options = array_merge($opt, $options);
}
} else if (!is_array($options)) {
$options = array('content' => $options);
}
parent::__construct($options);
}
/**
* Load translation data
*
* @param string|array $filename Filename and full path to the translation source
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param array $option OPTIONAL Options to use
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
$options = $options + $this->_options;
$this->_file = @fopen($filename, 'rb');
if (!$this->_file) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Error opening translation file \'' . $filename . '\'.');
}
while(($data = fgetcsv($this->_file, $options['length'], $options['delimiter'], $options['enclosure'])) !== false) {
if (substr($data[0], 0, 1) === '#') {
continue;
}
if (!isset($data[1])) {
continue;
}
if (count($data) == 2) {
$this->_data[$locale][$data[0]] = $data[1];
} else {
$singular = array_shift($data);
$this->_data[$locale][$singular] = $data;
}
}
return $this->_data;
}
/**
* returns the adapters name
*
* @return string
*/
public function toString()
{
return "Csv";
}
}

View File

@ -1,169 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Gettext.php 23961 2011-05-03 11:20:34Z yoshida@zend.co.jp $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Gettext extends Zend_Translate_Adapter {
// Internal variables
private $_bigEndian = false;
private $_file = false;
private $_adapterInfo = array();
private $_data = array();
/**
* Read values from the MO file
*
* @param string $bytes
*/
private function _readMOData($bytes)
{
if ($this->_bigEndian === false) {
return unpack('V' . $bytes, fread($this->_file, 4 * $bytes));
} else {
return unpack('N' . $bytes, fread($this->_file, 4 * $bytes));
}
}
/**
* Load translation data (MO file reader)
*
* @param string $filename MO file to add, full path must be given for access
* @param string $locale New Locale/Language to set, identical with locale identifier,
* see Zend_Locale for more information
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
$this->_bigEndian = false;
$this->_file = @fopen($filename, 'rb');
if (!$this->_file) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Error opening translation file \'' . $filename . '\'.');
}
if (@filesize($filename) < 10) {
@fclose($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('\'' . $filename . '\' is not a gettext file');
}
// get Endian
$input = $this->_readMOData(1);
if (strtolower(substr(dechex($input[1]), -8)) == "950412de") {
$this->_bigEndian = false;
} else if (strtolower(substr(dechex($input[1]), -8)) == "de120495") {
$this->_bigEndian = true;
} else {
@fclose($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('\'' . $filename . '\' is not a gettext file');
}
// read revision - not supported for now
$input = $this->_readMOData(1);
// number of bytes
$input = $this->_readMOData(1);
$total = $input[1];
// number of original strings
$input = $this->_readMOData(1);
$OOffset = $input[1];
// number of translation strings
$input = $this->_readMOData(1);
$TOffset = $input[1];
// fill the original table
fseek($this->_file, $OOffset);
$origtemp = $this->_readMOData(2 * $total);
fseek($this->_file, $TOffset);
$transtemp = $this->_readMOData(2 * $total);
for($count = 0; $count < $total; ++$count) {
if ($origtemp[$count * 2 + 1] != 0) {
fseek($this->_file, $origtemp[$count * 2 + 2]);
$original = @fread($this->_file, $origtemp[$count * 2 + 1]);
$original = explode("\0", $original);
} else {
$original[0] = '';
}
if ($transtemp[$count * 2 + 1] != 0) {
fseek($this->_file, $transtemp[$count * 2 + 2]);
$translate = fread($this->_file, $transtemp[$count * 2 + 1]);
$translate = explode("\0", $translate);
if ((count($original) > 1) && (count($translate) > 1)) {
$this->_data[$locale][$original[0]] = $translate;
array_shift($original);
foreach ($original as $orig) {
$this->_data[$locale][$orig] = '';
}
} else {
$this->_data[$locale][$original[0]] = $translate[0];
}
}
}
@fclose($this->_file);
$this->_data[$locale][''] = trim($this->_data[$locale]['']);
if (empty($this->_data[$locale][''])) {
$this->_adapterInfo[$filename] = 'No adapter information available';
} else {
$this->_adapterInfo[$filename] = $this->_data[$locale][''];
}
unset($this->_data[$locale]['']);
return $this->_data;
}
/**
* Returns the adapter informations
*
* @return array Each loaded adapter information as array value
*/
public function getAdapterInfo()
{
return $this->_adapterInfo;
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "Gettext";
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Ini.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Ini extends Zend_Translate_Adapter
{
private $_data = array();
/**
* Load translation data
*
* @param string|array $data
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param array $options OPTIONAL Options to use
* @throws Zend_Translate_Exception Ini file not found
* @return array
*/
protected function _loadTranslationData($data, $locale, array $options = array())
{
$this->_data = array();
if (!file_exists($data)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception("Ini file '".$data."' not found");
}
$inidata = parse_ini_file($data, false);
if (!isset($this->_data[$locale])) {
$this->_data[$locale] = array();
}
$this->_data[$locale] = array_merge($this->_data[$locale], $inidata);
return $this->_data;
}
/**
* returns the adapters name
*
* @return string
*/
public function toString()
{
return "Ini";
}
}

View File

@ -1,160 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Qt.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Qt extends Zend_Translate_Adapter {
// Internal variables
private $_file = false;
private $_cleared = array();
private $_transunit = null;
private $_source = null;
private $_target = null;
private $_scontent = null;
private $_tcontent = null;
private $_stag = false;
private $_ttag = true;
private $_data = array();
/**
* Load translation data (QT file reader)
*
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param string $filename QT file to add, full path must be given for access
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
if (!is_readable($filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Translation file \'' . $filename . '\' is not readable.');
}
$this->_target = $locale;
$encoding = $this->_findEncoding($filename);
$this->_file = xml_parser_create($encoding);
xml_set_object($this->_file, $this);
xml_parser_set_option($this->_file, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($this->_file, "_startElement", "_endElement");
xml_set_character_data_handler($this->_file, "_contentElement");
if (!xml_parse($this->_file, file_get_contents($filename))) {
$ex = sprintf('XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->_file)),
xml_get_current_line_number($this->_file));
xml_parser_free($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception($ex);
}
return $this->_data;
}
private function _startElement($file, $name, $attrib)
{
switch(strtolower($name)) {
case 'message':
$this->_source = null;
$this->_stag = false;
$this->_ttag = false;
$this->_scontent = null;
$this->_tcontent = null;
break;
case 'source':
$this->_stag = true;
break;
case 'translation':
$this->_ttag = true;
break;
default:
break;
}
}
private function _endElement($file, $name)
{
switch (strtolower($name)) {
case 'source':
$this->_stag = false;
break;
case 'translation':
if (!empty($this->_scontent) and !empty($this->_tcontent) or
(isset($this->_data[$this->_target][$this->_scontent]) === false)) {
$this->_data[$this->_target][$this->_scontent] = $this->_tcontent;
}
$this->_ttag = false;
break;
default:
break;
}
}
private function _contentElement($file, $data)
{
if ($this->_stag === true) {
$this->_scontent .= $data;
}
if ($this->_ttag === true) {
$this->_tcontent .= $data;
}
}
private function _findEncoding($filename)
{
$file = file_get_contents($filename, null, null, 0, 100);
if (strpos($file, "encoding") !== false) {
$encoding = substr($file, strpos($file, "encoding") + 9);
$encoding = substr($encoding, 1, strpos($encoding, $encoding[0], 1) - 1);
return $encoding;
}
return 'UTF-8';
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "Qt";
}
}

View File

@ -1,165 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Tbx.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Tbx extends Zend_Translate_Adapter {
// Internal variables
private $_file = false;
private $_cleared = array();
private $_langset = null;
private $_termentry = null;
private $_content = null;
private $_term = null;
private $_data = array();
/**
* Load translation data (TBX file reader)
*
* @param string $filename TBX file to add, full path must be given for access
* @param string $locale Locale has no effect for TBX because TBX defines all languages within
* the source file
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
if (!is_readable($filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Translation file \'' . $filename . '\' is not readable.');
}
$encoding = $this->_findEncoding($filename);
$this->_file = xml_parser_create($encoding);
xml_set_object($this->_file, $this);
xml_parser_set_option($this->_file, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($this->_file, "_startElement", "_endElement");
xml_set_character_data_handler($this->_file, "_contentElement");
if (!xml_parse($this->_file, file_get_contents($filename))) {
$ex = sprintf('XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->_file)),
xml_get_current_line_number($this->_file));
xml_parser_free($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception($ex);
}
return $this->_data;
}
private function _startElement($file, $name, $attrib)
{
if ($this->_term !== null) {
$this->_content .= "<".$name;
foreach($attrib as $key => $value) {
$this->_content .= " $key=\"$value\"";
}
$this->_content .= ">";
} else {
switch(strtolower($name)) {
case 'termentry':
$this->_termentry = null;
break;
case 'langset':
if (isset($attrib['xml:lang']) === true) {
$this->_langset = $attrib['xml:lang'];
if (isset($this->_data[$this->_langset]) === false) {
$this->_data[$this->_langset] = array();
}
}
break;
case 'term':
$this->_term = true;
$this->_content = null;
break;
default:
break;
}
}
}
private function _endElement($file, $name)
{
if (($this->_term !== null) and ($name != "term")) {
$this->_content .= "</".$name.">";
} else {
switch (strtolower($name)) {
case 'langset':
$this->_langset = null;
break;
case 'term':
$this->_term = null;
if (empty($this->_termentry)) {
$this->_termentry = $this->_content;
}
if (!empty($this->_content) or (isset($this->_data[$this->_langset][$this->_termentry]) === false)) {
$this->_data[$this->_langset][$this->_termentry] = $this->_content;
}
break;
default:
break;
}
}
}
private function _contentElement($file, $data)
{
if ($this->_term !== null) {
$this->_content .= $data;
}
}
private function _findEncoding($filename)
{
$file = file_get_contents($filename, null, null, 0, 100);
if (strpos($file, "encoding") !== false) {
$encoding = substr($file, strpos($file, "encoding") + 9);
$encoding = substr($encoding, 1, strpos($encoding, $encoding[0], 1) - 1);
return $encoding;
}
return 'UTF-8';
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "Tbx";
}
}

View File

@ -1,233 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Tmx.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Tmx extends Zend_Translate_Adapter {
// Internal variables
private $_file = false;
private $_useId = true;
private $_srclang = null;
private $_tu = null;
private $_tuv = null;
private $_seg = null;
private $_content = null;
private $_data = array();
/**
* Load translation data (TMX file reader)
*
* @param string $filename TMX file to add, full path must be given for access
* @param string $locale Locale has no effect for TMX because TMX defines all languages within
* the source file
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
if (!is_readable($filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Translation file \'' . $filename . '\' is not readable.');
}
if (isset($options['useId'])) {
$this->_useId = (boolean) $options['useId'];
}
$encoding = $this->_findEncoding($filename);
$this->_file = xml_parser_create($encoding);
xml_set_object($this->_file, $this);
xml_parser_set_option($this->_file, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($this->_file, "_startElement", "_endElement");
xml_set_character_data_handler($this->_file, "_contentElement");
if (!xml_parse($this->_file, file_get_contents($filename))) {
$ex = sprintf('XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->_file)),
xml_get_current_line_number($this->_file));
xml_parser_free($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception($ex);
}
return $this->_data;
}
/**
* Internal method, called by xml element handler at start
*
* @param resource $file File handler
* @param string $name Elements name
* @param array $attrib Attributes for this element
*/
protected function _startElement($file, $name, $attrib)
{
if ($this->_seg !== null) {
$this->_content .= "<".$name;
foreach($attrib as $key => $value) {
$this->_content .= " $key=\"$value\"";
}
$this->_content .= ">";
} else {
switch(strtolower($name)) {
case 'header':
if (empty($this->_useId) && isset($attrib['srclang'])) {
if (Zend_Locale::isLocale($attrib['srclang'])) {
$this->_srclang = Zend_Locale::findLocale($attrib['srclang']);
} else {
if (!$this->_options['disableNotices']) {
if ($this->_options['log']) {
$this->_options['log']->notice("The language '{$attrib['srclang']}' can not be set because it does not exist.");
} else {
trigger_error("The language '{$attrib['srclang']}' can not be set because it does not exist.", E_USER_NOTICE);
}
}
$this->_srclang = $attrib['srclang'];
}
}
break;
case 'tu':
if (isset($attrib['tuid'])) {
$this->_tu = $attrib['tuid'];
}
break;
case 'tuv':
if (isset($attrib['xml:lang'])) {
if (Zend_Locale::isLocale($attrib['xml:lang'])) {
$this->_tuv = Zend_Locale::findLocale($attrib['xml:lang']);
} else {
if (!$this->_options['disableNotices']) {
if ($this->_options['log']) {
$this->_options['log']->notice("The language '{$attrib['xml:lang']}' can not be set because it does not exist.");
} else {
trigger_error("The language '{$attrib['xml:lang']}' can not be set because it does not exist.", E_USER_NOTICE);
}
}
$this->_tuv = $attrib['xml:lang'];
}
if (!isset($this->_data[$this->_tuv])) {
$this->_data[$this->_tuv] = array();
}
}
break;
case 'seg':
$this->_seg = true;
$this->_content = null;
break;
default:
break;
}
}
}
/**
* Internal method, called by xml element handler at end
*
* @param resource $file File handler
* @param string $name Elements name
*/
protected function _endElement($file, $name)
{
if (($this->_seg !== null) and ($name !== 'seg')) {
$this->_content .= "</".$name.">";
} else {
switch (strtolower($name)) {
case 'tu':
$this->_tu = null;
break;
case 'tuv':
$this->_tuv = null;
break;
case 'seg':
$this->_seg = null;
if (!empty($this->_srclang) && ($this->_srclang == $this->_tuv)) {
$this->_tu = $this->_content;
}
if (!empty($this->_content) or (!isset($this->_data[$this->_tuv][$this->_tu]))) {
$this->_data[$this->_tuv][$this->_tu] = $this->_content;
}
break;
default:
break;
}
}
}
/**
* Internal method, called by xml element handler for content
*
* @param resource $file File handler
* @param string $data Elements content
*/
protected function _contentElement($file, $data)
{
if (($this->_seg !== null) and ($this->_tu !== null) and ($this->_tuv !== null)) {
$this->_content .= $data;
}
}
/**
* Internal method, detects the encoding of the xml file
*
* @param string $name Filename
* @return string Encoding
*/
protected function _findEncoding($filename)
{
$file = file_get_contents($filename, null, null, 0, 100);
if (strpos($file, "encoding") !== false) {
$encoding = substr($file, strpos($file, "encoding") + 9);
$encoding = substr($encoding, 1, strpos($encoding, $encoding[0], 1) - 1);
return $encoding;
}
return 'UTF-8';
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "Tmx";
}
}

View File

@ -1,229 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Xliff.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_Xliff extends Zend_Translate_Adapter {
// Internal variables
private $_file = false;
private $_useId = true;
private $_cleared = array();
private $_transunit = null;
private $_source = null;
private $_target = null;
private $_langId = null;
private $_scontent = null;
private $_tcontent = null;
private $_stag = false;
private $_ttag = false;
private $_data = array();
/**
* Load translation data (XLIFF file reader)
*
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param string $filename XLIFF file to add, full path must be given for access
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
if (!is_readable($filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Translation file \'' . $filename . '\' is not readable.');
}
if (empty($options['useId'])) {
$this->_useId = false;
} else {
$this->_useId = true;
}
$encoding = $this->_findEncoding($filename);
$this->_target = $locale;
$this->_file = xml_parser_create($encoding);
xml_set_object($this->_file, $this);
xml_parser_set_option($this->_file, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($this->_file, "_startElement", "_endElement");
xml_set_character_data_handler($this->_file, "_contentElement");
if (!xml_parse($this->_file, file_get_contents($filename))) {
$ex = sprintf('XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->_file)),
xml_get_current_line_number($this->_file));
xml_parser_free($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception($ex);
}
return $this->_data;
}
private function _startElement($file, $name, $attrib)
{
if ($this->_stag === true) {
$this->_scontent .= "<".$name;
foreach($attrib as $key => $value) {
$this->_scontent .= " $key=\"$value\"";
}
$this->_scontent .= ">";
} else if ($this->_ttag === true) {
$this->_tcontent .= "<".$name;
foreach($attrib as $key => $value) {
$this->_tcontent .= " $key=\"$value\"";
}
$this->_tcontent .= ">";
} else {
switch(strtolower($name)) {
case 'file':
$this->_source = $attrib['source-language'];
if (isset($attrib['target-language'])) {
$this->_target = $attrib['target-language'];
}
if (!isset($this->_data[$this->_source])) {
$this->_data[$this->_source] = array();
}
if (!isset($this->_data[$this->_target])) {
$this->_data[$this->_target] = array();
}
break;
case 'trans-unit':
$this->_transunit = true;
$this->_langId = $attrib['id'];
break;
case 'source':
if ($this->_transunit === true) {
$this->_scontent = null;
$this->_stag = true;
$this->_ttag = false;
}
break;
case 'target':
if ($this->_transunit === true) {
$this->_tcontent = null;
$this->_ttag = true;
$this->_stag = false;
}
break;
default:
break;
}
}
}
private function _endElement($file, $name)
{
if (($this->_stag === true) and ($name !== 'source')) {
$this->_scontent .= "</".$name.">";
} else if (($this->_ttag === true) and ($name !== 'target')) {
$this->_tcontent .= "</".$name.">";
} else {
switch (strtolower($name)) {
case 'trans-unit':
$this->_transunit = null;
$this->_langId = null;
$this->_scontent = null;
$this->_tcontent = null;
break;
case 'source':
if ($this->_useId) {
if (!empty($this->_scontent) && !empty($this->_langId) &&
!isset($this->_data[$this->_source][$this->_langId])) {
$this->_data[$this->_source][$this->_langId] = $this->_scontent;
}
} else {
if (!empty($this->_scontent) &&
!isset($this->_data[$this->_source][$this->_scontent])) {
$this->_data[$this->_source][$this->_scontent] = $this->_scontent;
}
}
$this->_stag = false;
break;
case 'target':
if ($this->_useId) {
if (!empty($this->_tcontent) && !empty($this->_langId) &&
!isset($this->_data[$this->_target][$this->_langId])) {
$this->_data[$this->_target][$this->_langId] = $this->_tcontent;
}
} else {
if (!empty($this->_tcontent) && !empty($this->_scontent) &&
!isset($this->_data[$this->_target][$this->_scontent])) {
$this->_data[$this->_target][$this->_scontent] = $this->_tcontent;
}
}
$this->_ttag = false;
break;
default:
break;
}
}
}
private function _contentElement($file, $data)
{
if (($this->_transunit !== null) and ($this->_source !== null) and ($this->_stag === true)) {
$this->_scontent .= $data;
}
if (($this->_transunit !== null) and ($this->_target !== null) and ($this->_ttag === true)) {
$this->_tcontent .= $data;
}
}
private function _findEncoding($filename)
{
$file = file_get_contents($filename, null, null, 0, 100);
if (strpos($file, "encoding") !== false) {
$encoding = substr($file, strpos($file, "encoding") + 9);
$encoding = substr($encoding, 1, strpos($encoding, $encoding[0], 1) - 1);
return $encoding;
}
return 'UTF-8';
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "Xliff";
}
}

View File

@ -1,139 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: XmlTm.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/** Zend_Locale */
require_once 'Zend/Locale.php';
/** Zend_Translate_Adapter */
require_once 'Zend/Translate/Adapter.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Adapter_XmlTm extends Zend_Translate_Adapter {
// Internal variables
private $_file = false;
private $_cleared = array();
private $_lang = null;
private $_content = null;
private $_tag = null;
private $_data = array();
/**
* Load translation data (XMLTM file reader)
*
* @param string $locale Locale/Language to add data for, identical with locale identifier,
* see Zend_Locale for more information
* @param string $filename XMLTM file to add, full path must be given for access
* @param array $option OPTIONAL Options to use
* @throws Zend_Translation_Exception
* @return array
*/
protected function _loadTranslationData($filename, $locale, array $options = array())
{
$this->_data = array();
$this->_lang = $locale;
if (!is_readable($filename)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('Translation file \'' . $filename . '\' is not readable.');
}
$encoding = $this->_findEncoding($filename);
$this->_file = xml_parser_create($encoding);
xml_set_object($this->_file, $this);
xml_parser_set_option($this->_file, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($this->_file, "_startElement", "_endElement");
xml_set_character_data_handler($this->_file, "_contentElement");
if (!xml_parse($this->_file, file_get_contents($filename))) {
$ex = sprintf('XML error: %s at line %d',
xml_error_string(xml_get_error_code($this->_file)),
xml_get_current_line_number($this->_file));
xml_parser_free($this->_file);
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception($ex);
}
return $this->_data;
}
private function _startElement($file, $name, $attrib)
{
switch(strtolower($name)) {
case 'tm:tu':
$this->_tag = $attrib['id'];
$this->_content = null;
break;
default:
break;
}
}
private function _endElement($file, $name)
{
switch (strtolower($name)) {
case 'tm:tu':
if (!empty($this->_tag) and !empty($this->_content) or
(isset($this->_data[$this->_lang][$this->_tag]) === false)) {
$this->_data[$this->_lang][$this->_tag] = $this->_content;
}
$this->_tag = null;
$this->_content = null;
break;
default:
break;
}
}
private function _contentElement($file, $data)
{
if (($this->_tag !== null)) {
$this->_content .= $data;
}
}
private function _findEncoding($filename)
{
$file = file_get_contents($filename, null, null, 0, 100);
if (strpos($file, "encoding") !== false) {
$encoding = substr($file, strpos($file, "encoding") + 9);
$encoding = substr($encoding, 1, strpos($encoding, $encoding[0], 1) - 1);
return $encoding;
}
return 'UTF-8';
}
/**
* Returns the adapter name
*
* @return string
*/
public function toString()
{
return "XmlTm";
}
}

View File

@ -1,37 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @version $Id: Exception.php 23775 2011-03-01 17:25:24Z ralph $
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
/**
* Zend_Exception
*/
require_once 'Zend/Exception.php';
/**
* @category Zend
* @package Zend_Translate
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Exception extends Zend_Exception
{
}

View File

@ -1,224 +0,0 @@
<?php
/**
* Zend Framework
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://framework.zend.com/license/new-bsd
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@zend.com so we can send you a copy immediately.
*
* @category Zend
* @package Zend_Locale
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Plural.php 23775 2011-03-01 17:25:24Z ralph $
*/
/**
* Utility class for returning the plural rules according to the given locale
*
* @category Zend
* @package Zend_Locale
* @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Translate_Plural
{
/**
* Manual rule to use
*
* @var string
*/
protected static $_plural = array();
/**
* Returns the plural definition to use
*
* @param integer $number Number for plural selection
* @param string $locale Locale to use
* @return integer Plural number to use
*/
public static function getPlural($number, $locale)
{
if ($locale == "pt_BR") {
// temporary set a locale for brasilian
$locale = "xbr";
}
if (strlen($locale) > 3) {
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
}
if (isset(self::$_plural[$locale])) {
$return = call_user_func(self::$_plural[$locale], $number);
if (!is_int($return) || ($return < 0)) {
$return = 0;
}
return $return;
}
switch($locale) {
case 'bo':
case 'dz':
case 'id':
case 'ja':
case 'jv':
case 'ka':
case 'km':
case 'kn':
case 'ko':
case 'ms':
case 'th':
case 'tr':
case 'vi':
case 'zh':
return 0;
break;
case 'af':
case 'az':
case 'bn':
case 'bg':
case 'ca':
case 'da':
case 'de':
case 'el':
case 'en':
case 'eo':
case 'es':
case 'et':
case 'eu':
case 'fa':
case 'fi':
case 'fo':
case 'fur':
case 'fy':
case 'gl':
case 'gu':
case 'ha':
case 'he':
case 'hu':
case 'is':
case 'it':
case 'ku':
case 'lb':
case 'ml':
case 'mn':
case 'mr':
case 'nah':
case 'nb':
case 'ne':
case 'nl':
case 'nn':
case 'no':
case 'om':
case 'or':
case 'pa':
case 'pap':
case 'ps':
case 'pt':
case 'so':
case 'sq':
case 'sv':
case 'sw':
case 'ta':
case 'te':
case 'tk':
case 'ur':
case 'zu':
return ($number == 1) ? 0 : 1;
case 'am':
case 'bh':
case 'fil':
case 'fr':
case 'gun':
case 'hi':
case 'ln':
case 'mg':
case 'nso':
case 'xbr':
case 'ti':
case 'wa':
return (($number == 0) || ($number == 1)) ? 0 : 1;
case 'be':
case 'bs':
case 'hr':
case 'ru':
case 'sr':
case 'uk':
return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
case 'cs':
case 'sk':
return ($number == 1) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2);
case 'ga':
return ($number == 1) ? 0 : (($number == 2) ? 1 : 2);
case 'lt':
return (($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
case 'sl':
return ($number % 100 == 1) ? 0 : (($number % 100 == 2) ? 1 : ((($number % 100 == 3) || ($number % 100 == 4)) ? 2 : 3));
case 'mk':
return ($number % 10 == 1) ? 0 : 1;
case 'mt':
return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3));
case 'lv':
return ($number == 0) ? 0 : ((($number % 10 == 1) && ($number % 100 != 11)) ? 1 : 2);
case 'pl':
return ($number == 1) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2);
case 'cy':
return ($number == 1) ? 0 : (($number == 2) ? 1 : ((($number == 8) || ($number == 11)) ? 2 : 3));
case 'ro':
return ($number == 1) ? 0 : ((($number == 0) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2);
case 'ar':
return ($number == 0) ? 0 : (($number == 1) ? 1 : (($number == 2) ? 2 : ((($number >= 3) && ($number <= 10)) ? 3 : ((($number >= 11) && ($number <= 99)) ? 4 : 5))));
default:
return 0;
}
}
/**
* Set's a new plural rule
*
* @param string $rule Callback which acts as rule
* @param string $locale Locale which is used for this callback
* @return null
*/
public static function setPlural($rule, $locale)
{
if ($locale == "pt_BR") {
// temporary set a locale for brasilian
$locale = "xbr";
}
if (strlen($locale) > 3) {
$locale = substr($locale, 0, -strlen(strrchr($locale, '_')));
}
if (!is_callable($rule)) {
require_once 'Zend/Translate/Exception.php';
throw new Zend_Translate_Exception('The given rule can not be called');
}
self::$_plural[$locale] = $rule;
}
}

View File

@ -45,6 +45,26 @@ class ParserRegexp {
* for result construction and building
*/
class Parser {
/**
* @var string
*/
public $string;
/**
* @var int
*/
public $pos;
/**
* @var int
*/
public $depth;
/**
* @var array
*/
public $regexps;
function __construct( $string ) {
$this->string = $string ;
$this->pos = 0 ;
@ -105,7 +125,7 @@ class Parser {
}
function packread( $key, $pos ) {
throw 'PackRead after PackHas=>false in Parser.php' ;
throw new \Exception('PackRead after PackHas=>false in Parser.php');
}
function packwrite( $key, $pos, $res ) {