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\Package;
18: use Cake\Core\App;
19: use Cake\Core\Plugin;
20: use Cake\Utility\Inflector;
21: use Locale;
22: use RuntimeException;
23:
24: /**
25: * A generic translations package factory that will load translations files
26: * based on the file extension and the package name.
27: *
28: * This class is a callable, so it can be used as a package loader argument.
29: */
30: class MessagesFileLoader
31: {
32: /**
33: * The package (domain) name.
34: *
35: * @var string
36: */
37: protected $_name;
38:
39: /**
40: * The locale to load for the given package.
41: *
42: * @var string
43: */
44: protected $_locale;
45:
46: /**
47: * The extension name.
48: *
49: * @var string
50: */
51: protected $_extension;
52:
53: /**
54: * Creates a translation file loader. The file to be loaded corresponds to
55: * the following rules:
56: *
57: * - The locale is a folder under the `Locale` directory, a fallback will be
58: * used if the folder is not found.
59: * - The $name corresponds to the file name to load
60: * - If there is a loaded plugin with the underscored version of $name, the
61: * translation file will be loaded from such plugin.
62: *
63: * ### Examples:
64: *
65: * Load and parse src/Locale/fr/validation.po
66: *
67: * ```
68: * $loader = new MessagesFileLoader('validation', 'fr_FR', 'po');
69: * $package = $loader();
70: * ```
71: *
72: * Load and parse src/Locale/fr_FR/validation.mo
73: *
74: * ```
75: * $loader = new MessagesFileLoader('validation', 'fr_FR', 'mo');
76: * $package = $loader();
77: * ```
78: *
79: * Load the plugins/MyPlugin/src/Locale/fr/my_plugin.po file:
80: *
81: * ```
82: * $loader = new MessagesFileLoader('my_plugin', 'fr_FR', 'mo');
83: * $package = $loader();
84: * ```
85: *
86: * @param string $name The name (domain) of the translations package.
87: * @param string $locale The locale to load, this will be mapped to a folder
88: * in the system.
89: * @param string $extension The file extension to use. This will also be mapped
90: * to a messages parser class.
91: */
92: public function __construct($name, $locale, $extension = 'po')
93: {
94: $this->_name = $name;
95: $this->_locale = $locale;
96: $this->_extension = $extension;
97: }
98:
99: /**
100: * Loads the translation file and parses it. Returns an instance of a translations
101: * package containing the messages loaded from the file.
102: *
103: * @return \Aura\Intl\Package|false
104: * @throws \RuntimeException if no file parser class could be found for the specified
105: * file extension.
106: */
107: public function __invoke()
108: {
109: $folders = $this->translationsFolders();
110: $ext = $this->_extension;
111: $file = false;
112:
113: $fileName = $this->_name;
114: $pos = strpos($fileName, '/');
115: if ($pos !== false) {
116: $fileName = substr($fileName, $pos + 1);
117: }
118: foreach ($folders as $folder) {
119: $path = $folder . $fileName . ".$ext";
120: if (is_file($path)) {
121: $file = $path;
122: break;
123: }
124: }
125:
126: if (!$file) {
127: return false;
128: }
129:
130: $name = ucfirst($ext);
131: $class = App::className($name, 'I18n\Parser', 'FileParser');
132:
133: if (!$class) {
134: throw new RuntimeException(sprintf('Could not find class %s', "{$name}FileParser"));
135: }
136:
137: $messages = (new $class())->parse($file);
138: $package = new Package('default');
139: $package->setMessages($messages);
140:
141: return $package;
142: }
143:
144: /**
145: * Returns the folders where the file should be looked for according to the locale
146: * and package name.
147: *
148: * @return array The list of folders where the translation file should be looked for
149: */
150: public function translationsFolders()
151: {
152: $locale = Locale::parseLocale($this->_locale) + ['region' => null];
153:
154: $folders = [
155: implode('_', [$locale['language'], $locale['region']]),
156: $locale['language']
157: ];
158:
159: $searchPaths = [];
160:
161: $localePaths = App::path('Locale');
162: if (empty($localePaths) && defined('APP')) {
163: $localePaths[] = APP . 'Locale' . DIRECTORY_SEPARATOR;
164: }
165: foreach ($localePaths as $path) {
166: foreach ($folders as $folder) {
167: $searchPaths[] = $path . $folder . DIRECTORY_SEPARATOR;
168: }
169: }
170:
171: // If space is not added after slash, the character after it remains lowercased
172: $pluginName = Inflector::camelize(str_replace('/', '/ ', $this->_name));
173: if (Plugin::isLoaded($pluginName)) {
174: $basePath = Plugin::classPath($pluginName) . 'Locale' . DIRECTORY_SEPARATOR;
175: foreach ($folders as $folder) {
176: $searchPaths[] = $basePath . $folder . DIRECTORY_SEPARATOR;
177: }
178: }
179:
180: return $searchPaths;
181: }
182: }
183: