|
1 <?php |
|
2 |
|
3 /* |
|
4 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between |
|
5 * Version 1.1.1 |
|
6 * Copyright (C) 2006-2007 Dan Fuhry |
|
7 * |
|
8 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License |
|
9 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. |
|
10 * |
|
11 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied |
|
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details. |
|
13 */ |
|
14 |
|
15 /** |
|
16 * Language class - processes, stores, and retrieves language strings. |
|
17 * @package Enano |
|
18 * @subpackage Localization |
|
19 * @copyright 2007 Dan Fuhry |
|
20 * @license GNU General Public License |
|
21 */ |
|
22 |
|
23 class Language |
|
24 { |
|
25 |
|
26 /** |
|
27 * The numerical ID of the loaded language. |
|
28 * @var int |
|
29 */ |
|
30 |
|
31 var $lang_id; |
|
32 |
|
33 /** |
|
34 * The ISO-639-3 code for the loaded language. This should be grabbed directly from the database. |
|
35 * @var string |
|
36 */ |
|
37 |
|
38 var $lang_code; |
|
39 |
|
40 /** |
|
41 * Will be an object that holds an instance of the class configured with the site's default language. Only instanciated when needed. |
|
42 * @var object |
|
43 */ |
|
44 |
|
45 var $default; |
|
46 |
|
47 /** |
|
48 * The list of loaded strings. |
|
49 * @var array |
|
50 * @access private |
|
51 */ |
|
52 |
|
53 var $strings = array(); |
|
54 |
|
55 /** |
|
56 * Constructor. |
|
57 * @param int|string Language ID or code to load. |
|
58 */ |
|
59 |
|
60 function __construct($lang) |
|
61 { |
|
62 global $db, $session, $paths, $template, $plugins; // Common objects |
|
63 |
|
64 if ( defined('IN_ENANO_INSTALL') ) |
|
65 { |
|
66 // special case for the Enano installer: it will load its own strings from a JSON file and just use this API for fetching and templatizing them. |
|
67 $this->lang_id = LANG_DEFAULT; |
|
68 $this->lang_code = 'neutral'; |
|
69 return true; |
|
70 } |
|
71 if ( is_string($lang) ) |
|
72 { |
|
73 $sql_col = 'lang_code="' . $db->escape($lang) . '"'; |
|
74 } |
|
75 else if ( is_int($lang) ) |
|
76 { |
|
77 $sql_col = 'lang_id=' . $lang . ''; |
|
78 } |
|
79 else |
|
80 { |
|
81 $db->_die('lang.php - attempting to pass invalid value to constructor'); |
|
82 } |
|
83 |
|
84 $lang_default = ( $x = getConfig('default_language') ) ? intval($x) : 1; |
|
85 $q = $db->sql_query("SELECT lang_id, lang_code, ( lang_id = $lang_default ) AS is_default FROM " . table_prefix . "language WHERE $sql_col OR lang_id = $lang_default ORDER BY is_default DESC LIMIT 1;"); |
|
86 |
|
87 if ( !$q ) |
|
88 $db->_die('lang.php - main select query'); |
|
89 |
|
90 if ( $db->numrows() < 1 ) |
|
91 $db->_die('lang.php - There are no languages installed'); |
|
92 |
|
93 $row = $db->fetchrow(); |
|
94 |
|
95 $this->lang_id = intval( $row['lang_id'] ); |
|
96 $this->lang_code = $row['lang_code']; |
|
97 } |
|
98 |
|
99 /** |
|
100 * PHP 4 constructor. |
|
101 * @param int|string Language ID or code to load. |
|
102 */ |
|
103 |
|
104 function Language($lang) |
|
105 { |
|
106 $this->__construct($lang); |
|
107 } |
|
108 |
|
109 /** |
|
110 * Fetches language strings from the database, or a cache file if it's available. |
|
111 * @param bool If true (default), allows the cache to be used. |
|
112 */ |
|
113 |
|
114 function fetch($allow_cache = true) |
|
115 { |
|
116 global $db, $session, $paths, $template, $plugins; // Common objects |
|
117 |
|
118 $lang_file = ENANO_ROOT . "/cache/lang_{$this->lang_id}.php"; |
|
119 // Attempt to load the strings from a cache file |
|
120 if ( file_exists($lang_file) && $allow_cache ) |
|
121 { |
|
122 // Yay! found it |
|
123 $this->load_cache_file($lang_file); |
|
124 } |
|
125 else |
|
126 { |
|
127 // No cache file - select and retrieve from the database |
|
128 $q = $db->sql_unbuffered_query("SELECT string_category, string_name, string_content FROM " . table_prefix . "language_strings WHERE lang_id = {$this->lang_id};"); |
|
129 if ( !$q ) |
|
130 $db->_die('lang.php - selecting language string data'); |
|
131 if ( $row = $db->fetchrow() ) |
|
132 { |
|
133 $strings = array(); |
|
134 do |
|
135 { |
|
136 $cat =& $row['string_category']; |
|
137 if ( !is_array($strings[$cat]) ) |
|
138 { |
|
139 $strings[$cat] = array(); |
|
140 } |
|
141 $strings[$cat][ $row['string_name'] ] = $row['string_content']; |
|
142 } |
|
143 while ( $row = $db->fetchrow() ); |
|
144 // all done fetching |
|
145 $this->merge($strings); |
|
146 } |
|
147 else |
|
148 { |
|
149 $db->_die('lang.php - No strings for language ' . $this->lang_code); |
|
150 } |
|
151 } |
|
152 } |
|
153 |
|
154 /** |
|
155 * Loads a file from the disk cache (treated as PHP) and merges it into RAM. |
|
156 * @param string File to load |
|
157 */ |
|
158 |
|
159 function load_cache_file($file) |
|
160 { |
|
161 global $db, $session, $paths, $template, $plugins; // Common objects |
|
162 |
|
163 // We're using eval() here because it makes handling scope easier. |
|
164 |
|
165 if ( !file_exists($file) ) |
|
166 $db->_die('lang.php - requested cache file doesn\'t exist'); |
|
167 |
|
168 $contents = file_get_contents($file); |
|
169 $contents = preg_replace('/([\s]*)<\?php/', '', $contents); |
|
170 |
|
171 @eval($contents); |
|
172 |
|
173 if ( !isset($lang_cache) || ( isset($lang_cache) && !is_array($lang_cache) ) ) |
|
174 $db->_die('lang.php - the cache file is invalid (didn\'t set $lang_cache as an array)'); |
|
175 |
|
176 $this->merge($lang_cache); |
|
177 } |
|
178 |
|
179 /** |
|
180 * Merges a standard language assoc array ($arr[cat][stringid]) with the master in RAM. |
|
181 * @param array |
|
182 */ |
|
183 |
|
184 function merge($strings) |
|
185 { |
|
186 // This is stupidly simple. |
|
187 foreach ( $strings as $cat_id => $contents ) |
|
188 { |
|
189 if ( !is_array($this->strings[$cat_id]) ) |
|
190 $this->strings[$cat_id] = array(); |
|
191 foreach ( $contents as $string_id => $string ) |
|
192 { |
|
193 $this->strings[$cat_id][$string_id] = $string; |
|
194 } |
|
195 } |
|
196 } |
|
197 |
|
198 /** |
|
199 * Imports a JSON-format language file into the database and merges with current strings. |
|
200 * @param string Path to the JSON file to load |
|
201 */ |
|
202 |
|
203 function import($file) |
|
204 { |
|
205 global $db, $session, $paths, $template, $plugins; // Common objects |
|
206 |
|
207 if ( !file_exists($file) ) |
|
208 $db->_die('lang.php - can\'t import language file: string file doesn\'t exist'); |
|
209 |
|
210 $contents = trim(@file_get_contents($file)); |
|
211 |
|
212 if ( empty($contents) ) |
|
213 $db->_die('lang.php - can\'t load the contents of the language file'); |
|
214 |
|
215 // Trim off all text before and after the starting and ending braces |
|
216 $contents = preg_replace('/^([^{]+)\{/', '{', $contents); |
|
217 $contents = preg_replace('/\}([^}]+)$/', '}', $contents); |
|
218 |
|
219 $json = new Services_JSON(SERVICES_JSON_LOOSE_TYPE); |
|
220 $langdata = $json->decode($contents); |
|
221 |
|
222 if ( !is_array($langdata) ) |
|
223 $db->_die('lang.php - invalid language file'); |
|
224 |
|
225 if ( !isset($langdata['categories']) || !isset($langdata['strings']) ) |
|
226 $db->_die('lang.php - language file does not contain the proper items'); |
|
227 |
|
228 $insert_list = array(); |
|
229 $delete_list = array(); |
|
230 |
|
231 foreach ( $langdata['categories'] as $category ) |
|
232 { |
|
233 if ( isset($langdata['strings'][$category]) ) |
|
234 { |
|
235 foreach ( $langdata['strings'][$category] as $string_name => $string_value ) |
|
236 { |
|
237 $string_name = $db->escape($string_name); |
|
238 $string_value = $db->escape($string_value); |
|
239 $category_name = $db->escape($category); |
|
240 $insert_list[] = "({$this->lang_id}, '$category_name', '$string_name', '$string_value')"; |
|
241 $delete_list[] = "( lang_id = {$this->lang_id} AND string_category = '$category_name' AND string_name = '$string_name' )"; |
|
242 } |
|
243 } |
|
244 } |
|
245 |
|
246 $delete_list = implode(" OR\n ", $delete_list); |
|
247 $sql = "DELETE FROM " . table_prefix . "language_strings WHERE $delete_list;"; |
|
248 |
|
249 // Free some memory |
|
250 unset($delete_list); |
|
251 |
|
252 // Run the query |
|
253 $q = $db->sql_query($sql); |
|
254 if ( !$q ) |
|
255 $db->_die('lang.php - couldn\'t kill off them old strings'); |
|
256 |
|
257 $insert_list = implode(",\n ", $insert_list); |
|
258 $sql = "INSERT INTO " . table_prefix . "language_strings(lang_id, string_category, string_name, string_content) VALUES\n $insert_list;"; |
|
259 |
|
260 // Free some memory |
|
261 unset($insert_list); |
|
262 |
|
263 // Run the query |
|
264 $q = $db->sql_query($sql); |
|
265 if ( !$q ) |
|
266 $db->_die('lang.php - couldn\'t insert strings in import()'); |
|
267 |
|
268 // YAY! done! |
|
269 // This will regenerate the cache file if possible. |
|
270 $this->regen_caches(); |
|
271 } |
|
272 |
|
273 /** |
|
274 * Refetches the strings and writes out the cache file. |
|
275 */ |
|
276 |
|
277 function regen_caches() |
|
278 { |
|
279 global $db, $session, $paths, $template, $plugins; // Common objects |
|
280 |
|
281 $lang_file = ENANO_ROOT . "/cache/lang_{$this->lang_id}.php"; |
|
282 |
|
283 // Refresh the strings in RAM to the latest copies in the DB |
|
284 $this->fetch(false); |
|
285 |
|
286 $handle = @fopen($lang_file, 'w'); |
|
287 if ( !$handle ) |
|
288 // Couldn't open the file. Silently fail and let the strings come from the database. |
|
289 return false; |
|
290 |
|
291 // The file's open, that means we should be good. |
|
292 fwrite($handle, '<?php |
|
293 // This file was generated automatically by Enano. You should not edit this file because any changes you make |
|
294 // to it will not be visible in the ACP and all changes will be lost upon any changes to strings in the admin panel. |
|
295 |
|
296 $lang_cache = '); |
|
297 |
|
298 $exported = $this->var_export_string($this->strings); |
|
299 if ( empty($exported) ) |
|
300 // Ehh, that's not good |
|
301 $db->_die('lang.php - var_export_string() failed'); |
|
302 |
|
303 fwrite($handle, $exported . '; ?>'); |
|
304 |
|
305 // Done =) |
|
306 fclose($handle); |
|
307 } |
|
308 |
|
309 /** |
|
310 * Calls var_export() on whatever, and returns the function's output. |
|
311 * @param mixed Whatever you want var_exported. Usually an array. |
|
312 * @return string |
|
313 */ |
|
314 |
|
315 function var_export_string($val) |
|
316 { |
|
317 ob_start(); |
|
318 var_export($val); |
|
319 $contents = ob_get_contents(); |
|
320 ob_end_clean(); |
|
321 return $contents; |
|
322 } |
|
323 |
|
324 /** |
|
325 * Fetches a language string from the cache in RAM. If it isn't there, it will call fetch() again and then try. If it still can't find it, it will ask for the string |
|
326 * in the default language. If even then the string can't be found, this function will return what was passed to it. |
|
327 * |
|
328 * This will also templatize strings. If a string contains variables in the format %foo%, you may specify the second parameter as an associative array in the format |
|
329 * of 'foo' => 'foo substitute'. |
|
330 * |
|
331 * @param string ID of the string to fetch. This will always be in the format of category_stringid. |
|
332 * @param array Optional. Associative array of substitutions. |
|
333 * @return string |
|
334 */ |
|
335 |
|
336 function get($string_id, $substitutions = false) |
|
337 { |
|
338 // Extract the category and string ID |
|
339 $category = substr($string_id, 0, ( strpos($string_id, '_') )); |
|
340 $string_name = substr($string_id, ( strpos($string_id, '_') + 1 )); |
|
341 $found = false; |
|
342 if ( isset($this->strings[$category]) && isset($this->strings[$category][$string_name]) ) |
|
343 { |
|
344 $found = true; |
|
345 $string = $this->strings[$category][$string_name]; |
|
346 } |
|
347 if ( !$found ) |
|
348 { |
|
349 // Ehh, the string wasn't found. Rerun fetch() and try again. |
|
350 $this->fetch(); |
|
351 if ( isset($this->strings[$category]) && isset($this->strings[$category][$string_name]) ) |
|
352 { |
|
353 $found = true; |
|
354 $string = $this->strings[$category][$string_name]; |
|
355 } |
|
356 if ( !$found ) |
|
357 { |
|
358 // STILL not found. Check the default language. |
|
359 $lang_default = ( $x = getConfig('default_language') ) ? intval($x) : $this->lang_id; |
|
360 if ( $lang_default != $this->lang_id ) |
|
361 { |
|
362 if ( !is_object($this->default) ) |
|
363 $this->default = new Language($lang_default); |
|
364 return $this->default->get($string_id, $substitutions); |
|
365 } |
|
366 } |
|
367 } |
|
368 if ( !$found ) |
|
369 { |
|
370 // Alright, it's nowhere. Return the input, grumble grumble... |
|
371 return $string_id; |
|
372 } |
|
373 // Found it! |
|
374 // Perform substitutions. |
|
375 if ( !is_array($substitutions) ) |
|
376 $substitutions = array(); |
|
377 return $this->substitute($string, $substitutions); |
|
378 } |
|
379 |
|
380 /** |
|
381 * Processes substitutions. |
|
382 * @param string |
|
383 * @param array |
|
384 * @return string |
|
385 */ |
|
386 |
|
387 function substitute($string, $subs) |
|
388 { |
|
389 preg_match_all('/%this\.([a-z0-9_]+)%/', $string, $matches); |
|
390 if ( count($matches[0]) > 0 ) |
|
391 { |
|
392 foreach ( $matches[1] as $i => $string_id ) |
|
393 { |
|
394 $result = $this->get($string_id); |
|
395 $string = str_replace($matches[0][$i], $result, $string); |
|
396 } |
|
397 } |
|
398 foreach ( $subs as $key => $value ) |
|
399 { |
|
400 $string = str_replace("%$key%", $value, $string); |
|
401 } |
|
402 return $string; |
|
403 } |
|
404 |
|
405 } // class Language |
|
406 |
|
407 ?> |