1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\I18n;
16:
17: use Aura\Intl\Exception;
18: use Aura\Intl\FormatterLocator;
19: use Aura\Intl\PackageLocator;
20: use Aura\Intl\TranslatorLocator;
21: use Cake\Cache\CacheEngine;
22:
23: /**
24: * Constructs and stores instances of translators that can be
25: * retrieved by name and locale.
26: */
27: class TranslatorRegistry extends TranslatorLocator
28: {
29: /**
30: * A list of loader functions indexed by domain name. Loaders are
31: * callables that are invoked as a default for building translation
32: * packages where none can be found for the combination of translator
33: * name and locale.
34: *
35: * @var callable[]
36: */
37: protected $_loaders = [];
38:
39: /**
40: * Fallback loader name
41: *
42: * @var string
43: */
44: protected $_fallbackLoader = '_fallback';
45:
46: /**
47: * The name of the default formatter to use for newly created
48: * translators from the fallback loader
49: *
50: * @var string
51: */
52: protected $_defaultFormatter = 'default';
53:
54: /**
55: * Use fallback-domain for translation loaders.
56: *
57: * @var bool
58: */
59: protected $_useFallback = true;
60:
61: /**
62: * A CacheEngine object that is used to remember translator across
63: * requests.
64: *
65: * @var \Cake\Cache\CacheEngine
66: */
67: protected $_cacher;
68:
69: /**
70: * Constructor.
71: *
72: * @param \Aura\Intl\PackageLocator $packages The package locator.
73: * @param \Aura\Intl\FormatterLocator $formatters The formatter locator.
74: * @param \Cake\I18n\TranslatorFactory $factory A translator factory to
75: * create translator objects for the locale and package.
76: * @param string $locale The default locale code to use.
77: */
78: public function __construct(
79: PackageLocator $packages,
80: FormatterLocator $formatters,
81: TranslatorFactory $factory,
82: $locale
83: ) {
84: parent::__construct($packages, $formatters, $factory, $locale);
85:
86: $this->registerLoader($this->_fallbackLoader, function ($name, $locale) {
87: $chain = new ChainMessagesLoader([
88: new MessagesFileLoader($name, $locale, 'mo'),
89: new MessagesFileLoader($name, $locale, 'po')
90: ]);
91:
92: // \Aura\Intl\Package by default uses formatter configured with key "basic".
93: // and we want to make sure the cake domain always uses the default formatter
94: $formatter = $name === 'cake' ? 'default' : $this->_defaultFormatter;
95: $chain = function () use ($formatter, $chain) {
96: $package = $chain();
97: $package->setFormatter($formatter);
98:
99: return $package;
100: };
101:
102: return $chain;
103: });
104: }
105:
106: /**
107: * Sets the CacheEngine instance used to remember translators across
108: * requests.
109: *
110: * @param \Cake\Cache\CacheEngine $cacher The cacher instance.
111: * @return void
112: */
113: public function setCacher(CacheEngine $cacher)
114: {
115: $this->_cacher = $cacher;
116: }
117:
118: /**
119: * Gets a translator from the registry by package for a locale.
120: *
121: * @param string $name The translator package to retrieve.
122: * @param string|null $locale The locale to use; if empty, uses the default
123: * locale.
124: * @return \Aura\Intl\TranslatorInterface|null A translator object.
125: * @throws \Aura\Intl\Exception If no translator with that name could be found
126: * for the given locale.
127: */
128: public function get($name, $locale = null)
129: {
130: if (!$name) {
131: return null;
132: }
133:
134: if ($locale === null) {
135: $locale = $this->getLocale();
136: }
137:
138: if (isset($this->registry[$name][$locale])) {
139: return $this->registry[$name][$locale];
140: }
141:
142: if (!$this->_cacher) {
143: return $this->registry[$name][$locale] = $this->_getTranslator($name, $locale);
144: }
145:
146: $key = "translations.$name.$locale";
147: $translator = $this->_cacher->read($key);
148: if (!$translator || !$translator->getPackage()) {
149: $translator = $this->_getTranslator($name, $locale);
150: $this->_cacher->write($key, $translator);
151: }
152:
153: return $this->registry[$name][$locale] = $translator;
154: }
155:
156: /**
157: * Gets a translator from the registry by package for a locale.
158: *
159: * @param string $name The translator package to retrieve.
160: * @param string|null $locale The locale to use; if empty, uses the default
161: * locale.
162: * @return \Aura\Intl\TranslatorInterface A translator object.
163: */
164: protected function _getTranslator($name, $locale)
165: {
166: try {
167: return parent::get($name, $locale);
168: } catch (Exception $e) {
169: }
170:
171: if (!isset($this->_loaders[$name])) {
172: $this->registerLoader($name, $this->_partialLoader());
173: }
174:
175: return $this->_getFromLoader($name, $locale);
176: }
177:
178: /**
179: * Registers a loader function for a package name that will be used as a fallback
180: * in case no package with that name can be found.
181: *
182: * Loader callbacks will get as first argument the package name and the locale as
183: * the second argument.
184: *
185: * @param string $name The name of the translator package to register a loader for
186: * @param callable $loader A callable object that should return a Package
187: * @return void
188: */
189: public function registerLoader($name, callable $loader)
190: {
191: $this->_loaders[$name] = $loader;
192: }
193:
194: /**
195: * Sets the name of the default messages formatter to use for future
196: * translator instances.
197: *
198: * If called with no arguments, it will return the currently configured value.
199: *
200: * @param string|null $name The name of the formatter to use.
201: * @return string The name of the formatter.
202: */
203: public function defaultFormatter($name = null)
204: {
205: if ($name === null) {
206: return $this->_defaultFormatter;
207: }
208:
209: return $this->_defaultFormatter = $name;
210: }
211:
212: /**
213: * Set if the default domain fallback is used.
214: *
215: * @param bool $enable flag to enable or disable fallback
216: * @return void
217: */
218: public function useFallback($enable = true)
219: {
220: $this->_useFallback = $enable;
221: }
222:
223: /**
224: * Returns a new translator instance for the given name and locale
225: * based of conventions.
226: *
227: * @param string $name The translation package name.
228: * @param string $locale The locale to create the translator for.
229: * @return \Aura\Intl\Translator
230: */
231: protected function _fallbackLoader($name, $locale)
232: {
233: return $this->_loaders[$this->_fallbackLoader]($name, $locale);
234: }
235:
236: /**
237: * Returns a function that can be used as a loader for the registerLoaderMethod
238: *
239: * @return callable
240: */
241: protected function _partialLoader()
242: {
243: return function ($name, $locale) {
244: return $this->_fallbackLoader($name, $locale);
245: };
246: }
247:
248: /**
249: * Registers a new package by passing the register loaded function for the
250: * package name.
251: *
252: * @param string $name The name of the translator package
253: * @param string $locale The locale that should be built the package for
254: * @return \Aura\Intl\TranslatorInterface A translator object.
255: */
256: protected function _getFromLoader($name, $locale)
257: {
258: $loader = $this->_loaders[$name]($name, $locale);
259: $package = $loader;
260:
261: if (!is_callable($loader)) {
262: $loader = function () use ($package) {
263: return $package;
264: };
265: }
266:
267: $loader = $this->setLoaderFallback($name, $loader);
268:
269: $this->packages->set($name, $locale, $loader);
270:
271: return parent::get($name, $locale);
272: }
273:
274: /**
275: * Set domain fallback for loader.
276: *
277: * @param string $name The name of the loader domain
278: * @param callable $loader invokable loader
279: * @return callable loader
280: */
281: public function setLoaderFallback($name, callable $loader)
282: {
283: $fallbackDomain = 'default';
284: if (!$this->_useFallback || $name === $fallbackDomain) {
285: return $loader;
286: }
287: $loader = function () use ($loader, $fallbackDomain) {
288: /* @var \Aura\Intl\Package $package */
289: $package = $loader();
290: if (!$package->getFallback()) {
291: $package->setFallback($fallbackDomain);
292: }
293:
294: return $package;
295: };
296:
297: return $loader;
298: }
299: }
300: