diff --git a/src/Extension/DateExtension.php b/src/Extension/DateExtension.php new file mode 100644 index 0000000..187dbde --- /dev/null +++ b/src/Extension/DateExtension.php @@ -0,0 +1,83 @@ + 'year', + 'm' => 'month', + 'd' => 'day', + 'h' => 'hour', + 'i' => 'minute', + 's' => 'second', + ]; + + /** + * @return array|TwigFilter[] + */ + public function getFilters() + { + return [ + new TwigFilter('time_diff', [$this, 'diff'], ['needs_environment' => true]), + ]; + } + + /** + * Filters for converting dates to a time ago string like Facebook and Twitter has. + * + * @param Environment $env + * @param string|\DateTime $date a string or DateTime object to convert + * @param string|\DateTime $now A string or DateTime object to compare with. + * If none given, the current time will be used. + * + * @return string the converted time + */ + public function diff(Environment $env, $date, $now = null) + { + // Convert both dates to DateTime instances. + $date = twig_date_converter($env, $date); + $now = twig_date_converter($env, $now); + + // Get the difference between the two DateTime objects. + $diff = $date->diff($now); + + // Check for each interval if it appears in the $diff object. + foreach (self::$units as $attribute => $unit) { + $count = $diff->$attribute; + + if (0 !== $count) { + return $this->getPluralizedInterval($count, $diff->invert, $unit); + } + } + + return ''; + } + + /** + * @param $count + * @param $invert + * @param $unit + * @return string + */ + private function getPluralizedInterval($count, $invert, $unit) + { + + if (1 !== $count) { + $unit .= 's'; + } + + return $invert ? "in $count $unit" : "$count $unit ago"; + } +} diff --git a/src/Extension/Translation/TransNode.php b/src/Extension/Translation/TransNode.php new file mode 100644 index 0000000..d0aaa2f --- /dev/null +++ b/src/Extension/Translation/TransNode.php @@ -0,0 +1,186 @@ + $body]; + if (null !== $count) { + $nodes['count'] = $count; + } + if (null !== $plural) { + $nodes['plural'] = $plural; + } + if (null !== $notes) { + $nodes['notes'] = $notes; + } + + parent::__construct($nodes, [], $lineno, $tag); + } + + /** + * {@inheritdoc} + */ + public function compile(Compiler $compiler) + { + $compiler->addDebugInfo($this); + $msg1 = ''; + + [$msg, $vars] = $this->compileString($this->getNode('body')); + + if ($this->hasNode('plural')) { + [$msg1, $vars1] = $this->compileString($this->getNode('plural')); + + $vars = array_merge($vars, $vars1); + } + + $function = $this->getTransFunction($this->hasNode('plural')); + + if ($this->hasNode('notes')) { + $message = trim($this->getNode('notes')->getAttribute('data')); + + // line breaks are not allowed cause we want a single line comment + $message = str_replace(["\n", "\r"], ' ', $message); + $compiler->write("// notes: {$message}\n"); + } + + if ($vars) { + $compiler + ->write('echo strtr(' . $function . '(') + ->subcompile($msg) + ; + + if ($this->hasNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1) + ->raw(', abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw(')') + ; + } + + $compiler->raw('), array('); + + foreach ($vars as $var) { + if ('count' === $var->getAttribute('name')) { + $compiler + ->string('%count%') + ->raw(' => abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw('), ') + ; + } else { + $compiler + ->string('%' . $var->getAttribute('name') . '%') + ->raw(' => ') + ->subcompile($var) + ->raw(', ') + ; + } + } + + $compiler->raw("));\n"); + } else { + $compiler + ->write('echo ' . $function . '(') + ->subcompile($msg) + ; + + if ($this->hasNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1) + ->raw(', abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw(')') + ; + } + + $compiler->raw(");\n"); + } + } + + /** + * @param Node $body A Twig_Node instance + * + * @return array + */ + protected function compileString(Node $body) + { + if ( + $body instanceof NameExpression || + $body instanceof ConstantExpression || + $body instanceof TempNameExpression + ) { + return [$body, []]; + } + + $vars = []; + if (count($body)) { + $msg = ''; + + /** @var Node $node */ + foreach ($body as $node) { + if (get_class($node) === 'Node' && $node->getNode(0) instanceof TempNameExpression) { + $node = $node->getNode(1); + } + + if ($node instanceof PrintNode) { + $n = $node->getNode('expr'); + while ($n instanceof FilterExpression) { + $n = $n->getNode('node'); + } + $msg .= sprintf('%%%s%%', $n->getAttribute('name')); + $vars[] = new NameExpression($n->getAttribute('name'), $n->getTemplateLine()); + } else { + $msg .= $node->getAttribute('data'); + } + } + } else { + $msg = $body->getAttribute('data'); + } + + return [new Node([new ConstantExpression(trim($msg), $body->getTemplateLine())]), $vars]; + } + + /** + * @param bool $plural Return plural or singular function to use + * + * @return string + */ + protected function getTransFunction($plural) + { + return $plural ? 'ngettext' : 'gettext'; + } +} diff --git a/src/Extension/Translation/TransTokenParser.php b/src/Extension/Translation/TransTokenParser.php new file mode 100644 index 0000000..8a42db1 --- /dev/null +++ b/src/Extension/Translation/TransTokenParser.php @@ -0,0 +1,109 @@ +getLine(); + $stream = $this->parser->getStream(); + $count = null; + $plural = null; + $notes = null; + + if (!$stream->test(Token::BLOCK_END_TYPE)) { + $body = $this->parser->getExpressionParser()->parseExpression(); + } else { + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideForFork']); + $next = $stream->next()->getValue(); + + if ('plural' === $next) { + $count = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + $plural = $this->parser->subparse([$this, 'decideForFork']); + + if ('notes' === $stream->next()->getValue()) { + $stream->expect(Token::BLOCK_END_TYPE); + $notes = $this->parser->subparse([$this, 'decideForEnd'], true); + } + } elseif ('notes' === $next) { + $stream->expect(Token::BLOCK_END_TYPE); + $notes = $this->parser->subparse([$this, 'decideForEnd'], true); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $this->checkTransString($body, $lineno); + + return new TransNode($body, $plural, $count, $notes, $lineno, $this->getTag()); + } + + /** + * @param Token $token + * @return bool + */ + public function decideForFork(Token $token) + { + return $token->test(['plural', 'notes', 'endtrans']); + } + + /** + * @param Token $token + * @return bool + */ + public function decideForEnd(Token $token) + { + return $token->test('endtrans'); + } + + /** + * @return string + */ + public function getTag() + { + return 'trans'; + } + + /** + * @param Node $body + * @param $lineno + * @throws SyntaxError + */ + private function checkTransString(Node $body, $lineno) + { + foreach ($body as $i => $node) { + if ( + $node instanceof TextNode + || + ($node instanceof PrintNode && $node->getNode('expr') instanceof NameExpression) + ) { + continue; + } + + throw new SyntaxError( + sprintf('The text to be translated with "trans" can only contain references to simple variables'), + $lineno + ); + } + } +} diff --git a/src/Extension/TranslationExtension.php b/src/Extension/TranslationExtension.php new file mode 100644 index 0000000..6183727 --- /dev/null +++ b/src/Extension/TranslationExtension.php @@ -0,0 +1,32 @@ +