Translator.php 12KB

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