[Translation] added the component

This commit is contained in:
Fabien Potencier 2010-09-27 09:45:29 +02:00
parent 6317ddfbe1
commit a7537906b4
16 changed files with 3786 additions and 0 deletions

View File

@ -0,0 +1,33 @@
<?php
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* ArrayLoader loads translations from a PHP array.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class ArrayLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
function load($resource, $locale, $domain = 'messages')
{
$catalogue = new MessageCatalogue($locale);
$catalogue->addMessages($resource, $domain);
return $catalogue;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* LoaderInterface is the interface implemented by all translation loaders.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
interface LoaderInterface
{
/**
* Loads a locale.
*
* @param mixed $resource A resource
* @param string $locale A locale
* @param string $domain The domain
*
* @return MessageCatalogue A MessageCatalogue instance
*/
function load($resource, $locale, $domain = 'messages');
}

View File

@ -0,0 +1,35 @@
<?php
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Resource\FileResource;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* PhpFileLoader loads translations from PHP files returning an array of translations.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class PhpFileLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
function load($resource, $locale, $domain = 'messages')
{
$catalogue = new MessageCatalogue($locale);
$catalogue->addMessages(require($resource), $domain);
$catalogue->addResource(new FileResource($resource));
return $catalogue;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Symfony\Component\Translation\Loader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Resource\FileResource;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* XliffFileLoader loads translations from XLIFF files.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class XliffFileLoader implements LoaderInterface
{
/**
* {@inheritdoc}
*/
function load($resource, $locale, $domain = 'messages')
{
$xml = $this->parseFile($resource);
$xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
$catalogue = new MessageCatalogue($locale);
foreach ($xml->xpath('//xliff:trans-unit') as $translation) {
$catalogue->setMessage((string) $translation->source, (string) $translation->target, $domain);
}
$catalogue->addResource(new FileResource($resource));
return $catalogue;
}
/**
* Validates and parses the given file into a SimpleXMLElement
*
* @param string $file
* @return SimpleXMLElement
*/
protected function parseFile($file)
{
$dom = new \DOMDocument();
$current = libxml_use_internal_errors(true);
if (!@$dom->load($file, LIBXML_COMPACT)) {
throw new \Exception(implode("\n", $this->getXmlErrors()));
}
$parts = explode('/', str_replace('\\', '/', __DIR__).'/schema/dic/xliff-core/xml.xsd');
$drive = '\\' === DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$location = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
$source = file_get_contents(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd');
$source = str_replace('http://www.w3.org/2001/xml.xsd', $location, $source);
if (!@$dom->schemaValidateSource($source)) {
throw new \Exception(implode("\n", $this->getXmlErrors()));
}
$dom->validateOnParse = true;
$dom->normalizeDocument();
libxml_use_internal_errors($current);
return simplexml_import_dom($dom);
}
/**
* Returns the XML errors of the internal XML parser
*
* @return array An array of errors
*/
protected function getXmlErrors()
{
$errors = array();
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
$error->code,
trim($error->message),
$error->file ? $error->file : 'n/a',
$error->line,
$error->column
);
}
libxml_clear_errors();
libxml_use_internal_errors(false);
return $errors;
}
}

View File

@ -0,0 +1,309 @@
<?xml version='1.0'?>
<?xml-stylesheet href="../2008/09/xsd.xsl" type="text/xsl"?>
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns ="http://www.w3.org/1999/xhtml"
xml:lang="en">
<xs:annotation>
<xs:documentation>
<div>
<h1>About the XML namespace</h1>
<div class="bodytext">
<p>
This schema document describes the XML namespace, in a form
suitable for import by other schema documents.
</p>
<p>
See <a href="http://www.w3.org/XML/1998/namespace.html">
http://www.w3.org/XML/1998/namespace.html</a> and
<a href="http://www.w3.org/TR/REC-xml">
http://www.w3.org/TR/REC-xml</a> for information
about this namespace.
</p>
<p>
Note that local names in this namespace are intended to be
defined only by the World Wide Web Consortium or its subgroups.
The names currently defined in this namespace are listed below.
They should not be used with conflicting semantics by any Working
Group, specification, or document instance.
</p>
<p>
See further below in this document for more information about <a
href="#usage">how to refer to this schema document from your own
XSD schema documents</a> and about <a href="#nsversioning">the
namespace-versioning policy governing this schema document</a>.
</p>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:attribute name="lang">
<xs:annotation>
<xs:documentation>
<div>
<h3>lang (as an attribute name)</h3>
<p>
denotes an attribute whose value
is a language code for the natural language of the content of
any element; its value is inherited. This name is reserved
by virtue of its definition in the XML specification.</p>
</div>
<div>
<h4>Notes</h4>
<p>
Attempting to install the relevant ISO 2- and 3-letter
codes as the enumerated possible values is probably never
going to be a realistic possibility.
</p>
<p>
See BCP 47 at <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">
http://www.rfc-editor.org/rfc/bcp/bcp47.txt</a>
and the IANA language subtag registry at
<a href="http://www.iana.org/assignments/language-subtag-registry">
http://www.iana.org/assignments/language-subtag-registry</a>
for further information.
</p>
<p>
The union allows for the 'un-declaration' of xml:lang with
the empty string.
</p>
</div>
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:union memberTypes="xs:language">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value=""/>
</xs:restriction>
</xs:simpleType>
</xs:union>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="space">
<xs:annotation>
<xs:documentation>
<div>
<h3>space (as an attribute name)</h3>
<p>
denotes an attribute whose
value is a keyword indicating what whitespace processing
discipline is intended for the content of the element; its
value is inherited. This name is reserved by virtue of its
definition in the XML specification.</p>
</div>
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:NCName">
<xs:enumeration value="default"/>
<xs:enumeration value="preserve"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="base" type="xs:anyURI"> <xs:annotation>
<xs:documentation>
<div>
<h3>base (as an attribute name)</h3>
<p>
denotes an attribute whose value
provides a URI to be used as the base for interpreting any
relative URIs in the scope of the element on which it
appears; its value is inherited. This name is reserved
by virtue of its definition in the XML Base specification.</p>
<p>
See <a
href="http://www.w3.org/TR/xmlbase/">http://www.w3.org/TR/xmlbase/</a>
for information about this attribute.
</p>
</div>
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="id" type="xs:ID">
<xs:annotation>
<xs:documentation>
<div>
<h3>id (as an attribute name)</h3>
<p>
denotes an attribute whose value
should be interpreted as if declared to be of type ID.
This name is reserved by virtue of its definition in the
xml:id specification.</p>
<p>
See <a
href="http://www.w3.org/TR/xml-id/">http://www.w3.org/TR/xml-id/</a>
for information about this attribute.
</p>
</div>
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup name="specialAttrs">
<xs:attribute ref="xml:base"/>
<xs:attribute ref="xml:lang"/>
<xs:attribute ref="xml:space"/>
<xs:attribute ref="xml:id"/>
</xs:attributeGroup>
<xs:annotation>
<xs:documentation>
<div>
<h3>Father (in any context at all)</h3>
<div class="bodytext">
<p>
denotes Jon Bosak, the chair of
the original XML Working Group. This name is reserved by
the following decision of the W3C XML Plenary and
XML Coordination groups:
</p>
<blockquote>
<p>
In appreciation for his vision, leadership and
dedication the W3C XML Plenary on this 10th day of
February, 2000, reserves for Jon Bosak in perpetuity
the XML name "xml:Father".
</p>
</blockquote>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>
<div xml:id="usage" id="usage">
<h2><a name="usage">About this schema document</a></h2>
<div class="bodytext">
<p>
This schema defines attributes and an attribute group suitable
for use by schemas wishing to allow <code>xml:base</code>,
<code>xml:lang</code>, <code>xml:space</code> or
<code>xml:id</code> attributes on elements they define.
</p>
<p>
To enable this, such a schema must import this schema for
the XML namespace, e.g. as follows:
</p>
<pre>
&lt;schema . . .>
. . .
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/xml.xsd"/>
</pre>
<p>
or
</p>
<pre>
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
</pre>
<p>
Subsequently, qualified reference to any of the attributes or the
group defined below will have the desired effect, e.g.
</p>
<pre>
&lt;type . . .>
. . .
&lt;attributeGroup ref="xml:specialAttrs"/>
</pre>
<p>
will define a type which will schema-validate an instance element
with any of those attributes.
</p>
</div>
</div>
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>
<div id="nsversioning" xml:id="nsversioning">
<h2><a name="nsversioning">Versioning policy for this schema document</a></h2>
<div class="bodytext">
<p>
In keeping with the XML Schema WG's standard versioning
policy, this schema document will persist at
<a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd</a>.
</p>
<p>
At the date of issue it can also be found at
<a href="http://www.w3.org/2001/xml.xsd">
http://www.w3.org/2001/xml.xsd</a>.
</p>
<p>
The schema document at that URI may however change in the future,
in order to remain compatible with the latest version of XML
Schema itself, or with the XML namespace itself. In other words,
if the XML Schema or XML namespaces change, the version of this
document at <a href="http://www.w3.org/2001/xml.xsd">
http://www.w3.org/2001/xml.xsd
</a>
will change accordingly; the version at
<a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd
</a>
will not change.
</p>
<p>
Previous dated (and unchanging) versions of this schema
document are at:
</p>
<ul>
<li><a href="http://www.w3.org/2009/01/xml.xsd">
http://www.w3.org/2009/01/xml.xsd</a></li>
<li><a href="http://www.w3.org/2007/08/xml.xsd">
http://www.w3.org/2007/08/xml.xsd</a></li>
<li><a href="http://www.w3.org/2004/10/xml.xsd">
http://www.w3.org/2004/10/xml.xsd</a></li>
<li><a href="http://www.w3.org/2001/03/xml.xsd">
http://www.w3.org/2001/03/xml.xsd</a></li>
</ul>
</div>
</div>
</xs:documentation>
</xs:annotation>
</xs:schema>

View File

@ -0,0 +1,145 @@
<?php
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Resource\ResourceInterface;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* MessageCatalogue.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class MessageCatalogue implements MessageCatalogueInterface
{
protected $messages = array();
protected $locale;
protected $resources;
/**
* Constructor.
*
* @param string $locale The locale
* @param array $messages An array of messages classified by domain
*/
public function __construct($locale, array $messages = array())
{
$this->locale = $locale;
$this->messages = $messages;
$this->resources = array();
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->locale;
}
/**
* {@inheritdoc}
*/
public function getDomains()
{
return array_keys($this->messages);
}
/**
* {@inheritdoc}
*/
public function getMessages($domain = null)
{
if (null === $domain) {
return $this->messages;
}
return isset($this->messages[$domain]) ? $this->messages[$domain] : array();
}
/**
* {@inheritdoc}
*/
public function setMessage($id, $translation, $domain = 'messages')
{
$this->addMessages(array($id => $translation), $domain);
}
/**
* {@inheritdoc}
*/
public function hasMessage($id, $domain = 'messages')
{
return isset($this->messages[$domain][$id]);
}
/**
* {@inheritdoc}
*/
public function getMessage($id, $domain = 'messages')
{
return isset($this->messages[$domain][$id]) ? $this->messages[$domain][$id] : $id;
}
/**
* {@inheritdoc}
*/
public function setMessages($messages, $domain = 'messages')
{
if (isset($this->messages[$domain])) {
$this->messages[$domain] = array();
}
$this->addMessages($messages, $domain);
}
/**
* {@inheritdoc}
*/
public function addMessages($messages, $domain = 'messages')
{
if (!isset($this->messages[$domain])) {
$this->messages[$domain] = $messages;
} else {
$this->messages[$domain] = array_replace($this->messages[$domain], $messages);
}
}
/**
* {@inheritdoc}
*/
public function addCatalogue(MessageCatalogueInterface $catalogue)
{
foreach ($catalogue->getMessages() as $domain => $messages) {
$this->addMessages($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {
$this->addResource($resource);
}
}
/**
* {@inheritdoc}
*/
public function getResources()
{
return array_unique($this->resources);
}
/**
* {@inheritdoc}
*/
public function addResource(ResourceInterface $resource)
{
$this->resources[] = $resource;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Resource\ResourceInterface;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* MessageCatalogueInterface.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
interface MessageCatalogueInterface
{
/**
* Gets the catalogue locale.
*
* @return string The locale
*/
function getLocale();
/**
* Gets the domains.
*
* @param array An array of domains
*/
function getDomains();
/**
* Gets the messages within a given domain.
*
* If $domain is null, it returns all messages.
*
* @param string $domain The domain name
*
* @param array An array of messages
*/
function getMessages($domain = null);
/**
* Sets a message translation.
*
* @param string $id The message id
* @param string $translation The messages translation
* @param string $domain The domain name
*/
function setMessage($id, $translation, $domain = 'messages');
/**
* Checks if a message has a translation.
*
* @param string $id The message id
* @param string $domain The domain name
*
* @return Boolean true if the message has a translation, false otherwise
*/
function hasMessage($id, $domain = 'messages');
/**
* Gets a message translation.
*
* @param string $id The message id
* @param string $domain The domain name
*
* @return string The message translation
*/
function getMessage($id, $domain = 'messages');
/**
* Sets translations for a given domain.
*
* @param string $messages An array of translations
* @param string $domain The domain name
*/
function setMessages($messages, $domain = 'messages');
/**
* Adds translations for a given domain.
*
* @param string $messages An array of translations
* @param string $domain The domain name
*/
function addMessages($messages, $domain = 'messages');
/**
* Merges translations from the given Catalogue into the current one.
*
* @param MessageCatalogueInterface $catalogue A MessageCatalogueInterface instance
*/
function addCatalogue(MessageCatalogueInterface $catalogue);
/**
* Returns an array of resources loaded to build this collection.
*
* @return ResourceInterface[] An array of resources
*/
function getResources();
/**
* Adds a resource for this collection.
*
* @param ResourceInterface $resource A resource instance
*/
function addResource(ResourceInterface $resource);
}

View File

@ -0,0 +1,214 @@
<?php
namespace Symfony\Component\Translation;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* Returns the plural rules for a given locale.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class PluralizationRules
{
static protected $rules = array();
/**
* Returns the plural position to use for the given locale and number.
*
* @param integer $number The number
* @param string $locale The locale
*
* @return integer The plural position
*/
static public function get($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::$rules[$locale])) {
$return = call_user_func(self::$rules[$locale], $number);
if (!is_int($return) || $return < 0) {
return 0;
}
return $return;
}
/*
* The plural rules are derived from code of the Zend Framework (2010-09-25),
* which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
* Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
*/
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;
}
}
/**
* Overrides the default plural rule for a given locale.
*
* @param string $rule A PHP callable
* @param string $locale The locale
*
* @return null
*/
static public function set($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)) {
throw new Exception('The given rule can not be called');
}
self::$rules[$locale] = $rule;
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace Symfony\Component\Translation;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* Range tests if a given number belongs to a given range.
*
* A range can represent a finite set of numbers:
*
* {1,2,3,4}
*
* A range can represent numbers between two numbers:
*
* [1, +Inf]
* ]-1,2[
*
* The left delimiter can be [ (inclusive) or ] (exclusive).
* The right delimiter can be [ (exclusive) or ] (inclusive).
* Beside numbers, you can use -Inf and +Inf for the infinite.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class Range
{
/**
* Tests if the given number is in the range.
*
* @param integer $number A number
* @param string $range A range of numbers
*/
static public function test($number, $range)
{
$range = trim($range);
if (!preg_match('/^'.self::getRangeRegexp().'$/x', $range, $matches)) {
throw new \InvalidArgumentException(sprintf('"%s" is not a valid range expression.', $range));
}
if ($matches[1]) {
foreach (explode(',', $matches[2]) as $n) {
if ($number == $n) {
return true;
}
}
} else {
$leftNumber = self::convertNumber($matches['left']);
$rightNumber = self::convertNumber($matches['right']);
return
('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber)
&&
(']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber)
;
}
return false;
}
/**
* Returns a Regexp that matches valid ranges.
*
* @return string A Regexp (without the delimiters)
*/
static public function getRangeRegexp()
{
return <<<EOF
({\s*
(\-?\d+[\s*,\s*\-?\d+]*)
\s*})
|
(?<left_delimiter>[\[\]])
\s*
(?<left>-Inf|\-?\d+)
\s*,\s*
(?<right>\+?Inf|\-?\d+)
\s*
(?<right_delimiter>[\[\]])
EOF;
}
static protected function convertNumber($number)
{
if ('-Inf' === $number) {
return log(0);
} elseif ('+Inf' === $number || 'Inf' === $number) {
return -log(0);
} else {
return (int) $number;
}
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Symfony\Component\Translation\Resource;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* FileResource represents a resource stored on the filesystem.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class FileResource implements ResourceInterface
{
protected $resource;
/**
* Constructor.
*
* @param string $resource The file path to the resource
*/
public function __construct($resource)
{
$this->resource = realpath($resource);
}
/**
* Returns a string representation of the Resource.
*
* @return string A string representation of the Resource
*/
public function __toString()
{
return (string) $this->resource;
}
/**
* Returns the resource tied to this Resource.
*
* @return mixed The resource
*/
public function getResource()
{
return $this->resource;
}
/**
* Returns true if the resource has not been updated since the given timestamp.
*
* @param timestamp $timestamp The last time the resource was loaded
*
* @return Boolean true if the resource has not been updated, false otherwise
*/
public function isUptodate($timestamp)
{
if (!file_exists($this->resource)) {
return false;
}
return filemtime($this->resource) < $timestamp;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Symfony\Component\Translation\Resource;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* ResourceInterface is the interface that must be implemented by all Resource classes.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
interface ResourceInterface
{
/**
* Returns a string representation of the Resource.
*
* @return string A string representation of the Resource
*/
function __toString();
/**
* Returns true if the resource has not been updated since the given timestamp.
*
* @param int $timestamp The last time the resource was loaded
*
* @return Boolean true if the resource has not been updated, false otherwise
*/
function isUptodate($timestamp);
/**
* Returns the resource tied to this Resource.
*
* @return mixed The resource
*/
function getResource();
}

View File

@ -0,0 +1,201 @@
<?php
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Loader\LoaderInterface;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* Translator.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
class Translator implements TranslatorInterface
{
protected $catalogues;
protected $locale;
protected $fallbackLocale;
protected $loaders;
protected $resources;
/**
* Constructor.
*
* @param string $locale The locale
*/
public function __construct($locale = null)
{
$this->locale = $locale;
$this->loaders = array();
$this->resources = array();
$this->catalogues = array();
}
/**
* Adds a Loader.
*
* @param string $format The name of the loader (@see addResource())
* @param LoaderInterface $loader A LoaderInterface instance
*/
public function addLoader($format, LoaderInterface $loader)
{
$this->loaders[$format] = $loader;
}
/**
* Adds a Resource.
*
* @param string $format The name of the loader (@see addLoader())
* @param mixed $resource The resource name
* @param string $locale The locale
* @param string $domain The domain
*/
public function addResource($format, $resource, $locale, $domain = 'messages')
{
if (!isset($this->resources[$locale])) {
$this->resources[$locale] = array();
}
$this->resources[$locale][] = array($format, $resource, $domain);
}
/**
* {@inheritdoc}
*/
public function setLocale($locale)
{
$this->locale = $locale;
}
/**
* Sets the fallback locale.
*
* @param string $locale The fallback locale
*/
public function setFallbackLocale($locale)
{
if (null !== $this->fallbackLocale) {
// needed as the fallback locale is used to fill-in non-yet translated messages
$this->catalogues = array();
}
$this->fallbackLocale = $locale;
}
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = 'messages', $locale = null)
{
if (!isset($locale)) {
$locale = $this->locale;
}
if (!isset($this->catalogues[$locale])) {
$this->loadCatalogue($locale);
}
return strtr($this->catalogues[$locale]->getMessage($id, $domain), $parameters);
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null)
{
if (!isset($locale)) {
$locale = $this->locale;
}
if (!isset($this->catalogues[$locale])) {
$this->loadCatalogue($locale);
}
return strtr($this->chooseMessage($this->catalogues[$locale]->getMessage($id, $domain), (int) $number, $locale), $parameters);
}
protected function chooseMessage($message, $number, $locale)
{
$parts = explode('|', $message);
$explicitRules = array();
$standardRules = array();
foreach ($parts as $part) {
$part = trim($part);
if (preg_match('/^(?<range>'.Range::getRangeRegexp().')\s+(?<message>.+?)$/x', $part, $matches)) {
$explicitRules[$matches['range']] = $matches['message'];
} elseif (preg_match('/^\w+\: +(.+)$/', $part, $matches)) {
$standardRules[] = $matches[1];
} else {
$standardRules[] = $part;
}
}
// try to match an explicit rule, then fallback to the standard ones
foreach ($explicitRules as $range => $m) {
if (Range::test($number, $range)) {
return $m;
}
}
$position = PluralizationRules::get($number, $locale);
if (!isset($standardRules[$position])) {
throw new \InvalidArgumentException('Unable to choose a translation.');
}
return $standardRules[$position];
}
protected function loadCatalogue($locale)
{
if (isset($this->catalogues[$locale])) {
return;
}
$this->catalogues[$locale] = new MessageCatalogue($locale);
if (!isset($this->resources[$locale])) {
return;
}
foreach ($this->resources[$locale] as $resource) {
if (!isset($this->loaders[$resource[0]])) {
throw new \RuntimeException(sprintf('The "%s" translation loader is not registered.', $resource[0]));
}
$this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2]));
}
$this->optimizeCatalogue($locale);
}
protected function optimizeCatalogue($locale)
{
if (strlen($locale) > 3) {
$fallback = substr($locale, 0, -strlen(strrchr($locale, '_')));
} else {
$fallback = $this->fallbackLocale;
}
if (!isset($this->catalogues[$fallback])) {
$this->loadCatalogue($fallback);
}
foreach ($this->catalogues[$fallback]->getResources() as $resource) {
$this->catalogues[$locale]->addResource($resource);
}
foreach ($this->catalogues[$fallback]->getDomains() as $domain) {
foreach ($this->catalogues[$fallback]->getMessages($domain) as $id => $translation) {
if (false === $this->catalogues[$locale]->hasMessage($id, $domain)) {
$this->catalogues[$locale]->setMessage($id, $translation, $domain);
}
}
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Symfony\Component\Translation;
/*
* This file is part of the Symfony framework.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
/**
* TranslatorInterface.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
*/
interface TranslatorInterface
{
/**
* Translates the given message.
*
* @param string $id The message id
* @param array $parameters An array of parameters for the message
* @param string $domain The domain for the message
* @param string $locale The locale
*
* @return string The translated string
*/
function trans($id, array $parameters = array(), $domain = null, $locale = null);
/**
* Translates the given choice message by choosing a translation according to a number.
*
* @param string $id The message id
* @param integer $number The number to use to find the indice of the message
* @param array $parameters An array of parameters for the message
* @param string $domain The domain for the message
* @param string $locale The locale
*
* @return string The translated string
*/
function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);
/**
* Sets the current locale.
*
* @param string $locale The locale
*/
function setLocale($locale);
}

View File

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Translation;
use Symfony\Component\Translation\Range;
class RangeTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getTests
*/
public function testTest($expected, $number, $range)
{
$this->assertEquals($expected, Range::test($number, $range));
}
/**
* @expectedException \InvalidArgumentException
*/
public function testTestException()
{
Range::test(1, 'foobar');
}
public function getTests()
{
return array(
array(true, 3, '{1,2, 3 ,4}'),
array(false, 10, '{1,2, 3 ,4}'),
array(false, 3, '[1,2]'),
array(true, 1, '[1,2]'),
array(true, 2, '[1,2]'),
array(false, 1, ']1,2['),
array(false, 2, ']1,2['),
array(true, log(0), '[-Inf,2['),
array(true, -log(0), '[-2,+Inf]'),
);
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Translation;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;
class TranslatorTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getTransTests
*/
public function testTrans($expected, $id, $translation, $parameters, $locale, $domain)
{
$translator = new Translator();
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array($id => $translation), $locale, $domain);
$this->assertEquals($expected, $translator->trans($id, $parameters, $domain, $locale));
}
/**
* @dataProvider getTransChoiceTests
*/
public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain)
{
$translator = new Translator();
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array($id => $translation), $locale, $domain);
$this->assertEquals($expected, $translator->transChoice($id, $number, $parameters, $domain, $locale));
}
public function getTransTests()
{
return array(
array('Symfony2 est super !', 'Symfony2 is great!', 'Symfony2 est super !', array(), 'fr', ''),
array('Symfony2 est awesome !', 'Symfony2 is %what%!', 'Symfony2 est %what% !', array('%what%' => 'awesome'), 'fr', ''),
);
}
public function getTransChoiceTests()
{
return array(
array('Il y a 0 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
array('Il y a 1 pomme', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
array('Il y a 10 pommes', '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples', '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
array('Il y a 0 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
array('Il y a 1 pomme', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
array('Il y a 10 pommes', 'There is one apple|There is %count% apples', 'Il y a %count% pomme|Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
array('Il y a 0 pomme', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
array('Il y a 1 pomme', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
array('Il y a 10 pommes', 'one: There is one apple|more: There is %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
array('Il n\'y a aucune pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 0, array('%count%' => 0), 'fr', ''),
array('Il y a 1 pomme', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 1, array('%count%' => 1), 'fr', ''),
array('Il y a 10 pommes', '{0} There is no apple|one: There is one apple|more: There is %count% apples', '{0} Il n\'y a aucune pomme|one: Il y a %count% pomme|more: Il y a %count% pommes', 10, array('%count%' => 10), 'fr', ''),
);
}
}