@@ -11,160 +11,160 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | class PhpCode extends Extractor implements ExtractorInterface, ExtractorMultiInterface |
| 13 | 13 | { |
| 14 | - public static $options = [ |
|
| 15 | - // - false: to not extract comments |
|
| 16 | - // - empty string: to extract all comments |
|
| 17 | - // - non-empty string: to extract comments that start with that string |
|
| 18 | - // - array with strings to extract comments format. |
|
| 19 | - 'extractComments' => false, |
|
| 20 | - |
|
| 21 | - 'constants' => [], |
|
| 22 | - |
|
| 23 | - 'functions' => [ |
|
| 24 | - 'gettext' => 'gettext', |
|
| 25 | - '__' => 'gettext', |
|
| 26 | - 'ngettext' => 'ngettext', |
|
| 27 | - 'n__' => 'ngettext', |
|
| 28 | - 'pgettext' => 'pgettext', |
|
| 29 | - 'p__' => 'pgettext', |
|
| 30 | - 'dgettext' => 'dgettext', |
|
| 31 | - 'd__' => 'dgettext', |
|
| 32 | - 'dngettext' => 'dngettext', |
|
| 33 | - 'dn__' => 'dngettext', |
|
| 34 | - 'dpgettext' => 'dpgettext', |
|
| 35 | - 'dp__' => 'dpgettext', |
|
| 36 | - 'npgettext' => 'npgettext', |
|
| 37 | - 'np__' => 'npgettext', |
|
| 38 | - 'dnpgettext' => 'dnpgettext', |
|
| 39 | - 'dnp__' => 'dnpgettext', |
|
| 40 | - 'noop' => 'noop', |
|
| 41 | - 'noop__' => 'noop', |
|
| 42 | - ], |
|
| 43 | - ]; |
|
| 44 | - |
|
| 45 | - protected static $functionsScannerClass = 'Gettext\Utils\PhpFunctionsScanner'; |
|
| 46 | - |
|
| 47 | - /** |
|
| 48 | - * {@inheritdoc} |
|
| 49 | - * @throws Exception |
|
| 50 | - */ |
|
| 51 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 52 | - { |
|
| 53 | - static::fromStringMultiple($string, [$translations], $options); |
|
| 54 | - } |
|
| 55 | - |
|
| 56 | - /** |
|
| 57 | - * @inheritDoc |
|
| 58 | - * @throws Exception |
|
| 59 | - */ |
|
| 60 | - public static function fromStringMultiple($string, array $translations, array $options = []) |
|
| 61 | - { |
|
| 62 | - $options += static::$options; |
|
| 63 | - |
|
| 64 | - /** @var FunctionsScanner $functions */ |
|
| 65 | - $functions = new static::$functionsScannerClass($string); |
|
| 66 | - |
|
| 67 | - if ($options['extractComments'] !== false) { |
|
| 68 | - $functions->enableCommentsExtraction($options['extractComments']); |
|
| 69 | - } |
|
| 70 | - |
|
| 71 | - $functions->saveGettextFunctions($translations, $options); |
|
| 72 | - } |
|
| 73 | - |
|
| 74 | - /** |
|
| 75 | - * @inheritDoc |
|
| 76 | - */ |
|
| 77 | - public static function fromFileMultiple($file, array $translations, array $options = []) |
|
| 78 | - { |
|
| 79 | - foreach (static::getFiles($file) as $file) { |
|
| 80 | - $options['file'] = $file; |
|
| 81 | - static::fromStringMultiple(static::readFile($file), $translations, $options); |
|
| 82 | - } |
|
| 83 | - } |
|
| 84 | - |
|
| 85 | - |
|
| 86 | - /** |
|
| 87 | - * Decodes a T_CONSTANT_ENCAPSED_STRING string. |
|
| 88 | - * |
|
| 89 | - * @param string $value |
|
| 90 | - * |
|
| 91 | - * @return string |
|
| 92 | - */ |
|
| 93 | - public static function convertString($value) |
|
| 94 | - { |
|
| 95 | - if (strpos($value, '\\') === false) { |
|
| 96 | - return substr($value, 1, -1); |
|
| 97 | - } |
|
| 98 | - |
|
| 99 | - if ($value[0] === "'") { |
|
| 100 | - return strtr(substr($value, 1, -1), ['\\\\' => '\\', '\\\'' => '\'']); |
|
| 101 | - } |
|
| 102 | - |
|
| 103 | - $value = substr($value, 1, -1); |
|
| 104 | - |
|
| 105 | - return preg_replace_callback( |
|
| 106 | - '/\\\(n|r|t|v|e|f|\$|"|\\\|x[0-9A-Fa-f]{1,2}|u{[0-9a-f]{1,6}}|[0-7]{1,3})/', |
|
| 107 | - function ($match) { |
|
| 108 | - switch ($match[1][0]) { |
|
| 109 | - case 'n': |
|
| 110 | - return "\n"; |
|
| 111 | - case 'r': |
|
| 112 | - return "\r"; |
|
| 113 | - case 't': |
|
| 114 | - return "\t"; |
|
| 115 | - case 'v': |
|
| 116 | - return "\v"; |
|
| 117 | - case 'e': |
|
| 118 | - return "\e"; |
|
| 119 | - case 'f': |
|
| 120 | - return "\f"; |
|
| 121 | - case '$': |
|
| 122 | - return '$'; |
|
| 123 | - case '"': |
|
| 124 | - return '"'; |
|
| 125 | - case '\\': |
|
| 126 | - return '\\'; |
|
| 127 | - case 'x': |
|
| 128 | - return chr(hexdec(substr($match[1], 1))); |
|
| 129 | - case 'u': |
|
| 130 | - return static::unicodeChar(hexdec(substr($match[1], 1))); |
|
| 131 | - default: |
|
| 132 | - return chr(octdec($match[1])); |
|
| 133 | - } |
|
| 134 | - }, |
|
| 135 | - $value |
|
| 136 | - ); |
|
| 137 | - } |
|
| 138 | - |
|
| 139 | - /** |
|
| 140 | - * @param $dec |
|
| 141 | - * @return string|null |
|
| 142 | - * @see http://php.net/manual/en/function.chr.php#118804 |
|
| 143 | - */ |
|
| 144 | - protected static function unicodeChar($dec) |
|
| 145 | - { |
|
| 146 | - if ($dec < 0x80) { |
|
| 147 | - return chr($dec); |
|
| 148 | - } |
|
| 149 | - |
|
| 150 | - if ($dec < 0x0800) { |
|
| 151 | - return chr(0xC0 + ($dec >> 6)) |
|
| 152 | - . chr(0x80 + ($dec & 0x3f)); |
|
| 153 | - } |
|
| 154 | - |
|
| 155 | - if ($dec < 0x010000) { |
|
| 156 | - return chr(0xE0 + ($dec >> 12)) |
|
| 157 | - . chr(0x80 + (($dec >> 6) & 0x3f)) |
|
| 158 | - . chr(0x80 + ($dec & 0x3f)); |
|
| 159 | - } |
|
| 160 | - |
|
| 161 | - if ($dec < 0x200000) { |
|
| 162 | - return chr(0xF0 + ($dec >> 18)) |
|
| 163 | - . chr(0x80 + (($dec >> 12) & 0x3f)) |
|
| 164 | - . chr(0x80 + (($dec >> 6) & 0x3f)) |
|
| 165 | - . chr(0x80 + ($dec & 0x3f)); |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - return null; |
|
| 169 | - } |
|
| 14 | + public static $options = [ |
|
| 15 | + // - false: to not extract comments |
|
| 16 | + // - empty string: to extract all comments |
|
| 17 | + // - non-empty string: to extract comments that start with that string |
|
| 18 | + // - array with strings to extract comments format. |
|
| 19 | + 'extractComments' => false, |
|
| 20 | + |
|
| 21 | + 'constants' => [], |
|
| 22 | + |
|
| 23 | + 'functions' => [ |
|
| 24 | + 'gettext' => 'gettext', |
|
| 25 | + '__' => 'gettext', |
|
| 26 | + 'ngettext' => 'ngettext', |
|
| 27 | + 'n__' => 'ngettext', |
|
| 28 | + 'pgettext' => 'pgettext', |
|
| 29 | + 'p__' => 'pgettext', |
|
| 30 | + 'dgettext' => 'dgettext', |
|
| 31 | + 'd__' => 'dgettext', |
|
| 32 | + 'dngettext' => 'dngettext', |
|
| 33 | + 'dn__' => 'dngettext', |
|
| 34 | + 'dpgettext' => 'dpgettext', |
|
| 35 | + 'dp__' => 'dpgettext', |
|
| 36 | + 'npgettext' => 'npgettext', |
|
| 37 | + 'np__' => 'npgettext', |
|
| 38 | + 'dnpgettext' => 'dnpgettext', |
|
| 39 | + 'dnp__' => 'dnpgettext', |
|
| 40 | + 'noop' => 'noop', |
|
| 41 | + 'noop__' => 'noop', |
|
| 42 | + ], |
|
| 43 | + ]; |
|
| 44 | + |
|
| 45 | + protected static $functionsScannerClass = 'Gettext\Utils\PhpFunctionsScanner'; |
|
| 46 | + |
|
| 47 | + /** |
|
| 48 | + * {@inheritdoc} |
|
| 49 | + * @throws Exception |
|
| 50 | + */ |
|
| 51 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 52 | + { |
|
| 53 | + static::fromStringMultiple($string, [$translations], $options); |
|
| 54 | + } |
|
| 55 | + |
|
| 56 | + /** |
|
| 57 | + * @inheritDoc |
|
| 58 | + * @throws Exception |
|
| 59 | + */ |
|
| 60 | + public static function fromStringMultiple($string, array $translations, array $options = []) |
|
| 61 | + { |
|
| 62 | + $options += static::$options; |
|
| 63 | + |
|
| 64 | + /** @var FunctionsScanner $functions */ |
|
| 65 | + $functions = new static::$functionsScannerClass($string); |
|
| 66 | + |
|
| 67 | + if ($options['extractComments'] !== false) { |
|
| 68 | + $functions->enableCommentsExtraction($options['extractComments']); |
|
| 69 | + } |
|
| 70 | + |
|
| 71 | + $functions->saveGettextFunctions($translations, $options); |
|
| 72 | + } |
|
| 73 | + |
|
| 74 | + /** |
|
| 75 | + * @inheritDoc |
|
| 76 | + */ |
|
| 77 | + public static function fromFileMultiple($file, array $translations, array $options = []) |
|
| 78 | + { |
|
| 79 | + foreach (static::getFiles($file) as $file) { |
|
| 80 | + $options['file'] = $file; |
|
| 81 | + static::fromStringMultiple(static::readFile($file), $translations, $options); |
|
| 82 | + } |
|
| 83 | + } |
|
| 84 | + |
|
| 85 | + |
|
| 86 | + /** |
|
| 87 | + * Decodes a T_CONSTANT_ENCAPSED_STRING string. |
|
| 88 | + * |
|
| 89 | + * @param string $value |
|
| 90 | + * |
|
| 91 | + * @return string |
|
| 92 | + */ |
|
| 93 | + public static function convertString($value) |
|
| 94 | + { |
|
| 95 | + if (strpos($value, '\\') === false) { |
|
| 96 | + return substr($value, 1, -1); |
|
| 97 | + } |
|
| 98 | + |
|
| 99 | + if ($value[0] === "'") { |
|
| 100 | + return strtr(substr($value, 1, -1), ['\\\\' => '\\', '\\\'' => '\'']); |
|
| 101 | + } |
|
| 102 | + |
|
| 103 | + $value = substr($value, 1, -1); |
|
| 104 | + |
|
| 105 | + return preg_replace_callback( |
|
| 106 | + '/\\\(n|r|t|v|e|f|\$|"|\\\|x[0-9A-Fa-f]{1,2}|u{[0-9a-f]{1,6}}|[0-7]{1,3})/', |
|
| 107 | + function ($match) { |
|
| 108 | + switch ($match[1][0]) { |
|
| 109 | + case 'n': |
|
| 110 | + return "\n"; |
|
| 111 | + case 'r': |
|
| 112 | + return "\r"; |
|
| 113 | + case 't': |
|
| 114 | + return "\t"; |
|
| 115 | + case 'v': |
|
| 116 | + return "\v"; |
|
| 117 | + case 'e': |
|
| 118 | + return "\e"; |
|
| 119 | + case 'f': |
|
| 120 | + return "\f"; |
|
| 121 | + case '$': |
|
| 122 | + return '$'; |
|
| 123 | + case '"': |
|
| 124 | + return '"'; |
|
| 125 | + case '\\': |
|
| 126 | + return '\\'; |
|
| 127 | + case 'x': |
|
| 128 | + return chr(hexdec(substr($match[1], 1))); |
|
| 129 | + case 'u': |
|
| 130 | + return static::unicodeChar(hexdec(substr($match[1], 1))); |
|
| 131 | + default: |
|
| 132 | + return chr(octdec($match[1])); |
|
| 133 | + } |
|
| 134 | + }, |
|
| 135 | + $value |
|
| 136 | + ); |
|
| 137 | + } |
|
| 138 | + |
|
| 139 | + /** |
|
| 140 | + * @param $dec |
|
| 141 | + * @return string|null |
|
| 142 | + * @see http://php.net/manual/en/function.chr.php#118804 |
|
| 143 | + */ |
|
| 144 | + protected static function unicodeChar($dec) |
|
| 145 | + { |
|
| 146 | + if ($dec < 0x80) { |
|
| 147 | + return chr($dec); |
|
| 148 | + } |
|
| 149 | + |
|
| 150 | + if ($dec < 0x0800) { |
|
| 151 | + return chr(0xC0 + ($dec >> 6)) |
|
| 152 | + . chr(0x80 + ($dec & 0x3f)); |
|
| 153 | + } |
|
| 154 | + |
|
| 155 | + if ($dec < 0x010000) { |
|
| 156 | + return chr(0xE0 + ($dec >> 12)) |
|
| 157 | + . chr(0x80 + (($dec >> 6) & 0x3f)) |
|
| 158 | + . chr(0x80 + ($dec & 0x3f)); |
|
| 159 | + } |
|
| 160 | + |
|
| 161 | + if ($dec < 0x200000) { |
|
| 162 | + return chr(0xF0 + ($dec >> 18)) |
|
| 163 | + . chr(0x80 + (($dec >> 12) & 0x3f)) |
|
| 164 | + . chr(0x80 + (($dec >> 6) & 0x3f)) |
|
| 165 | + . chr(0x80 + ($dec & 0x3f)); |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + return null; |
|
| 169 | + } |
|
| 170 | 170 | } |
@@ -10,17 +10,17 @@ |
||
| 10 | 10 | */ |
| 11 | 11 | class JsonDictionary extends Extractor implements ExtractorInterface |
| 12 | 12 | { |
| 13 | - use DictionaryTrait; |
|
| 13 | + use DictionaryTrait; |
|
| 14 | 14 | |
| 15 | - /** |
|
| 16 | - * {@inheritdoc} |
|
| 17 | - */ |
|
| 18 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 19 | - { |
|
| 20 | - $messages = json_decode($string, true); |
|
| 15 | + /** |
|
| 16 | + * {@inheritdoc} |
|
| 17 | + */ |
|
| 18 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 19 | + { |
|
| 20 | + $messages = json_decode($string, true); |
|
| 21 | 21 | |
| 22 | - if (is_array($messages)) { |
|
| 23 | - static::fromArray($messages, $translations); |
|
| 24 | - } |
|
| 25 | - } |
|
| 22 | + if (is_array($messages)) { |
|
| 23 | + static::fromArray($messages, $translations); |
|
| 24 | + } |
|
| 25 | + } |
|
| 26 | 26 | } |
@@ -11,121 +11,121 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | class Mo extends Extractor implements ExtractorInterface |
| 13 | 13 | { |
| 14 | - const MAGIC1 = -1794895138; |
|
| 15 | - const MAGIC2 = -569244523; |
|
| 16 | - const MAGIC3 = 2500072158; |
|
| 17 | - |
|
| 18 | - protected static $stringReaderClass = 'Gettext\Utils\StringReader'; |
|
| 19 | - |
|
| 20 | - /** |
|
| 21 | - * {@inheritdoc} |
|
| 22 | - */ |
|
| 23 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 24 | - { |
|
| 25 | - /** @var StringReader $stream */ |
|
| 26 | - $stream = new static::$stringReaderClass($string); |
|
| 27 | - $magic = static::readInt($stream, 'V'); |
|
| 28 | - |
|
| 29 | - if (($magic === static::MAGIC1) || ($magic === static::MAGIC3)) { //to make sure it works for 64-bit platforms |
|
| 30 | - $byteOrder = 'V'; //low endian |
|
| 31 | - } elseif ($magic === (static::MAGIC2 & 0xFFFFFFFF)) { |
|
| 32 | - $byteOrder = 'N'; //big endian |
|
| 33 | - } else { |
|
| 34 | - throw new Exception('Not MO file'); |
|
| 35 | - } |
|
| 36 | - |
|
| 37 | - static::readInt($stream, $byteOrder); |
|
| 38 | - |
|
| 39 | - $total = static::readInt($stream, $byteOrder); //total string count |
|
| 40 | - $originals = static::readInt($stream, $byteOrder); //offset of original table |
|
| 41 | - $tran = static::readInt($stream, $byteOrder); //offset of translation table |
|
| 42 | - |
|
| 43 | - $stream->seekto($originals); |
|
| 44 | - $table_originals = static::readIntArray($stream, $byteOrder, $total * 2); |
|
| 45 | - |
|
| 46 | - $stream->seekto($tran); |
|
| 47 | - $table_translations = static::readIntArray($stream, $byteOrder, $total * 2); |
|
| 48 | - |
|
| 49 | - for ($i = 0; $i < $total; ++$i) { |
|
| 50 | - $next = $i * 2; |
|
| 51 | - |
|
| 52 | - $stream->seekto($table_originals[$next + 2]); |
|
| 53 | - $original = $stream->read($table_originals[$next + 1]); |
|
| 54 | - |
|
| 55 | - $stream->seekto($table_translations[$next + 2]); |
|
| 56 | - $translated = $stream->read($table_translations[$next + 1]); |
|
| 57 | - |
|
| 58 | - if ($original === '') { |
|
| 59 | - // Headers |
|
| 60 | - foreach (explode("\n", $translated) as $headerLine) { |
|
| 61 | - if ($headerLine === '') { |
|
| 62 | - continue; |
|
| 63 | - } |
|
| 64 | - |
|
| 65 | - $headerChunks = preg_split('/:\s*/', $headerLine, 2); |
|
| 66 | - $translations->setHeader($headerChunks[0], isset($headerChunks[1]) ? $headerChunks[1] : ''); |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - continue; |
|
| 70 | - } |
|
| 71 | - |
|
| 72 | - $chunks = explode("\x04", $original, 2); |
|
| 73 | - |
|
| 74 | - if (isset($chunks[1])) { |
|
| 75 | - $context = $chunks[0]; |
|
| 76 | - $original = $chunks[1]; |
|
| 77 | - } else { |
|
| 78 | - $context = ''; |
|
| 79 | - } |
|
| 80 | - |
|
| 81 | - $chunks = explode("\x00", $original, 2); |
|
| 82 | - |
|
| 83 | - if (isset($chunks[1])) { |
|
| 84 | - $original = $chunks[0]; |
|
| 85 | - $plural = $chunks[1]; |
|
| 86 | - } else { |
|
| 87 | - $plural = ''; |
|
| 88 | - } |
|
| 89 | - |
|
| 90 | - $translation = $translations->insert($context, $original, $plural); |
|
| 91 | - |
|
| 92 | - if ($translated === '') { |
|
| 93 | - continue; |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - if ($plural === '') { |
|
| 97 | - $translation->setTranslation($translated); |
|
| 98 | - continue; |
|
| 99 | - } |
|
| 100 | - |
|
| 101 | - $v = explode("\x00", $translated); |
|
| 102 | - $translation->setTranslation(array_shift($v)); |
|
| 103 | - $translation->setPluralTranslations($v); |
|
| 104 | - } |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - /** |
|
| 108 | - * @param StringReader $stream |
|
| 109 | - * @param string $byteOrder |
|
| 110 | - */ |
|
| 111 | - protected static function readInt(StringReader $stream, $byteOrder) |
|
| 112 | - { |
|
| 113 | - if (($read = $stream->read(4)) === false) { |
|
| 114 | - return false; |
|
| 115 | - } |
|
| 116 | - |
|
| 117 | - $read = unpack($byteOrder, $read); |
|
| 118 | - |
|
| 119 | - return array_shift($read); |
|
| 120 | - } |
|
| 121 | - |
|
| 122 | - /** |
|
| 123 | - * @param StringReader $stream |
|
| 124 | - * @param string $byteOrder |
|
| 125 | - * @param int $count |
|
| 126 | - */ |
|
| 127 | - protected static function readIntArray(StringReader $stream, $byteOrder, $count) |
|
| 128 | - { |
|
| 129 | - return unpack($byteOrder.$count, $stream->read(4 * $count)); |
|
| 130 | - } |
|
| 14 | + const MAGIC1 = -1794895138; |
|
| 15 | + const MAGIC2 = -569244523; |
|
| 16 | + const MAGIC3 = 2500072158; |
|
| 17 | + |
|
| 18 | + protected static $stringReaderClass = 'Gettext\Utils\StringReader'; |
|
| 19 | + |
|
| 20 | + /** |
|
| 21 | + * {@inheritdoc} |
|
| 22 | + */ |
|
| 23 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 24 | + { |
|
| 25 | + /** @var StringReader $stream */ |
|
| 26 | + $stream = new static::$stringReaderClass($string); |
|
| 27 | + $magic = static::readInt($stream, 'V'); |
|
| 28 | + |
|
| 29 | + if (($magic === static::MAGIC1) || ($magic === static::MAGIC3)) { //to make sure it works for 64-bit platforms |
|
| 30 | + $byteOrder = 'V'; //low endian |
|
| 31 | + } elseif ($magic === (static::MAGIC2 & 0xFFFFFFFF)) { |
|
| 32 | + $byteOrder = 'N'; //big endian |
|
| 33 | + } else { |
|
| 34 | + throw new Exception('Not MO file'); |
|
| 35 | + } |
|
| 36 | + |
|
| 37 | + static::readInt($stream, $byteOrder); |
|
| 38 | + |
|
| 39 | + $total = static::readInt($stream, $byteOrder); //total string count |
|
| 40 | + $originals = static::readInt($stream, $byteOrder); //offset of original table |
|
| 41 | + $tran = static::readInt($stream, $byteOrder); //offset of translation table |
|
| 42 | + |
|
| 43 | + $stream->seekto($originals); |
|
| 44 | + $table_originals = static::readIntArray($stream, $byteOrder, $total * 2); |
|
| 45 | + |
|
| 46 | + $stream->seekto($tran); |
|
| 47 | + $table_translations = static::readIntArray($stream, $byteOrder, $total * 2); |
|
| 48 | + |
|
| 49 | + for ($i = 0; $i < $total; ++$i) { |
|
| 50 | + $next = $i * 2; |
|
| 51 | + |
|
| 52 | + $stream->seekto($table_originals[$next + 2]); |
|
| 53 | + $original = $stream->read($table_originals[$next + 1]); |
|
| 54 | + |
|
| 55 | + $stream->seekto($table_translations[$next + 2]); |
|
| 56 | + $translated = $stream->read($table_translations[$next + 1]); |
|
| 57 | + |
|
| 58 | + if ($original === '') { |
|
| 59 | + // Headers |
|
| 60 | + foreach (explode("\n", $translated) as $headerLine) { |
|
| 61 | + if ($headerLine === '') { |
|
| 62 | + continue; |
|
| 63 | + } |
|
| 64 | + |
|
| 65 | + $headerChunks = preg_split('/:\s*/', $headerLine, 2); |
|
| 66 | + $translations->setHeader($headerChunks[0], isset($headerChunks[1]) ? $headerChunks[1] : ''); |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + continue; |
|
| 70 | + } |
|
| 71 | + |
|
| 72 | + $chunks = explode("\x04", $original, 2); |
|
| 73 | + |
|
| 74 | + if (isset($chunks[1])) { |
|
| 75 | + $context = $chunks[0]; |
|
| 76 | + $original = $chunks[1]; |
|
| 77 | + } else { |
|
| 78 | + $context = ''; |
|
| 79 | + } |
|
| 80 | + |
|
| 81 | + $chunks = explode("\x00", $original, 2); |
|
| 82 | + |
|
| 83 | + if (isset($chunks[1])) { |
|
| 84 | + $original = $chunks[0]; |
|
| 85 | + $plural = $chunks[1]; |
|
| 86 | + } else { |
|
| 87 | + $plural = ''; |
|
| 88 | + } |
|
| 89 | + |
|
| 90 | + $translation = $translations->insert($context, $original, $plural); |
|
| 91 | + |
|
| 92 | + if ($translated === '') { |
|
| 93 | + continue; |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + if ($plural === '') { |
|
| 97 | + $translation->setTranslation($translated); |
|
| 98 | + continue; |
|
| 99 | + } |
|
| 100 | + |
|
| 101 | + $v = explode("\x00", $translated); |
|
| 102 | + $translation->setTranslation(array_shift($v)); |
|
| 103 | + $translation->setPluralTranslations($v); |
|
| 104 | + } |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + /** |
|
| 108 | + * @param StringReader $stream |
|
| 109 | + * @param string $byteOrder |
|
| 110 | + */ |
|
| 111 | + protected static function readInt(StringReader $stream, $byteOrder) |
|
| 112 | + { |
|
| 113 | + if (($read = $stream->read(4)) === false) { |
|
| 114 | + return false; |
|
| 115 | + } |
|
| 116 | + |
|
| 117 | + $read = unpack($byteOrder, $read); |
|
| 118 | + |
|
| 119 | + return array_shift($read); |
|
| 120 | + } |
|
| 121 | + |
|
| 122 | + /** |
|
| 123 | + * @param StringReader $stream |
|
| 124 | + * @param string $byteOrder |
|
| 125 | + * @param int $count |
|
| 126 | + */ |
|
| 127 | + protected static function readIntArray(StringReader $stream, $byteOrder, $count) |
|
| 128 | + { |
|
| 129 | + return unpack($byteOrder.$count, $stream->read(4 * $count)); |
|
| 130 | + } |
|
| 131 | 131 | } |
@@ -9,47 +9,47 @@ |
||
| 9 | 9 | */ |
| 10 | 10 | class Jed extends Extractor implements ExtractorInterface |
| 11 | 11 | { |
| 12 | - /** |
|
| 13 | - * {@inheritdoc} |
|
| 14 | - */ |
|
| 15 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 16 | - { |
|
| 17 | - static::extract(json_decode($string, true), $translations); |
|
| 18 | - } |
|
| 19 | - |
|
| 20 | - /** |
|
| 21 | - * Handle an array of translations and append to the Translations instance. |
|
| 22 | - * |
|
| 23 | - * @param array $content |
|
| 24 | - * @param Translations $translations |
|
| 25 | - */ |
|
| 26 | - public static function extract(array $content, Translations $translations) |
|
| 27 | - { |
|
| 28 | - $messages = current($content); |
|
| 29 | - $headers = isset($messages['']) ? $messages[''] : null; |
|
| 30 | - unset($messages['']); |
|
| 31 | - |
|
| 32 | - if (!empty($headers['domain'])) { |
|
| 33 | - $translations->setDomain($headers['domain']); |
|
| 34 | - } |
|
| 35 | - |
|
| 36 | - if (!empty($headers['lang'])) { |
|
| 37 | - $translations->setLanguage($headers['lang']); |
|
| 38 | - } |
|
| 39 | - |
|
| 40 | - if (!empty($headers['plural-forms'])) { |
|
| 41 | - $translations->setHeader(Translations::HEADER_PLURAL, $headers['plural-forms']); |
|
| 42 | - } |
|
| 43 | - |
|
| 44 | - $context_glue = '\u0004'; |
|
| 45 | - |
|
| 46 | - foreach ($messages as $key => $translation) { |
|
| 47 | - $key = explode($context_glue, $key); |
|
| 48 | - $context = isset($key[1]) ? array_shift($key) : ''; |
|
| 49 | - |
|
| 50 | - $translations->insert($context, array_shift($key)) |
|
| 51 | - ->setTranslation(array_shift($translation)) |
|
| 52 | - ->setPluralTranslations($translation); |
|
| 53 | - } |
|
| 54 | - } |
|
| 12 | + /** |
|
| 13 | + * {@inheritdoc} |
|
| 14 | + */ |
|
| 15 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 16 | + { |
|
| 17 | + static::extract(json_decode($string, true), $translations); |
|
| 18 | + } |
|
| 19 | + |
|
| 20 | + /** |
|
| 21 | + * Handle an array of translations and append to the Translations instance. |
|
| 22 | + * |
|
| 23 | + * @param array $content |
|
| 24 | + * @param Translations $translations |
|
| 25 | + */ |
|
| 26 | + public static function extract(array $content, Translations $translations) |
|
| 27 | + { |
|
| 28 | + $messages = current($content); |
|
| 29 | + $headers = isset($messages['']) ? $messages[''] : null; |
|
| 30 | + unset($messages['']); |
|
| 31 | + |
|
| 32 | + if (!empty($headers['domain'])) { |
|
| 33 | + $translations->setDomain($headers['domain']); |
|
| 34 | + } |
|
| 35 | + |
|
| 36 | + if (!empty($headers['lang'])) { |
|
| 37 | + $translations->setLanguage($headers['lang']); |
|
| 38 | + } |
|
| 39 | + |
|
| 40 | + if (!empty($headers['plural-forms'])) { |
|
| 41 | + $translations->setHeader(Translations::HEADER_PLURAL, $headers['plural-forms']); |
|
| 42 | + } |
|
| 43 | + |
|
| 44 | + $context_glue = '\u0004'; |
|
| 45 | + |
|
| 46 | + foreach ($messages as $key => $translation) { |
|
| 47 | + $key = explode($context_glue, $key); |
|
| 48 | + $context = isset($key[1]) ? array_shift($key) : ''; |
|
| 49 | + |
|
| 50 | + $translations->insert($context, array_shift($key)) |
|
| 51 | + ->setTranslation(array_shift($translation)) |
|
| 52 | + ->setPluralTranslations($translation); |
|
| 53 | + } |
|
| 54 | + } |
|
| 55 | 55 | } |
@@ -11,205 +11,205 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | class Po extends Extractor implements ExtractorInterface |
| 13 | 13 | { |
| 14 | - use HeadersExtractorTrait; |
|
| 15 | - |
|
| 16 | - /** |
|
| 17 | - * Parses a .po file and append the translations found in the Translations instance. |
|
| 18 | - * |
|
| 19 | - * {@inheritdoc} |
|
| 20 | - */ |
|
| 21 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 22 | - { |
|
| 23 | - $lines = explode("\n", $string); |
|
| 24 | - $i = 0; |
|
| 25 | - |
|
| 26 | - $translation = $translations->createNewTranslation('', ''); |
|
| 27 | - |
|
| 28 | - for ($n = count($lines); $i < $n; ++$i) { |
|
| 29 | - $line = trim($lines[$i]); |
|
| 30 | - $line = static::fixMultiLines($line, $lines, $i); |
|
| 31 | - |
|
| 32 | - if ($line === '') { |
|
| 33 | - if ($translation->is('', '')) { |
|
| 34 | - static::extractHeaders($translation->getTranslation(), $translations); |
|
| 35 | - } elseif ($translation->hasOriginal()) { |
|
| 36 | - $translations[] = $translation; |
|
| 37 | - } |
|
| 38 | - |
|
| 39 | - $translation = $translations->createNewTranslation('', ''); |
|
| 40 | - continue; |
|
| 41 | - } |
|
| 42 | - |
|
| 43 | - $splitLine = preg_split('/\s+/', $line, 2); |
|
| 44 | - $key = $splitLine[0]; |
|
| 45 | - $data = isset($splitLine[1]) ? $splitLine[1] : ''; |
|
| 46 | - |
|
| 47 | - if ($key === '#~') { |
|
| 48 | - $translation->setDisabled(true); |
|
| 49 | - |
|
| 50 | - $splitLine = preg_split('/\s+/', $data, 2); |
|
| 51 | - $key = $splitLine[0]; |
|
| 52 | - $data = isset($splitLine[1]) ? $splitLine[1] : ''; |
|
| 53 | - } |
|
| 54 | - |
|
| 55 | - switch ($key) { |
|
| 56 | - case '#': |
|
| 57 | - $translation->addComment($data); |
|
| 58 | - $append = null; |
|
| 59 | - break; |
|
| 60 | - |
|
| 61 | - case '#.': |
|
| 62 | - $translation->addExtractedComment($data); |
|
| 63 | - $append = null; |
|
| 64 | - break; |
|
| 65 | - |
|
| 66 | - case '#,': |
|
| 67 | - foreach (array_map('trim', explode(',', trim($data))) as $value) { |
|
| 68 | - $translation->addFlag($value); |
|
| 69 | - } |
|
| 70 | - $append = null; |
|
| 71 | - break; |
|
| 72 | - |
|
| 73 | - case '#:': |
|
| 74 | - foreach (preg_split('/\s+/', trim($data)) as $value) { |
|
| 75 | - if (preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) { |
|
| 76 | - $translation->addReference($matches[1], isset($matches[3]) ? $matches[3] : null); |
|
| 77 | - } |
|
| 78 | - } |
|
| 79 | - $append = null; |
|
| 80 | - break; |
|
| 81 | - |
|
| 82 | - case 'msgctxt': |
|
| 83 | - $translation = $translation->getClone(static::convertString($data)); |
|
| 84 | - $append = 'Context'; |
|
| 85 | - break; |
|
| 86 | - |
|
| 87 | - case 'msgid': |
|
| 88 | - $translation = $translation->getClone(null, static::convertString($data)); |
|
| 89 | - $append = 'Original'; |
|
| 90 | - break; |
|
| 91 | - |
|
| 92 | - case 'msgid_plural': |
|
| 93 | - $translation->setPlural(static::convertString($data)); |
|
| 94 | - $append = 'Plural'; |
|
| 95 | - break; |
|
| 96 | - |
|
| 97 | - case 'msgstr': |
|
| 98 | - case 'msgstr[0]': |
|
| 99 | - $translation->setTranslation(static::convertString($data)); |
|
| 100 | - $append = 'Translation'; |
|
| 101 | - break; |
|
| 102 | - |
|
| 103 | - case 'msgstr[1]': |
|
| 104 | - $translation->setPluralTranslations([static::convertString($data)]); |
|
| 105 | - $append = 'PluralTranslation'; |
|
| 106 | - break; |
|
| 107 | - |
|
| 108 | - default: |
|
| 109 | - if (strpos($key, 'msgstr[') === 0) { |
|
| 110 | - $p = $translation->getPluralTranslations(); |
|
| 111 | - $p[] = static::convertString($data); |
|
| 112 | - |
|
| 113 | - $translation->setPluralTranslations($p); |
|
| 114 | - $append = 'PluralTranslation'; |
|
| 115 | - break; |
|
| 116 | - } |
|
| 117 | - |
|
| 118 | - if (isset($append)) { |
|
| 119 | - if ($append === 'Context') { |
|
| 120 | - $translation = $translation->getClone($translation->getContext() |
|
| 121 | - ."\n" |
|
| 122 | - .static::convertString($data)); |
|
| 123 | - break; |
|
| 124 | - } |
|
| 125 | - |
|
| 126 | - if ($append === 'Original') { |
|
| 127 | - $translation = $translation->getClone(null, $translation->getOriginal() |
|
| 128 | - ."\n" |
|
| 129 | - .static::convertString($data)); |
|
| 130 | - break; |
|
| 131 | - } |
|
| 132 | - |
|
| 133 | - if ($append === 'PluralTranslation') { |
|
| 134 | - $p = $translation->getPluralTranslations(); |
|
| 135 | - $p[] = array_pop($p)."\n".static::convertString($data); |
|
| 136 | - $translation->setPluralTranslations($p); |
|
| 137 | - break; |
|
| 138 | - } |
|
| 139 | - |
|
| 140 | - $getMethod = 'get'.$append; |
|
| 141 | - $setMethod = 'set'.$append; |
|
| 142 | - $translation->$setMethod($translation->$getMethod()."\n".static::convertString($data)); |
|
| 143 | - } |
|
| 144 | - break; |
|
| 145 | - } |
|
| 146 | - } |
|
| 147 | - |
|
| 148 | - if ($translation->hasOriginal() && !in_array($translation, iterator_to_array($translations))) { |
|
| 149 | - $translations[] = $translation; |
|
| 150 | - } |
|
| 151 | - } |
|
| 152 | - |
|
| 153 | - /** |
|
| 154 | - * Gets one string from multiline strings. |
|
| 155 | - * |
|
| 156 | - * @param string $line |
|
| 157 | - * @param array $lines |
|
| 158 | - * @param int &$i |
|
| 159 | - * |
|
| 160 | - * @return string |
|
| 161 | - */ |
|
| 162 | - protected static function fixMultiLines($line, array $lines, &$i) |
|
| 163 | - { |
|
| 164 | - for ($j = $i, $t = count($lines); $j < $t; ++$j) { |
|
| 165 | - if (substr($line, -1, 1) == '"' && isset($lines[$j + 1])) { |
|
| 166 | - $nextLine = trim($lines[$j + 1]); |
|
| 167 | - if (substr($nextLine, 0, 1) == '"') { |
|
| 168 | - $line = substr($line, 0, -1).substr($nextLine, 1); |
|
| 169 | - continue; |
|
| 170 | - } |
|
| 171 | - if (substr($nextLine, 0, 4) == '#~ "') { |
|
| 172 | - $line = substr($line, 0, -1).substr($nextLine, 4); |
|
| 173 | - continue; |
|
| 174 | - } |
|
| 175 | - } |
|
| 176 | - $i = $j; |
|
| 177 | - break; |
|
| 178 | - } |
|
| 179 | - |
|
| 180 | - return $line; |
|
| 181 | - } |
|
| 182 | - |
|
| 183 | - /** |
|
| 184 | - * Convert a string from its PO representation. |
|
| 185 | - * |
|
| 186 | - * @param string $value |
|
| 187 | - * |
|
| 188 | - * @return string |
|
| 189 | - */ |
|
| 190 | - public static function convertString($value) |
|
| 191 | - { |
|
| 192 | - if (!$value) { |
|
| 193 | - return ''; |
|
| 194 | - } |
|
| 195 | - |
|
| 196 | - if ($value[0] === '"') { |
|
| 197 | - $value = substr($value, 1, -1); |
|
| 198 | - } |
|
| 199 | - |
|
| 200 | - return strtr( |
|
| 201 | - $value, |
|
| 202 | - [ |
|
| 203 | - '\\\\' => '\\', |
|
| 204 | - '\\a' => "\x07", |
|
| 205 | - '\\b' => "\x08", |
|
| 206 | - '\\t' => "\t", |
|
| 207 | - '\\n' => "\n", |
|
| 208 | - '\\v' => "\x0b", |
|
| 209 | - '\\f' => "\x0c", |
|
| 210 | - '\\r' => "\r", |
|
| 211 | - '\\"' => '"', |
|
| 212 | - ] |
|
| 213 | - ); |
|
| 214 | - } |
|
| 14 | + use HeadersExtractorTrait; |
|
| 15 | + |
|
| 16 | + /** |
|
| 17 | + * Parses a .po file and append the translations found in the Translations instance. |
|
| 18 | + * |
|
| 19 | + * {@inheritdoc} |
|
| 20 | + */ |
|
| 21 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 22 | + { |
|
| 23 | + $lines = explode("\n", $string); |
|
| 24 | + $i = 0; |
|
| 25 | + |
|
| 26 | + $translation = $translations->createNewTranslation('', ''); |
|
| 27 | + |
|
| 28 | + for ($n = count($lines); $i < $n; ++$i) { |
|
| 29 | + $line = trim($lines[$i]); |
|
| 30 | + $line = static::fixMultiLines($line, $lines, $i); |
|
| 31 | + |
|
| 32 | + if ($line === '') { |
|
| 33 | + if ($translation->is('', '')) { |
|
| 34 | + static::extractHeaders($translation->getTranslation(), $translations); |
|
| 35 | + } elseif ($translation->hasOriginal()) { |
|
| 36 | + $translations[] = $translation; |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + $translation = $translations->createNewTranslation('', ''); |
|
| 40 | + continue; |
|
| 41 | + } |
|
| 42 | + |
|
| 43 | + $splitLine = preg_split('/\s+/', $line, 2); |
|
| 44 | + $key = $splitLine[0]; |
|
| 45 | + $data = isset($splitLine[1]) ? $splitLine[1] : ''; |
|
| 46 | + |
|
| 47 | + if ($key === '#~') { |
|
| 48 | + $translation->setDisabled(true); |
|
| 49 | + |
|
| 50 | + $splitLine = preg_split('/\s+/', $data, 2); |
|
| 51 | + $key = $splitLine[0]; |
|
| 52 | + $data = isset($splitLine[1]) ? $splitLine[1] : ''; |
|
| 53 | + } |
|
| 54 | + |
|
| 55 | + switch ($key) { |
|
| 56 | + case '#': |
|
| 57 | + $translation->addComment($data); |
|
| 58 | + $append = null; |
|
| 59 | + break; |
|
| 60 | + |
|
| 61 | + case '#.': |
|
| 62 | + $translation->addExtractedComment($data); |
|
| 63 | + $append = null; |
|
| 64 | + break; |
|
| 65 | + |
|
| 66 | + case '#,': |
|
| 67 | + foreach (array_map('trim', explode(',', trim($data))) as $value) { |
|
| 68 | + $translation->addFlag($value); |
|
| 69 | + } |
|
| 70 | + $append = null; |
|
| 71 | + break; |
|
| 72 | + |
|
| 73 | + case '#:': |
|
| 74 | + foreach (preg_split('/\s+/', trim($data)) as $value) { |
|
| 75 | + if (preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) { |
|
| 76 | + $translation->addReference($matches[1], isset($matches[3]) ? $matches[3] : null); |
|
| 77 | + } |
|
| 78 | + } |
|
| 79 | + $append = null; |
|
| 80 | + break; |
|
| 81 | + |
|
| 82 | + case 'msgctxt': |
|
| 83 | + $translation = $translation->getClone(static::convertString($data)); |
|
| 84 | + $append = 'Context'; |
|
| 85 | + break; |
|
| 86 | + |
|
| 87 | + case 'msgid': |
|
| 88 | + $translation = $translation->getClone(null, static::convertString($data)); |
|
| 89 | + $append = 'Original'; |
|
| 90 | + break; |
|
| 91 | + |
|
| 92 | + case 'msgid_plural': |
|
| 93 | + $translation->setPlural(static::convertString($data)); |
|
| 94 | + $append = 'Plural'; |
|
| 95 | + break; |
|
| 96 | + |
|
| 97 | + case 'msgstr': |
|
| 98 | + case 'msgstr[0]': |
|
| 99 | + $translation->setTranslation(static::convertString($data)); |
|
| 100 | + $append = 'Translation'; |
|
| 101 | + break; |
|
| 102 | + |
|
| 103 | + case 'msgstr[1]': |
|
| 104 | + $translation->setPluralTranslations([static::convertString($data)]); |
|
| 105 | + $append = 'PluralTranslation'; |
|
| 106 | + break; |
|
| 107 | + |
|
| 108 | + default: |
|
| 109 | + if (strpos($key, 'msgstr[') === 0) { |
|
| 110 | + $p = $translation->getPluralTranslations(); |
|
| 111 | + $p[] = static::convertString($data); |
|
| 112 | + |
|
| 113 | + $translation->setPluralTranslations($p); |
|
| 114 | + $append = 'PluralTranslation'; |
|
| 115 | + break; |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + if (isset($append)) { |
|
| 119 | + if ($append === 'Context') { |
|
| 120 | + $translation = $translation->getClone($translation->getContext() |
|
| 121 | + ."\n" |
|
| 122 | + .static::convertString($data)); |
|
| 123 | + break; |
|
| 124 | + } |
|
| 125 | + |
|
| 126 | + if ($append === 'Original') { |
|
| 127 | + $translation = $translation->getClone(null, $translation->getOriginal() |
|
| 128 | + ."\n" |
|
| 129 | + .static::convertString($data)); |
|
| 130 | + break; |
|
| 131 | + } |
|
| 132 | + |
|
| 133 | + if ($append === 'PluralTranslation') { |
|
| 134 | + $p = $translation->getPluralTranslations(); |
|
| 135 | + $p[] = array_pop($p)."\n".static::convertString($data); |
|
| 136 | + $translation->setPluralTranslations($p); |
|
| 137 | + break; |
|
| 138 | + } |
|
| 139 | + |
|
| 140 | + $getMethod = 'get'.$append; |
|
| 141 | + $setMethod = 'set'.$append; |
|
| 142 | + $translation->$setMethod($translation->$getMethod()."\n".static::convertString($data)); |
|
| 143 | + } |
|
| 144 | + break; |
|
| 145 | + } |
|
| 146 | + } |
|
| 147 | + |
|
| 148 | + if ($translation->hasOriginal() && !in_array($translation, iterator_to_array($translations))) { |
|
| 149 | + $translations[] = $translation; |
|
| 150 | + } |
|
| 151 | + } |
|
| 152 | + |
|
| 153 | + /** |
|
| 154 | + * Gets one string from multiline strings. |
|
| 155 | + * |
|
| 156 | + * @param string $line |
|
| 157 | + * @param array $lines |
|
| 158 | + * @param int &$i |
|
| 159 | + * |
|
| 160 | + * @return string |
|
| 161 | + */ |
|
| 162 | + protected static function fixMultiLines($line, array $lines, &$i) |
|
| 163 | + { |
|
| 164 | + for ($j = $i, $t = count($lines); $j < $t; ++$j) { |
|
| 165 | + if (substr($line, -1, 1) == '"' && isset($lines[$j + 1])) { |
|
| 166 | + $nextLine = trim($lines[$j + 1]); |
|
| 167 | + if (substr($nextLine, 0, 1) == '"') { |
|
| 168 | + $line = substr($line, 0, -1).substr($nextLine, 1); |
|
| 169 | + continue; |
|
| 170 | + } |
|
| 171 | + if (substr($nextLine, 0, 4) == '#~ "') { |
|
| 172 | + $line = substr($line, 0, -1).substr($nextLine, 4); |
|
| 173 | + continue; |
|
| 174 | + } |
|
| 175 | + } |
|
| 176 | + $i = $j; |
|
| 177 | + break; |
|
| 178 | + } |
|
| 179 | + |
|
| 180 | + return $line; |
|
| 181 | + } |
|
| 182 | + |
|
| 183 | + /** |
|
| 184 | + * Convert a string from its PO representation. |
|
| 185 | + * |
|
| 186 | + * @param string $value |
|
| 187 | + * |
|
| 188 | + * @return string |
|
| 189 | + */ |
|
| 190 | + public static function convertString($value) |
|
| 191 | + { |
|
| 192 | + if (!$value) { |
|
| 193 | + return ''; |
|
| 194 | + } |
|
| 195 | + |
|
| 196 | + if ($value[0] === '"') { |
|
| 197 | + $value = substr($value, 1, -1); |
|
| 198 | + } |
|
| 199 | + |
|
| 200 | + return strtr( |
|
| 201 | + $value, |
|
| 202 | + [ |
|
| 203 | + '\\\\' => '\\', |
|
| 204 | + '\\a' => "\x07", |
|
| 205 | + '\\b' => "\x08", |
|
| 206 | + '\\t' => "\t", |
|
| 207 | + '\\n' => "\n", |
|
| 208 | + '\\v' => "\x0b", |
|
| 209 | + '\\f' => "\x0c", |
|
| 210 | + '\\r' => "\r", |
|
| 211 | + '\\"' => '"', |
|
| 212 | + ] |
|
| 213 | + ); |
|
| 214 | + } |
|
| 215 | 215 | } |
@@ -16,408 +16,408 @@ |
||
| 16 | 16 | */ |
| 17 | 17 | class VueJs extends Extractor implements ExtractorInterface, ExtractorMultiInterface |
| 18 | 18 | { |
| 19 | - public static $options = [ |
|
| 20 | - 'constants' => [], |
|
| 21 | - |
|
| 22 | - 'functions' => [ |
|
| 23 | - 'gettext' => 'gettext', |
|
| 24 | - '__' => 'gettext', |
|
| 25 | - 'ngettext' => 'ngettext', |
|
| 26 | - 'n__' => 'ngettext', |
|
| 27 | - 'pgettext' => 'pgettext', |
|
| 28 | - 'p__' => 'pgettext', |
|
| 29 | - 'dgettext' => 'dgettext', |
|
| 30 | - 'd__' => 'dgettext', |
|
| 31 | - 'dngettext' => 'dngettext', |
|
| 32 | - 'dn__' => 'dngettext', |
|
| 33 | - 'dpgettext' => 'dpgettext', |
|
| 34 | - 'dp__' => 'dpgettext', |
|
| 35 | - 'npgettext' => 'npgettext', |
|
| 36 | - 'np__' => 'npgettext', |
|
| 37 | - 'dnpgettext' => 'dnpgettext', |
|
| 38 | - 'dnp__' => 'dnpgettext', |
|
| 39 | - 'noop' => 'noop', |
|
| 40 | - 'noop__' => 'noop', |
|
| 41 | - ], |
|
| 42 | - ]; |
|
| 43 | - |
|
| 44 | - protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; |
|
| 45 | - |
|
| 46 | - /** |
|
| 47 | - * @inheritDoc |
|
| 48 | - * @throws Exception |
|
| 49 | - */ |
|
| 50 | - public static function fromFileMultiple($file, array $translations, array $options = []) |
|
| 51 | - { |
|
| 52 | - foreach (static::getFiles($file) as $file) { |
|
| 53 | - $options['file'] = $file; |
|
| 54 | - static::fromStringMultiple(static::readFile($file), $translations, $options); |
|
| 55 | - } |
|
| 56 | - } |
|
| 57 | - |
|
| 58 | - /** |
|
| 59 | - * @inheritdoc |
|
| 60 | - * @throws Exception |
|
| 61 | - */ |
|
| 62 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 63 | - { |
|
| 64 | - static::fromStringMultiple($string, [$translations], $options); |
|
| 65 | - } |
|
| 66 | - |
|
| 67 | - /** |
|
| 68 | - * @inheritDoc |
|
| 69 | - * @throws Exception |
|
| 70 | - */ |
|
| 71 | - public static function fromStringMultiple($string, array $translations, array $options = []) |
|
| 72 | - { |
|
| 73 | - $options += static::$options; |
|
| 74 | - $options += [ |
|
| 75 | - // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions) |
|
| 76 | - 'attributePrefixes' => [ |
|
| 77 | - ':', |
|
| 78 | - 'v-bind:', |
|
| 79 | - 'v-on:', |
|
| 80 | - 'v-text', |
|
| 81 | - ], |
|
| 82 | - // HTML Tags to parse |
|
| 83 | - 'tagNames' => [ |
|
| 84 | - 'translate', |
|
| 85 | - ], |
|
| 86 | - // HTML tags to parse when attribute exists |
|
| 87 | - 'tagAttributes' => [ |
|
| 88 | - 'v-translate', |
|
| 89 | - ], |
|
| 90 | - // Comments |
|
| 91 | - 'commentAttributes' => [ |
|
| 92 | - 'translate-comment', |
|
| 93 | - ], |
|
| 94 | - 'contextAttributes' => [ |
|
| 95 | - 'translate-context', |
|
| 96 | - ], |
|
| 97 | - // Attribute with plural content |
|
| 98 | - 'pluralAttributes' => [ |
|
| 99 | - 'translate-plural', |
|
| 100 | - ], |
|
| 101 | - ]; |
|
| 102 | - |
|
| 103 | - // Ok, this is the weirdest hack, but let me explain: |
|
| 104 | - // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag. |
|
| 105 | - // So if there are new lines between <template> and next element, they are lost |
|
| 106 | - // So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements. |
|
| 107 | - // Same thing happens between template and script tag. |
|
| 108 | - $string = str_replace('<template>', '<template>.', $string); |
|
| 109 | - $string = str_replace('</template>', '</template>.', $string); |
|
| 110 | - |
|
| 111 | - // Normalize newlines |
|
| 112 | - $string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string); |
|
| 113 | - |
|
| 114 | - // VueJS files are valid HTML files, we will operate with the DOM here |
|
| 115 | - $dom = static::convertHtmlToDom($string); |
|
| 116 | - |
|
| 117 | - $script = static::extractScriptTag($string); |
|
| 118 | - |
|
| 119 | - // Parse the script part as a regular JS code |
|
| 120 | - if ($script) { |
|
| 121 | - $scriptLineNumber = $dom->getElementsByTagName('script')->item(0)->getLineNo(); |
|
| 122 | - static::getScriptTranslationsFromString( |
|
| 123 | - $script, |
|
| 124 | - $translations, |
|
| 125 | - $options, |
|
| 126 | - $scriptLineNumber - 1 |
|
| 127 | - ); |
|
| 128 | - } |
|
| 129 | - |
|
| 130 | - // Template part is parsed separately, all variables will be extracted |
|
| 131 | - // and handled as a regular JS code |
|
| 132 | - $template = $dom->getElementsByTagName('template')->item(0); |
|
| 133 | - if ($template) { |
|
| 134 | - static::getTemplateTranslations( |
|
| 135 | - $template, |
|
| 136 | - $translations, |
|
| 137 | - $options, |
|
| 138 | - $template->getLineNo() - 1 |
|
| 139 | - ); |
|
| 140 | - } |
|
| 141 | - } |
|
| 142 | - |
|
| 143 | - /** |
|
| 144 | - * Extracts script tag contents using regex instead of DOM operations. |
|
| 145 | - * If we parse using DOM, some contents may change, for example, tags within strings will be stripped |
|
| 146 | - * |
|
| 147 | - * @param $string |
|
| 148 | - * @return bool|string |
|
| 149 | - */ |
|
| 150 | - protected static function extractScriptTag($string) |
|
| 151 | - { |
|
| 152 | - if (preg_match('#<\s*?script\b[^>]*>(.*?)</script\b[^>]*>#s', $string, $matches)) { |
|
| 153 | - return $matches[1]; |
|
| 154 | - } |
|
| 155 | - |
|
| 156 | - return ''; |
|
| 157 | - } |
|
| 158 | - |
|
| 159 | - /** |
|
| 160 | - * @param string $html |
|
| 161 | - * @return DOMDocument |
|
| 162 | - */ |
|
| 163 | - protected static function convertHtmlToDom($html) |
|
| 164 | - { |
|
| 165 | - $dom = new DOMDocument; |
|
| 166 | - |
|
| 167 | - libxml_use_internal_errors(true); |
|
| 168 | - |
|
| 169 | - // Prepend xml encoding so DOMDocument document handles UTF8 correctly. |
|
| 170 | - // Assuming that vue template files will not have any xml encoding tags, because duplicate tags may be ignored. |
|
| 171 | - $dom->loadHTML('<?xml encoding="utf-8"?>' . $html); |
|
| 172 | - |
|
| 173 | - libxml_clear_errors(); |
|
| 174 | - |
|
| 175 | - return $dom; |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - /** |
|
| 179 | - * Extract translations from script part |
|
| 180 | - * |
|
| 181 | - * @param string $scriptContents Only script tag contents, not the whole template |
|
| 182 | - * @param Translations|Translations[] $translations One or multiple domain Translation objects |
|
| 183 | - * @param array $options |
|
| 184 | - * @param int $lineOffset Number of lines the script is offset in the vue template file |
|
| 185 | - * @throws Exception |
|
| 186 | - */ |
|
| 187 | - protected static function getScriptTranslationsFromString( |
|
| 188 | - $scriptContents, |
|
| 189 | - $translations, |
|
| 190 | - array $options = [], |
|
| 191 | - $lineOffset = 0 |
|
| 192 | - ) { |
|
| 193 | - /** @var FunctionsScanner $functions */ |
|
| 194 | - $functions = new static::$functionsScannerClass($scriptContents); |
|
| 195 | - $options['lineOffset'] = $lineOffset; |
|
| 196 | - $functions->saveGettextFunctions($translations, $options); |
|
| 197 | - } |
|
| 198 | - |
|
| 199 | - /** |
|
| 200 | - * Parse template to extract all translations (element content and dynamic element attributes) |
|
| 201 | - * |
|
| 202 | - * @param DOMNode $dom |
|
| 203 | - * @param Translations|Translations[] $translations One or multiple domain Translation objects |
|
| 204 | - * @param array $options |
|
| 205 | - * @param int $lineOffset Line number where the template part starts in the vue file |
|
| 206 | - * @throws Exception |
|
| 207 | - */ |
|
| 208 | - protected static function getTemplateTranslations( |
|
| 209 | - DOMNode $dom, |
|
| 210 | - $translations, |
|
| 211 | - array $options, |
|
| 212 | - $lineOffset = 0 |
|
| 213 | - ) { |
|
| 214 | - // Build a JS string from all template attribute expressions |
|
| 215 | - $fakeAttributeJs = static::getTemplateAttributeFakeJs($options, $dom); |
|
| 216 | - |
|
| 217 | - // 1 line offset is necessary because parent template element was ignored when converting to DOM |
|
| 218 | - static::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset); |
|
| 219 | - |
|
| 220 | - // Build a JS string from template element content expressions |
|
| 221 | - $fakeTemplateJs = static::getTemplateFakeJs($dom); |
|
| 222 | - static::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset); |
|
| 223 | - |
|
| 224 | - static::getTagTranslations($options, $dom, $translations); |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - /** |
|
| 228 | - * @param array $options |
|
| 229 | - * @param DOMNode $dom |
|
| 230 | - * @param Translations|Translations[] $translations |
|
| 231 | - */ |
|
| 232 | - protected static function getTagTranslations(array $options, DOMNode $dom, $translations) |
|
| 233 | - { |
|
| 234 | - // Since tag scanning does not support domains, we always use the first translation given |
|
| 235 | - $translations = is_array($translations) ? reset($translations) : $translations; |
|
| 236 | - |
|
| 237 | - $children = $dom->childNodes; |
|
| 238 | - for ($i = 0; $i < $children->length; $i++) { |
|
| 239 | - $node = $children->item($i); |
|
| 240 | - |
|
| 241 | - if (!($node instanceof DOMElement)) { |
|
| 242 | - continue; |
|
| 243 | - } |
|
| 244 | - |
|
| 245 | - $translatable = false; |
|
| 246 | - |
|
| 247 | - if (in_array($node->tagName, $options['tagNames'], true)) { |
|
| 248 | - $translatable = true; |
|
| 249 | - } |
|
| 250 | - |
|
| 251 | - $attrList = $node->attributes; |
|
| 252 | - $context = null; |
|
| 253 | - $plural = ""; |
|
| 254 | - $comment = null; |
|
| 255 | - |
|
| 256 | - for ($j = 0; $j < $attrList->length; $j++) { |
|
| 257 | - /** @var DOMAttr $domAttr */ |
|
| 258 | - $domAttr = $attrList->item($j); |
|
| 259 | - // Check if this is a dynamic vue attribute |
|
| 260 | - if (in_array($domAttr->name, $options['tagAttributes'])) { |
|
| 261 | - $translatable = true; |
|
| 262 | - } |
|
| 263 | - if (in_array($domAttr->name, $options['contextAttributes'])) { |
|
| 264 | - $context = $domAttr->value; |
|
| 265 | - } |
|
| 266 | - if (in_array($domAttr->name, $options['pluralAttributes'])) { |
|
| 267 | - $plural = $domAttr->value; |
|
| 268 | - } |
|
| 269 | - if (in_array($domAttr->name, $options['commentAttributes'])) { |
|
| 270 | - $comment = $domAttr->value; |
|
| 271 | - } |
|
| 272 | - } |
|
| 273 | - |
|
| 274 | - if ($translatable) { |
|
| 275 | - $translation = $translations->insert($context, trim($node->textContent), $plural); |
|
| 276 | - $translation->addReference($options['file'], $node->getLineNo()); |
|
| 277 | - if ($comment) { |
|
| 278 | - $translation->addExtractedComment($comment); |
|
| 279 | - } |
|
| 280 | - } |
|
| 281 | - |
|
| 282 | - if ($node->hasChildNodes()) { |
|
| 283 | - static::getTagTranslations($options, $node, $translations); |
|
| 284 | - } |
|
| 285 | - } |
|
| 286 | - } |
|
| 287 | - |
|
| 288 | - /** |
|
| 289 | - * Extract JS expressions from element attribute bindings (excluding text within elements) |
|
| 290 | - * For example: <span :title="__('extract this')"> skip element content </span> |
|
| 291 | - * |
|
| 292 | - * @param array $options |
|
| 293 | - * @param DOMNode $dom |
|
| 294 | - * @return string JS code |
|
| 295 | - */ |
|
| 296 | - protected static function getTemplateAttributeFakeJs(array $options, DOMNode $dom) |
|
| 297 | - { |
|
| 298 | - $expressionsByLine = static::getVueAttributeExpressions($options['attributePrefixes'], $dom); |
|
| 299 | - |
|
| 300 | - if (empty($expressionsByLine)) { |
|
| 301 | - return ''; |
|
| 302 | - } |
|
| 303 | - |
|
| 304 | - $maxLines = max(array_keys($expressionsByLine)); |
|
| 305 | - $fakeJs = ''; |
|
| 306 | - |
|
| 307 | - for ($line = 1; $line <= $maxLines; $line++) { |
|
| 308 | - if (isset($expressionsByLine[$line])) { |
|
| 309 | - $fakeJs .= implode("; ", $expressionsByLine[$line]); |
|
| 310 | - } |
|
| 311 | - $fakeJs .= "\n"; |
|
| 312 | - } |
|
| 313 | - |
|
| 314 | - return $fakeJs; |
|
| 315 | - } |
|
| 316 | - |
|
| 317 | - /** |
|
| 318 | - * Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions |
|
| 319 | - * |
|
| 320 | - * @param array $attributePrefixes List of attribute prefixes we parse as JS (may contain translations) |
|
| 321 | - * @param DOMNode $dom |
|
| 322 | - * @param array $expressionByLine [lineNumber => [jsExpression, ..], ..] |
|
| 323 | - * @return array [lineNumber => [jsExpression, ..], ..] |
|
| 324 | - */ |
|
| 325 | - protected static function getVueAttributeExpressions( |
|
| 326 | - array $attributePrefixes, |
|
| 327 | - DOMNode $dom, |
|
| 328 | - array &$expressionByLine = [] |
|
| 329 | - ) { |
|
| 330 | - $children = $dom->childNodes; |
|
| 331 | - |
|
| 332 | - for ($i = 0; $i < $children->length; $i++) { |
|
| 333 | - $node = $children->item($i); |
|
| 334 | - |
|
| 335 | - if (!($node instanceof DOMElement)) { |
|
| 336 | - continue; |
|
| 337 | - } |
|
| 338 | - $attrList = $node->attributes; |
|
| 339 | - |
|
| 340 | - for ($j = 0; $j < $attrList->length; $j++) { |
|
| 341 | - /** @var DOMAttr $domAttr */ |
|
| 342 | - $domAttr = $attrList->item($j); |
|
| 343 | - |
|
| 344 | - // Check if this is a dynamic vue attribute |
|
| 345 | - if (static::isAttributeMatching($domAttr->name, $attributePrefixes)) { |
|
| 346 | - $line = $domAttr->getLineNo(); |
|
| 347 | - $expressionByLine += [$line => []]; |
|
| 348 | - $expressionByLine[$line][] = $domAttr->value; |
|
| 349 | - } |
|
| 350 | - } |
|
| 351 | - |
|
| 352 | - if ($node->hasChildNodes()) { |
|
| 353 | - $expressionByLine = static::getVueAttributeExpressions($attributePrefixes, $node, $expressionByLine); |
|
| 354 | - } |
|
| 355 | - } |
|
| 356 | - |
|
| 357 | - return $expressionByLine; |
|
| 358 | - } |
|
| 359 | - |
|
| 360 | - /** |
|
| 361 | - * Check if this attribute name should be parsed for translations |
|
| 362 | - * |
|
| 363 | - * @param string $attributeName |
|
| 364 | - * @param string[] $attributePrefixes |
|
| 365 | - * @return bool |
|
| 366 | - */ |
|
| 367 | - protected static function isAttributeMatching($attributeName, $attributePrefixes) |
|
| 368 | - { |
|
| 369 | - foreach ($attributePrefixes as $prefix) { |
|
| 370 | - if (strpos($attributeName, $prefix) === 0) { |
|
| 371 | - return true; |
|
| 372 | - } |
|
| 373 | - } |
|
| 374 | - return false; |
|
| 375 | - } |
|
| 376 | - |
|
| 377 | - /** |
|
| 378 | - * Extract JS expressions from within template elements (excluding attributes) |
|
| 379 | - * For example: <span :title="skip attributes"> {{__("extract element content")}} </span> |
|
| 380 | - * |
|
| 381 | - * @param DOMNode $dom |
|
| 382 | - * @return string JS code |
|
| 383 | - */ |
|
| 384 | - protected static function getTemplateFakeJs(DOMNode $dom) |
|
| 385 | - { |
|
| 386 | - $fakeJs = ''; |
|
| 387 | - $lines = explode("\n", $dom->textContent); |
|
| 388 | - |
|
| 389 | - // Build a fake JS file from template by extracting JS expressions within each template line |
|
| 390 | - foreach ($lines as $line) { |
|
| 391 | - $expressionMatched = static::parseOneTemplateLine($line); |
|
| 392 | - |
|
| 393 | - $fakeJs .= implode("; ", $expressionMatched) . "\n"; |
|
| 394 | - } |
|
| 395 | - |
|
| 396 | - return $fakeJs; |
|
| 397 | - } |
|
| 398 | - |
|
| 399 | - /** |
|
| 400 | - * Match JS expressions in a template line |
|
| 401 | - * |
|
| 402 | - * @param string $line |
|
| 403 | - * @return string[] |
|
| 404 | - */ |
|
| 405 | - protected static function parseOneTemplateLine($line) |
|
| 406 | - { |
|
| 407 | - $line = trim($line); |
|
| 408 | - |
|
| 409 | - if (!$line) { |
|
| 410 | - return []; |
|
| 411 | - } |
|
| 412 | - |
|
| 413 | - $regex = '#\{\{(.*?)\}\}#'; |
|
| 414 | - |
|
| 415 | - preg_match_all($regex, $line, $matches); |
|
| 416 | - |
|
| 417 | - $matched = array_map(function ($v) { |
|
| 418 | - return trim($v, '\'"{}'); |
|
| 419 | - }, $matches[1]); |
|
| 420 | - |
|
| 421 | - return $matched; |
|
| 422 | - } |
|
| 19 | + public static $options = [ |
|
| 20 | + 'constants' => [], |
|
| 21 | + |
|
| 22 | + 'functions' => [ |
|
| 23 | + 'gettext' => 'gettext', |
|
| 24 | + '__' => 'gettext', |
|
| 25 | + 'ngettext' => 'ngettext', |
|
| 26 | + 'n__' => 'ngettext', |
|
| 27 | + 'pgettext' => 'pgettext', |
|
| 28 | + 'p__' => 'pgettext', |
|
| 29 | + 'dgettext' => 'dgettext', |
|
| 30 | + 'd__' => 'dgettext', |
|
| 31 | + 'dngettext' => 'dngettext', |
|
| 32 | + 'dn__' => 'dngettext', |
|
| 33 | + 'dpgettext' => 'dpgettext', |
|
| 34 | + 'dp__' => 'dpgettext', |
|
| 35 | + 'npgettext' => 'npgettext', |
|
| 36 | + 'np__' => 'npgettext', |
|
| 37 | + 'dnpgettext' => 'dnpgettext', |
|
| 38 | + 'dnp__' => 'dnpgettext', |
|
| 39 | + 'noop' => 'noop', |
|
| 40 | + 'noop__' => 'noop', |
|
| 41 | + ], |
|
| 42 | + ]; |
|
| 43 | + |
|
| 44 | + protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; |
|
| 45 | + |
|
| 46 | + /** |
|
| 47 | + * @inheritDoc |
|
| 48 | + * @throws Exception |
|
| 49 | + */ |
|
| 50 | + public static function fromFileMultiple($file, array $translations, array $options = []) |
|
| 51 | + { |
|
| 52 | + foreach (static::getFiles($file) as $file) { |
|
| 53 | + $options['file'] = $file; |
|
| 54 | + static::fromStringMultiple(static::readFile($file), $translations, $options); |
|
| 55 | + } |
|
| 56 | + } |
|
| 57 | + |
|
| 58 | + /** |
|
| 59 | + * @inheritdoc |
|
| 60 | + * @throws Exception |
|
| 61 | + */ |
|
| 62 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 63 | + { |
|
| 64 | + static::fromStringMultiple($string, [$translations], $options); |
|
| 65 | + } |
|
| 66 | + |
|
| 67 | + /** |
|
| 68 | + * @inheritDoc |
|
| 69 | + * @throws Exception |
|
| 70 | + */ |
|
| 71 | + public static function fromStringMultiple($string, array $translations, array $options = []) |
|
| 72 | + { |
|
| 73 | + $options += static::$options; |
|
| 74 | + $options += [ |
|
| 75 | + // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions) |
|
| 76 | + 'attributePrefixes' => [ |
|
| 77 | + ':', |
|
| 78 | + 'v-bind:', |
|
| 79 | + 'v-on:', |
|
| 80 | + 'v-text', |
|
| 81 | + ], |
|
| 82 | + // HTML Tags to parse |
|
| 83 | + 'tagNames' => [ |
|
| 84 | + 'translate', |
|
| 85 | + ], |
|
| 86 | + // HTML tags to parse when attribute exists |
|
| 87 | + 'tagAttributes' => [ |
|
| 88 | + 'v-translate', |
|
| 89 | + ], |
|
| 90 | + // Comments |
|
| 91 | + 'commentAttributes' => [ |
|
| 92 | + 'translate-comment', |
|
| 93 | + ], |
|
| 94 | + 'contextAttributes' => [ |
|
| 95 | + 'translate-context', |
|
| 96 | + ], |
|
| 97 | + // Attribute with plural content |
|
| 98 | + 'pluralAttributes' => [ |
|
| 99 | + 'translate-plural', |
|
| 100 | + ], |
|
| 101 | + ]; |
|
| 102 | + |
|
| 103 | + // Ok, this is the weirdest hack, but let me explain: |
|
| 104 | + // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag. |
|
| 105 | + // So if there are new lines between <template> and next element, they are lost |
|
| 106 | + // So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements. |
|
| 107 | + // Same thing happens between template and script tag. |
|
| 108 | + $string = str_replace('<template>', '<template>.', $string); |
|
| 109 | + $string = str_replace('</template>', '</template>.', $string); |
|
| 110 | + |
|
| 111 | + // Normalize newlines |
|
| 112 | + $string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string); |
|
| 113 | + |
|
| 114 | + // VueJS files are valid HTML files, we will operate with the DOM here |
|
| 115 | + $dom = static::convertHtmlToDom($string); |
|
| 116 | + |
|
| 117 | + $script = static::extractScriptTag($string); |
|
| 118 | + |
|
| 119 | + // Parse the script part as a regular JS code |
|
| 120 | + if ($script) { |
|
| 121 | + $scriptLineNumber = $dom->getElementsByTagName('script')->item(0)->getLineNo(); |
|
| 122 | + static::getScriptTranslationsFromString( |
|
| 123 | + $script, |
|
| 124 | + $translations, |
|
| 125 | + $options, |
|
| 126 | + $scriptLineNumber - 1 |
|
| 127 | + ); |
|
| 128 | + } |
|
| 129 | + |
|
| 130 | + // Template part is parsed separately, all variables will be extracted |
|
| 131 | + // and handled as a regular JS code |
|
| 132 | + $template = $dom->getElementsByTagName('template')->item(0); |
|
| 133 | + if ($template) { |
|
| 134 | + static::getTemplateTranslations( |
|
| 135 | + $template, |
|
| 136 | + $translations, |
|
| 137 | + $options, |
|
| 138 | + $template->getLineNo() - 1 |
|
| 139 | + ); |
|
| 140 | + } |
|
| 141 | + } |
|
| 142 | + |
|
| 143 | + /** |
|
| 144 | + * Extracts script tag contents using regex instead of DOM operations. |
|
| 145 | + * If we parse using DOM, some contents may change, for example, tags within strings will be stripped |
|
| 146 | + * |
|
| 147 | + * @param $string |
|
| 148 | + * @return bool|string |
|
| 149 | + */ |
|
| 150 | + protected static function extractScriptTag($string) |
|
| 151 | + { |
|
| 152 | + if (preg_match('#<\s*?script\b[^>]*>(.*?)</script\b[^>]*>#s', $string, $matches)) { |
|
| 153 | + return $matches[1]; |
|
| 154 | + } |
|
| 155 | + |
|
| 156 | + return ''; |
|
| 157 | + } |
|
| 158 | + |
|
| 159 | + /** |
|
| 160 | + * @param string $html |
|
| 161 | + * @return DOMDocument |
|
| 162 | + */ |
|
| 163 | + protected static function convertHtmlToDom($html) |
|
| 164 | + { |
|
| 165 | + $dom = new DOMDocument; |
|
| 166 | + |
|
| 167 | + libxml_use_internal_errors(true); |
|
| 168 | + |
|
| 169 | + // Prepend xml encoding so DOMDocument document handles UTF8 correctly. |
|
| 170 | + // Assuming that vue template files will not have any xml encoding tags, because duplicate tags may be ignored. |
|
| 171 | + $dom->loadHTML('<?xml encoding="utf-8"?>' . $html); |
|
| 172 | + |
|
| 173 | + libxml_clear_errors(); |
|
| 174 | + |
|
| 175 | + return $dom; |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + /** |
|
| 179 | + * Extract translations from script part |
|
| 180 | + * |
|
| 181 | + * @param string $scriptContents Only script tag contents, not the whole template |
|
| 182 | + * @param Translations|Translations[] $translations One or multiple domain Translation objects |
|
| 183 | + * @param array $options |
|
| 184 | + * @param int $lineOffset Number of lines the script is offset in the vue template file |
|
| 185 | + * @throws Exception |
|
| 186 | + */ |
|
| 187 | + protected static function getScriptTranslationsFromString( |
|
| 188 | + $scriptContents, |
|
| 189 | + $translations, |
|
| 190 | + array $options = [], |
|
| 191 | + $lineOffset = 0 |
|
| 192 | + ) { |
|
| 193 | + /** @var FunctionsScanner $functions */ |
|
| 194 | + $functions = new static::$functionsScannerClass($scriptContents); |
|
| 195 | + $options['lineOffset'] = $lineOffset; |
|
| 196 | + $functions->saveGettextFunctions($translations, $options); |
|
| 197 | + } |
|
| 198 | + |
|
| 199 | + /** |
|
| 200 | + * Parse template to extract all translations (element content and dynamic element attributes) |
|
| 201 | + * |
|
| 202 | + * @param DOMNode $dom |
|
| 203 | + * @param Translations|Translations[] $translations One or multiple domain Translation objects |
|
| 204 | + * @param array $options |
|
| 205 | + * @param int $lineOffset Line number where the template part starts in the vue file |
|
| 206 | + * @throws Exception |
|
| 207 | + */ |
|
| 208 | + protected static function getTemplateTranslations( |
|
| 209 | + DOMNode $dom, |
|
| 210 | + $translations, |
|
| 211 | + array $options, |
|
| 212 | + $lineOffset = 0 |
|
| 213 | + ) { |
|
| 214 | + // Build a JS string from all template attribute expressions |
|
| 215 | + $fakeAttributeJs = static::getTemplateAttributeFakeJs($options, $dom); |
|
| 216 | + |
|
| 217 | + // 1 line offset is necessary because parent template element was ignored when converting to DOM |
|
| 218 | + static::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset); |
|
| 219 | + |
|
| 220 | + // Build a JS string from template element content expressions |
|
| 221 | + $fakeTemplateJs = static::getTemplateFakeJs($dom); |
|
| 222 | + static::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset); |
|
| 223 | + |
|
| 224 | + static::getTagTranslations($options, $dom, $translations); |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + /** |
|
| 228 | + * @param array $options |
|
| 229 | + * @param DOMNode $dom |
|
| 230 | + * @param Translations|Translations[] $translations |
|
| 231 | + */ |
|
| 232 | + protected static function getTagTranslations(array $options, DOMNode $dom, $translations) |
|
| 233 | + { |
|
| 234 | + // Since tag scanning does not support domains, we always use the first translation given |
|
| 235 | + $translations = is_array($translations) ? reset($translations) : $translations; |
|
| 236 | + |
|
| 237 | + $children = $dom->childNodes; |
|
| 238 | + for ($i = 0; $i < $children->length; $i++) { |
|
| 239 | + $node = $children->item($i); |
|
| 240 | + |
|
| 241 | + if (!($node instanceof DOMElement)) { |
|
| 242 | + continue; |
|
| 243 | + } |
|
| 244 | + |
|
| 245 | + $translatable = false; |
|
| 246 | + |
|
| 247 | + if (in_array($node->tagName, $options['tagNames'], true)) { |
|
| 248 | + $translatable = true; |
|
| 249 | + } |
|
| 250 | + |
|
| 251 | + $attrList = $node->attributes; |
|
| 252 | + $context = null; |
|
| 253 | + $plural = ""; |
|
| 254 | + $comment = null; |
|
| 255 | + |
|
| 256 | + for ($j = 0; $j < $attrList->length; $j++) { |
|
| 257 | + /** @var DOMAttr $domAttr */ |
|
| 258 | + $domAttr = $attrList->item($j); |
|
| 259 | + // Check if this is a dynamic vue attribute |
|
| 260 | + if (in_array($domAttr->name, $options['tagAttributes'])) { |
|
| 261 | + $translatable = true; |
|
| 262 | + } |
|
| 263 | + if (in_array($domAttr->name, $options['contextAttributes'])) { |
|
| 264 | + $context = $domAttr->value; |
|
| 265 | + } |
|
| 266 | + if (in_array($domAttr->name, $options['pluralAttributes'])) { |
|
| 267 | + $plural = $domAttr->value; |
|
| 268 | + } |
|
| 269 | + if (in_array($domAttr->name, $options['commentAttributes'])) { |
|
| 270 | + $comment = $domAttr->value; |
|
| 271 | + } |
|
| 272 | + } |
|
| 273 | + |
|
| 274 | + if ($translatable) { |
|
| 275 | + $translation = $translations->insert($context, trim($node->textContent), $plural); |
|
| 276 | + $translation->addReference($options['file'], $node->getLineNo()); |
|
| 277 | + if ($comment) { |
|
| 278 | + $translation->addExtractedComment($comment); |
|
| 279 | + } |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + if ($node->hasChildNodes()) { |
|
| 283 | + static::getTagTranslations($options, $node, $translations); |
|
| 284 | + } |
|
| 285 | + } |
|
| 286 | + } |
|
| 287 | + |
|
| 288 | + /** |
|
| 289 | + * Extract JS expressions from element attribute bindings (excluding text within elements) |
|
| 290 | + * For example: <span :title="__('extract this')"> skip element content </span> |
|
| 291 | + * |
|
| 292 | + * @param array $options |
|
| 293 | + * @param DOMNode $dom |
|
| 294 | + * @return string JS code |
|
| 295 | + */ |
|
| 296 | + protected static function getTemplateAttributeFakeJs(array $options, DOMNode $dom) |
|
| 297 | + { |
|
| 298 | + $expressionsByLine = static::getVueAttributeExpressions($options['attributePrefixes'], $dom); |
|
| 299 | + |
|
| 300 | + if (empty($expressionsByLine)) { |
|
| 301 | + return ''; |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + $maxLines = max(array_keys($expressionsByLine)); |
|
| 305 | + $fakeJs = ''; |
|
| 306 | + |
|
| 307 | + for ($line = 1; $line <= $maxLines; $line++) { |
|
| 308 | + if (isset($expressionsByLine[$line])) { |
|
| 309 | + $fakeJs .= implode("; ", $expressionsByLine[$line]); |
|
| 310 | + } |
|
| 311 | + $fakeJs .= "\n"; |
|
| 312 | + } |
|
| 313 | + |
|
| 314 | + return $fakeJs; |
|
| 315 | + } |
|
| 316 | + |
|
| 317 | + /** |
|
| 318 | + * Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions |
|
| 319 | + * |
|
| 320 | + * @param array $attributePrefixes List of attribute prefixes we parse as JS (may contain translations) |
|
| 321 | + * @param DOMNode $dom |
|
| 322 | + * @param array $expressionByLine [lineNumber => [jsExpression, ..], ..] |
|
| 323 | + * @return array [lineNumber => [jsExpression, ..], ..] |
|
| 324 | + */ |
|
| 325 | + protected static function getVueAttributeExpressions( |
|
| 326 | + array $attributePrefixes, |
|
| 327 | + DOMNode $dom, |
|
| 328 | + array &$expressionByLine = [] |
|
| 329 | + ) { |
|
| 330 | + $children = $dom->childNodes; |
|
| 331 | + |
|
| 332 | + for ($i = 0; $i < $children->length; $i++) { |
|
| 333 | + $node = $children->item($i); |
|
| 334 | + |
|
| 335 | + if (!($node instanceof DOMElement)) { |
|
| 336 | + continue; |
|
| 337 | + } |
|
| 338 | + $attrList = $node->attributes; |
|
| 339 | + |
|
| 340 | + for ($j = 0; $j < $attrList->length; $j++) { |
|
| 341 | + /** @var DOMAttr $domAttr */ |
|
| 342 | + $domAttr = $attrList->item($j); |
|
| 343 | + |
|
| 344 | + // Check if this is a dynamic vue attribute |
|
| 345 | + if (static::isAttributeMatching($domAttr->name, $attributePrefixes)) { |
|
| 346 | + $line = $domAttr->getLineNo(); |
|
| 347 | + $expressionByLine += [$line => []]; |
|
| 348 | + $expressionByLine[$line][] = $domAttr->value; |
|
| 349 | + } |
|
| 350 | + } |
|
| 351 | + |
|
| 352 | + if ($node->hasChildNodes()) { |
|
| 353 | + $expressionByLine = static::getVueAttributeExpressions($attributePrefixes, $node, $expressionByLine); |
|
| 354 | + } |
|
| 355 | + } |
|
| 356 | + |
|
| 357 | + return $expressionByLine; |
|
| 358 | + } |
|
| 359 | + |
|
| 360 | + /** |
|
| 361 | + * Check if this attribute name should be parsed for translations |
|
| 362 | + * |
|
| 363 | + * @param string $attributeName |
|
| 364 | + * @param string[] $attributePrefixes |
|
| 365 | + * @return bool |
|
| 366 | + */ |
|
| 367 | + protected static function isAttributeMatching($attributeName, $attributePrefixes) |
|
| 368 | + { |
|
| 369 | + foreach ($attributePrefixes as $prefix) { |
|
| 370 | + if (strpos($attributeName, $prefix) === 0) { |
|
| 371 | + return true; |
|
| 372 | + } |
|
| 373 | + } |
|
| 374 | + return false; |
|
| 375 | + } |
|
| 376 | + |
|
| 377 | + /** |
|
| 378 | + * Extract JS expressions from within template elements (excluding attributes) |
|
| 379 | + * For example: <span :title="skip attributes"> {{__("extract element content")}} </span> |
|
| 380 | + * |
|
| 381 | + * @param DOMNode $dom |
|
| 382 | + * @return string JS code |
|
| 383 | + */ |
|
| 384 | + protected static function getTemplateFakeJs(DOMNode $dom) |
|
| 385 | + { |
|
| 386 | + $fakeJs = ''; |
|
| 387 | + $lines = explode("\n", $dom->textContent); |
|
| 388 | + |
|
| 389 | + // Build a fake JS file from template by extracting JS expressions within each template line |
|
| 390 | + foreach ($lines as $line) { |
|
| 391 | + $expressionMatched = static::parseOneTemplateLine($line); |
|
| 392 | + |
|
| 393 | + $fakeJs .= implode("; ", $expressionMatched) . "\n"; |
|
| 394 | + } |
|
| 395 | + |
|
| 396 | + return $fakeJs; |
|
| 397 | + } |
|
| 398 | + |
|
| 399 | + /** |
|
| 400 | + * Match JS expressions in a template line |
|
| 401 | + * |
|
| 402 | + * @param string $line |
|
| 403 | + * @return string[] |
|
| 404 | + */ |
|
| 405 | + protected static function parseOneTemplateLine($line) |
|
| 406 | + { |
|
| 407 | + $line = trim($line); |
|
| 408 | + |
|
| 409 | + if (!$line) { |
|
| 410 | + return []; |
|
| 411 | + } |
|
| 412 | + |
|
| 413 | + $regex = '#\{\{(.*?)\}\}#'; |
|
| 414 | + |
|
| 415 | + preg_match_all($regex, $line, $matches); |
|
| 416 | + |
|
| 417 | + $matched = array_map(function ($v) { |
|
| 418 | + return trim($v, '\'"{}'); |
|
| 419 | + }, $matches[1]); |
|
| 420 | + |
|
| 421 | + return $matched; |
|
| 422 | + } |
|
| 423 | 423 | } |
@@ -8,73 +8,73 @@ |
||
| 8 | 8 | |
| 9 | 9 | abstract class Extractor implements ExtractorInterface |
| 10 | 10 | { |
| 11 | - /** |
|
| 12 | - * {@inheritdoc} |
|
| 13 | - */ |
|
| 14 | - public static function fromFile($file, Translations $translations, array $options = []) |
|
| 15 | - { |
|
| 16 | - foreach (static::getFiles($file) as $file) { |
|
| 17 | - $options['file'] = $file; |
|
| 18 | - static::fromString(static::readFile($file), $translations, $options); |
|
| 19 | - } |
|
| 20 | - } |
|
| 11 | + /** |
|
| 12 | + * {@inheritdoc} |
|
| 13 | + */ |
|
| 14 | + public static function fromFile($file, Translations $translations, array $options = []) |
|
| 15 | + { |
|
| 16 | + foreach (static::getFiles($file) as $file) { |
|
| 17 | + $options['file'] = $file; |
|
| 18 | + static::fromString(static::readFile($file), $translations, $options); |
|
| 19 | + } |
|
| 20 | + } |
|
| 21 | 21 | |
| 22 | - /** |
|
| 23 | - * Checks and returns all files. |
|
| 24 | - * |
|
| 25 | - * @param string|array $file The file/s |
|
| 26 | - * |
|
| 27 | - * @return array The file paths |
|
| 28 | - */ |
|
| 29 | - protected static function getFiles($file) |
|
| 30 | - { |
|
| 31 | - if (empty($file)) { |
|
| 32 | - throw new InvalidArgumentException('There is not any file defined'); |
|
| 33 | - } |
|
| 22 | + /** |
|
| 23 | + * Checks and returns all files. |
|
| 24 | + * |
|
| 25 | + * @param string|array $file The file/s |
|
| 26 | + * |
|
| 27 | + * @return array The file paths |
|
| 28 | + */ |
|
| 29 | + protected static function getFiles($file) |
|
| 30 | + { |
|
| 31 | + if (empty($file)) { |
|
| 32 | + throw new InvalidArgumentException('There is not any file defined'); |
|
| 33 | + } |
|
| 34 | 34 | |
| 35 | - if (is_string($file)) { |
|
| 36 | - if (!is_file($file)) { |
|
| 37 | - throw new InvalidArgumentException("'$file' is not a valid file"); |
|
| 38 | - } |
|
| 35 | + if (is_string($file)) { |
|
| 36 | + if (!is_file($file)) { |
|
| 37 | + throw new InvalidArgumentException("'$file' is not a valid file"); |
|
| 38 | + } |
|
| 39 | 39 | |
| 40 | - if (!is_readable($file)) { |
|
| 41 | - throw new InvalidArgumentException("'$file' is not a readable file"); |
|
| 42 | - } |
|
| 40 | + if (!is_readable($file)) { |
|
| 41 | + throw new InvalidArgumentException("'$file' is not a readable file"); |
|
| 42 | + } |
|
| 43 | 43 | |
| 44 | - return [$file]; |
|
| 45 | - } |
|
| 44 | + return [$file]; |
|
| 45 | + } |
|
| 46 | 46 | |
| 47 | - if (is_array($file)) { |
|
| 48 | - $files = []; |
|
| 47 | + if (is_array($file)) { |
|
| 48 | + $files = []; |
|
| 49 | 49 | |
| 50 | - foreach ($file as $f) { |
|
| 51 | - $files = array_merge($files, static::getFiles($f)); |
|
| 52 | - } |
|
| 50 | + foreach ($file as $f) { |
|
| 51 | + $files = array_merge($files, static::getFiles($f)); |
|
| 52 | + } |
|
| 53 | 53 | |
| 54 | - return $files; |
|
| 55 | - } |
|
| 54 | + return $files; |
|
| 55 | + } |
|
| 56 | 56 | |
| 57 | - throw new InvalidArgumentException('The first argument must be string or array'); |
|
| 58 | - } |
|
| 57 | + throw new InvalidArgumentException('The first argument must be string or array'); |
|
| 58 | + } |
|
| 59 | 59 | |
| 60 | - /** |
|
| 61 | - * Reads and returns the content of a file. |
|
| 62 | - * |
|
| 63 | - * @param string $file |
|
| 64 | - * |
|
| 65 | - * @return string |
|
| 66 | - */ |
|
| 67 | - protected static function readFile($file) |
|
| 68 | - { |
|
| 69 | - $length = filesize($file); |
|
| 60 | + /** |
|
| 61 | + * Reads and returns the content of a file. |
|
| 62 | + * |
|
| 63 | + * @param string $file |
|
| 64 | + * |
|
| 65 | + * @return string |
|
| 66 | + */ |
|
| 67 | + protected static function readFile($file) |
|
| 68 | + { |
|
| 69 | + $length = filesize($file); |
|
| 70 | 70 | |
| 71 | - if (!($fd = fopen($file, 'rb'))) { |
|
| 72 | - throw new Exception("Cannot read the file '$file', probably permissions"); |
|
| 73 | - } |
|
| 71 | + if (!($fd = fopen($file, 'rb'))) { |
|
| 72 | + throw new Exception("Cannot read the file '$file', probably permissions"); |
|
| 73 | + } |
|
| 74 | 74 | |
| 75 | - $content = $length ? fread($fd, $length) : ''; |
|
| 76 | - fclose($fd); |
|
| 75 | + $content = $length ? fread($fd, $length) : ''; |
|
| 76 | + fclose($fd); |
|
| 77 | 77 | |
| 78 | - return $content; |
|
| 79 | - } |
|
| 78 | + return $content; |
|
| 79 | + } |
|
| 80 | 80 | } |
@@ -10,17 +10,17 @@ |
||
| 10 | 10 | */ |
| 11 | 11 | class Json extends Extractor implements ExtractorInterface |
| 12 | 12 | { |
| 13 | - use MultidimensionalArrayTrait; |
|
| 13 | + use MultidimensionalArrayTrait; |
|
| 14 | 14 | |
| 15 | - /** |
|
| 16 | - * {@inheritdoc} |
|
| 17 | - */ |
|
| 18 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 19 | - { |
|
| 20 | - $messages = json_decode($string, true); |
|
| 15 | + /** |
|
| 16 | + * {@inheritdoc} |
|
| 17 | + */ |
|
| 18 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 19 | + { |
|
| 20 | + $messages = json_decode($string, true); |
|
| 21 | 21 | |
| 22 | - if (is_array($messages)) { |
|
| 23 | - static::fromArray($messages, $translations); |
|
| 24 | - } |
|
| 25 | - } |
|
| 22 | + if (is_array($messages)) { |
|
| 23 | + static::fromArray($messages, $translations); |
|
| 24 | + } |
|
| 25 | + } |
|
| 26 | 26 | } |
@@ -11,17 +11,17 @@ |
||
| 11 | 11 | */ |
| 12 | 12 | class Yaml extends Extractor implements ExtractorInterface |
| 13 | 13 | { |
| 14 | - use MultidimensionalArrayTrait; |
|
| 14 | + use MultidimensionalArrayTrait; |
|
| 15 | 15 | |
| 16 | - /** |
|
| 17 | - * {@inheritdoc} |
|
| 18 | - */ |
|
| 19 | - public static function fromString($string, Translations $translations, array $options = []) |
|
| 20 | - { |
|
| 21 | - $messages = YamlParser::parse($string); |
|
| 16 | + /** |
|
| 17 | + * {@inheritdoc} |
|
| 18 | + */ |
|
| 19 | + public static function fromString($string, Translations $translations, array $options = []) |
|
| 20 | + { |
|
| 21 | + $messages = YamlParser::parse($string); |
|
| 22 | 22 | |
| 23 | - if (is_array($messages)) { |
|
| 24 | - static::fromArray($messages, $translations); |
|
| 25 | - } |
|
| 26 | - } |
|
| 23 | + if (is_array($messages)) { |
|
| 24 | + static::fromArray($messages, $translations); |
|
| 25 | + } |
|
| 26 | + } |
|
| 27 | 27 | } |