aboutsummaryrefslogtreecommitdiff
path: root/lib/Minz/Translate.php
blob: 07d48ec08b8ebec8865f6829282e2c73fb86c360 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
<?php
/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

/**
 * This class is used for the internationalization.
 * It uses files in `./app/i18n/`
 */
class Minz_Translate {
	/**
	 * $path_list is the list of registered base path to search translations.
	 */
	private static $path_list = array();

	/**
	 * $lang_name is the name of the current language to use.
	 */
	private static $lang_name;

	/**
	 * $lang_files is a list of registered i18n files.
	 */
	private static $lang_files = array();

	/**
	 * $translates is a cache for i18n translation.
	 */
	private static $translates = array();

	/**
	 * Init the translation object.
	 * @param string $lang_name the lang to show.
	 */
	public static function init($lang_name = null) {
		self::$lang_name = $lang_name;
		self::$lang_files = array();
		self::$translates = array();
		self::registerPath(APP_PATH . '/i18n');
		foreach (self::$path_list as $path) {
			self::loadLang($path);
		}
	}

	/**
	 * Reset the translation object with a new language.
	 * @param string $lang_name the new language to use
	 */
	public static function reset($lang_name) {
		self::$lang_name = $lang_name;
		self::$lang_files = array();
		self::$translates = array();
		foreach (self::$path_list as $path) {
			self::loadLang($path);
		}
	}

	/**
	 * Return the list of available languages.
	 * @return array<string> containing langs found in different registered paths.
	 */
	public static function availableLanguages() {
		$list_langs = array();

		self::registerPath(APP_PATH . '/i18n');

		foreach (self::$path_list as $path) {
			$scan = scandir($path);
			if (is_array($scan)) {
				$path_langs = array_values(array_diff(
					$scan,
					array('..', '.')
				));
				if (is_array($path_langs)) {
					$list_langs = array_merge($list_langs, $path_langs);
				}
			}
		}

		return array_unique($list_langs);
	}

	/**
	 * Return the language to use in the application.
	 * It returns the connected language if it exists then returns the first match from the
	 * preferred languages then returns the default language
	 * @param string|null $user the connected user language (nullable)
	 * @param array<string> $preferred an array of the preferred languages
	 * @param string|null $default the preferred language to use
	 * @return string containing the language to use
	 */
	public static function getLanguage(?string $user, array $preferred, ?string $default): string {
		if (null !== $user) {
			return $user;
		}

		$languages = Minz_Translate::availableLanguages();
		foreach ($preferred as $language) {
			$language = strtolower($language);
			if (in_array($language, $languages, true)) {
				return $language;
			}
		}

		return $default ? $default : 'en';
	}

	/**
	 * Register a new path.
	 * @param string $path a path containing i18n directories (e.g. ./en/, ./fr/).
	 */
	public static function registerPath($path) {
		if (!in_array($path, self::$path_list) && is_dir($path)) {
			self::$path_list[] = $path;
			self::loadLang($path);
		}
	}

	/**
	 * Load translations of the current language from the given path.
	 * @param string $path the path containing i18n directories.
	 */
	private static function loadLang($path) {
		$lang_path = $path . '/' . self::$lang_name;
		if (!file_exists($lang_path) || self::$lang_name == '') {
			// The lang path does not exist, nothing more to do.
			return;
		}

		$list_i18n_files = array_values(array_diff(
			scandir($lang_path),
			array('..', '.')
		));

		// Each file basename correspond to a top-level i18n key. For each of
		// these keys we store the file pathname and mark translations must be
		// reloaded (by setting $translates[$i18n_key] to null).
		foreach ($list_i18n_files as $i18n_filename) {
			$i18n_key = basename($i18n_filename, '.php');
			if (!isset(self::$lang_files[$i18n_key])) {
				self::$lang_files[$i18n_key] = array();
			}
			self::$lang_files[$i18n_key][] = $lang_path . '/' . $i18n_filename;
			self::$translates[$i18n_key] = null;
		}
	}

	/**
	 * Load the files associated to $key into $translates.
	 * @param string $key the top level i18n key we want to load.
	 */
	private static function loadKey($key) {
		// The top level key is not in $lang_files, it means it does not exist!
		if (!isset(self::$lang_files[$key])) {
			Minz_Log::debug($key . ' is not a valid top level key');
			return false;
		}

		self::$translates[$key] = array();

		foreach (self::$lang_files[$key] as $lang_pathname) {
			$i18n_array = include($lang_pathname);
			if (!is_array($i18n_array)) {
				Minz_Log::warning('`' . $lang_pathname . '` does not contain a PHP array');
				continue;
			}

			// We must avoid to erase previous data so we just override them if
			// needed.
			self::$translates[$key] = array_replace_recursive(
				self::$translates[$key], $i18n_array
			);
		}

		return true;
	}

	/**
	 * Translate a key into its corresponding value based on selected language.
	 * @param string $key the key to translate.
	 * @param mixed ...$args additional parameters for variable keys.
	 * @return string value corresponding to the key.
	 *         If no value is found, return the key itself.
	 */
	public static function t($key, ...$args) {
		$group = explode('.', $key);

		if (count($group) < 2) {
			Minz_Log::debug($key . ' is not in a valid format');
			$top_level = 'gen';
		} else {
			$top_level = array_shift($group);
		}

		// If $translates[$top_level] is null it means we have to load the
		// corresponding files.
		if (!isset(self::$translates[$top_level]) ||
				is_null(self::$translates[$top_level])) {
			$res = self::loadKey($top_level);
			if (!$res) {
				return $key;
			}
		}

		// Go through the i18n keys to get the correct translation value.
		$translates = self::$translates[$top_level];
		$size_group = count($group);
		$level_processed = 0;
		$translation_value = $key;
		foreach ($group as $i18n_level) {
			$level_processed++;
			if (!isset($translates[$i18n_level])) {
				Minz_Log::debug($key . ' is not a valid key');
				return $key;
			}

			if ($level_processed < $size_group) {
				$translates = $translates[$i18n_level];
			} else {
				$translation_value = $translates[$i18n_level];
			}
		}

		if (is_array($translation_value)) {
			if (isset($translation_value['_'])) {
				$translation_value = $translation_value['_'];
			} else {
				Minz_Log::debug($key . ' is not a valid key');
				return $key;
			}
		}

		// Get the facultative arguments to replace i18n variables.
		return empty($args) ? $translation_value : vsprintf($translation_value, $args);
	}

	/**
	 * Return the current language.
	 */
	public static function language() {
		return self::$lang_name;
	}
}


/**
 * Alias for Minz_Translate::t()
 * @param string $key
 * @param mixed ...$args
 */
function _t($key, ...$args) {
	return Minz_Translate::t($key, ...$args);
}