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