Initial commit

This commit is contained in:
2020-10-07 10:37:15 +02:00
commit ce5f440392
28157 changed files with 4429172 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal;
use PrestaShop\Decimal\Number;
/**
* Builds Number instances
*/
class Builder
{
/**
* Pattern for most numbers
*/
const NUMBER_PATTERN = "/^(?<sign>[-+])?(?<integerPart>\d+)?(?:\.(?<fractionalPart>\d+)(?<exponentPart>[eE](?<exponentSign>[-+])(?<exponent>\d+))?)?$/";
/**
* Pattern for integer numbers in scientific notation (rare but supported by spec)
*/
const INT_EXPONENTIAL_PATTERN = "/^(?<sign>[-+])?(?<integerPart>\d+)(?<exponentPart>[eE](?<exponentSign>[-+])(?<exponent>\d+))$/";
/**
* Builds a Number from a string
*
* @param string $number
*
* @return Number
*/
public static function parseNumber($number)
{
if (!self::itLooksLikeANumber($number, $numberParts)) {
throw new \InvalidArgumentException(
sprintf('"%s" cannot be interpreted as a number', print_r($number, true))
);
}
$integerPart = '';
if (array_key_exists('integerPart', $numberParts)) {
// extract the integer part and remove leading zeroes
$integerPart = ltrim($numberParts['integerPart'], '0');
}
$fractionalPart = '';
if (array_key_exists('fractionalPart', $numberParts)) {
// extract the fractional part and remove trailing zeroes
$fractionalPart = rtrim($numberParts['fractionalPart'], '0');
}
$fractionalDigits = strlen($fractionalPart);
$coefficient = $integerPart . $fractionalPart;
// when coefficient is '0' or a sequence of '0'
if ('' === $coefficient) {
$coefficient = '0';
}
// when the number has been provided in scientific notation
if (array_key_exists('exponentPart', $numberParts)) {
$givenExponent = (int) ($numberParts['exponentSign'] . $numberParts['exponent']);
// we simply add or subtract fractional digits from the given exponent (depending if it's positive or negative)
$fractionalDigits -= $givenExponent;
if ($fractionalDigits < 0) {
// if the resulting fractional digits is negative, it means there is no fractional part anymore
// we need to add trailing zeroes as needed
$coefficient = str_pad($coefficient, strlen($coefficient) - $fractionalDigits, '0');
// there's no fractional part anymore
$fractionalDigits = 0;
}
}
return new Number($numberParts['sign'] . $coefficient, $fractionalDigits);
}
/**
* @param string $number
* @param array $numberParts
*
* @return bool
*/
private static function itLooksLikeANumber($number, &$numberParts)
{
return (
strlen((string) $number) > 0
&& (
preg_match(self::NUMBER_PATTERN, $number, $numberParts)
|| preg_match(self::INT_EXPONENTIAL_PATTERN, $number, $numberParts)
)
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Exception;
/**
* Thrown when attempting to divide by zero
*/
class DivisionByZeroException extends \Exception
{
}

521
vendor/prestashop/decimal/src/Number.php vendored Normal file
View File

@@ -0,0 +1,521 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal;
use PrestaShop\Decimal\Operation\Rounding;
/**
* Decimal number.
*
* Allows for arbitrary precision math operations.
*/
class Number
{
/**
* Indicates if the number is negative
* @var bool
*/
private $isNegative = false;
/**
* Integer representation of this number
* @var string
*/
private $coefficient = '';
/**
* Scientific notation exponent. For practical reasons, it's always stored as a positive value.
* @var int
*/
private $exponent = 0;
/**
* Number constructor.
*
* This constructor can be used in two ways:
*
* 1) With a number string:
*
* ```php
* (string) new Number('0.123456'); // -> '0.123456'
* ```
*
* 2) With an integer string as coefficient and an exponent
*
* ```php
* // 123456 * 10^(-6)
* (string) new Number('123456', 6); // -> '0.123456'
* ```
*
* Note: decimal positions must always be a positive number.
*
* @param string $number Number or coefficient
* @param int $exponent [default=null] If provided, the number can be considered as the negative
* exponent of the scientific notation, or the number of fractional digits.
*/
public function __construct($number, $exponent = null)
{
if (!is_string($number)) {
throw new \InvalidArgumentException(
sprintf('Invalid type - expected string, but got (%s) "%s"', gettype($number), print_r($number, true))
);
}
if (null === $exponent) {
$decimalNumber = Builder::parseNumber($number);
$number = $decimalNumber->getSign() . $decimalNumber->getCoefficient();
$exponent = $decimalNumber->getExponent();
}
$this->initFromScientificNotation($number, $exponent);
if ('0' === $this->coefficient) {
// make sure the sign is always positive for zero
$this->isNegative = false;
}
}
/**
* Returns the integer part of the number.
* Note that this does NOT include the sign.
*
* @return string
*/
public function getIntegerPart()
{
if ('0' === $this->coefficient) {
return $this->coefficient;
}
if (0 === $this->exponent) {
return $this->coefficient;
}
if ($this->exponent >= strlen($this->coefficient)) {
return '0';
}
return substr($this->coefficient, 0, -$this->exponent);
}
/**
* Returns the fractional part of the number.
* Note that this does NOT include the sign.
*
* @return string
*/
public function getFractionalPart()
{
if (0 === $this->exponent || '0' === $this->coefficient) {
return '0';
}
if ($this->exponent > strlen($this->coefficient)) {
return str_pad($this->coefficient, $this->exponent, '0', STR_PAD_LEFT);
}
return substr($this->coefficient, -$this->exponent);
}
/**
* Returns the number of digits in the fractional part.
*
* @see self::getExponent() This method is an alias of getExponent().
*
* @return int
*/
public function getPrecision()
{
return $this->getExponent();
}
/**
* Returns the number's sign.
* Note that this method will return an empty string if the number is positive!
*
* @return string '-' if negative, empty string if positive
*/
public function getSign()
{
return $this->isNegative ? '-' : '';
}
/**
* Returns the exponent of this number. For practical reasons, this exponent is always >= 0.
*
* This value can also be interpreted as the number of significant digits on the fractional part.
*
* @return int
*/
public function getExponent()
{
return $this->exponent;
}
/**
* Returns the raw number as stored internally. This coefficient is always an integer.
*
* It can be transformed to float by computing:
* ```
* getCoefficient() * 10^(-getExponent())
* ```
*
* @return string
*/
public function getCoefficient()
{
return $this->coefficient;
}
/**
* Returns a string representation of this object
*
* @return string
*/
public function __toString()
{
$output = $this->getSign() . $this->getIntegerPart();
$fractionalPart = $this->getFractionalPart();
if ('0' !== $fractionalPart) {
$output .= '.' . $fractionalPart;
}
return $output;
}
/**
* Returns the number as a string, with exactly $precision decimals
*
* Example:
* ```
* $n = new Number('123.4560');
* (string) $n->round(1); // '123.4'
* (string) $n->round(2); // '123.45'
* (string) $n->round(3); // '123.456'
* (string) $n->round(4); // '123.4560' (trailing zeroes are added)
* (string) $n->round(5); // '123.45600' (trailing zeroes are added)
* ```
*
* @param int $precision Exact number of desired decimals
* @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm
*
* @return string
*/
public function toPrecision($precision, $roundingMode = Rounding::ROUND_TRUNCATE)
{
$currentPrecision = $this->getPrecision();
if ($precision === $currentPrecision) {
return (string) $this;
}
$return = $this;
if ($precision < $currentPrecision) {
$return = (new Operation\Rounding())->compute($this, $precision, $roundingMode);
}
if ($precision > $return->getPrecision()) {
return (
$return->getSign()
.$return->getIntegerPart()
.'.'
.str_pad($return->getFractionalPart(), $precision, '0')
);
}
return (string) $return;
}
/**
* Returns the number as a string, with up to $maxDecimals significant digits.
*
* Example:
* ```
* $n = new Number('123.4560');
* (string) $n->round(1); // '123.4'
* (string) $n->round(2); // '123.45'
* (string) $n->round(3); // '123.456'
* (string) $n->round(4); // '123.456' (does not add trailing zeroes)
* (string) $n->round(5); // '123.456' (does not add trailing zeroes)
* ```
*
* @param int $maxDecimals Maximum number of decimals
* @param string $roundingMode [default=Rounding::ROUND_TRUNCATE] Rounding algorithm
*
* @return string
*/
public function round($maxDecimals, $roundingMode = Rounding::ROUND_TRUNCATE)
{
$currentPrecision = $this->getPrecision();
if ($maxDecimals < $currentPrecision) {
return (string) (new Operation\Rounding())->compute($this, $maxDecimals, $roundingMode);
}
return (string) $this;
}
/**
* Returns this number as a positive number
*
* @return self
*/
public function toPositive()
{
if (!$this->isNegative) {
return $this;
}
return $this->invert();
}
/**
* Returns this number as a negative number
*
* @return self
*/
public function toNegative()
{
if ($this->isNegative) {
return $this;
}
return $this->invert();
}
/**
* Returns the computed result of adding another number to this one
*
* @param self $addend Number to add
*
* @return self
*/
public function plus(self $addend)
{
return (new Operation\Addition())->compute($this, $addend);
}
/**
* Returns the computed result of subtracting another number to this one
*
* @param self $subtrahend Number to subtract
*
* @return self
*/
public function minus(self $subtrahend)
{
return (new Operation\Subtraction())->compute($this, $subtrahend);
}
/**
* Returns the computed result of multiplying this number with another one
*
* @param self $factor
*
* @return self
*/
public function times(self $factor)
{
return (new Operation\Multiplication())->compute($this, $factor);
}
/**
* Returns the computed result of dividing this number by another one, with up to $precision number of decimals.
*
* A target maximum precision is required in order to handle potential infinite number of decimals
* (e.g. 1/3 = 0.3333333...).
*
* If the division yields more decimal positions than the requested precision,
* the remaining decimals are truncated, with **no rounding**.
*
* @param self $divisor
* @param int $precision [optional] By default, up to Operation\Division::DEFAULT_PRECISION number of decimals.
*
* @return self
*/
public function dividedBy(self $divisor, $precision = Operation\Division::DEFAULT_PRECISION)
{
return (new Operation\Division())->compute($this, $divisor, $precision);
}
/**
* Indicates if this number is greater than the provided one
*
* @param self $number
*
* @return bool
*/
public function isGreaterThan(self $number)
{
return (1 === (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is greater or equal compared to the provided one
*
* @param self $number
*
* @return bool
*/
public function isGreaterOrEqualThan(self $number)
{
return (0 <= (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is greater than the provided one
*
* @param self $number
*
* @return bool
*/
public function isLowerThan(self $number)
{
return (-1 === (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is lower or equal compared to the provided one
*
* @param self $number
*
* @return bool
*/
public function isLowerOrEqualThan(self $number)
{
return (0 >= (new Operation\Comparison())->compare($this, $number));
}
/**
* Indicates if this number is positive
*
* @return bool
*/
public function isPositive()
{
return !$this->isNegative;
}
/**
* Indicates if this number is negative
*
* @return bool
*/
public function isNegative()
{
return $this->isNegative;
}
/**
* Indicates if this number equals another one
*
* @param self $number
*
* @return bool
*/
public function equals(self $number)
{
return (
$this->isNegative === $number->isNegative
&& $this->coefficient === $number->getCoefficient()
&& $this->exponent === $number->getExponent()
);
}
/**
* Returns the additive inverse of this number (that is, N * -1).
*
* @return static
*/
public function invert()
{
// invert sign
$sign = $this->isNegative ? '' : '-';
return new static($sign . $this->getCoefficient(), $this->getExponent());
}
/**
* Creates a new copy of this number multiplied by 10^$exponent
*
* @param int $exponent
*
* @return static
*/
public function toMagnitude($exponent)
{
return (new Operation\MagnitudeChange())->compute($this, $exponent);
}
/**
* Initializes the number using a coefficient and exponent
*
* @param string $coefficient
* @param int $exponent
*/
private function initFromScientificNotation($coefficient, $exponent)
{
if ($exponent < 0) {
throw new \InvalidArgumentException(
sprintf('Invalid value for exponent. Expected a positive integer or 0, but got "%s"', $coefficient)
);
}
if (!preg_match("/^(?<sign>[-+])?(?<integerPart>\d+)$/", $coefficient, $parts)) {
throw new \InvalidArgumentException(
sprintf('"%s" cannot be interpreted as a number', $coefficient)
);
}
$this->isNegative = ('-' === $parts['sign']);
$this->exponent = (int) $exponent;
// trim leading zeroes
$this->coefficient = ltrim($parts['integerPart'], '0');
// when coefficient is '0' or a sequence of '0'
if ('' === $this->coefficient) {
$this->exponent = 0;
$this->coefficient = '0';
return;
}
$this->removeTrailingZeroesIfNeeded();
}
/**
* Removes trailing zeroes from the fractional part and adjusts the exponent accordingly
*/
private function removeTrailingZeroesIfNeeded()
{
$exponent = $this->getExponent();
$coefficient = $this->getCoefficient();
// trim trailing zeroes from the fractional part
// for example 1000e-1 => 100.0
if (0 < $exponent && '0' === substr($coefficient, -1)) {
$fractionalPart = $this->getFractionalPart();
$trailingZeroesToRemove = 0;
for ($i = $exponent - 1; $i >= 0; $i--) {
if ('0' !== $fractionalPart[$i]) {
break;
}
$trailingZeroesToRemove++;
}
if ($trailingZeroesToRemove > 0) {
$this->coefficient = substr($coefficient, 0, -$trailingZeroesToRemove);
$this->exponent = $exponent - $trailingZeroesToRemove;
}
}
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Computes the addition of two decimal numbers
*/
class Addition
{
/**
* Maximum safe string size in order to be confident
* that it won't overflow the max int size when operating with it
* @var int
*/
private $maxSafeIntStringSize;
/**
* Constructor
*/
public function __construct()
{
$this->maxSafeIntStringSize = strlen((string) PHP_INT_MAX) - 1;
}
/**
* Performs the addition
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcadd')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the addition using BC Math
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcadd($a, $b, max($precision1, $precision2)));
}
/**
* Performs the addition without BC Math
*
* @param DecimalNumber $a Base number
* @param DecimalNumber $b Addend
*
* @return DecimalNumber Result of the addition
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
if ($a->isNegative()) {
if ($b->isNegative()) {
// if both numbers are negative,
// we can just add them as positive numbers and then invert the sign
// f(x, y) = -(|x| + |y|)
// eg. f(-1, -2) = -(|-1| + |-2|) = -3
// eg. f(-2, -1) = -(|-2| + |-1|) = -3
return $this
->computeWithoutBcMath($a->toPositive(), $b->toPositive())
->invert();
}
// if the number is negative and the addend positive,
// perform an inverse subtraction by inverting the terms
// f(x, y) = y - |x|
// eg. f(-2, 1) = 1 - |-2| = -1
// eg. f(-1, 2) = 2 - |-1| = 1
// eg. f(-1, 1) = 1 - |-1| = 0
return $b->minus(
$a->toPositive()
);
}
if ($b->isNegative()) {
// if the number is positive and the addend is negative
// perform subtraction instead: 2 - 1
// f(x, y) = x - |y|
// f(2, -1) = 2 - |-1| = 1
// f(1, -2) = 1 - |-2| = -1
// f(1, -1) = 1 - |-1| = 0
return $a->minus(
$b->toPositive()
);
}
// optimization: 0 + x = x
if ('0' === (string) $a) {
return $b;
}
// optimization: x + 0 = x
if ('0' === (string) $b) {
return $a;
}
// pad coefficients with leading/trailing zeroes
list($coeff1, $coeff2) = $this->normalizeCoefficients($a, $b);
// compute the coefficient sum
$sum = $this->addStrings($coeff1, $coeff2);
// both signs are equal, so we can use either
$sign = $a->getSign();
// keep the bigger exponent
$exponent = max($a->getExponent(), $b->getExponent());
return new DecimalNumber($sign . $sum, $exponent);
}
/**
* Normalizes coefficients by adding leading or trailing zeroes as needed so that both are the same length
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return array An array containing the normalized coefficients
*/
private function normalizeCoefficients(DecimalNumber $a, DecimalNumber $b)
{
$exp1 = $a->getExponent();
$exp2 = $b->getExponent();
$coeff1 = $a->getCoefficient();
$coeff2 = $b->getCoefficient();
// add trailing zeroes if needed
if ($exp1 > $exp2) {
$coeff2 = str_pad($coeff2, strlen($coeff2) + $exp1 - $exp2, '0', STR_PAD_RIGHT);
} elseif ($exp1 < $exp2) {
$coeff1 = str_pad($coeff1, strlen($coeff1) + $exp2 - $exp1, '0', STR_PAD_RIGHT);
}
$len1 = strlen($coeff1);
$len2 = strlen($coeff2);
// add leading zeroes if needed
if ($len1 > $len2) {
$coeff2 = str_pad($coeff2, $len1, '0', STR_PAD_LEFT);
} elseif ($len1 < $len2) {
$coeff1 = str_pad($coeff1, $len2, '0', STR_PAD_LEFT);
}
return [$coeff1, $coeff2];
}
/**
* Adds two integer numbers as strings.
*
* @param string $number1
* @param string $number2
* @param bool $fractional [default=false]
* If true, the numbers will be treated as the fractional part of a number (padded with trailing zeroes).
* Otherwise, they will be treated as the integer part (padded with leading zeroes).
*
* @return string
*/
private function addStrings($number1, $number2, $fractional = false)
{
// optimization - numbers can be treated as integers as long as they don't overflow the max int size
if ('0' !== $number1[0]
&& '0' !== $number2[0]
&& strlen($number1) <= $this->maxSafeIntStringSize
&& strlen($number2) <= $this->maxSafeIntStringSize
) {
return (string) ((int) $number1 + (int) $number2);
}
// find out which of the strings is longest
$maxLength = max(strlen($number1), strlen($number2));
// add leading or trailing zeroes as needed
$number1 = str_pad($number1, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$number2 = str_pad($number2, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$result = '';
$carryOver = 0;
for ($i = $maxLength - 1; 0 <= $i; $i--) {
$sum = $number1[$i] + $number2[$i] + $carryOver;
$result .= $sum % 10;
$carryOver = (int) ($sum >= 10);
}
if ($carryOver > 0) {
$result .= '1';
}
return strrev($result);
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Compares two decimal numbers
*/
class Comparison
{
/**
* Compares two decimal numbers.
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compare(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bccomp')) {
return $this->compareUsingBcMath($a, $b);
}
return $this->compareWithoutBcMath($a, $b);
}
/**
* Compares two decimal numbers using BC Math
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compareUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
return bccomp((string) $a, (string) $b, max($a->getExponent(), $b->getExponent()));
}
/**
* Compares two decimal numbers without using BC Math
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
public function compareWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
$signCompare = $this->compareSigns($a->getSign(), $b->getSign());
if ($signCompare !== 0) {
return $signCompare;
}
// signs are equal, compare regardless of sign
$result = $this->positiveCompare($a, $b);
// inverse the result if the signs are negative
if ($a->isNegative()) {
return -$result;
}
return $result;
}
/**
* Compares two decimal numbers as positive regardless of sign.
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function positiveCompare(DecimalNumber $a, DecimalNumber $b)
{
// compare integer length
$intLengthCompare = $this->compareNumeric(
strlen($a->getIntegerPart()),
strlen($b->getIntegerPart())
);
if ($intLengthCompare !== 0) {
return $intLengthCompare;
}
// integer parts are equal in length, compare integer part
$intPartCompare = $this->compareBinary($a->getIntegerPart(), $b->getIntegerPart());
if ($intPartCompare !== 0) {
return $intPartCompare;
}
// integer parts are equal, compare fractional part
return $this->compareBinary($a->getFractionalPart(), $b->getFractionalPart());
}
/**
* Compares positive/negative signs.
*
* @param string $a
* @param string $b
*
* @return int Returns 0 if both signs are equal, 1 if $a is positive, and -1 if $b is positive
*/
private function compareSigns($a, $b)
{
if ($a === $b) {
return 0;
}
// empty string means positive sign
if ($a === '') {
return 1;
}
return -1;
}
/**
* Compares two values numerically.
*
* @param mixed $a
* @param mixed $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function compareNumeric($a, $b)
{
if ($a < $b) {
return -1;
}
if ($a > $b) {
return 1;
}
return 0;
}
/**
* Compares two strings binarily.
*
* @param string $a
* @param string $b
*
* @return int Returns 1 if $a > $b, -1 if $a < $b, and 0 if they are equal.
*/
private function compareBinary($a, $b)
{
$comparison = strcmp($a, $b);
if ($comparison > 0) {
return 1;
}
if ($comparison < 0) {
return -1;
}
return 0;
}
}

View File

@@ -0,0 +1,175 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Exception\DivisionByZeroException;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Computes the division between two decimal numbers.
*/
class Division
{
const DEFAULT_PRECISION = 6;
/**
* Performs the division.
*
* A target maximum precision is required in order to handle potential infinite number of decimals
* (e.g. 1/3 = 0.3333333...).
*
* If the division yields more decimal positions than the requested precision,
* the remaining decimals are truncated, with **no rounding**.
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function compute(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
if (function_exists('bcdiv')) {
return $this->computeUsingBcMath($a, $b, $precision);
}
return $this->computeWithoutBcMath($a, $b, $precision);
}
/**
* Performs the division using BC Math
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
if ((string) $b === '0') {
throw new DivisionByZeroException();
}
return new DecimalNumber((string) bcdiv($a, $b, $precision));
}
/**
* Performs the division without BC Math
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum decimal precision
*
* @return DecimalNumber Result of the division
* @throws DivisionByZeroException
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b, $precision = self::DEFAULT_PRECISION)
{
$bString = (string) $b;
if ('0' === $bString) {
throw new DivisionByZeroException();
}
$aString = (string) $a;
// 0 as dividend always yields 0
if ('0' === $aString) {
return $a;
}
// 1 as divisor always yields the dividend
if ('1' === $bString) {
return $a;
}
// -1 as divisor always yields the the inverted dividend
if ('-1' === $bString) {
return $a->invert();
}
// if dividend and divisor are equal, the result is always 1
if ($a->equals($b)) {
return new DecimalNumber('1');
}
$aPrecision = $a->getPrecision();
$bPrecision = $b->getPrecision();
$maxPrecision = max($aPrecision, $bPrecision);
if ($maxPrecision > 0) {
// make $a and $b integers by multiplying both by 10^(maximum number of decimals)
$a = $a->toMagnitude($maxPrecision);
$b = $b->toMagnitude($maxPrecision);
}
$result = $this->integerDivision($a, $b, max($precision, $aPrecision));
return $result;
}
/**
* Computes the division between two integer DecimalNumbers
*
* @param DecimalNumber $a Dividend
* @param DecimalNumber $b Divisor
* @param int $precision Maximum number of decimals to try
*
* @return DecimalNumber
*/
private function integerDivision(DecimalNumber $a, DecimalNumber $b, $precision)
{
$dividend = $a->getCoefficient();
$divisor = new DecimalNumber($b->getCoefficient());
$dividendLength = strlen($dividend);
$result = '';
$exponent = 0;
$currentSequence = '';
for ($i = 0; $i < $dividendLength; $i++) {
// append digits until we get a number big enough to divide
$currentSequence .= $dividend[$i];
if ($currentSequence < $divisor) {
if (!empty($result)) {
$result .= '0';
}
} else {
// subtract divisor as many times as we can
$remainder = new DecimalNumber($currentSequence);
$multiple = 0;
do {
$multiple++;
$remainder = $remainder->minus($divisor);
} while ($remainder->isGreaterOrEqualThan($divisor));
$result .= (string) $multiple;
// reset sequence to the reminder
$currentSequence = (string) $remainder;
}
// add up to $precision decimals
if ($currentSequence > 0 && $i === $dividendLength - 1 && $precision > 0) {
// "borrow" up to $precision digits
--$precision;
$dividend .= '0';
$dividendLength++;
$exponent++;
}
}
$sign = ($a->isNegative() xor $b->isNegative()) ? '-' : '';
return new DecimalNumber($sign . $result, $exponent);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Computes relative magnitude changes on a decimal number
*/
class MagnitudeChange
{
/**
* Multiplies a number by 10^$exponent.
*
* Examples:
* ```php
* $n = new Decimal\Number('123.45678');
* $o = new Decimal\Operation\MagnitudeChange();
* $o->compute($n, 2); // 12345.678
* $o->compute($n, 6); // 123456780
* $o->compute($n, -2); // 1.2345678
* $o->compute($n, -6); // 0.00012345678
* ```
*
* @param DecimalNumber $number
* @param int $exponent
*
* @return DecimalNumber
*/
public function compute(DecimalNumber $number, $exponent)
{
$exponent = (int) $exponent;
if ($exponent === 0) {
return $number;
}
$resultingExponent = $exponent - $number->getExponent();
if ($resultingExponent <= 0) {
return new DecimalNumber(
$number->getSign() . $number->getCoefficient(),
abs($resultingExponent)
);
}
// add zeroes
$targetLength = strlen($number->getCoefficient()) + $resultingExponent;
return new DecimalNumber(
$number->getSign() . str_pad($number->getCoefficient(), $targetLength, '0')
);
}
}

View File

@@ -0,0 +1,155 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Computes the multiplication between two decimal numbers
*/
class Multiplication
{
/**
* Performs the multiplication
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcmul')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the multiplication using BC Math
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcmul($a, $b, $precision1 + $precision2));
}
/**
* Performs the multiplication without BC Math
*
* @param DecimalNumber $a Left operand
* @param DecimalNumber $b Right operand
*
* @return DecimalNumber Result of the multiplication
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
$aAsString = (string) $a;
$bAsString = (string) $b;
// optimization: if either one is zero, the result is zero
if ('0' === $aAsString || '0' === $bAsString) {
return new DecimalNumber('0');
}
// optimization: if either one is one, the result is the other one
if ('1' === $aAsString) {
return $b;
}
if ('1' === $bAsString) {
return $a;
}
$result = $this->multiplyStrings(
ltrim($a->getCoefficient(), '0'),
ltrim($b->getCoefficient(), '0')
);
$sign = ($a->isNegative() xor $b->isNegative()) ? '-' : '';
// a multiplication has at most as many decimal figures as the sum
// of the number of decimal figures the factors have
$exponent = $a->getExponent() + $b->getExponent();
return new DecimalNumber($sign . $result, $exponent);
}
/**
* Multiplies two integer numbers as strings.
*
* This method implements a naive "long multiplication" algorithm.
*
* @param string $topNumber
* @param string $bottomNumber
*
* @return string
*/
private function multiplyStrings($topNumber, $bottomNumber)
{
$topNumberLength = strlen($topNumber);
$bottomNumberLength = strlen($bottomNumber);
if ($topNumberLength < $bottomNumberLength) {
// multiplication is commutative, and this algorithm
// performs better if the bottom number is shorter.
return $this->multiplyStrings($bottomNumber, $topNumber);
}
$stepNumber = 0;
$result = new DecimalNumber('0');
for ($i = $bottomNumberLength - 1; $i >= 0; $i--) {
$carryOver = 0;
$partialResult = '';
// optimization: we don't need to bother multiplying by zero
if ($bottomNumber[$i] === '0') {
$stepNumber++;
continue;
}
if ($bottomNumber[$i] === '1') {
// multiplying by one is the same as copying the top number
$partialResult = strrev($topNumber);
} else {
// digit-by-digit multiplication using carry-over
for ($j = $topNumberLength - 1; $j >= 0; $j--) {
$multiplicationResult = ($bottomNumber[$i] * $topNumber[$j]) + $carryOver;
$carryOver = floor($multiplicationResult / 10);
$partialResult .= $multiplicationResult % 10;
}
if ($carryOver > 0) {
$partialResult .= $carryOver;
}
}
// pad the partial result with as many zeros as performed steps
$padding = str_pad('', $stepNumber, '0');
$partialResult = $padding . $partialResult;
// add to the result
$result = $result->plus(
new DecimalNumber(strrev($partialResult))
);
$stepNumber++;
}
return (string) $result;
}
}

View File

@@ -0,0 +1,420 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Allows transforming a decimal number's precision
*/
class Rounding
{
const ROUND_TRUNCATE = 'truncate';
const ROUND_CEIL = 'ceil';
const ROUND_FLOOR = 'floor';
const ROUND_HALF_UP = 'up';
const ROUND_HALF_DOWN = 'down';
const ROUND_HALF_EVEN = 'even';
/**
* Rounds a decimal number to a specified precision
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
* @param string $roundingMode Rounding algorithm
*
* @return DecimalNumber
*/
public function compute(DecimalNumber $number, $precision, $roundingMode)
{
switch ($roundingMode) {
case self::ROUND_HALF_UP:
return $this->roundHalfUp($number, $precision);
break;
case self::ROUND_CEIL:
return $this->ceil($number, $precision);
break;
case self::ROUND_FLOOR:
return $this->floor($number, $precision);
break;
case self::ROUND_HALF_DOWN:
return $this->roundHalfDown($number, $precision);
break;
case self::ROUND_TRUNCATE:
return $this->truncate($number, $precision);
break;
case self::ROUND_HALF_EVEN:
return $this->roundHalfEven($number, $precision);
break;
}
throw new \InvalidArgumentException(sprintf("Invalid rounding mode: %s", print_r($roundingMode, true)));
}
/**
* Truncates a number to a target number of decimal digits.
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function truncate(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if (0 === $precision) {
return new DecimalNumber($number->getSign() . $number->getIntegerPart());
}
return new DecimalNumber(
$number->getSign()
. $number->getIntegerPart()
. '.'
. substr($number->getFractionalPart(), 0, $precision)
);
}
/**
* Rounds a number up if its precision is greater than the target one.
*
* Ceil always rounds towards positive infinity.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->ceil($n, 0); // '124'
* $this->ceil($n, 1); // '123.5'
* $this->ceil($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->ceil($n, 0); // '-122'
* $this->ceil($n, 1); // '-123.3'
* $this->ceil($n, 2); // '-123.44'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function ceil(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if ($number->isNegative()) {
// ceil works exactly as truncate for negative numbers
return $this->truncate($number, $precision);
}
/**
* The principle for ceil is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
*
* if D > 0, ceil(X, P) = truncate(X + 10^(-P), P)
* if D = 0, ceil(X, P) = truncate(X, P)
*/
if ($precision > 0) {
// we know that D > 0, because we have already checked that the number's precision
// is greater than the target precision
$numberToAdd = '0.' . str_pad('1', $precision, '0', STR_PAD_LEFT);
} else {
$numberToAdd = '1';
}
return $this
->truncate($number, $precision)
->plus(new DecimalNumber($numberToAdd));
}
/**
* Rounds a number down if its precision is greater than the target one.
*
* Floor always rounds towards negative infinity.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->floor($n, 0); // '123'
* $this->floor($n, 1); // '123.4'
* $this->floor($n, 2); // '123.45'
*
* $n = new Decimal\Number('-123.456');
* $this->floor($n, 0); // '-124'
* $this->floor($n, 1); // '-123.5'
* $this->floor($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function floor(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
if ($number->isPositive()) {
// floor works exactly as truncate for positive numbers
return $this->truncate($number, $precision);
}
/**
* The principle for ceil is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
*
* if D < 0, ceil(X, P) = truncate(X - 10^(-P), P)
* if D = 0, ceil(X, P) = truncate(X, P)
*/
if ($precision > 0) {
// we know that D > 0, because we have already checked that the number's precision
// is greater than the target precision
$numberToSubtract = '0.' . str_pad('1', $precision, '0', STR_PAD_LEFT);
} else {
$numberToSubtract = '1';
}
return $this
->truncate($number, $precision)
->minus(new DecimalNumber($numberToSubtract));
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D >= 5
* - It rounds towards zero if D < 5
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.5'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.5'
* $this->roundHalfUp($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfUp(DecimalNumber $number, $precision)
{
return $this->roundHalf($number, $precision, 5);
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D > 5
* - It rounds towards zero if D <= 5
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.4'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.4'
* $this->roundHalfUp($n, 2); // '-123.46'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfDown(DecimalNumber $number, $precision)
{
return $this->roundHalf($number, $precision, 6);
}
/**
* Rounds a number according to "banker's rounding".
*
* The number is rounded according to the digit D located at precision P.
* - Away from zero if D > 5
* - Towards zero if D < 5
* - if D = 5, then
* - If the last significant digit is even, the number is rounded away from zero
* - If the last significant digit is odd, the number is rounded towards zero.
*
* Examples:
*
* ```
* $n = new Decimal\Number('123.456');
* $this->roundHalfUp($n, 0); // '123'
* $this->roundHalfUp($n, 1); // '123.4'
* $this->roundHalfUp($n, 2); // '123.46'
*
* $n = new Decimal\Number('-123.456');
* $this->roundHalfUp($n, 0); // '-123'
* $this->roundHalfUp($n, 1); // '-123.4'
* $this->roundHalfUp($n, 2); // '-123.46'
*
* $n = new Decimal\Number('1.1525354556575859505');
* $this->roundHalfEven($n, 0); // '1'
* $this->roundHalfEven($n, 1); // '1.2'
* $this->roundHalfEven($n, 2); // '1.15'
* $this->roundHalfEven($n, 3); // '1.152'
* $this->roundHalfEven($n, 4); // '1.1525'
* $this->roundHalfEven($n, 5); // '1.15255'
* $this->roundHalfEven($n, 6); // '1.152535'
* $this->roundHalfEven($n, 7); // '1.1525354'
* $this->roundHalfEven($n, 8); // '1.15253546'
* $this->roundHalfEven($n, 9); // '1.152535456'
* $this->roundHalfEven($n, 10); // '1.1525354556'
* ```
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
*
* @return DecimalNumber
*/
public function roundHalfEven(DecimalNumber $number, $precision)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
/**
* The principle for roundHalfEven is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
* E = digit to the left of D
*
* if D != 5, roundHalfEven(X, P) = roundHalfUp(X, P)
* if D = 5 and E is even, roundHalfEven(X, P) = truncate(X, P)
* if D = 5 and E is odd and X is positive, roundHalfUp(X, P) = ceil(X, P)
* if D = 5 and E is odd and X is negative, roundHalfUp(X, P) = floor(X, P)
*/
$fractionalPart = $number->getFractionalPart();
$digit = (int) $fractionalPart[$precision];
if ($digit !== 5) {
return $this->roundHalfUp($number, $precision);
}
// retrieve the digit to the left of it
if ($precision === 0) {
$referenceDigit = (int) substr($number->getIntegerPart(), -1);
} else {
$referenceDigit = (int) $fractionalPart[$precision - 1];
}
// truncate if even
$isEven = $referenceDigit % 2 === 0;
if ($isEven) {
return $this->truncate($number, $precision);
}
// round away from zero
$method = ($number->isPositive()) ? self::ROUND_CEIL : self::ROUND_FLOOR;
return $this->compute($number, $precision, $method);
}
/**
* Rounds the number according to the digit D located at precision P.
* - It rounds away from zero if D >= $halfwayValue
* - It rounds towards zero if D < $halfWayValue
*
* @param DecimalNumber $number Number to round
* @param int $precision Maximum number of decimals
* @param int $halfwayValue Threshold upon which the rounding will be performed
* away from zero instead of towards zero.
*
* @return DecimalNumber
*/
private function roundHalf(DecimalNumber $number, $precision, $halfwayValue)
{
$precision = $this->sanitizePrecision($precision);
if ($number->getPrecision() <= $precision) {
return $number;
}
/**
* The principle for roundHalf is the following:
*
* let X = number to round
* P = number of decimal digits that we want
* D = digit from the fractional part at index P
* Y = digit considered as the half-way value on which we round up (usually 5 or 6)
*
* if D >= Y, roundHalf(X, P) = ceil(X, P)
* if D < Y, roundHalf(X, P) = truncate(X, P)
*/
$fractionalPart = $number->getFractionalPart();
$digit = (int) $fractionalPart[$precision];
if ($digit >= $halfwayValue) {
// round away from zero
$mode = ($number->isPositive()) ? self::ROUND_CEIL : self::ROUND_FLOOR;
return $this->compute($number, $precision, $mode);
}
// round towards zero
return $this->truncate($number, $precision);
}
/**
* Ensures that precision is a positive int
*
* @param mixed $precision
*
* @return int Precision
*
* @throws \InvalidArgumentException if precision is not a positive integer
*/
private function sanitizePrecision($precision)
{
if (!is_numeric($precision) || $precision < 0) {
throw new \InvalidArgumentException(sprintf('Invalid precision: %s', print_r($precision, true)));
}
return (int) $precision;
}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* This file is part of the PrestaShop\Decimal package
*
* @author PrestaShop SA <contact@prestashop.com>
* @license https://opensource.org/licenses/MIT MIT License
*/
namespace PrestaShop\Decimal\Operation;
use PrestaShop\Decimal\Number as DecimalNumber;
/**
* Computes the subtraction of two decimal numbers
*/
class Subtraction
{
/**
* Maximum safe string size in order to be confident
* that it won't overflow the max int size when operating with it
* @var int
*/
private $maxSafeIntStringSize;
/**
* Constructor
*/
public function __construct()
{
$this->maxSafeIntStringSize = strlen((string) PHP_INT_MAX) - 1;
}
/**
* Performs the subtraction
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function compute(DecimalNumber $a, DecimalNumber $b)
{
if (function_exists('bcsub')) {
return $this->computeUsingBcMath($a, $b);
}
return $this->computeWithoutBcMath($a, $b);
}
/**
* Performs the subtraction using BC Math
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function computeUsingBcMath(DecimalNumber $a, DecimalNumber $b)
{
$precision1 = $a->getPrecision();
$precision2 = $b->getPrecision();
return new DecimalNumber((string) bcsub($a, $b, max($precision1, $precision2)));
}
/**
* Performs the subtraction without using BC Math
*
* @param DecimalNumber $a Minuend
* @param DecimalNumber $b Subtrahend
*
* @return DecimalNumber Result of the subtraction
*/
public function computeWithoutBcMath(DecimalNumber $a, DecimalNumber $b)
{
if ($a->isNegative()) {
if ($b->isNegative()) {
// if both minuend and subtrahend are negative
// perform the subtraction with inverted coefficients position and sign
// f(x, y) = |y| - |x|
// eg. f(-1, -2) = |-2| - |-1| = 2 - 1 = 1
// e.g. f(-2, -1) = |-1| - |-2| = 1 - 2 = -1
return $this->computeWithoutBcMath($b->toPositive(), $a->toPositive());
} else {
// if the minuend is negative and the subtrahend is positive,
// we can just add them as positive numbers and then invert the sign
// f(x, y) = -(|x| + y)
// eg. f(1, 2) = -(|-1| + 2) = -3
// eg. f(-2, 1) = -(|-2| + 1) = -3
return $a
->toPositive()
->plus($b)
->toNegative();
}
} else if ($b->isNegative()) {
// if the minuend is positive subtrahend is negative, perform an addition
// f(x, y) = x + |y|
// eg. f(2, -1) = 2 + |-1| = 2 + 1 = 3
return $a->plus($b->toPositive());
}
// optimization: 0 - x = -x
if ('0' === (string) $a) {
return (!$b->isNegative()) ? $b->toNegative() : $b;
}
// optimization: x - 0 = x
if ('0' === (string) $b) {
return $a;
}
// pad coefficients with leading/trailing zeroes
list($coeff1, $coeff2) = $this->normalizeCoefficients($a, $b);
// compute the coefficient subtraction
if ($a->isGreaterThan($b)) {
$sub = $this->subtractStrings($coeff1, $coeff2);
$sign = '';
} else {
$sub = $this->subtractStrings($coeff2, $coeff1);
$sign = '-';
}
// keep the bigger exponent
$exponent = max($a->getExponent(), $b->getExponent());
return new DecimalNumber($sign . $sub, $exponent);
}
/**
* Normalizes coefficients by adding leading or trailing zeroes as needed so that both are the same length
*
* @param DecimalNumber $a
* @param DecimalNumber $b
*
* @return array An array containing the normalized coefficients
*/
private function normalizeCoefficients(DecimalNumber $a, DecimalNumber $b)
{
$exp1 = $a->getExponent();
$exp2 = $b->getExponent();
$coeff1 = $a->getCoefficient();
$coeff2 = $b->getCoefficient();
// add trailing zeroes if needed
if ($exp1 > $exp2) {
$coeff2 = str_pad($coeff2, strlen($coeff2) + $exp1 - $exp2, '0', STR_PAD_RIGHT);
} elseif ($exp1 < $exp2) {
$coeff1 = str_pad($coeff1, strlen($coeff1) + $exp2 - $exp1, '0', STR_PAD_RIGHT);
}
$len1 = strlen($coeff1);
$len2 = strlen($coeff2);
// add leading zeroes if needed
if ($len1 > $len2) {
$coeff2 = str_pad($coeff2, $len1, '0', STR_PAD_LEFT);
} elseif ($len1 < $len2) {
$coeff1 = str_pad($coeff1, $len2, '0', STR_PAD_LEFT);
}
return [$coeff1, $coeff2];
}
/**
* Subtracts $number2 to $number1.
* For this algorithm to work, $number1 has to be >= $number 2.
*
* @param string $number1
* @param string $number2
* @param bool $fractional [default=false]
* If true, the numbers will be treated as the fractional part of a number (padded with trailing zeroes).
* Otherwise, they will be treated as the integer part (padded with leading zeroes).
*
* @return string
*/
private function subtractStrings($number1, $number2, $fractional = false)
{
// find out which of the strings is longest
$maxLength = max(strlen($number1), strlen($number2));
// add leading or trailing zeroes as needed
$number1 = str_pad($number1, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$number2 = str_pad($number2, $maxLength, '0', $fractional ? STR_PAD_RIGHT : STR_PAD_LEFT);
$result = '';
$carryOver = 0;
for ($i = $maxLength -1; 0 <= $i; $i--) {
$operand1 = $number1[$i] - $carryOver;
$operand2 = $number2[$i];
if ($operand1 >= $operand2) {
$result .= $operand1 - $operand2;
$carryOver = 0;
} else {
$result .= 10 + $operand1 - $operand2;
$carryOver = 1;
}
}
return strrev($result);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Yaml\Yaml;
class Configuration
{
/**
* @var array
*/
private static $paths = [];
/**
* @var array
*/
private static $excludeFiles = [];
/**
* @var string
*/
private static $projectDirectory = '';
/**
* @var string
*/
private static $cacheDir;
/**
* @param array $arr
*/
public static function fromArray(array $arr)
{
$optionsResolver = new OptionsResolver();
$options = $optionsResolver->setRequired([
'paths',
'exclude_files',
])
->setDefaults([
'cache_dir' => null,
])
->addAllowedTypes('paths', 'array')
->addAllowedTypes('exclude_files', ['array', 'null'])
->addAllowedTypes('cache_dir', ['string', 'null'])
->resolve($arr);
self::$paths = (array) $options['paths'];
self::$excludeFiles = (array) $options['exclude_files'];
self::$cacheDir = $options['cache_dir'];
}
/**
* @param string $yamlFile
*/
public static function fromYamlFile($yamlFile)
{
self::$projectDirectory = realpath(dirname($yamlFile));
self::fromArray(Yaml::parse(file_get_contents($yamlFile)));
}
/**
* @return array
*/
public static function getPaths()
{
return self::$paths;
}
/**
* @return array
*/
public static function getExcludeFiles()
{
return self::$excludeFiles;
}
/**
* @return string
*/
public static function getProjectDirectory()
{
return self::$projectDirectory;
}
/**
* @param string $path
* @param string|bool $rootDir
*
* @return string
*/
public static function getRelativePath($path, $rootDir = false)
{
$realpath = realpath($path);
$path = empty($realpath) ? $path : $realpath;
if (!empty($rootDir)) {
$rootDir = rtrim($rootDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
} else {
$rootDir = '';
}
return str_replace($rootDir, '', $path);
}
/**
* @return string
*/
public static function getCacheDir()
{
return empty(self::$cacheDir) ? sys_get_temp_dir().DIRECTORY_SEPARATOR : self::$cacheDir;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* 2007-2015 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class ExtractorCompilerPass implements CompilerPassInterface
{
/**
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
foreach ($container->findTaggedServiceIds('prestashop.translation.extractor') as $id => $attributes) {
$container->getDefinition('prestashop.translation.chainextractor')->addMethodCall('addExtractor', [reset($attributes)['format'], new Reference($id)]);
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\DependencyInjection\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TranslationCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->register(
'translation_tools.translation.node_visitor',
'PrestaShop\TranslationToolsBundle\Twig\NodeVisitor\TranslationNodeVisitor'
);
$translationDefinition = $container->getDefinition('twig.extension.trans');
$translationDefinition->addArgument(new Reference('translation_tools.translation.node_visitor'));
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* 2007-2015 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\DependencyInjection;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$treeBuilder->root('translation_tools');
return $treeBuilder;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* 2007-2015 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
class TranslationToolsExtension extends Extension
{
/**
* @param array $configs
* @param ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
$this->processConfiguration(new Configuration(), $configs);
}
}

View File

@@ -0,0 +1,66 @@
services:
prestashop.smarty:
class: PrestaShop\TranslationToolsBundle\Smarty
calls:
- [setCompileDir, ['%kernel.cache_dir%/smarty']]
- [forceCompile, [true]]
- [registerResource, ['module', "@prestashop.translation.helper.smarty.smarty_resource_module"]]
- [registerResource, ['parent', "@prestashop.translation.helper.smarty.smarty_resource_parent"]]
prestashop.compiler.smarty.template:
class: PrestaShop\TranslationToolsBundle\Translation\Compiler\Smarty\TranslationTemplateCompiler
arguments:
- "Smarty_Internal_Templatelexer"
- "Smarty_Internal_Templateparser"
- "@prestashop.smarty"
prestashop.translation.helper.smarty.smarty_resource_module:
class: PrestaShop\TranslationToolsBundle\Translation\Helper\Smarty\SmartyResourceModule
prestashop.translation.helper.smarty.smarty_resource_parent:
class: PrestaShop\TranslationToolsBundle\Translation\Helper\Smarty\SmartyResourceParent
prestashop.translation.parser.crowdin_php_parser:
class: PrestaShop\TranslationToolsBundle\Translation\Parser\CrowdinPhpParser
prestashop.translation.manager.original_string_manager:
class: PrestaShop\TranslationToolsBundle\Translation\Manager\OriginalStringManager
arguments:
- "@prestashop.translation.parser.crowdin_php_parser"
prestashop.translation.manager.translation_manager:
class: PrestaShop\TranslationToolsBundle\Translation\Manager\TranslationManager
arguments:
- "@prestashop.translation.parser.crowdin_php_parser"
prestashop.translation.extractor.crowdin.php:
class: PrestaShop\TranslationToolsBundle\Translation\Extractor\CrowdinPhpExtractor
arguments:
- "@prestashop.translation.parser.crowdin_php_parser"
- "@prestashop.translation.manager.original_string_manager"
prestashop.translation.chainextractor:
class: PrestaShop\TranslationToolsBundle\Translation\Extractor\ChainExtractor
prestashop.translation.extractor.php:
class: PrestaShop\TranslationToolsBundle\Translation\Extractor\PhpExtractor
tags:
- { name: prestashop.translation.extractor, format: php }
prestashop.translation.extractor.twig:
class: PrestaShop\TranslationToolsBundle\Translation\Extractor\TwigExtractor
arguments:
- "@twig"
tags:
- { name: prestashop.translation.extractor, format: twig }
prestashop.translation.extractor.smarty:
class: PrestaShop\TranslationToolsBundle\Translation\Extractor\SmartyExtractor
arguments:
- "@prestashop.compiler.smarty.template"
tags:
- { name: prestashop.translation.extractor, format: smarty }
prestashop.dumper.xliff:
class: PrestaShop\TranslationToolsBundle\Translation\Dumper\XliffFileDumper

View File

@@ -0,0 +1,13 @@
<?php
namespace PrestaShop\TranslationToolsBundle;
use Smarty as BaseSmarty;
class Smarty extends BaseSmarty
{
public function forceCompile($value)
{
return $this->force_compile = $value;
}
}

View File

@@ -0,0 +1,165 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Builder;
use Exception;
class PhpBuilder
{
const POS_NEWLINE = 0;
const POS_VAR = 1;
const POS_ARRAY_KEY = 2;
const POS_ASSIGN = 3;
const POS_VALUE = 4;
protected $fileName;
protected $output;
protected $pos = self::POS_NEWLINE;
public function __construct()
{
$this->open();
}
/**
* @return string
*/
public function build()
{
return $this->output;
}
/**
* @param string $varName
*/
public function appendGlobalDeclaration($varName)
{
$this->output .= 'global ';
$this->appendVar($varName);
$this->appendEndOfLine();
$this->output .= PHP_EOL;
}
/**
* @param string $varName
* @param string $key
* @param string $value
*
* @return \PrestaShop\TranslationToolsBundle\Translation\Builder\PhpBuilder
*
* @throws Exception
*/
public function appendStringLine($varName, $key, $value)
{
if ($this->pos !== self::POS_NEWLINE) {
throw new Exception('Unable to append new line (current pos is '.$this->pos.')');
}
$this->appendVar($varName)
->appendKey($key)
->appendVarAssignation()
->appendValue("'".$value."'")
->appendEndOfLine();
return $this;
}
/**
* @return PhpBuilder
*/
protected function open()
{
$this->output .= '<?php'.PHP_EOL.PHP_EOL;
$this->pos = self::POS_NEWLINE;
return $this;
}
/**
* @param string $varName
*
* @return PhpBuilder
*/
protected function appendVar($varName)
{
$this->output .= '$'.$varName;
$this->pos = self::POS_VAR;
return $this;
}
/**
* @param string $key
*
* @return PhpBuilder
*/
protected function appendKey($key)
{
$this->output .= "['".$key."']";
$this->pos = self::POS_ARRAY_KEY;
return $this;
}
/**
* @return PhpBuilder
*/
protected function appendVarAssignation()
{
$this->output .= ' = ';
$this->pos = self::POS_ASSIGN;
return $this;
}
/**
* @param string $value
*
* @return PhpBuilder
*/
protected function appendValue($value)
{
$this->output .= (string) $value;
$this->pos = self::POS_VALUE;
return $this;
}
/**
* @return PhpBuilder
*/
protected function appendEndOfLine()
{
$this->output .= ';'.PHP_EOL;
$this->pos = self::POS_NEWLINE;
return $this;
}
}

View File

@@ -0,0 +1,151 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Builder;
use DOMDocument;
class XliffBuilder
{
/**
* @var DOMDocument
*/
protected $dom;
/**
* @var string
*/
protected $version;
/**
* @var array
*/
protected $originalFiles = [];
/**
* @var array
*/
protected $transUnits = [];
public function __construct()
{
$this->dom = new DOMDocument('1.0', 'UTF-8');
$this->dom->formatOutput = true;
}
/**
* @return DOMDocument
*/
public function build()
{
$xliff = $this->dom->appendChild($this->dom->createElement('xliff'));
$xliff->setAttribute('version', $this->version);
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:'.$this->version);
ksort($this->originalFiles);
foreach ($this->originalFiles as $key => $file) {
$body = $file->appendChild($this->dom->createElement('body'));
foreach ($this->transUnits[$key] as $transUnit) {
$body->appendChild($transUnit);
}
$xliff->appendChild($file);
}
return $this->dom;
}
/**
* @param string $filename
* @param string $sourceLanguage
* @param string $targetLanguage
*
* @return \PrestaShop\TranslationToolsBundle\Translation\Builder\XliffBuilder
*/
public function addFile($filename, $sourceLanguage, $targetLanguage)
{
if (!isset($this->originalFiles[$filename])) {
$xliffFile = $this->dom->createElement('file');
$xliffFile->setAttribute('original', $filename);
$xliffFile->setAttribute('source-language', $sourceLanguage);
$xliffFile->setAttribute('target-language', $targetLanguage);
$xliffFile->setAttribute('datatype', 'plaintext');
$this->originalFiles[$filename] = $xliffFile;
}
return $this;
}
/**
* @param string $filename
* @param string $source
* @param string $target
* @param string $note
*
* @return \PrestaShop\TranslationToolsBundle\Translation\Builder\XliffBuilder
*/
public function addTransUnit($filename, $source, $target, $note)
{
$id = md5($source);
$translation = $this->dom->createElement('trans-unit');
$translation->setAttribute('id', $id);
// Does the target contain characters requiring a CDATA section?
$source_value = 1 === preg_match('/[&<>]/', $source) ? $this->dom->createCDATASection($source) : $this->dom->createTextNode($source);
$target_value = 1 === preg_match('/[&<>]/', $target) ? $this->dom->createCDATASection($target) : $this->dom->createTextNode($target);
$note_value = 1 === preg_match('/[&<>]/', $note) ? $this->dom->createCDATASection($note) : $this->dom->createTextNode($note);
$s = $translation->appendChild($this->dom->createElement('source'));
$s->appendChild($source_value);
// Skip metadata
$z = $translation->appendChild($this->dom->createElement('target'));
$z->appendChild($target_value);
$n = $translation->appendChild($this->dom->createElement('note'));
$n->appendChild($note_value);
$this->transUnits[$filename][$id] = $translation;
return $this;
}
/**
* @param string $version
*
* @return \PrestaShop\TranslationToolsBundle\Translation\Builder\XliffBuilder
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
}

View File

@@ -0,0 +1,207 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Compiler\Smarty;
use SmartyException;
use SmartyCompilerException;
use Smarty_Internal_SmartyTemplateCompiler;
use Smarty_Internal_Templateparser;
use Smarty_Internal_Template;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
class TranslationTemplateCompiler extends Smarty_Internal_SmartyTemplateCompiler
{
/**
* Inherited from Smarty_Internal_TemplateCompilerBase.
*
* @var Smarty_Internal_Template
* @var bool $inheritance_child
*
* Inherited from Smarty_Internal_SmartyTemplateCompiler
* @var Smarty $smarty
* @var string $lexer_class
* @var object $lex
* @var string $parser_class
* @var object $parser
*/
/**
* @var bool
*/
public $nocache = false;
/**
* @var bool
*/
public $tag_nocache = false;
/**
* @var bool
*/
private $abort_and_recompile = false;
/**
* @var string
*/
private $templateFile;
/**
* @return array
*/
public function getTranslationTags()
{
$this->init();
$tagFound = [];
$comment = [];
// get tokens from lexer and parse them
while ($this->lex->yylex() && !$this->abort_and_recompile) {
try {
if ('extends' == $this->lex->value) {
$this->lex->value = 'dummy';
}
$this->parser->doParse($this->lex->token, $this->lex->value);
if ($this->lex->token === Smarty_Internal_Templateparser::TP_TEXT) {
$comment = [
'line' => $this->lex->line,
'value' => $this->lex->value,
];
}
} catch (SmartyCompilerException $e) {
if (($tag = $this->explodeLTag($this->parser->yystack))) {
$tagFound[] = $this->getTag($tag, $e, $comment);
}
$this->parser->yy_accept();
} catch (SmartyException $e) {
}
}
return $tagFound;
}
/**
* @param string $templateFile
*/
public function setTemplateFile($templateFile)
{
if (!file_exists($templateFile)) {
throw new FileNotFoundException(null, 0, null, $templateFile);
}
$this->templateFile = $templateFile;
return $this;
}
private function init()
{
/* here is where the compiling takes place. Smarty
tags in the templates are replaces with PHP code,
then written to compiled files. */
// init the lexer/parser to compile the template
$this->parent_compiler = $this;
$this->template = new Smarty_Internal_Template($this->templateFile, $this->smarty);
$this->lex = new $this->lexer_class(file_get_contents($this->templateFile), $this);
$this->parser = new $this->parser_class($this->lex, $this);
if (((int) ini_get('mbstring.func_overload')) & 2) {
mb_internal_encoding('ASCII');
}
}
/**
* @param string $string
* @param null|int $token
*
* @return string
*/
private function naturalize($string, $token = null)
{
switch ($token) {
case Smarty_Internal_Templateparser::TP_TEXT:
return trim($string, " \t\n\r\0\x0B{*}");
default:
return substr($string, 1, -1);
}
}
/**
* @param array $tagStack
*
* @return array|null
*/
private function explodeLTag(array $tagStack)
{
$tag = null;
foreach ($tagStack as $entry) {
if ($entry->minor === 'l') {
$tag = [];
}
if (is_array($tag) && is_array($entry->minor)) {
foreach ($entry->minor as $minor) {
foreach ($minor as $attr => $val) {
// Skip on variables
if (0 === strpos($val, '$') && 's' === $attr) {
return;
}
$tag[$attr] = $this->naturalize($val);
}
}
}
}
return $tag;
}
/**
* @param array $value
* @param SmartyCompilerException $exception
* @param array $previousComment
*
* @return array
*/
private function getTag(array $value, SmartyCompilerException $exception, array $previousComment)
{
$tag = [
'tag' => $value,
'line' => $exception->line,
'template' => $exception->template,
];
if (!empty($previousComment) && $previousComment['line'] == $tag['line'] - 1) {
$tag['comment'] = $this->naturalize($previousComment['value'], Smarty_Internal_Templateparser::TP_TEXT);
}
return $tag;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Dumper;
use PrestaShop\TranslationToolsBundle\Translation\Builder\PhpBuilder;
use PrestaShop\TranslationToolsBundle\Translation\Helper\LegacyHelper;
use Symfony\Component\Translation\Dumper\FileDumper;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Filesystem\Filesystem;
class PhpDumper extends FileDumper
{
/**
* @var PhpBuilder[]
*/
private $builders = [];
/**
* {@inheritdoc}
*/
public function dump(MessageCatalogue $messages, $options = array())
{
if (!array_key_exists('path', $options)) {
throw new \InvalidArgumentException('The file dumper needs a path option.');
}
if (array_key_exists('default_locale', $options)) {
$defaultLocale = $options['default_locale'];
} else {
$defaultLocale = $messages->getLocale();
}
// Add/update all Php builders (1/file)
foreach ($messages->getDomains() as $domain) {
$this->formatCatalogue($messages, $domain);
}
// Create files
foreach ($this->builders as $filename => $builder) {
$fullpath = $options['path'].'/'.$filename;
$directory = dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new \RuntimeException(sprintf('Unable to create directory "%s".', $directory));
}
$fs = new Filesystem();
$fs->dumpFile($fullpath, $builder->build());
}
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
foreach ($messages->all($domain) as $source => $target) {
$metadata = $messages->getMetadata($source, $domain);
// Skip if output info can't be guessed
if (!($outputInfo = LegacyHelper::getOutputInfo($metadata['file']))) {
continue;
}
$outputFile = str_replace('[locale]', $messages->getLocale(), $outputInfo['file']);
if (!isset($this->builders[$outputFile])) {
$this->builders[$outputFile] = new PhpBuilder();
$this->builders[$outputFile]->appendGlobalDeclaration($outputInfo['var']);
}
$this->builders[$outputFile]->appendStringLine(
$outputInfo['var'],
$outputInfo['generateKey']($target),
$target
);
}
}
/**
* {@inheritdoc}
*/
public function getExtension()
{
return 'php';
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Dumper;
use Locale;
use Symfony\Component\Translation\Dumper\XliffFileDumper as BaseXliffFileDumper;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Filesystem\Filesystem;
use PrestaShop\TranslationToolsBundle\Translation\Builder\XliffBuilder;
use PrestaShop\TranslationToolsBundle\Translation\Helper\DomainHelper;
use PrestaShop\TranslationToolsBundle\Configuration;
class XliffFileDumper extends BaseXliffFileDumper
{
protected $relativePathTemplate = '%locale%/%domain%.%extension%';
/**
* Gets the relative file path using the template.
*
* @param string $domain The domain
* @param string $locale The locale
*
* @return string The relative file path
*/
private function getRelativePath($domain, $locale)
{
return strtr($this->relativePathTemplate, array(
'%locale%' => $locale,
'%domain%' => $domain,
'%extension%' => $this->getExtension(),
));
}
/**
* {@inheritdoc}
*/
public function dump(MessageCatalogue $messages, $options = array())
{
if (!array_key_exists('path', $options)) {
throw new \InvalidArgumentException('The file dumper needs a path option.');
}
$fs = new Filesystem();
// save a file for each domain
foreach ($messages->getDomains() as $domain) {
$domainPath = DomainHelper::getExportPath($domain);
$fullpath = sprintf('%s/%s', $options['path'], $this->getRelativePath($domainPath, $messages->getLocale()));
$directory = dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
throw new \RuntimeException(sprintf('Unable to create directory "%s".', $directory));
}
$fs->dumpFile($fullpath, $this->formatCatalogue($messages, $domain, $options));
}
}
/**
* {@inheritdoc}
*/
public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = array())
{
if (array_key_exists('default_locale', $options)) {
$defaultLocale = $options['default_locale'];
} else {
$defaultLocale = Locale::getDefault();
}
$xliffBuilder = new XliffBuilder();
$xliffBuilder->setVersion('1.2');
foreach ($messages->all($domain) as $source => $target) {
if (!empty($source)) {
$metadata = $messages->getMetadata($source, $domain);
/**
* Handle original file information from xliff file.
* This is needed if at least part of the catalogue was read from xliff files
*/
if (is_array($metadata['file']) && !empty($metadata['file']['original'])) {
$metadata['file'] = $metadata['file']['original'];
}
$metadata['file'] = Configuration::getRelativePath(
$metadata['file'],
!empty($options['root_dir']) ? realpath($options['root_dir']) : false
);
$xliffBuilder->addFile($metadata['file'], $defaultLocale, $messages->getLocale());
$xliffBuilder->addTransUnit($metadata['file'], $source, $target, $this->getNote($metadata));
}
}
return html_entity_decode($xliffBuilder->build()->saveXML());
}
/**
* @param array $transMetadata
*
* @return string
*/
private function getNote($transMetadata)
{
$notes = [];
if (!empty($transMetadata['file'])) {
if (isset($transMetadata['line'])) {
$notes['line'] = 'Line: '.$transMetadata['line'];
}
if (isset($transMetadata['comment'])) {
$notes['comment'] = 'Comment: '.$transMetadata['comment'];
}
}
if (empty($notes) && isset($transMetadata['notes'][0]['content'])) {
// use notes loaded from xliff file
return $transMetadata['notes'][0]['content'];
}
return implode(PHP_EOL, $notes);
}
/**
* {@inheritdoc}
*/
public function getExtension()
{
return 'xlf';
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use Symfony\Component\Translation\Extractor\ChainExtractor as BaseChaineExtractor;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use PrestaShop\TranslationToolsBundle\Configuration;
class ChainExtractor extends BaseChaineExtractor
{
/**
* The extractors.
*
* @var ExtractorInterface[]
*/
private $extractors = [];
/**
* @param string $format
* @param ExtractorInterface $extractor
*
* @return self
*/
public function addExtractor($format, ExtractorInterface $extractor)
{
$this->extractors[$format] = $extractor;
return $this;
}
/**
* {@inheritdoc}
*/
public function extract($directory, MessageCatalogue $catalogue)
{
$finder = new Finder();
$finder->ignoreUnreadableDirs();
foreach (Configuration::getPaths() as $item) {
$finder->path('{^'.$item.'}');
}
foreach (Configuration::getExcludeFiles() as $item) {
$finder->notPath('{^'.$item.'}');
}
foreach ($this->extractors as $extractor) {
$extractor->setFinder(clone $finder);
$extractor->extract($directory, $catalogue);
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;
use PrestaShop\TranslationToolsBundle\Translation\Manager\OriginalStringManager;
use PrestaShop\TranslationToolsBundle\Translation\Parser\CrowdinPhpParser;
class CrowdinPhpExtractor extends AbstractFileExtractor implements ExtractorInterface
{
/**
* Prefix for new found message.
*
* @var string
*/
private $prefix = '';
/** @var CrowdinPhpParser $crodwinPhpParser */
private $crodwinPhpParser;
/** @var OriginalStringManager $originalStringManager */
private $originalStringManager;
public function __construct(CrowdinPhpParser $crodwinPhpParser, OriginalStringManager $originalStringManager)
{
$this->crodwinPhpParser = $crodwinPhpParser;
$this->originalStringManager = $originalStringManager;
}
/**
* {@inheritdoc}
*/
public function extract($resource, MessageCatalogue $catalog)
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
$generator = $this->crodwinPhpParser->parseFileTokens($file);
for (; $generator->valid(); $generator->next()) {
$translation = $generator->current();
$originalTranslation = $this->originalStringManager->get($file, $translation['key']);
$catalog->set($originalTranslation, $translation['message']);
$catalog->setMetadata(
$originalTranslation,
[
'key' => $translation['key'],
'file' => basename($file),
]
);
}
if (PHP_VERSION_ID >= 70000) {
// PHP 7 memory manager will not release after token_get_all(), see https://bugs.php.net/70098
gc_mem_caches();
}
}
}
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* @param string $file
*
* @throws \InvalidArgumentException
*
* @return bool
*/
protected function canBeExtracted($file)
{
return $this->isFile($file) && 'php' === pathinfo($file, PATHINFO_EXTENSION);
}
/**
* @param string|array $directory
*
* @return array
*/
protected function extractFromDirectory($directory)
{
$finder = new Finder();
return $finder->files()->name('*.php')->in($directory);
}
}

View File

@@ -0,0 +1,192 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use PhpParser\NodeDumper;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\ArrayTranslationDefinition;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\ExplicitTranslationCall;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\FormType\FormTypeDeclaration;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;
use PhpParser\ParserFactory;
use PhpParser\Lexer;
use PhpParser\NodeTraverser;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\CommentsNodeVisitor;
class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
{
use TraitExtractor;
/**
* @var array
*/
protected $visitors = [];
/**
* Prefix for new found message.
*
* @var string
*/
private $prefix = '';
/**
* @var \PhpParser\Parser\Multiple
*/
private $parser;
public function __construct()
{
$lexer = new Lexer(
array(
'usedAttributes' => array(
'comments',
'startLine',
'endLine',
'startTokenPos',
'endTokenPos',
),
)
);
$this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, $lexer);
}
/**
* {@inheritdoc}
*/
public function extract($resource, MessageCatalogue $catalogue)
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
$this->parseFileTokens($file, $catalogue);
}
}
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* @param $file
* @param MessageCatalogue $catalog
*
* @throws \Exception
*/
protected function parseFileTokens($file, MessageCatalogue $catalog)
{
$code = file_get_contents($file);
$translationCollection = new TranslationCollection();
$commentsNodeVisitor = new CommentsNodeVisitor($file->getFilename());
$translationVisitors = [
new ArrayTranslationDefinition($translationCollection),
new ExplicitTranslationCall($translationCollection),
new FormTypeDeclaration($translationCollection),
];
$traverser = new NodeTraverser();
$traverser->addVisitor($commentsNodeVisitor);
foreach ($translationVisitors as $visitor) {
$traverser->addVisitor($visitor);
}
//$nodeDumper = new NodeDumper();
try {
$stmts = $this->parser->parse($code);
//$debug = $nodeDumper->dump($stmts);
$traverser->traverse($stmts);
$comments = $commentsNodeVisitor->getComments();
foreach ($translationCollection->getTranslations() as $translation) {
$translation['domain'] = empty($translation['domain'])
? $this->resolveDomain(null)
: $translation['domain'];
$comment = $metadata['comment'] = $this->getEntryComment(
$comments,
$file->getFilename(),
($translation['line'] - 1)
);
$catalog->set(
$translation['source'],
$this->prefix.trim($translation['source']),
$translation['domain']
);
$catalog->setMetadata(
$translation['source'],
[
'line' => $translation['line'],
'file' => $file->getRealPath(),
'comment' => $comment,
],
$translation['domain']
);
}
} catch (\PhpParser\Error $e) {
throw new \Exception(
sprintf('Could not parse tokens in "%s" file. Is it syntactically valid?', $file),
$e->getCode(),
$e
);
}
}
/**
* @param string $file
*
* @throws \InvalidArgumentException
*
* @return bool
*/
protected function canBeExtracted($file)
{
return $this->isFile($file) && 'php' === pathinfo($file, PATHINFO_EXTENSION);
}
/**
* @param string|array $directory
*
* @return array
*/
protected function extractFromDirectory($directory)
{
return $this->getFinder()->files()->name('*.php')->in($directory);
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use SplFileInfo;
use PrestaShop\TranslationToolsBundle\Translation\Helper\DomainHelper;
use PrestaShop\TranslationToolsBundle\Translation\Compiler\Smarty\TranslationTemplateCompiler;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\MessageCatalogue;
class SmartyExtractor extends AbstractFileExtractor implements ExtractorInterface
{
use TraitExtractor;
const INCLUDE_EXTERNAL_MODULES = true;
const EXCLUDE_EXTERNAL_MODULES = false;
/**
* @var TranslationTemplateCompiler
*/
private $smartyCompiler;
private $prefix;
/**
* @var bool
*/
private $includeExternalWordings;
/**
* @param TranslationTemplateCompiler $smartyCompiler
* @param bool $includeExternalWordings Set to SmartyCompiler::INCLUDE_EXTERNAL_MODULES to include wordings signed with 'mod' (external modules)
*/
public function __construct(
TranslationTemplateCompiler $smartyCompiler,
$includeExternalWordings = self::EXCLUDE_EXTERNAL_MODULES
) {
$this->smartyCompiler = $smartyCompiler;
$this->includeExternalWordings = $includeExternalWordings;
}
/**
* {@inheritdoc}
*/
public function extract($resource, MessageCatalogue $catalogue)
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
if (!$this->canBeExtracted($file->getRealpath())) {
continue;
}
$this->extractFromFile($file, $catalogue);
}
}
/**
* @param SplFileInfo $resource
* @param MessageCatalogue $catalogue
*/
protected function extractFromFile(SplFileInfo $resource, MessageCatalogue $catalogue)
{
$compiler = $this->smartyCompiler->setTemplateFile($resource->getPathname());
$translationTags = $compiler->getTranslationTags();
foreach ($translationTags as $translation) {
$extractedDomain = null;
// skip "old styled" external translations
if (isset($translation['tag']['mod'])) {
if (!$this->includeExternalWordings) {
continue;
}
// domain
$extractedDomain = DomainHelper::buildModuleDomainFromLegacySource(
$translation['tag']['mod'],
$resource->getBasename()
);
} elseif (isset($translation['tag']['d'])) {
$extractedDomain = $translation['tag']['d'];
}
$domain = $this->resolveDomain($extractedDomain);
$string = stripslashes($translation['tag']['s']);
$catalogue->set($this->prefix . $string, $string, $domain);
$catalogue->setMetadata(
$this->prefix . $string,
[
'line' => $translation['line'],
'file' => $translation['template'],
],
$domain
);
}
}
/**
* {@inheritdoc}
*/
public function setPrefix($prefix)
{
$this->prefix = $prefix;
}
/**
* {@inheritdoc}
*/
protected function canBeExtracted($file)
{
return $this->isFile($file) && 'tpl' === pathinfo($file, PATHINFO_EXTENSION);
}
/**
* {@inheritdoc}
*/
protected function extractFromDirectory($directory)
{
return $this->getFinder()->name('*.tpl')->in($directory);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use Symfony\Component\Finder\Finder;
trait TraitExtractor
{
protected $defaultDomain = 'messages';
/**
* @var Finder
*/
protected $finder;
/**
* @param $domainName
*
* @return string
*/
protected function resolveDomain($domainName)
{
if (empty($domainName)) {
return $this->defaultDomain;
}
return $domainName;
}
/**
* @param $comments
* @param $file
* @param $line
*
* @return array
*/
public function getEntryComment(array $comments, $file, $line)
{
foreach ($comments as $comment) {
if ($comment['file'] == $file && $comment['line'] == $line) {
return $comment['comment'];
}
}
}
/**
* @param $finder
*
* @return $this
*/
public function setFinder(Finder $finder)
{
$this->finder = $finder;
return $this;
}
/**
* @return Finder
*/
public function getFinder()
{
if (null === $this->finder) {
return new Finder();
}
return $this->finder;
}
}

View File

@@ -0,0 +1,181 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor;
use Twig_Source;
use Twig_Environment;
use Twig_Error;
use PrestaShop\TranslationToolsBundle\Twig\Lexer;
use Symfony\Bridge\Twig\Translation\TwigExtractor as BaseTwigExtractor;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
class TwigExtractor extends BaseTwigExtractor implements ExtractorInterface
{
use TraitExtractor;
/**
* Prefix for found message.
*
* @var string
*/
private $prefix = '';
/**
* The twig environment.
*
* @var Twig_Environment
*/
private $twig;
/**
* @var Lexer
*/
private $twigLexer;
/**
* The twig environment.
*
* @var Twig_Environment
*/
public function __construct(Twig_Environment $twig)
{
$this->twig = $twig;
$this->twigLexer = new Lexer($this->twig);
}
/**
* {@inheritdoc}
*/
public function extract($resource, MessageCatalogue $catalogue)
{
$files = $this->extractFiles($resource);
foreach ($files as $file) {
if (!$this->canBeExtracted($file->getRealpath())) {
continue;
}
try {
$this->extractTemplateFile($file, $catalogue);
} catch (Twig_Error $e) {
if ($file instanceof SplFileInfo) {
$e->setSourceContext(new Twig_Source(
$e->getSourceContext()->getCode(),
$e->getSourceContext()->getName(),
$file->getRelativePathname()
));
} elseif ($file instanceof \SplFileInfo) {
$e->setSourceContext(new Twig_Source(
$e->getSourceContext()->getCode(),
$e->getSourceContext()->getName(),
$file->getRealPath()
));
}
throw $e;
}
}
}
/**
* {@inheritdoc}
*/
protected function extractTemplateFile($file, MessageCatalogue $catalogue)
{
if (!$file instanceof \SplFileInfo) {
$file = new \SplFileInfo($file);
}
$visitor = $this->twig->getExtension(TranslationExtension::class)->getTranslationNodeVisitor();
$visitor->enable();
$this->twig->setLexer(new Lexer($this->twig));
$tokens = $this->twig->tokenize(new Twig_Source(file_get_contents($file->getPathname()), $file->getFilename()));
$this->twig->parse($tokens);
$comments = $this->twigLexer->getComments();
foreach ($visitor->getMessages() as $message) {
$domain = $this->resolveDomain(isset($message[1]) ? $message[1] : null);
$catalogue->set(
$message[0],
$this->prefix.trim($message[0]),
$domain
);
$metadata = [
'file' => $file->getRealpath(),
'line' => $message['line'],
];
$comment = $this->getEntryComment($comments, $file->getFilename(), ($message['line'] - 1));
if (null != $comment) {
$metadata['comment'] = $comment;
}
if (isset($message['line'])) {
$metadata['comment'] = $this->getEntryComment($comments, $file->getFilename(), ($message['line'] - 1));
}
$catalogue->setMetadata($message[0], $metadata, $domain);
}
$visitor->disable();
}
/**
* @param $comments
* @param $file
* @param $line
*
* @return array
*/
public function getEntryComment($comments, $file, $line)
{
foreach ($comments as $comment) {
if ($comment['file'] == $file && $comment['line'] == $line) {
return $comment['comment'];
}
}
}
/**
* @param string $directory
*
* @return Finder
*/
protected function extractFromDirectory($directory)
{
return $this->getFinder()->files()->name('*.twig')->in($directory);
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Util;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
* This class have only one thing to do, transform:
*
* en-US
* ├── Admin
* │ └── Catalog
* │ ├── Feature.xlf
* │ ├── Help.xlf
* │ └── Notification.xlf
* └── Shop
* └── PDF.xlf
*
*
* Into:
*
* ├── AdminCatalogFeature.en-US.xlf
* ├── AdminCatalogHelp.en-US.xlf
* ├── AdminCatalogNotification.en-US.xlf
* └── ShopPDF.en-US.xlf
*/
class Flattenizer
{
public static $finder = null;
public static $filesystem = null;
/**
* @input string $inputPath Path of directory to flattenize
* @input string $outputPath Location of flattenized files newly created
* @input string $locale Selected locale for theses files.
* @input boolean $cleanPath Clean input path after flatten.
*/
public static function flatten($inputPath, $outputPath, $locale, $cleanPath = true)
{
$finder = self::$finder;
$filesystem = self::$filesystem;
if (is_null(self::$finder)) {
$finder = new Finder();
}
if (is_null(self::$filesystem)) {
$filesystem = new Filesystem();
}
if ($cleanPath) {
$filesystem->remove($outputPath);
$filesystem->mkdir($outputPath);
}
return self::flattenFiles($finder->in($inputPath)->files(), $outputPath, $locale, $filesystem);
}
/**
* @param SplFileInfo $files List of files to flattenize
* @param string $outputPath Location of flattenized files newly created
* @param string $locale Selected locale for theses files
* @param Filesystem $filesystem Instance of Filesystem
* @param bool $addLocale Should add the locale to filename
* @return bool
*/
public static function flattenFiles($files, $outputPath, $locale, $filesystem, $addLocale = true)
{
foreach ($files as $file) {
$flatName = preg_replace('#[\/\\\]#', '', $file->getRelativePath()).$file->getFilename();
if ($addLocale) {
$flatName = preg_replace('#\.xlf#', '.'.$locale.'.xlf', $flatName);
}
$filesystem->copy($file->getRealpath(), $outputPath.'/'.$flatName);
}
return true;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Util;
class TranslationCollection
{
private $translations = [];
/**
* Creates a new translation item. Note that it is not added to the collection.
*
* @param string $source Wording
* @param int $line
* @param string $domain
*
* @return array Translation item
*/
public static function newTranslationItem($source, $line, $domain = '')
{
$translation = [
'source' => $source,
'line' => $line,
'domain' => $domain,
];
return $translation;
}
/**
* Adds an array of translations to the collection.
*
* @param array[] $translations Array of translation items
*/
public function add(array $translations)
{
if (!empty($translations)) {
$this->translations = array_merge($this->translations, $translations);
}
}
/**
* Applies the provided translation domain for all translation items that don't specify a domain
* @param string $domain
*/
public function applyDefaultTranslationDomain($domain)
{
foreach ($this->translations as &$translation) {
if (empty($translation['domain'])) {
$translation['domain'] = $domain;
}
}
}
/**
* Returns all the translations as an array.
*
* @return array[]
*/
public function getTranslations()
{
return $this->translations;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node;
/**
* Extracts comment information
*/
class CommentsNodeVisitor extends NodeVisitorAbstract
{
protected $file;
/**
* @var array
*/
protected $comments = [];
/**
* TranslationNodeVisitor constructor.
*
* @param $file
*/
public function __construct($file)
{
$this->file = $file;
}
/**
* {@inheritdoc}
*/
public function leaveNode(Node $node)
{
$this->tryExtractComments($node);
}
/**
* @return array
*/
public function getComments()
{
return $this->comments;
}
/**
* @param Node $node
*/
private function tryExtractComments(Node $node)
{
$comments = $node->getAttribute('comments');
if (is_array($comments)) {
foreach ($comments as $comment) {
$this->comments[] = [
'line' => $comment->getLine(),
'file' => $this->file,
'comment' => trim($comment->getText(), " \t\n\r\0\x0B/*"),
];
}
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation;
use PhpParser\NodeVisitorAbstract;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
abstract class AbstractTranslationNodeVisitor extends NodeVisitorAbstract implements TranslationVisitorInterface
{
/**
* @var TranslationCollection
*/
protected $translations;
public function __construct(TranslationCollection $collection)
{
$this->translations = $collection;
}
/**
* @inheritDoc
*/
public function getTranslationCollection()
{
return $this->translations;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation;
use PhpParser\Node;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
/**
* This class looks for arrays like this:
*
* ```php
* [
* 'key' => 'This text is lonely',
* 'parameters' => [],
* 'domain' => 'Admin.Notifications.Error',
* ]
*
* [
* 'key' => 'This text is lonely',
* 'domain' => 'Admin.Notifications.Error',
* ]
* ```
*
* Parameters can be in any order
*/
class ArrayTranslationDefinition extends AbstractTranslationNodeVisitor
{
public function leaveNode(Node $node)
{
$this->translations->add($this->extractFrom($node));
}
/**
* @param Node $node
*
* @return array Array of translations to add
*/
public function extractFrom(Node $node)
{
if (!$this->appliesFor($node)) {
return [];
}
/** @var $node Node\Expr\Array_ */
$translation = [
'source' => null,
'domain' => null,
'line' => $node->getAttribute('startLine')
];
$parametersFound = false;
foreach ($node->items as $item) {
if (!($item instanceof Node\Expr\ArrayItem && $item->key instanceof String_)) {
return [];
}
switch($item->key->value) {
case 'key':
if (!$item->value instanceof String_) {
return [];
}
$translation['source'] = $item->value->value;
continue 2;
case 'domain':
if (!$item->value instanceof String_) {
return [];
}
$translation['domain'] = $item->value->value;
continue 2;
case 'parameters':
$parametersFound = true;
continue 2;
}
// break if the key isn't one of the three above
return [];
}
if ($translation['source'] === null
|| $translation['domain'] === null
|| (count($node->items) === 3 && !$parametersFound)
) {
return [];
}
return [$translation];
}
/**
* @param Node $node
*
* @return bool
*/
private function appliesFor(Node $node)
{
return (
$node instanceof Node\Expr\Array_
&& (in_array(count($node->items), [2, 3]))
);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation;
use PhpParser\Node;
/**
* Looks up for a translation call using l(), trans() or t()
*/
class ExplicitTranslationCall extends AbstractTranslationNodeVisitor
{
const SUPPORTED_METHODS = ['l', 'trans', 't'];
public function leaveNode(Node $node)
{
$this->translations->add($this->extractFrom($node));
}
/**
* @inheritdoc
*/
public function extractFrom(Node $node)
{
if (!$this->appliesFor($node)) {
return [];
}
/** @var $node \PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall */
$nodeName = $this->getNodeName($node);
$key = $this->getValue($node->args[0]);
if (!in_array($nodeName, self::SUPPORTED_METHODS) || empty($key)) {
return [];
}
$translation = [
'source' => $key,
'line' => $node->args[0]->getLine(),
];
if ($nodeName == 'trans') {
// First line is Symfony Style, second is Prestashop FrameworkBundle Style
if (count($node->args) > 2 && $node->args[2]->value instanceof Node\Scalar\String_) {
$translation['domain'] = $node->args[2]->value->value;
} elseif (count($node->args) > 1 && $node->args[1]->value instanceof Node\Scalar\String_) {
$translation['domain'] = $node->args[1]->value->value;
}
} elseif ($nodeName == 't') {
$translation['domain'] = 'Emails.Body';
}
return [$translation];
}
/**
* @param Node $node
*
* @return bool
*/
private function appliesFor(Node $node)
{
return (
($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\FuncCall)
&& (
(is_string($node->name) && $node->name instanceof Node\Name)
|| !empty($node->args)
)
);
}
/**
* @param Node\Arg $arg
*
* @return string|null
*/
private function getValue(Node\Arg $arg)
{
if ($arg->value instanceof Node\Scalar\String_) {
return $arg->value->value;
} elseif (gettype($arg) === 'string') {
return $arg->value;
}
}
/**
* @param Node|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall $node
*
* @return mixed
*/
private function getNodeName(Node $node)
{
if ($node->name instanceof Node\Name) {
return $node->name->parts[0];
}
return $node->name;
}
}

View File

@@ -0,0 +1,195 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\FormType;
use PhpParser\Node;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
/**
* Extracts choices from a choice declaration.
*
* A choice declaration looks like this:
*
* ```php
* ->add('default_order_way', ChoiceType::class, [
* 'choices' => [
* 'Ascending' => 0,
* 'Descending' => 1,
* ],
* 'required' => true,
* 'choice_translation_domain' => 'Admin.Global',
* ]);
* ```
*/
class ChoiceExtractor
{
const METHOD_NAME = 'add';
const EXPECTED_ARG_COUNT = 3;
const CLASS_ARG_INDEX = 1;
const OPTIONS_ARG_INDEX = 2;
const OPTION_NAME_CHOICES = 'choices';
const OPTION_NAME_TRANSLATION_DOMAIN = 'choice_translation_domain';
const CHOICE_CLASS_NAME = 'ChoiceType';
/**
* @var Node|Node\Expr\MethodCall
*/
private $rootNode;
/**
* @var string
*/
private $defaultTranslationDomain;
public function __construct(Node $node, $defaultTranslationDomain = '')
{
$this->rootNode = $node;
$this->defaultTranslationDomain = $defaultTranslationDomain;
}
/**
* @return bool
*/
public function isChoiceDeclaration()
{
$node = $this->rootNode;
return (
$node instanceof Node\Expr\MethodCall
&& $node->name === self::METHOD_NAME
&& count($node->args) >= self::EXPECTED_ARG_COUNT
&& $this->argIsChoiceType($node->args[self::CLASS_ARG_INDEX])
);
}
/**
* Returns the list of choices as translation items.
*
* Note: the translation domain is only
*
* @return array[] Translation items
*/
public function getChoiceWordings()
{
$choices = [];
try {
$domain = $this->getTranslationDomain();
$choicesNode = $this->getChoicesNode();
foreach ($choicesNode->items as $optionItem) {
if ($optionItem instanceof Node\Expr\ArrayItem
&& $optionItem->key instanceof Node\Scalar\String_
) {
$choices[] = TranslationCollection::newTranslationItem(
$optionItem->key->value,
$optionItem->getLine(),
$domain
);
}
}
} catch (\RuntimeException $e) {
// choices not found
return [];
}
return $choices;
}
/**
* @return string
*/
private function getTranslationDomain()
{
$options = $this->getOptionsNode();
try {
$translationDomainNode = $this->getOptionNodeByKey(self::OPTION_NAME_TRANSLATION_DOMAIN, $options);
if (!$translationDomainNode instanceof Node\Scalar\String_) {
throw new \RuntimeException("Translation Domain node is not a string");
}
return $translationDomainNode->value;
} catch (\RuntimeException $e) {
// translation domain is optional, return the default
return $this->defaultTranslationDomain;
}
}
/**
* @return Node\Expr\Array_
*/
private function getChoicesNode()
{
$options = $this->getOptionsNode();
$choicesNode = $this->getOptionNodeByKey(self::OPTION_NAME_CHOICES, $options);
if (!$choicesNode instanceof Node\Expr\Array_) {
throw new \RuntimeException("Choices node is not an array");
}
return $choicesNode;
}
/**
* @return Node\Expr\Array_
*/
private function getOptionsNode()
{
$node = $this->rootNode;
$arg = $node->args[self::OPTIONS_ARG_INDEX];
if (!$arg->value instanceof Node\Expr\Array_) {
throw new \RuntimeException("The options node is not an array");
}
return $arg->value;
}
/**
* Look up option in the options node
*
* @param string $optionName Option name to look up
* @param Node\Expr\Array_ $optionsNode
*
* @return Node\Expr Node
*/
private function getOptionNodeByKey($optionName, Node\Expr\Array_ $optionsNode)
{
foreach ($optionsNode->items as $option) {
if ($option instanceof Node\Expr\ArrayItem
&& $option->key instanceof Node\Scalar\String_
&& $option->key->value === $optionName
) {
return $option->value;
}
}
throw new \RuntimeException(sprintf('Could not the requested option "%s"', $optionName));
}
/**
* @param Node\Arg $node
*
* @return bool
*/
private function argIsChoiceType(Node\Arg $node)
{
return (
$node->value instanceof Node\Expr\ClassConstFetch
&& $node->value->class instanceof Node\Name
&& in_array(self::CHOICE_CLASS_NAME, $node->value->class->parts)
);
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\FormType;
use PhpParser\Node;
/**
* This class looks for a default translation domain declaration.
*
* It looks like this:
*
* ```php
* public function configureOptions(OptionsResolver $resolver)
* {
* $resolver->setDefaults([
* 'translation_domain' => 'Admin.Shopparameters.Feature',
* ]);
* }
* ```
*/
class DefaultTranslationDomainExtractor
{
const CONFIGURE_OPTIONS = 'configureOptions';
/**
* Types of nodes that we are interested in inspecting
*/
const INTERESTING_NODE_TYPES = [
Node\Stmt\ClassMethod::class,
Node\Expr\MethodCall::class,
];
/**
* Name of the method that sets default settings
*/
const SET_DEFAULTS_DECLARATION_METHOD_NAME = 'setDefaults';
/**
* Index of the OptionsResolver parameter in the configureOptions method declaration
*/
const OPTIONS_RESOLVER_PARAM_INDEX = 0;
/**
* The extracted default translation domain
* @var string
*/
private $defaultTranslationDomain = '';
/**
* Indicates if the default translation domain has been found
* @var bool
*/
private $defaultTranslationDomainFound = false;
/**
* Indicates if we are currently analyzing nodes inside the configureOptions method declaration
* @var bool
*/
private $insideConfigureOptions = false;
/**
* Name of the OptionsResolver parameter in the configureOptions method declaration
* @var string
*/
private $optionsResolverName = '';
/**
* @param Node $node
*
* @return bool
*/
public function lookForDefaultTranslationDomain(Node $node)
{
return (
$this->isAnInterestingNode($node)
&& !$this->defaultDomainHasBeenFound()
&& $this->process($node)
);
}
/**
* @return bool
*/
public function defaultDomainHasBeenFound()
{
return $this->defaultTranslationDomainFound;
}
/**
* @return string
*/
public function getDefaultTranslationDomain()
{
return $this->defaultTranslationDomain;
}
/**
* @param Node $node
*
* @return bool
*/
private function process(Node $node)
{
if ($this->isThisNodeInsideConfigureOptionsMethod($node)
&& $this->isDefaultsDeclaration($node)
) {
$this->extractDefaultTranslationDomain($node);
}
return $this->defaultDomainHasBeenFound();
}
/**
* Check if this node should be inspected
*
* @param Node $node
*
* @return bool
*/
private function isAnInterestingNode(Node $node)
{
foreach (self::INTERESTING_NODE_TYPES as $nodeType) {
if ($node instanceof $nodeType) {
return true;
}
}
return false;
}
/**
* @param Node $node
*
* @return bool
*/
private function isThisNodeInsideConfigureOptionsMethod(Node $node)
{
if ($node instanceof Node\Stmt\ClassMethod) {
if ($this->nodeIsConfigurationOptionsMethod($node)) {
$this->optionsResolverName = $this->getOptionsResolverName($node);
// we are inside the configuration options method all right
// but we won't acknowledge it if the options resolver parameter name cannot be found
$this->insideConfigureOptions = !empty($this->optionsResolverName);
} else {
$this->insideConfigureOptions = false;
}
return false;
}
return $this->insideConfigureOptions;
}
/**
* @param Node\Stmt\ClassMethod $node
*
* @return bool
*/
private function nodeIsConfigurationOptionsMethod(Node\Stmt\ClassMethod $node)
{
return ($node->name === self::CONFIGURE_OPTIONS);
}
/**
* Returns the name of the OptionsResolver parameter in the configureOptions method declaration.
*
* @param Node\Stmt\ClassMethod $classMethod
*
* @return string
*/
private function getOptionsResolverName(Node\Stmt\ClassMethod $classMethod)
{
if (isset($classMethod->params[self::OPTIONS_RESOLVER_PARAM_INDEX])) {
$resolverParam = $classMethod->params[self::OPTIONS_RESOLVER_PARAM_INDEX];
return $resolverParam->name;
}
return '';
}
/**
* @param Node $node
*
* @return bool
*/
private function isDefaultsDeclaration(Node $node)
{
return ($node instanceof Node\Expr\MethodCall
&& $node->var instanceof Node\Expr\Variable
&& $node->var->name === $this->optionsResolverName
&& $node->name === self::SET_DEFAULTS_DECLARATION_METHOD_NAME
&& count($node->args) > 0
);
}
/**
* @param Node\Expr\MethodCall $node
*
* @return bool
*/
private function extractDefaultTranslationDomain(Node\Expr\MethodCall $node)
{
$defaults = $node->args[0];
if ($defaults instanceof Node\Arg
&& $defaults->value instanceof Node\Expr\Array_
&& !empty($defaults->value->items)
) {
foreach ($defaults->value->items as $item) {
if ($item instanceof Node\Expr\ArrayItem
&& $item->key instanceof Node\Scalar\String_
&& $item->key->value === 'translation_domain'
&& $item->value instanceof Node\Scalar\String_
) {
$this->setDefaultTranslationDomain($item->value->value);
return true;
}
}
}
return false;
}
/**
* @param string $defaultTranslationDomain
*/
private function setDefaultTranslationDomain($defaultTranslationDomain)
{
$this->defaultTranslationDomain = $defaultTranslationDomain;
$this->defaultTranslationDomainFound = true;
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\FormType;
use PhpParser\Node;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation\AbstractTranslationNodeVisitor;
/**
* Extracts wordings from FormType classes
*
* Only supports wordings in ChoiceType for now
*/
class FormTypeDeclaration extends AbstractTranslationNodeVisitor
{
/**
* FQCN to symfony form AbstractType, separated into parts
*/
const SUPPORTED_FORM_TYPES = [
['Symfony', 'Component', 'Form', 'AbstractType'],
['PrestaShopBundle', 'Form', 'Admin', 'Type', 'TranslatorAwareType']
];
/**
* Types of nodes that we are interested in inspecting
*/
const INTERESTING_NODE_TYPES = [
Node\Stmt\Use_::class,
Node\Stmt\Class_::class,
Node\Stmt\ClassMethod::class,
Node\Expr\MethodCall::class,
];
/**
* @var bool Indicates if a Use statement for Symfony\Component\Form\AbstractType has been found
*/
private $useSfFormAbstractTypeFound = false;
/**
* @var string Alias used for AbstractType class in the Use statement (if found)
*/
private $sfFormAbstractTypeAlias = '';
/**
* @var bool Indicates if the we have identified a class that should be inspected by this visitor
*/
private $classIsKnownToBeFormType = false;
/**
* @var bool Indicates if we should keep continue to inspect nodes in this file
*/
private $continueVisiting = true;
private $defaultTranslationDomain = '';
private $defaultTranslationDomainExtractor;
public function __construct(TranslationCollection $collection)
{
parent::__construct($collection);
$this->defaultTranslationDomainExtractor = new DefaultTranslationDomainExtractor();
}
public function enterNode(Node $node)
{
if ($this->shouldTryToExtractWordings($node)) {
$this->tryToExtractDefaultTranslationDomain($node);
$this->tryToExtractChoiceWordings($node);
}
}
public function afterTraverse(array $nodes)
{
if ($this->needsPostProcessing()) {
$this->translations->applyDefaultTranslationDomain($this->defaultTranslationDomain);
}
}
private function needsPostProcessing()
{
return (
$this->classIsKnownToBeFormType
&& !empty($this->defaultTranslationDomain)
);
}
/**
* @param Node $node
*
* @return bool
*/
private function shouldTryToExtractWordings(Node $node)
{
return (
$this->continueVisiting
&& $this->isAnInterestingNode($node)
&& $this->checkIfClassIsFormType($node)
);
}
/**
* @param Node $node
*
* @return bool
*/
private function checkIfClassIsFormType(Node $node)
{
if (!$this->classIsKnownToBeFormType) {
$this->checkIfUseSfAbstractTypeIsPresent($node);
$this->checkIfClassDeclarationExtendsSfAbstractType($node);
}
return $this->classIsKnownToBeFormType;
}
/**
* Check if this node should be inspected
*
* @param Node $node
*
* @return bool
*/
private function isAnInterestingNode(Node $node)
{
foreach (self::INTERESTING_NODE_TYPES as $nodeType) {
if ($node instanceof $nodeType) {
return true;
}
}
return false;
}
/**
* Find out if this node is a Use for a supported form type
*
* @param Node $node
*/
private function checkIfUseSfAbstractTypeIsPresent(Node $node)
{
if ($node instanceof Node\Stmt\Use_ && !$this->useSfFormAbstractTypeFound) {
foreach ($node->uses as $useData) {
if ($useData instanceof Node\Stmt\UseUse
&& $useData->name instanceof Node\Name
&& in_array($useData->name->parts, self::SUPPORTED_FORM_TYPES)
) {
$this->useSfFormAbstractTypeFound = true;
$this->sfFormAbstractTypeAlias = $useData->alias;
}
}
}
}
/**
* Find out if this node is a Class node which extends AbstractType
*
* @param Node $node
*/
private function checkIfClassDeclarationExtendsSfAbstractType(Node $node)
{
if ($node instanceof Node\Stmt\Class_) {
if (!$this->useSfFormAbstractTypeFound) {
// no Use statement for AbstractType was found, no need to continue
return;
}
if ($node->extends instanceof Node\Name) {
$this->classIsKnownToBeFormType = in_array($this->sfFormAbstractTypeAlias, $node->extends->parts);
}
}
}
/**
* @param Node $node
*/
private function tryToExtractChoiceWordings(Node $node)
{
if ($node instanceof Node\Expr\MethodCall) {
$choiceExtractor = new ChoiceExtractor($node, $this->defaultTranslationDomain);
if ($choiceExtractor->isChoiceDeclaration()) {
$translations = $choiceExtractor->getChoiceWordings();
$this->translations->add($translations);
}
}
}
private function tryToExtractDefaultTranslationDomain($node)
{
$extractor = $this->defaultTranslationDomainExtractor;
if ($extractor->lookForDefaultTranslationDomain($node)) {
$this->defaultTranslationDomain = $extractor->getDefaultTranslationDomain();
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Translation\Extractor\Visitor\Translation;
use PrestaShop\TranslationToolsBundle\Translation\Extractor\Util\TranslationCollection;
interface TranslationVisitorInterface
{
/**
* @return TranslationCollection
*/
public function getTranslationCollection();
}

View File

@@ -0,0 +1,43 @@
<?php
$_FIELDS['Customer_342f5c77ed008542e78094607ce1f7f3'] = 'firstname';
$_FIELDS['Customer_8ad75c5a8821cc294f189181722acb56'] = 'lastname';
$_FIELDS['Customer_cf673f7ee88828c9fb8f6acf2cb08403'] = 'birthday';
$_FIELDS['Customer_0c83f57c786a0b4a39efab23731c7ebc'] = 'email';
$_FIELDS['Customer_d1befa03c79ca0b84ecc488dea96bc68'] = 'website';
$_FIELDS['Customer_93c731f1c3a84ef05cd54d044c379eaa'] = 'company';
$_FIELDS['Customer_01b0fd027f8764f9c069506b8de8bf2e'] = 'siret';
$_FIELDS['Warehouse_b8af13ea9c8fe890c9979a1fa8dbde22'] = 'reference';
$_FIELDS['Warehouse_b068931cc450442b63f5b3d276ea4297'] = 'name';
$_FIELDS['Warehouse_23c3b4d168a45ef94635494ce42eb658'] = 'management_type';
$_FIELDS['Country_213ecf1210f43736b6d4997fcfa27954'] = 'iso_code';
$_FIELDS['Country_b068931cc450442b63f5b3d276ea4297'] = 'name';
$_FIELDS['Country_37f577d6d447ff0743d74245446223b3'] = 'zip_code_format';
$_FIELDS['State_213ecf1210f43736b6d4997fcfa27954'] = 'iso_code';
$_FIELDS['State_b068931cc450442b63f5b3d276ea4297'] = 'name';
$_FIELDS['Address_93c731f1c3a84ef05cd54d044c379eaa'] = 'company';
$_FIELDS['Address_8ad75c5a8821cc294f189181722acb56'] = 'lastname';
$_FIELDS['Address_342f5c77ed008542e78094607ce1f7f3'] = 'firstname';
$_FIELDS['Address_81e70cb16ec45f5ab19bb6638e8e6c2d'] = 'address1';
$_FIELDS['Address_f669f8e9f6599d0dfcd613bc6e2f347e'] = 'address2';
$_FIELDS['Address_e90ebd9556fa4031171f043013794b61'] = 'postcode';
$_FIELDS['Address_4ed5d2eaed1a1fadcc41ad1d58ed603e'] = 'city';
$_FIELDS['Address_795f3202b17cb6bc3d4b771d8c6c9eaf'] = 'other';
$_FIELDS['Address_f7a42fe7211f98ac7a60a285ac3a9e87'] = 'phone';
$_FIELDS['Address_2df2ca5cf808744c2977e4073f6b59c8'] = 'phone_mobile';
$_FIELDS['Address_eec0a9661213354fa7a52519eea3f827'] = 'vat_number';
$_FIELDS['Address_d56f5e97524d5d1ad77ec197ae11dad0'] = 'dni';
$catalogue = new \Symfony\Component\Translation\MessageCatalogue();
function addEntry($catalogue, $original, $translation, $domain = 'messages')
{
$catalogue->set($original, $translation, $domain);
$catalogue->setMetadata($original, ['line' => 0, 'file' => __FILE__], $domain);
}
foreach ($_FIELDS as $key => $value) {
addEntry($catalogue, $value, $value, reset(explode('_', $key)));
}
return $catalogue;

View File

@@ -0,0 +1,152 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Helper;
class DomainHelper
{
/**
* @param string $localPath
*
* @return string
*/
public static function getCrowdinPath($localPath)
{
$segments = explode(DIRECTORY_SEPARATOR, $localPath);
$directorySegments = array_slice($segments, 0, 2);
if (count($directorySegments) !== count($segments)) {
return implode(DIRECTORY_SEPARATOR, $directorySegments).DIRECTORY_SEPARATOR.implode('.', array_slice($segments, 2));
}
return implode(DIRECTORY_SEPARATOR, $directorySegments);
}
/**
* @param string $domain
*
* @return string
*/
public static function getExportPath($domain)
{
return str_replace('.', DIRECTORY_SEPARATOR, $domain);
}
/**
* @param string $exportPath
*
* @return string
*/
public static function getDomain($exportPath)
{
return str_replace(DIRECTORY_SEPARATOR, '.', $exportPath);
}
/**
* Builds a module domain name for the legacy system
*
* @param string $moduleName Name of the module (eg. ps_themecusto)
* @param string $sourceFileName Filename where the wording was found (eg. someFile.tpl)
*
* @return string The domain name (eg. Modules.Psthemecusto.somefile)
*/
public static function buildModuleDomainFromLegacySource($moduleName, $sourceFileName)
{
$transformedModuleName = self::buildModuleDomainNameComponent($moduleName);
if (empty($sourceFileName)) {
$source = self::transformDomainComponent($moduleName);
} else {
$source = strtolower(basename($sourceFileName, '.tpl'));
// sourced from https://github.com/PrestaShop/PrestaShop/blob/1.6.1.x/classes/Translate.php#L174-L178
if ('controller' == substr($source, -10, 10)) {
$source = substr($source, 0, -10);
}
$source = ucfirst($source);
}
$domain = 'Modules.' . $transformedModuleName . '.' . $source;
return $domain;
}
/**
* Returns the base domain for the provided module name
*
* @param string $moduleName
* @param bool $withDots True to use separating dots
*
* @return string
*/
public static function buildModuleBaseDomain($moduleName, $withDots = false)
{
$domain = 'Modules';
if ($withDots) {
$domain .= '.';
}
$domain .= self::buildModuleDomainNameComponent($moduleName);
return $domain;
}
/**
* Transforms the module name to use in a domain
*
* @param string $moduleName
*
* @return string
*/
private static function buildModuleDomainNameComponent($moduleName)
{
if ('ps_' === substr($moduleName, 0, 3)) {
$moduleName = substr($moduleName, 3);
}
return self::transformDomainComponent($moduleName);
}
/**
* Formats a domain component by removing unwanted characters
*
* @param string $component
*
* @return string
*/
private static function transformDomainComponent($component)
{
return ucfirst(
strtr(
strtolower($component),
['_' => '']
)
);
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Helper;
class LegacyHelper
{
/**
* @param string $inputFilename NB: Remove the working directory (eg: controllers/foo|themes/bar)
*
* @return array|null
*/
public static function getOutputInfo($inputFilename)
{
switch (1) {
case preg_match('#controllers/admin#', $inputFilename):
case preg_match('#override/controllers/admin#', $inputFilename):
case preg_match('#classes/helper#', $inputFilename):
return [
'file' => 'translations/[locale]/admin.php',
'var' => '_LANGADM',
'generateKey' => function ($string) use ($inputFilename) {
return self::getKeyPrefix($inputFilename).self::getKey($string);
},
];
case preg_match('#^themes/([A-Za-z0-9_]+).+\.tpl$#', $inputFilename, $matches):
case preg_match('#^themes/([A-Za-z0-9_]+)(?!modules/)[a-zA-Z0-9/-]+(?!_ss\d|\d)\.tpl$#', $inputFilename, $matches):
return [
'file' => 'themes/'.$matches[1].'/lang/[locale].php',
'var' => '_LANG',
'generateKey' => function ($string) use ($inputFilename) {
return pathinfo(basename($inputFilename), PATHINFO_FILENAME).'_'.self::getKey($string);
},
];
// when we get a simple theme
case preg_match('#themes/([A-Za-z0-9_-]+).+\.tpl$#', $inputFilename, $matches):
return [
'file' => '/lang/[locale].php',
'var' => '_LANG',
'generateKey' => function ($string) use ($inputFilename) {
return pathinfo(basename($inputFilename), PATHINFO_FILENAME).'_'.self::getKey($string);
},
];
case preg_match('#override/classes/pdf/(?!index)\w+\.php$#', $inputFilename):
case preg_match('#classes/pdf/(?!index)\w+\.php$#', $inputFilename):
return [
'file' => 'translations/[locale]/pdf.php',
'var' => '_LANGPDF',
'generateKey' => function ($string) {
return 'PDF'.self::getKey($string);
},
];
case preg_match('#(?:/|^)modules/([A-Za-z0-9_]+)/#', $inputFilename, $matches):
return [
'file' => 'modules/'.$matches[1].'/translations/[locale].php',
'var' => '_MODULE',
'generateKey' => function ($string) use ($inputFilename, $matches) {
return '<{'.$matches[1].'}prestashop>'.pathinfo(basename($inputFilename), PATHINFO_FILENAME).'_'.self::getKey($string);
},
];
case preg_match('#^mails/#', $inputFilename):
return [
'file' => 'mails/[locale]/lang.php',
'var' => '_LANGMAIL',
'generateKey' => function ($string) {
return $string;
},
];
case preg_match('/fields_catalogue.php$/', $inputFilename):
return [
'file' => 'translations/[locale]/fields.php',
'var' => '_FIELDS',
'generateKey' => function ($string, $domain) use ($inputFilename) {
return $domain.'_'.md5($string);
},
];
case preg_match('#controllers/admin/([A-Za-z]+)Controller.php$#', $inputFilename):
return [
'file' => 'translations/[locale]/tabs.php',
'var' => '_TABS',
'generateKey' => function () use ($inputFilename) {
return self::getKeyPrefix($inputFilename);
},
];
// case !preg_match('#tools/|cache/|\.tpl\.php$|[a-z]{2}\.php$#', $inputFilename) && preg_match('/\.php$/', $inputFilename):
// return [
// 'file' => 'translations/[locale]/errors.php',
// 'var' => '_ERRORS',
// 'generateKey' => function ($string) {
// return self::getKey($string);
// },
// ];
}
}
/**
* @param string $string
*
* @return string
*/
public static function getKey($string)
{
return md5(preg_replace("/\\\*'/", "\'", $string));
}
/**
* @param string $file
*
* @return string|null
*/
public static function getKeyPrefix($file)
{
$fileName = basename($file);
switch (${false} = true) {
case $fileName === 'AdminController.php':
return 'AdminController';
case $fileName === 'PaymentModule.php':
return 'PaymentModule';
case strpos($file, 'Helper') !== false:
return 'Helper';
case strpos($file, 'Controller.php') !== false:
return basename(substr($file, 0, -14));
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* 2007-2018 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2018 PrestaShop SA
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Helper\Smarty;
/**
* Stub to support "module" as a resource in smarty files
*/
class SmartyResourceModule extends \Smarty_Resource_Custom
{
/**
* Fetch a template.
*
* @param string $name template name
* @param string $source template source
* @param int $mtime template modification timestamp (epoch)
*/
protected function fetch($name, &$source, &$mtime)
{
return;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* 2007-2018 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2018 PrestaShop SA
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Helper\Smarty;
/**
* Stub to support "parent" as a resource in smarty files
*/
class SmartyResourceParent extends \Smarty_Resource_Custom
{
/**
* Fetch a template.
*
* @param string $name template name
* @param string $source template source
* @param int $mtime template modification timestamp (epoch)
*/
protected function fetch($name, &$source, &$mtime)
{
return;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Manager;
use Symfony\Component\Translation\MessageCatalogue;
use PrestaShop\TranslationToolsBundle\Translation\Parser\CrowdinPhpParser;
class OriginalStringManager
{
/** @var string $defaultLocale */
private $defaultLocale = 'en-US';
/** @var MessageCatalogue $catalogue */
private $catalogue;
/** @var CrowdinPhpParser $parser */
private $parser;
public function __construct(CrowdinPhpParser $crodwinPhpParser)
{
$this->parser = $crodwinPhpParser;
$this->catalogue = new MessageCatalogue($this->defaultLocale);
}
/**
* @param string $filePath
* @param string $key
*
* @return string
*/
public function get($filePath, $key)
{
if (!$this->catalogue->has($key)) {
$this->extractFile($filePath);
}
return $this->catalogue->get($key);
}
/**
* @param string $filePath
*/
private function extractFile($filePath)
{
preg_match('/([a-z]{2}-[A-Z]{2})/', $filePath, $matches);
$locale = end($matches);
$originalFile = str_replace($locale, $this->defaultLocale, $filePath);
$generator = $this->parser->parseFileTokens($originalFile);
for (; $generator->valid(); $generator->next()) {
$translation = $generator->current();
$this->catalogue->set($translation['key'], $translation['message']);
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Manager;
use PrestaShop\TranslationToolsBundle\Translation\Parser\CrowdinPhpParser;
use PrestaShop\TranslationToolsBundle\Translation\MultilanguageCatalog;
use Symfony\Component\Finder\Finder;
class TranslationManager
{
/** @var MultilanguageCatalog $catalogue */
private $catalog;
/** @var CrowdinPhpParser $parser */
private $parser;
/**
* @param CrowdinPhpParser $crodwinPhpParser
*/
public function __construct(CrowdinPhpParser $crodwinPhpParser)
{
$this->parser = $crodwinPhpParser;
$this->catalog = new MultilanguageCatalog();
}
/**
* @param string $filePath
* @param string $key
*
* @return string
*/
public function get($filePath, $key)
{
if (!$this->catalog->has($key)) {
$this->extractFile($filePath);
}
return $this->catalog->has($key) ? $this->catalog->get($key) : null;
}
/**
* @param string $filePath
*/
private function extractFile($filePath)
{
$finder = new Finder();
$fullpath = preg_replace('/([a-z]{2}-[A-Z]{2})/', '*', $filePath);
$filename = basename($fullpath);
$directory = pathinfo(str_replace('*', '', $fullpath), PATHINFO_DIRNAME);
if (!file_exists($directory)) {
return false;
}
$files = $finder->files()->name($filename)->in($directory);
foreach ($files as $file) {
if (preg_match('/([a-z]{2}-[A-Z]{2})/', $file->getRealpath(), $matches)) {
$generator = $this->parser->parseFileTokens($file->getRealpath());
for (; $generator->valid(); $generator->next()) {
$translation = $generator->current();
if (!empty($translation['message'])) {
$this->catalog->set($translation['key'], $matches[1], $translation['message']);
}
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation;
class MultilanguageCatalog
{
/** @var [] $messages (key/locale) */
private $messages = [];
/**
* @param string|int $key
* @param string|int $locale
*
* @return bool
*/
public function has($key, $locale = null)
{
return !empty($this->messages[$key]) && (is_null($locale) || !empty($this->messages[$key][$locale]));
}
/**
* @param string|int $key
* @param string|int $locale
*
* @return mixed
*/
public function get($key, $locale = null)
{
if (is_null($locale)) {
return $this->messages[$key];
}
return $this->messages[$key][$locale];
}
/**
* @param string|int $key
* @param string|int $locale
* @param mixed $translation
*/
public function set($key, $locale, $translation)
{
if (!isset($this->messages[$key])) {
$this->messages[$key] = [];
}
$this->messages[$key][$locale] = $translation;
return $this;
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* 2007-2016 PrestaShop.
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* 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@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\TranslationToolsBundle\Translation\Parser;
class CrowdinPhpParser
{
/**
* Extracts trans message from PHP tokens.
*
* @param $file $tokens
* @param MessageCatalogue $catalog
*/
public function parseFileTokens($file)
{
preg_match_all('/^(\$_\w+\[\'.+\'\]) = \'(.*)\';/m', file_get_contents($file), $matches);
foreach ($matches[0] as $key => $match) {
yield [
'message' => $matches[2][$key],
'key' => $matches[1][$key],
];
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace PrestaShop\TranslationToolsBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use PrestaShop\TranslationToolsBundle\DependencyInjection\CompilerPass\ExtractorCompilerPass;
use PrestaShop\TranslationToolsBundle\DependencyInjection\CompilerPass\TranslationCompilerPass;
class TranslationToolsBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new ExtractorCompilerPass());
$container->addCompilerPass(new TranslationCompilerPass());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Twig\Extension;
use Symfony\Component\Translation\TranslatorInterface;
use \Twig_Extension_InitRuntimeInterface;
use \Twig_Environment;
class AppExtension extends \Twig_Extension implements Twig_Extension_InitRuntimeInterface
{
/**
* @var TranslatorInterface
*/
private $translation;
/**
* AppExtension constructor.
*
* @param TranslatorInterface $translation
*/
public function __construct(TranslatorInterface $translation)
{
$this->translation = $translation;
}
public function initRuntime(Twig_Environment $environment)
{
$environment->registerUndefinedFunctionCallback(function () {
return;
});
$environment->registerUndefinedFilterCallback(function() {
return;
});
}
/**
* We need to define and reset each twig function as the definition
* of theses function is stored in PrestaShop codebase.
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('renderhooksarray', array($this, 'transChoice')),
);
}
/**
* @param $string
*
* @return string
*/
public function transChoice($string)
{
return $this->translation->transChoice($string);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'app';
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Twig;
class Lexer extends \Twig_Lexer
{
/**
* @var array
*/
protected $comments = [];
/**
* @var bool
*/
protected $call = false;
protected function lexComment()
{
parent::lexComment();
if (true === $this->call) {
return;
}
preg_match_all('|\{#\s(.+)\s#\}|i', $this->code, $commentMatch);
if (is_array($commentMatch[1])) {
foreach ($commentMatch[1] as $comment) {
$this->comments[] = array(
'line' => $this->lineno,
'comment' => $comment,
'file' => $this->filename,
);
}
}
$this->call = true;
}
public function getComments()
{
return $this->comments;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace PrestaShop\TranslationToolsBundle\Twig\NodeVisitor;
use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor as BaseTranslationNodeVisitor;
use Symfony\Bridge\Twig\Node\TransNode;
class TranslationNodeVisitor extends BaseTranslationNodeVisitor
{
const UNDEFINED_DOMAIN = '_undefined';
private $enabled = true;
private $messages = array();
public function enable()
{
$this->enabled = true;
$this->messages = array();
}
public function getMessages()
{
return $this->messages;
}
/**
* {@inheritdoc}
*/
protected function doEnterNode(\Twig_Node $node, \Twig_Environment $env)
{
if (!$this->enabled) {
return $node;
}
if (
$node instanceof \Twig_Node_Expression_Filter &&
'trans' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof \Twig_Node_Expression_Constant
) {
// extract constant nodes with a trans filter
$this->messages[] = array(
$node->getNode('node')->getAttribute('value'),
$this->getReadDomainFromArguments($node->getNode('arguments'), 1),
'line' => $node->getTemplateLine(),
);
} elseif (
$node instanceof \Twig_Node_Expression_Filter &&
'transchoice' === $node->getNode('filter')->getAttribute('value') &&
$node->getNode('node') instanceof \Twig_Node_Expression_Constant
) {
// extract constant nodes with a trans filter
$this->messages[] = array(
$node->getNode('node')->getAttribute('value'),
$this->getReadDomainFromArguments($node->getNode('arguments'), 2),
'line' => $node->getTemplateLine(),
);
} elseif ($node instanceof TransNode) {
// extract trans nodes
$this->messages[] = array(
$node->getNode('body')->getAttribute('data'),
$this->getReadDomainFromNode($node->getNode('domain')),
'line' => $node->getTemplateLine(),
);
}
return $node;
}
/**
* @param \Twig_Node $arguments
* @param int $index
*
* @return string|null
*/
private function getReadDomainFromArguments(\Twig_Node $arguments, $index)
{
if ($arguments->hasNode('domain')) {
$argument = $arguments->getNode('domain');
} elseif ($arguments->hasNode($index)) {
$argument = $arguments->getNode($index);
} else {
return;
}
return $this->getReadDomainFromNode($argument);
}
/**
* @param \Twig_Node $node
*
* @return string|null
*/
private function getReadDomainFromNode(\Twig_Node $node = null)
{
if (null === $node) {
return;
}
if ($node instanceof \Twig_Node_Expression_Constant) {
return $node->getAttribute('value');
}
return self::UNDEFINED_DOMAIN;
}
}

3355
vendor/prestashop/translationtools-bundle/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff