efabrica-team /
translatte
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace Efabrica\Translatte; |
||
| 6 | |||
| 7 | use Efabrica\Translatte\Cache\ICache; |
||
| 8 | use Efabrica\Translatte\Cache\NullCache; |
||
| 9 | use Efabrica\Translatte\Record\NullRecord; |
||
| 10 | use Efabrica\Translatte\Record\RecordInterface; |
||
| 11 | use Efabrica\Translatte\Resolver\IResolver; |
||
| 12 | use Efabrica\Translatte\Resolver\StaticResolver; |
||
| 13 | use Efabrica\Translatte\Resource\IResource; |
||
| 14 | use InvalidArgumentException; |
||
| 15 | use Nette\Localization\ITranslator; |
||
| 16 | use Nette\Utils\Arrays; |
||
| 17 | |||
| 18 | class Translator implements ITranslator |
||
|
0 ignored issues
–
show
|
|||
| 19 | { |
||
| 20 | public const PLURAL_DELIMITER = '|'; |
||
| 21 | public const PLURAL_DELIMITER_ESCAPED = '\|'; |
||
| 22 | public const PLURAL_DELIMITER_TMP = '_PLURAL_DELIMITER_'; |
||
| 23 | |||
| 24 | /** @var string */ |
||
| 25 | private $defaultLang; |
||
| 26 | |||
| 27 | /** @var string */ |
||
| 28 | private $lang; |
||
| 29 | |||
| 30 | /** @var IResolver */ |
||
| 31 | private $resolver; |
||
| 32 | |||
| 33 | /** @var ICache */ |
||
| 34 | private $cache; |
||
| 35 | |||
| 36 | /** @var IResource[] */ |
||
| 37 | private $resources = []; |
||
| 38 | 15 | ||
| 39 | /** @var array */ |
||
| 40 | private $fallbackLanguages = []; |
||
| 41 | |||
| 42 | /** @var Dictionary[] */ |
||
| 43 | 15 | private $dictionaries = []; |
|
| 44 | 15 | ||
| 45 | 15 | /** @var array */ |
|
| 46 | 15 | public $onTranslate = []; |
|
| 47 | |||
| 48 | /** @var RecordInterface */ |
||
| 49 | private $recordTranslate; |
||
| 50 | |||
| 51 | public function __construct( |
||
| 52 | string $defaultLang, |
||
| 53 | IResolver $resolver = null, |
||
| 54 | ICache $cache = null, |
||
| 55 | RecordInterface $recordTranslate = null |
||
| 56 | ) { |
||
| 57 | $this->defaultLang = $defaultLang; |
||
| 58 | $this->resolver = $resolver ?: new StaticResolver($defaultLang); |
||
| 59 | $this->cache = $cache ?: new NullCache(); |
||
| 60 | $this->recordTranslate = $recordTranslate ?: new NullRecord(); |
||
| 61 | } |
||
| 62 | 15 | ||
| 63 | /** |
||
| 64 | 15 | * Fallback languages will be used as waterfall, first with valid result is used |
|
| 65 | 15 | * @param array $fallbackLanguages |
|
| 66 | */ |
||
| 67 | public function setFallbackLanguages(array $fallbackLanguages): void |
||
| 68 | { |
||
| 69 | $this->fallbackLanguages = $fallbackLanguages; |
||
| 70 | } |
||
| 71 | |||
| 72 | /** |
||
| 73 | * Add new resource to parse translations from |
||
| 74 | 15 | * @param IResource $resource |
|
| 75 | * @return Translator |
||
| 76 | */ |
||
| 77 | public function addResource(IResource $resource): self |
||
| 78 | { |
||
| 79 | 15 | $this->resources[] = $resource; |
|
| 80 | 15 | return $this; |
|
| 81 | } |
||
| 82 | |||
| 83 | 15 | /** |
|
| 84 | * Provide translation |
||
| 85 | * @param string|int $message |
||
| 86 | * @param mixed ...$parameters |
||
| 87 | 15 | * @return string |
|
| 88 | 15 | */ |
|
| 89 | public function translate($message, ...$parameters): string |
||
| 90 | { |
||
| 91 | // translate($message, int $count, array $params, string $lang = null) |
||
| 92 | // translate($message, array $params, string $lang = null) |
||
| 93 | |||
| 94 | $message = (string)$message; |
||
| 95 | |||
| 96 | $this->recordTranslate->save($message); |
||
| 97 | |||
| 98 | list($count, $params, $lang) = array_values($this->parseParameters($parameters)); |
||
| 99 | |||
| 100 | // If wrong input arguments passed, return message key |
||
| 101 | if (!is_int($count) || !is_array($params) || !is_string($lang)) { |
||
| 102 | Arrays::invoke($this->onTranslate, $this, $message, $message, $lang, (int)strval($count), (array)$params); |
||
| 103 | 15 | return $message; // @ maybe throw exception? |
|
| 104 | 15 | } |
|
| 105 | |||
| 106 | 15 | $translation = $this->getDictionary($lang)->findTranslation($message); |
|
| 107 | |||
| 108 | if ($translation === null) { |
||
| 109 | // Try find translation in fallback languages |
||
| 110 | foreach ($this->fallbackLanguages as $fallbackLanguage) { |
||
| 111 | $translation = $this->getDictionary($fallbackLanguage)->findTranslation($message); |
||
| 112 | if ($translation !== null) { |
||
| 113 | break; |
||
| 114 | } |
||
| 115 | } |
||
| 116 | 15 | ||
| 117 | // If translation not found in base either fallback languages return message key |
||
| 118 | 15 | if ($translation === null) { |
|
| 119 | 15 | Arrays::invoke($this->onTranslate, $this, $message, $translation, $lang, $count, $params); |
|
| 120 | 6 | return $message; |
|
| 121 | } |
||
| 122 | } |
||
| 123 | 9 | ||
| 124 | 9 | $translation = $this->fixEscapedDelimiter($translation); |
|
| 125 | $translation = $this->selectRightPluralForm($translation, $lang, $count); |
||
| 126 | $translation = $this->fixBackEscapedDelimiter($translation); |
||
| 127 | $translation = $this->replaceParams($translation, $params); |
||
| 128 | |||
| 129 | Arrays::invoke($this->onTranslate, $this, $message, $translation, $lang, $count, $params); |
||
| 130 | return $translation; |
||
| 131 | } |
||
| 132 | |||
| 133 | 15 | /** |
|
| 134 | * Select right plural form based on selected language |
||
| 135 | 15 | * @param string $translation |
|
| 136 | 15 | * @param string $lang |
|
| 137 | 15 | * @param int $count |
|
| 138 | * @return string |
||
| 139 | */ |
||
| 140 | 15 | private function selectRightPluralForm(string $translation, string $lang, int $count): string |
|
| 141 | { |
||
| 142 | $exploded = explode('|', $translation); |
||
| 143 | if (count($exploded) === 1) { |
||
| 144 | return $translation; |
||
| 145 | } |
||
| 146 | foreach ($exploded as $value) { |
||
| 147 | $translationPlural = $this->findSpecialFormat($value, $count); |
||
| 148 | 15 | if ($translationPlural !== null) { |
|
| 149 | return $translationPlural; |
||
| 150 | 15 | } |
|
| 151 | } |
||
| 152 | 9 | $pluralForm = PluralForm::get($count, $lang); |
|
| 153 | return $exploded[$pluralForm] ?? $exploded[0]; |
||
| 154 | 9 | } |
|
| 155 | |||
| 156 | private function fixEscapedDelimiter(string $translation): string |
||
| 157 | { |
||
| 158 | 15 | return str_replace(self::PLURAL_DELIMITER_ESCAPED, self::PLURAL_DELIMITER_TMP, $translation); |
|
| 159 | } |
||
| 160 | 12 | ||
| 161 | 12 | private function fixBackEscapedDelimiter(string $translation): string |
|
| 162 | 12 | { |
|
| 163 | return str_replace(self::PLURAL_DELIMITER_TMP, self::PLURAL_DELIMITER, $translation); |
||
| 164 | } |
||
| 165 | |||
| 166 | 15 | private function findSpecialFormat(string $translationForm, int $count): ?string |
|
| 167 | 15 | { |
|
| 168 | 15 | preg_match('/^\{ *[\d, ]+ *\}/', $translationForm, $result); |
|
| 169 | $match = reset($result); |
||
| 170 | if (!empty($match)) { |
||
| 171 | $translationForm = str_replace($match, '', $translationForm); |
||
| 172 | 15 | $match = str_replace(['{', '}', ' '], '', trim($match)); |
|
| 173 | 15 | $foundCountArray = explode(',', $match); |
|
| 174 | 15 | foreach ($foundCountArray as $foundCount) { |
|
| 175 | if ($count === (int)$foundCount) { |
||
| 176 | return $translationForm; |
||
| 177 | } |
||
| 178 | } |
||
| 179 | } |
||
| 180 | preg_match('/^[\[,\]] *[+,-]? *[\d,Inf]+ *, *[+,-]? *[\d,Inf]+ *[\[,\]]/', $translationForm, $result); |
||
| 181 | $match = reset($result); |
||
| 182 | 15 | if (!empty($match)) { |
|
| 183 | $startChar = substr($match, 0, 1); |
||
| 184 | 15 | $endChar = substr($match, -1); |
|
| 185 | 15 | $translationForm = str_replace($match, '', $translationForm); |
|
| 186 | 15 | $range = substr($match, 1, strlen($match) - 2); |
|
| 187 | $rangeArray = explode(',', $range); |
||
| 188 | 15 | $fromRaw = str_replace(' ', '', $rangeArray[0]); |
|
| 189 | $toRaw = str_replace(' ', '', $rangeArray[1]); |
||
| 190 | $from = $fromRaw === '-Inf' ? PHP_INT_MIN : (int)$fromRaw; |
||
| 191 | $to = ($toRaw === 'Inf' || $toRaw === '+Inf') ? PHP_INT_MAX : (int)$toRaw; |
||
| 192 | if (($startChar === '[' && $endChar === ']' && $from <= $count && $count <= $to) || |
||
| 193 | ($startChar === ']' && $endChar === ']' && $from < $count && $count <= $to) || |
||
| 194 | ($startChar === ']' && $endChar === '[' && $from < $count && $count < $to) || |
||
| 195 | ($startChar === '[' && $endChar === '[' && $from <= $count && $count < $to) |
||
| 196 | 15 | ) { |
|
| 197 | return $translationForm; |
||
| 198 | 15 | } |
|
| 199 | 12 | } |
|
| 200 | return null; |
||
| 201 | } |
||
| 202 | 15 | ||
| 203 | 15 | /** |
|
| 204 | * Replace parameters in translation string |
||
| 205 | * @param string $translation |
||
| 206 | * @param array $params |
||
| 207 | * @return string |
||
| 208 | 15 | */ |
|
| 209 | 15 | private function replaceParams(string $translation, array $params): string |
|
| 210 | 15 | { |
|
| 211 | 15 | $transParams = []; |
|
| 212 | 15 | foreach ($params as $key => $value) { |
|
| 213 | $transParams['%' . $key . '%'] = $value; |
||
| 214 | } |
||
| 215 | 15 | ||
| 216 | return strtr($translation, $transParams); |
||
| 217 | } |
||
| 218 | |||
| 219 | 15 | /** |
|
| 220 | * Parse translation input parameters |
||
| 221 | * @param array $parameters |
||
| 222 | * @return array |
||
| 223 | */ |
||
| 224 | private function parseParameters(array $parameters): array |
||
| 225 | { |
||
| 226 | if (count($parameters) === 0) { |
||
| 227 | return [ |
||
| 228 | 'count' => 1, |
||
| 229 | 'params' => ['count' => 1], |
||
| 230 | 'lang' => $this->getResolvedLang(), |
||
| 231 | ]; |
||
| 232 | } |
||
| 233 | |||
| 234 | if (is_array($parameters[0])) { |
||
| 235 | return [ |
||
| 236 | 'count' => isset($parameters[0]['count']) ? $parameters[0]['count'] : 1, |
||
| 237 | 'params' => $parameters[0], |
||
| 238 | 'lang' => array_key_exists(1, $parameters) ? $parameters[1] : $this->getResolvedLang(), |
||
| 239 | ]; |
||
| 240 | } |
||
| 241 | |||
| 242 | $params = array_key_exists(1, $parameters) ? $parameters[1] : []; |
||
| 243 | if ($parameters[0] && !isset($params['count'])) { |
||
| 244 | $params['count'] = $parameters[0]; |
||
| 245 | } |
||
| 246 | |||
| 247 | return [ |
||
| 248 | 'count' => $parameters[0] ?? 1, |
||
| 249 | 'params' => $params, |
||
| 250 | 'lang' => array_key_exists(2, $parameters) ? $parameters[2] : $this->getResolvedLang(), |
||
| 251 | ]; |
||
| 252 | } |
||
| 253 | |||
| 254 | /** |
||
| 255 | * Use resolvers to resolve language to use |
||
| 256 | * @return string |
||
| 257 | */ |
||
| 258 | private function getResolvedLang(): string |
||
| 259 | { |
||
| 260 | if ($this->lang === null) { |
||
| 261 | $resolvedLang = $this->resolver->resolve(); |
||
| 262 | $this->lang = $resolvedLang !== null ? $resolvedLang : $this->defaultLang; |
||
| 263 | } |
||
| 264 | return $this->lang; |
||
| 265 | } |
||
| 266 | |||
| 267 | /** |
||
| 268 | * Prepare and return dictionary ready to use |
||
| 269 | * @param string $lang |
||
| 270 | * @return Dictionary |
||
| 271 | */ |
||
| 272 | private function getDictionary(string $lang): Dictionary |
||
| 273 | { |
||
| 274 | if (array_key_exists($lang, $this->dictionaries)) { |
||
| 275 | return $this->dictionaries[$lang]; |
||
| 276 | } |
||
| 277 | |||
| 278 | $dictionaryCache = $this->cache->load($lang); |
||
| 279 | if ($dictionaryCache !== null) { |
||
| 280 | $this->dictionaries[$lang] = $dictionaryCache; |
||
| 281 | return $this->dictionaries[$lang]; |
||
| 282 | } |
||
| 283 | |||
| 284 | $this->dictionaries[$lang] = new Dictionary($lang); |
||
| 285 | foreach ($this->resources as $resource) { |
||
| 286 | $dictionaries = $resource->load($lang); |
||
| 287 | foreach ($dictionaries as $dictionary) { |
||
| 288 | if (!$dictionary instanceof Dictionary) { |
||
| 289 | throw new InvalidArgumentException(sprintf('%s expected. Resource returned %s', Dictionary::class, get_class($dictionary))); |
||
| 290 | } |
||
| 291 | $this->dictionaries[$lang]->extend($dictionary); |
||
| 292 | } |
||
| 293 | } |
||
| 294 | $this->cache->store($lang, $this->dictionaries[$lang]->getRecords()); |
||
| 295 | |||
| 296 | return $this->dictionaries[$lang]; |
||
| 297 | } |
||
| 298 | |||
| 299 | public function reset(): void |
||
| 300 | { |
||
| 301 | $this->resources = []; |
||
| 302 | $this->dictionaries = []; |
||
| 303 | } |
||
| 304 | } |
||
| 305 |
This interface has been deprecated. The supplier of the interface has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the interface will be removed and what other interface to use instead.