├── src ├── Node │ ├── I18nNode.php │ └── TransNode.php ├── I18nExtension.php └── TokenParser │ └── TransTokenParser.php ├── LICENSE ├── composer.json ├── CHANGELOG.md └── README.rst /src/Node/I18nNode.php: -------------------------------------------------------------------------------- 1 | $attributes An array of attributes (should not be nodes) 28 | * @param int $lineno The line number 29 | */ 30 | public function __construct(Node|null $node, array $attributes, int $lineno) 31 | { 32 | parent::__construct($node === null ? [] : [$node], $attributes, $lineno); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2019 Fabien Potencier 2 | Copyright (c) 2019-2021 phpMyAdmin contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/I18nExtension.php: -------------------------------------------------------------------------------- 1 | = 7.1 62 | * Setup and apply phpmyadmin/coding-standard 63 | * Apply changes for php 8.0 compatibility (https://github.com/twigphp/Twig/issues/3327) 64 | 65 | ## 2.0.0 - 2020-01-14 66 | 67 | * First release of this library. 68 | 69 | [5.0.1]: https://github.com/phpmyadmin/twig-i18n-extension/compare/5.0.0...5.0.1 70 | [5.0.0]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.3...5.0.0 71 | [4.1.5]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.4...4.1.5 72 | [4.1.4]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.3...4.1.4 73 | [4.1.3]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.2...4.1.3 74 | [4.1.2]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.1...4.1.2 75 | [4.1.1]: https://github.com/phpmyadmin/twig-i18n-extension/compare/4.1.0...4.1.1 76 | [4.1.0]: https://github.com/phpmyadmin/twig-i18n-extension/compare/v4.0.1...4.1.0 77 | [4.0.1]: https://github.com/phpmyadmin/twig-i18n-extension/compare/v4.0.0...v4.0.1 78 | [4.0.0]: https://github.com/phpmyadmin/twig-i18n-extension/compare/v3.0.0...v4.0.0 79 | [3.0.0]: https://github.com/phpmyadmin/twig-i18n-extension/compare/v2.0.0...v3.0.0 80 | -------------------------------------------------------------------------------- /src/TokenParser/TransTokenParser.php: -------------------------------------------------------------------------------- 1 | preParse($token); 47 | 48 | return new TransNode($body, $plural, $count, $context, $notes, $domain, $lineno, $tag); 49 | } 50 | 51 | /** @psalm-return array{Node, Node|null, AbstractExpression|null, Node|null, Node|null, Node|null, int, string} */ 52 | protected function preParse(Token $token): array 53 | { 54 | $lineno = $token->getLine(); 55 | $stream = $this->parser->getStream(); 56 | $domain = null; 57 | $count = null; 58 | $plural = null; 59 | $notes = null; 60 | $context = null; 61 | 62 | /* If we aren't closing the block, do we have a domain? */ 63 | if ($stream->test(Token::NAME_TYPE)) { 64 | $stream->expect(Token::NAME_TYPE, 'from'); 65 | $domain = method_exists($this->parser, 'parseExpression') 66 | ? $this->parser->parseExpression() 67 | : $this->parser->getExpressionParser()->parseExpression(); 68 | } 69 | 70 | if (! $stream->test(Token::BLOCK_END_TYPE)) { 71 | $body = method_exists($this->parser, 'parseExpression') 72 | ? $this->parser->parseExpression() 73 | : $this->parser->getExpressionParser()->parseExpression(); 74 | } else { 75 | $stream->expect(Token::BLOCK_END_TYPE); 76 | $body = $this->parser->subparse([$this, 'decideForFork']); 77 | $next = $stream->next()->getValue(); 78 | 79 | if ($next === 'plural') { 80 | $count = method_exists($this->parser, 'parseExpression') 81 | ? $this->parser->parseExpression() 82 | : $this->parser->getExpressionParser()->parseExpression(); 83 | $stream->expect(Token::BLOCK_END_TYPE); 84 | $plural = $this->parser->subparse([$this, 'decideForFork']); 85 | $next = $stream->next()->getValue(); 86 | if ($next === 'notes') { 87 | $stream->expect(Token::BLOCK_END_TYPE); 88 | $notes = $this->parser->subparse([$this, 'decideForEnd'], true); 89 | } elseif ($next === 'context') { 90 | $stream->expect(Token::BLOCK_END_TYPE); 91 | $context = $this->parser->subparse([$this, 'decideForEnd'], true); 92 | } 93 | } elseif ($next === 'context') { 94 | $stream->expect(Token::BLOCK_END_TYPE); 95 | $context = $this->parser->subparse([$this, 'decideForEnd'], true); 96 | } elseif ($next === 'notes') { 97 | $stream->expect(Token::BLOCK_END_TYPE); 98 | $notes = $this->parser->subparse([$this, 'decideForEnd'], true); 99 | } 100 | } 101 | 102 | $stream->expect(Token::BLOCK_END_TYPE); 103 | 104 | $this->checkTransString($body, $lineno); 105 | 106 | if ($notes instanceof TextNode) { 107 | // Don't use TextNode for $notes to avoid it getting merged with $body when optimizing. 108 | $notes = new I18nNode(null, ['data' => $notes->getAttribute('data')], $notes->getTemplateLine()); 109 | } 110 | 111 | if ($context instanceof TextNode) { 112 | // Don't use TextNode for $context to avoid it getting merged with $body when optimizing. 113 | $context = new I18nNode(null, ['data' => $context->getAttribute('data')], $context->getTemplateLine()); 114 | } 115 | 116 | return [$body, $plural, $count, $context, $notes, $domain, $lineno, $this->getTag()]; 117 | } 118 | 119 | public function decideForFork(Token $token): bool 120 | { 121 | return $token->test(['plural', 'context', 'notes', 'endtrans']); 122 | } 123 | 124 | public function decideForEnd(Token $token): bool 125 | { 126 | return $token->test('endtrans'); 127 | } 128 | 129 | /** 130 | * {@inheritdoc} 131 | */ 132 | public function getTag() 133 | { 134 | return 'trans'; 135 | } 136 | 137 | /** @throws SyntaxError */ 138 | protected function checkTransString(Node $body, int $lineno): void 139 | { 140 | foreach ($body as $i => $node) { 141 | if ( 142 | $node instanceof TextNode 143 | || 144 | ($node instanceof PrintNode && $node->getNode('expr') instanceof ContextVariable) 145 | ) { 146 | continue; 147 | } 148 | 149 | throw new SyntaxError( 150 | 'The text to be translated with "trans" can only contain references to simple variables.', 151 | $lineno, 152 | ); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Twig i18n Extension 2 | =================== 3 | 4 | The ``i18n`` extension adds `gettext`_ support to Twig. It defines one tag, 5 | ``trans``. 6 | 7 | Code status 8 | ----------- 9 | 10 | .. image:: https://github.com/phpmyadmin/twig-i18n-extension/actions/workflows/tests.yml/badge.svg?branch=master 11 | :alt: Tests 12 | :target: https://github.com/phpmyadmin/twig-i18n-extension/actions/workflows/tests.yml?query=branch%3Amaster 13 | 14 | .. image:: https://codecov.io/gh/phpmyadmin/twig-i18n-extension/branch/master/graph/badge.svg 15 | :alt: Code coverage 16 | :target: https://codecov.io/gh/phpmyadmin/twig-i18n-extension 17 | 18 | Installation 19 | ------------ 20 | 21 | This library can be installed via Composer running the following from the 22 | command line: 23 | 24 | .. code-block:: bash 25 | 26 | composer require phpmyadmin/twig-i18n-extension 27 | 28 | Configuration 29 | ------------- 30 | 31 | You need to register this extension before using the ``trans`` block 32 | 33 | .. code-block:: php 34 | 35 | use PhpMyAdmin\Twig\Extensions\I18nExtension; 36 | 37 | $twig->addExtension(new I18nExtension()); 38 | 39 | Note that you must configure the ``gettext`` extension before rendering any 40 | internationalized template. Here is a simple configuration example from the 41 | PHP `documentation`_ 42 | 43 | .. code-block:: php 44 | 45 | // Set language to French 46 | putenv('LC_ALL=fr_FR'); 47 | setlocale(LC_ALL, 'fr_FR'); 48 | 49 | // Specify the location of the translation tables 50 | bindtextdomain('myAppPhp', 'includes/locale'); 51 | bind_textdomain_codeset('myAppPhp', 'UTF-8'); 52 | 53 | // Choose domain 54 | textdomain('myAppPhp'); 55 | 56 | .. caution:: 57 | 58 | The ``i18n`` extension only works if the PHP `gettext`_ extension is 59 | enabled. 60 | 61 | Usage 62 | ----- 63 | 64 | Use the ``trans`` block to mark parts in the template as translatable: 65 | 66 | .. code-block:: twig 67 | 68 | {% trans "Hello World!" %} 69 | 70 | {% trans string_var %} 71 | 72 | {% trans %} 73 | Hello World! 74 | {% endtrans %} 75 | 76 | In a translatable string, you can embed variables: 77 | 78 | .. code-block:: twig 79 | 80 | {% trans %} 81 | Hello {{ name }}! 82 | {% endtrans %} 83 | 84 | During the gettext lookup these placeholders are converted. ``{{ name }}`` becomes ``%name%`` so the gettext ``msgid`` for this string would be ``Hello %name%!``. 85 | 86 | .. note:: 87 | 88 | ``{% trans "Hello {{ name }}!" %}`` is not a valid statement. 89 | 90 | If you need to apply filters to the variables, you first need to assign the 91 | result to a variable: 92 | 93 | .. code-block:: twig 94 | 95 | {% set name = name|capitalize %} 96 | 97 | {% trans %} 98 | Hello {{ name }}! 99 | {% endtrans %} 100 | 101 | To pluralize a translatable string, use the ``plural`` block: 102 | 103 | .. code-block:: twig 104 | 105 | {% trans %} 106 | Hey {{ name }}, I have one apple. 107 | {% plural apple_count %} 108 | Hey {{ name }}, I have {{ count }} apples. 109 | {% endtrans %} 110 | 111 | The ``plural`` tag should provide the ``count`` used to select the right 112 | string. Within the translatable string, the special ``count`` variable always 113 | contain the count value (here the value of ``apple_count``). 114 | 115 | To add notes for translators, use the ``notes`` block: 116 | 117 | .. code-block:: twig 118 | 119 | {% trans %} 120 | Hey {{ name }}, I have one apple. 121 | {% plural apple_count %} 122 | Hey {{ name }}, I have {{ count }} apples. 123 | {% notes %} 124 | This is shown in the user menu. This string should be shorter than 30 chars 125 | {% endtrans %} 126 | 127 | You can use ``notes`` with or without ``plural``. Once you get your templates compiled you should 128 | configure the ``gettext`` parser to get something like this: ``xgettext --add-comments=notes`` 129 | 130 | Within an expression or in a tag, you can use the ``trans`` filter to translate 131 | simple strings or variables: 132 | 133 | .. code-block:: twig 134 | 135 | {{ var|default(default_value|trans) }} 136 | 137 | Complex Translations within an Expression or Tag 138 | ------------------------------------------------ 139 | 140 | Translations can be done with both the ``trans`` tag and the ``trans`` filter. 141 | The filter is less powerful as it only works for simple variables or strings. 142 | For more complex scenario, like pluralization, you can use a two-step 143 | strategy: 144 | 145 | .. code-block:: twig 146 | 147 | {# assign the translation to a temporary variable #} 148 | {% set default_value %} 149 | {% trans %} 150 | Hey {{ name }}, I have one apple. 151 | {% plural apple_count %} 152 | Hey {{ name }}, I have {{ count }} apples. 153 | {% endtrans %} 154 | {% endset %} 155 | 156 | {# use the temporary variable within an expression #} 157 | {{ var|default(default_value|trans) }} 158 | 159 | Extracting Template Strings 160 | --------------------------- 161 | 162 | If you use the Twig I18n extension, you will probably need to extract the 163 | template strings at some point. 164 | 165 | Using Poedit 2 166 | ~~~~~~~~~~~~~~ 167 | 168 | Poedit 2 has native support for extracting from Twig files and no extra 169 | setup is necessary (Pro version). 170 | 171 | Using ``xgettext`` or Poedit 1 172 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 173 | 174 | Unfortunately, the ``xgettext`` utility does not understand Twig templates 175 | natively and neither do tools based on it such as free versions of Poedit. 176 | But there is a simple workaround: as Twig converts templates to 177 | PHP files, you can use ``xgettext`` on the template cache instead. 178 | 179 | Create a script that forces the generation of the cache for all your 180 | templates. Here is a simple example to get you started 181 | 182 | .. code-block:: php 183 | 184 | use Twig\Environment; 185 | use Twig\Loader\FilesystemLoader; 186 | use PhpMyAdmin\Twig\Extensions\I18nExtension; 187 | 188 | $tplDir = __DIR__ . '/templates'; 189 | $tmpDir = '/tmp/cache/'; 190 | $loader = new FilesystemLoader($tplDir); 191 | 192 | // force auto-reload to always have the latest version of the template 193 | $twig = new Environment($loader, [ 194 | 'auto_reload' => true, 195 | 'cache' => $tmpDir, 196 | ]); 197 | $twig->addExtension(new I18nExtension()); 198 | // configure Twig the way you want 199 | 200 | // iterate over all your templates 201 | foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tplDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file) 202 | { 203 | // force compilation 204 | if ($file->isFile()) { 205 | $twig->load(str_replace($tplDir . '/', '', $file->getRealPath())); 206 | } 207 | } 208 | 209 | Use the standard ``xgettext`` utility as you would have done with plain PHP 210 | code: 211 | 212 | .. code-block:: text 213 | 214 | xgettext --default-domain=messages -p ./locale --from-code=UTF-8 -n --omit-header -L PHP /tmp/cache/*.php 215 | 216 | Another workaround is to use `Twig Gettext Extractor`_ and extract the template 217 | strings right from `Poedit`_. 218 | 219 | .. _`gettext`: https://www.php.net/gettext 220 | .. _`documentation`: https://www.php.net/manual/en/function.gettext.php 221 | .. _`Twig Gettext Extractor`: https://github.com/umpirsky/Twig-Gettext-Extractor#readme 222 | .. _`Poedit`: https://poedit.net/ 223 | 224 | History 225 | ------- 226 | 227 | This project was forked in 2019 by the phpMyAdmin team, since it was abandoned by the 228 | `Twig project`_ but was still in use for phpMyAdmin. 229 | 230 | .. _`Twig project`: https://github.com/twigphp/Twig-extensions 231 | 232 | If you find this work useful, or have a pull request to contribute, please find us on 233 | `Github`_. 234 | 235 | .. _`Github`: https://github.com/phpmyadmin/twig-i18n-extension/ 236 | -------------------------------------------------------------------------------- /src/Node/TransNode.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | #[YieldReady] 41 | class TransNode extends Node 42 | { 43 | /** 44 | * The label for gettext notes to be exported 45 | */ 46 | public static string $notesLabel = '// notes: '; 47 | 48 | /** 49 | * Enable MoTranslator functions 50 | */ 51 | public static bool $enableMoTranslator = false; 52 | 53 | /** 54 | * Enable calls to addDebugInfo 55 | */ 56 | public static bool $enableAddDebugInfo = false; 57 | 58 | /** 59 | * Enables context functions usage 60 | */ 61 | public static bool $hasContextFunctions = false; 62 | 63 | /** @phpstan-ignore constructor.unusedParameter */ 64 | public function __construct( 65 | Node $body, 66 | Node|null $plural, 67 | AbstractExpression|null $count, 68 | Node|null $context = null, 69 | Node|null $notes = null, 70 | Node|null $domain = null, 71 | int $lineno = 0, 72 | string|null $tag = null, 73 | ) { 74 | $nodes = ['body' => $body]; 75 | if ($count !== null) { 76 | $nodes['count'] = $count; 77 | } 78 | 79 | if ($plural !== null) { 80 | $nodes['plural'] = $plural; 81 | } 82 | 83 | if ($notes !== null) { 84 | $nodes['notes'] = $notes; 85 | } 86 | 87 | if ($domain !== null) { 88 | $nodes['domain'] = $domain; 89 | } 90 | 91 | if ($context !== null) { 92 | $nodes['context'] = $context; 93 | } 94 | 95 | parent::__construct($nodes, [], $lineno); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function compile(Compiler $compiler) 102 | { 103 | if (self::$enableAddDebugInfo) { 104 | $compiler->addDebugInfo($this); 105 | } 106 | 107 | [$msg, $vars] = $this->compileString($this->getNode('body')); 108 | 109 | $hasPlural = $this->hasNode('plural'); 110 | 111 | if ($hasPlural) { 112 | [$msg1, $vars1] = $this->compileString($this->getNode('plural')); 113 | 114 | $vars = array_merge($vars, $vars1); 115 | } 116 | 117 | $hasDomain = $this->hasNode('domain'); 118 | $hasContext = $this->hasNode('context'); 119 | 120 | $function = $this->getTransFunction($hasPlural, $hasContext, $hasDomain); 121 | 122 | if ($this->hasNode('notes')) { 123 | $message = trim($this->getNode('notes')->getAttribute('data')); 124 | 125 | // line breaks are not allowed because we want a single line comment 126 | $message = str_replace(["\n", "\r"], ' ', $message); 127 | $compiler->raw(static::$notesLabel . $message . "\n"); 128 | } 129 | 130 | if ($vars) { 131 | $compiler->raw('yield strtr(' . $function . '('); 132 | 133 | if ($hasDomain) { 134 | [$domain] = $this->compileString($this->getNode('domain')); 135 | $compiler 136 | ->subcompile($domain) 137 | ->raw(', '); 138 | } 139 | 140 | if ($hasContext && (static::$hasContextFunctions || static::$enableMoTranslator)) { 141 | [$context] = $this->compileString($this->getNode('context')); 142 | $compiler 143 | ->subcompile($context) 144 | ->raw(', '); 145 | } 146 | 147 | $compiler 148 | ->subcompile($msg); 149 | 150 | if ($hasPlural) { 151 | $compiler 152 | ->raw(', ') 153 | ->subcompile($msg1) 154 | ->raw(', abs(') 155 | ->subcompile($this->getNode('count')) 156 | ->raw(')'); 157 | } 158 | 159 | $compiler->raw('), array('); 160 | 161 | foreach ($vars as $var) { 162 | $attributeName = $var->getAttribute('name'); 163 | if ($attributeName === 'count') { 164 | $compiler 165 | ->string('%count%') 166 | ->raw(' => abs(') 167 | ->subcompile($this->getNode('count')) 168 | ->raw('), '); 169 | } else { 170 | $compiler 171 | ->string('%' . $attributeName . '%') 172 | ->raw(' => ') 173 | ->subcompile($var) 174 | ->raw(', '); 175 | } 176 | } 177 | 178 | $compiler->raw("));\n"); 179 | } else { 180 | $compiler->raw('yield ' . $function . '('); 181 | 182 | if ($hasDomain) { 183 | [$domain] = $this->compileString($this->getNode('domain')); 184 | $compiler 185 | ->subcompile($domain) 186 | ->raw(', '); 187 | } 188 | 189 | if ($hasContext) { 190 | if (static::$hasContextFunctions || static::$enableMoTranslator) { 191 | [$context] = $this->compileString($this->getNode('context')); 192 | $compiler 193 | ->subcompile($context) 194 | ->raw(', '); 195 | } 196 | } 197 | 198 | $compiler 199 | ->subcompile($msg); 200 | 201 | if ($hasPlural) { 202 | $compiler 203 | ->raw(', ') 204 | ->subcompile($msg1) 205 | ->raw(', abs(') 206 | ->subcompile($this->getNode('count')) 207 | ->raw(')'); 208 | } 209 | 210 | $compiler->raw(");\n"); 211 | } 212 | } 213 | 214 | /** 215 | * Keep this method protected instead of private some implementations may use it 216 | * 217 | * @psalm-return array{Node, list} 218 | */ 219 | protected function compileString(Node $body): array 220 | { 221 | if ($body instanceof ContextVariable || $body instanceof ConstantExpression || $body instanceof LocalVariable) { 222 | return [$body, []]; 223 | } 224 | 225 | $vars = []; 226 | if (count($body)) { 227 | $msg = ''; 228 | 229 | foreach ($body as $node) { 230 | if ($node instanceof PrintNode) { 231 | $n = $node->getNode('expr'); 232 | while ($n instanceof FilterExpression) { 233 | $n = $n->getNode('node'); 234 | } 235 | 236 | while ($n instanceof CheckToStringNode) { 237 | $n = $n->getNode('expr'); 238 | } 239 | 240 | $attributeName = $n->getAttribute('name'); 241 | $msg .= sprintf('%%%s%%', $attributeName); 242 | $vars[] = new ContextVariable($attributeName, $n->getTemplateLine()); 243 | } else { 244 | /** @phpstan-var TextNode $node */ 245 | $msg .= $node->getAttribute('data'); 246 | } 247 | } 248 | } else { 249 | $msg = $body->getAttribute('data'); 250 | } 251 | 252 | return [new I18nNode(new ConstantExpression(trim($msg), $body->getTemplateLine()), [], 0), $vars]; 253 | } 254 | 255 | /** 256 | * Keep this protected to allow people to override it with their own logic 257 | */ 258 | protected function getTransFunction(bool $hasPlural, bool $hasContext, bool $hasDomain): string 259 | { 260 | $functionPrefix = ''; 261 | 262 | if (static::$enableMoTranslator) { 263 | // The functions are prefixed with an underscore 264 | $functionPrefix = '_'; 265 | } 266 | 267 | // If it has not context function support or not MoTranslator 268 | if (! static::$hasContextFunctions && ! static::$enableMoTranslator) { 269 | // Not found on native PHP: dnpgettext, npgettext, dpgettext, pgettext 270 | // No domain plural context support 271 | // No domain context support 272 | // No context support 273 | // No plural context support 274 | 275 | if ($hasDomain) { 276 | // dngettext($domain, $msgid, $msgidPlural, $number); 277 | // dgettext($domain, $msgid); 278 | return $functionPrefix . ($hasPlural ? 'dngettext' : 'dgettext'); 279 | } 280 | 281 | // ngettext($msgid, $msgidPlural, $number); 282 | // gettext($msgid); 283 | return $functionPrefix . ($hasPlural ? 'ngettext' : 'gettext'); 284 | } 285 | 286 | if ($hasDomain) { 287 | if ($hasPlural) { 288 | // dnpgettext($domain, $msgctxt, $msgid, $msgidPlural, $number); 289 | // dngettext($domain, $msgid, $msgidPlural, $number); 290 | return $functionPrefix . ($hasContext ? 'dnpgettext' : 'dngettext'); 291 | } 292 | 293 | // dpgettext($domain, $msgctxt, $msgid); 294 | // dgettext($domain, $msgid); 295 | return $functionPrefix . ($hasContext ? 'dpgettext' : 'dgettext'); 296 | } 297 | 298 | if ($hasPlural) { 299 | // npgettext($msgctxt, $msgid, $msgidPlural, $number); 300 | // ngettext($msgid, $msgidPlural, $number); 301 | return $functionPrefix . ($hasContext ? 'npgettext' : 'ngettext'); 302 | } 303 | 304 | // pgettext($msgctxt, $msgid); 305 | // gettext($msgid); 306 | return $functionPrefix . ($hasContext ? 'pgettext' : 'gettext'); 307 | } 308 | } 309 | --------------------------------------------------------------------------------