Translator.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. <?php
  2. namespace Illuminate\Translation;
  3. use Closure;
  4. use Illuminate\Contracts\Translation\Loader;
  5. use Illuminate\Contracts\Translation\Translator as TranslatorContract;
  6. use Illuminate\Support\Arr;
  7. use Illuminate\Support\NamespacedItemResolver;
  8. use Illuminate\Support\Str;
  9. use Illuminate\Support\Traits\Macroable;
  10. use Illuminate\Support\Traits\ReflectsClosures;
  11. use InvalidArgumentException;
  12. use function Illuminate\Support\enum_value;
  13. class Translator extends NamespacedItemResolver implements TranslatorContract
  14. {
  15. use Macroable, ReflectsClosures;
  16. /**
  17. * The loader implementation.
  18. *
  19. * @var \Illuminate\Contracts\Translation\Loader
  20. */
  21. protected $loader;
  22. /**
  23. * The default locale being used by the translator.
  24. *
  25. * @var string
  26. */
  27. protected $locale;
  28. /**
  29. * The fallback locale used by the translator.
  30. *
  31. * @var string
  32. */
  33. protected $fallback;
  34. /**
  35. * The array of loaded translation groups.
  36. *
  37. * @var array
  38. */
  39. protected $loaded = [];
  40. /**
  41. * The message selector.
  42. *
  43. * @var \Illuminate\Translation\MessageSelector
  44. */
  45. protected $selector;
  46. /**
  47. * The callable that should be invoked to determine applicable locales.
  48. *
  49. * @var callable
  50. */
  51. protected $determineLocalesUsing;
  52. /**
  53. * The custom rendering callbacks for stringable objects.
  54. *
  55. * @var array
  56. */
  57. protected $stringableHandlers = [];
  58. /**
  59. * The callback that is responsible for handling missing translation keys.
  60. *
  61. * @var callable|null
  62. */
  63. protected $missingTranslationKeyCallback;
  64. /**
  65. * Indicates whether missing translation keys should be handled.
  66. *
  67. * @var bool
  68. */
  69. protected $handleMissingTranslationKeys = true;
  70. /**
  71. * Create a new translator instance.
  72. *
  73. * @param \Illuminate\Contracts\Translation\Loader $loader
  74. * @param string $locale
  75. */
  76. public function __construct(Loader $loader, $locale)
  77. {
  78. $this->loader = $loader;
  79. $this->setLocale($locale);
  80. }
  81. /**
  82. * Determine if a translation exists for a given locale.
  83. *
  84. * @param string $key
  85. * @param string|null $locale
  86. * @return bool
  87. */
  88. public function hasForLocale($key, $locale = null)
  89. {
  90. return $this->has($key, $locale, false);
  91. }
  92. /**
  93. * Determine if a translation exists.
  94. *
  95. * @param string $key
  96. * @param string|null $locale
  97. * @param bool $fallback
  98. * @return bool
  99. */
  100. public function has($key, $locale = null, $fallback = true)
  101. {
  102. $locale = $locale ?: $this->locale;
  103. // We should temporarily disable the handling of missing translation keys
  104. // while performing the existence check. After the check, we will turn
  105. // the missing translation keys handling back to its original value.
  106. $handleMissingTranslationKeys = $this->handleMissingTranslationKeys;
  107. $this->handleMissingTranslationKeys = false;
  108. $line = $this->get($key, [], $locale, $fallback);
  109. $this->handleMissingTranslationKeys = $handleMissingTranslationKeys;
  110. // For JSON translations, the loaded files will contain the correct line.
  111. // Otherwise, we must assume we are handling typical translation file
  112. // and check if the returned line is not the same as the given key.
  113. if (! is_null($this->loaded['*']['*'][$locale][$key] ?? null)) {
  114. return true;
  115. }
  116. return $line !== $key;
  117. }
  118. /**
  119. * Get the translation for the given key.
  120. *
  121. * @param string $key
  122. * @param array $replace
  123. * @param string|null $locale
  124. * @param bool $fallback
  125. * @return string|array
  126. */
  127. public function get($key, array $replace = [], $locale = null, $fallback = true)
  128. {
  129. $locale = $locale ?: $this->locale;
  130. // For JSON translations, there is only one file per locale, so we will simply load
  131. // that file and then we will be ready to check the array for the key. These are
  132. // only one level deep so we do not need to do any fancy searching through it.
  133. $this->load('*', '*', $locale);
  134. $line = $this->loaded['*']['*'][$locale][$key] ?? null;
  135. // If we can't find a translation for the JSON key, we will attempt to translate it
  136. // using the typical translation file. This way developers can always just use a
  137. // helper such as __ instead of having to pick between trans or __ with views.
  138. if (! isset($line)) {
  139. [$namespace, $group, $item] = $this->parseKey($key);
  140. // Here we will get the locale that should be used for the language line. If one
  141. // was not passed, we will use the default locales which was given to us when
  142. // the translator was instantiated. Then, we can load the lines and return.
  143. $locales = $fallback ? $this->localeArray($locale) : [$locale];
  144. foreach ($locales as $languageLineLocale) {
  145. if (! is_null($line = $this->getLine(
  146. $namespace, $group, $languageLineLocale, $item, $replace
  147. ))) {
  148. return $line;
  149. }
  150. }
  151. $key = $this->handleMissingTranslationKey(
  152. $key, $replace, $locale, $fallback
  153. );
  154. }
  155. // If the line doesn't exist, we will return back the key which was requested as
  156. // that will be quick to spot in the UI if language keys are wrong or missing
  157. // from the application's language files. Otherwise we can return the line.
  158. return $this->makeReplacements($line ?: $key, $replace);
  159. }
  160. /**
  161. * Get a translation according to an integer value.
  162. *
  163. * @param string $key
  164. * @param \Countable|int|float|array $number
  165. * @param array $replace
  166. * @param string|null $locale
  167. * @return string
  168. */
  169. public function choice($key, $number, array $replace = [], $locale = null)
  170. {
  171. $line = $this->get(
  172. $key, [], $locale = $this->localeForChoice($key, $locale)
  173. );
  174. // If the given "number" is actually an array or countable we will simply count the
  175. // number of elements in an instance. This allows developers to pass an array of
  176. // items without having to count it on their end first which gives bad syntax.
  177. if (is_countable($number)) {
  178. $number = count($number);
  179. }
  180. if (! isset($replace['count'])) {
  181. $replace['count'] = $number;
  182. }
  183. return $this->makeReplacements(
  184. $this->getSelector()->choose($line, $number, $locale), $replace
  185. );
  186. }
  187. /**
  188. * Get the proper locale for a choice operation.
  189. *
  190. * @param string $key
  191. * @param string|null $locale
  192. * @return string
  193. */
  194. protected function localeForChoice($key, $locale)
  195. {
  196. $locale = $locale ?: $this->locale;
  197. return $this->hasForLocale($key, $locale) ? $locale : $this->fallback;
  198. }
  199. /**
  200. * Retrieve a language line out the loaded array.
  201. *
  202. * @param string $namespace
  203. * @param string $group
  204. * @param string $locale
  205. * @param string $item
  206. * @param array $replace
  207. * @return string|array|null
  208. */
  209. protected function getLine($namespace, $group, $locale, $item, array $replace)
  210. {
  211. $this->load($namespace, $group, $locale);
  212. $line = Arr::get($this->loaded[$namespace][$group][$locale], $item);
  213. if (is_string($line)) {
  214. return $this->makeReplacements($line, $replace);
  215. } elseif (is_array($line) && count($line) > 0) {
  216. array_walk_recursive($line, function (&$value, $key) use ($replace) {
  217. $value = $this->makeReplacements($value, $replace);
  218. });
  219. return $line;
  220. }
  221. }
  222. /**
  223. * Make the place-holder replacements on a line.
  224. *
  225. * @param string $line
  226. * @param array $replace
  227. * @return string
  228. */
  229. protected function makeReplacements($line, array $replace)
  230. {
  231. if (empty($replace)) {
  232. return $line;
  233. }
  234. $shouldReplace = [];
  235. foreach ($replace as $key => $value) {
  236. if ($value instanceof Closure) {
  237. $line = preg_replace_callback(
  238. '/<'.$key.'>(.*?)<\/'.$key.'>/',
  239. fn ($args) => $value($args[1]),
  240. $line
  241. );
  242. continue;
  243. }
  244. if (is_object($value)) {
  245. $value = isset($this->stringableHandlers[get_class($value)])
  246. ? call_user_func($this->stringableHandlers[get_class($value)], $value)
  247. : enum_value($value);
  248. }
  249. $shouldReplace[':'.Str::ucfirst($key)] = Str::ucfirst($value ?? '');
  250. $shouldReplace[':'.Str::upper($key)] = Str::upper($value ?? '');
  251. $shouldReplace[':'.$key] = $value;
  252. }
  253. return strtr($line, $shouldReplace);
  254. }
  255. /**
  256. * Add translation lines to the given locale.
  257. *
  258. * @param array $lines
  259. * @param string $locale
  260. * @param string $namespace
  261. * @return void
  262. */
  263. public function addLines(array $lines, $locale, $namespace = '*')
  264. {
  265. foreach ($lines as $key => $value) {
  266. [$group, $item] = explode('.', $key, 2);
  267. Arr::set($this->loaded, "$namespace.$group.$locale.$item", $value);
  268. }
  269. }
  270. /**
  271. * Load the specified language group.
  272. *
  273. * @param string $namespace
  274. * @param string $group
  275. * @param string $locale
  276. * @return void
  277. */
  278. public function load($namespace, $group, $locale)
  279. {
  280. if ($this->isLoaded($namespace, $group, $locale)) {
  281. return;
  282. }
  283. // The loader is responsible for returning the array of language lines for the
  284. // given namespace, group, and locale. We'll set the lines in this array of
  285. // lines that have already been loaded so that we can easily access them.
  286. $lines = $this->loader->load($locale, $group, $namespace);
  287. $this->loaded[$namespace][$group][$locale] = $lines;
  288. }
  289. /**
  290. * Determine if the given group has been loaded.
  291. *
  292. * @param string $namespace
  293. * @param string $group
  294. * @param string $locale
  295. * @return bool
  296. */
  297. protected function isLoaded($namespace, $group, $locale)
  298. {
  299. return isset($this->loaded[$namespace][$group][$locale]);
  300. }
  301. /**
  302. * Handle a missing translation key.
  303. *
  304. * @param string $key
  305. * @param array $replace
  306. * @param string|null $locale
  307. * @param bool $fallback
  308. * @return string
  309. */
  310. protected function handleMissingTranslationKey($key, $replace, $locale, $fallback)
  311. {
  312. if (! $this->handleMissingTranslationKeys ||
  313. ! isset($this->missingTranslationKeyCallback)) {
  314. return $key;
  315. }
  316. // Prevent infinite loops...
  317. $this->handleMissingTranslationKeys = false;
  318. $key = call_user_func(
  319. $this->missingTranslationKeyCallback,
  320. $key, $replace, $locale, $fallback
  321. ) ?? $key;
  322. $this->handleMissingTranslationKeys = true;
  323. return $key;
  324. }
  325. /**
  326. * Register a callback that is responsible for handling missing translation keys.
  327. *
  328. * @param callable|null $callback
  329. * @return static
  330. */
  331. public function handleMissingKeysUsing(?callable $callback)
  332. {
  333. $this->missingTranslationKeyCallback = $callback;
  334. return $this;
  335. }
  336. /**
  337. * Add a new namespace to the loader.
  338. *
  339. * @param string $namespace
  340. * @param string $hint
  341. * @return void
  342. */
  343. public function addNamespace($namespace, $hint)
  344. {
  345. $this->loader->addNamespace($namespace, $hint);
  346. }
  347. /**
  348. * Add a new path to the loader.
  349. *
  350. * @param string $path
  351. * @return void
  352. */
  353. public function addPath($path)
  354. {
  355. $this->loader->addPath($path);
  356. }
  357. /**
  358. * Add a new JSON path to the loader.
  359. *
  360. * @param string $path
  361. * @return void
  362. */
  363. public function addJsonPath($path)
  364. {
  365. $this->loader->addJsonPath($path);
  366. }
  367. /**
  368. * Parse a key into namespace, group, and item.
  369. *
  370. * @param string $key
  371. * @return array
  372. */
  373. public function parseKey($key)
  374. {
  375. $segments = parent::parseKey($key);
  376. if (is_null($segments[0])) {
  377. $segments[0] = '*';
  378. }
  379. return $segments;
  380. }
  381. /**
  382. * Get the array of locales to be checked.
  383. *
  384. * @param string|null $locale
  385. * @return array
  386. */
  387. protected function localeArray($locale)
  388. {
  389. $locales = array_filter([$locale ?: $this->locale, $this->fallback]);
  390. $determined = call_user_func($this->determineLocalesUsing ?: fn () => $locales, $locales);
  391. return array_values(array_unique($determined));
  392. }
  393. /**
  394. * Specify a callback that should be invoked to determined the applicable locale array.
  395. *
  396. * @param callable $callback
  397. * @return void
  398. */
  399. public function determineLocalesUsing($callback)
  400. {
  401. $this->determineLocalesUsing = $callback;
  402. }
  403. /**
  404. * Get the message selector instance.
  405. *
  406. * @return \Illuminate\Translation\MessageSelector
  407. */
  408. public function getSelector()
  409. {
  410. if (! isset($this->selector)) {
  411. $this->selector = new MessageSelector;
  412. }
  413. return $this->selector;
  414. }
  415. /**
  416. * Set the message selector instance.
  417. *
  418. * @param \Illuminate\Translation\MessageSelector $selector
  419. * @return void
  420. */
  421. public function setSelector(MessageSelector $selector)
  422. {
  423. $this->selector = $selector;
  424. }
  425. /**
  426. * Get the language line loader implementation.
  427. *
  428. * @return \Illuminate\Contracts\Translation\Loader
  429. */
  430. public function getLoader()
  431. {
  432. return $this->loader;
  433. }
  434. /**
  435. * Get the default locale being used.
  436. *
  437. * @return string
  438. */
  439. public function locale()
  440. {
  441. return $this->getLocale();
  442. }
  443. /**
  444. * Get the default locale being used.
  445. *
  446. * @return string
  447. */
  448. public function getLocale()
  449. {
  450. return $this->locale;
  451. }
  452. /**
  453. * Set the default locale.
  454. *
  455. * @param string $locale
  456. * @return void
  457. *
  458. * @throws \InvalidArgumentException
  459. */
  460. public function setLocale($locale)
  461. {
  462. if (Str::contains($locale, ['/', '\\'])) {
  463. throw new InvalidArgumentException('Invalid characters present in locale.');
  464. }
  465. $this->locale = $locale;
  466. }
  467. /**
  468. * Get the fallback locale being used.
  469. *
  470. * @return string
  471. */
  472. public function getFallback()
  473. {
  474. return $this->fallback;
  475. }
  476. /**
  477. * Set the fallback locale being used.
  478. *
  479. * @param string $fallback
  480. * @return void
  481. */
  482. public function setFallback($fallback)
  483. {
  484. $this->fallback = $fallback;
  485. }
  486. /**
  487. * Set the loaded translation groups.
  488. *
  489. * @param array $loaded
  490. * @return void
  491. */
  492. public function setLoaded(array $loaded)
  493. {
  494. $this->loaded = $loaded;
  495. }
  496. /**
  497. * Add a handler to be executed in order to format a given class to a string during translation replacements.
  498. *
  499. * @param callable|string $class
  500. * @param callable|null $handler
  501. * @return void
  502. */
  503. public function stringable($class, $handler = null)
  504. {
  505. if ($class instanceof Closure) {
  506. [$class, $handler] = [
  507. $this->firstClosureParameterType($class),
  508. $class,
  509. ];
  510. }
  511. $this->stringableHandlers[$class] = $handler;
  512. }
  513. }