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