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