Erebot  latest
A modular IRC bot for PHP 5.3+
Intl.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot;
22 
28 class Intl implements \Erebot\IntlInterface
29 {
31  const EXPIRE_CACHE = 60;
32 
34  static protected $cache = array();
35 
37  protected $locales;
38 
40  protected $component;
41 
50  public function __construct($component)
51  {
52  $this->locales = array();
53  $categories = array(
54  self::LC_CTYPE,
55  self::LC_NUMERIC,
56  self::LC_TIME,
57  self::LC_COLLATE,
58  self::LC_MONETARY,
59  self::LC_MESSAGES,
60  self::LC_PAPER,
61  self::LC_NAME,
62  self::LC_ADDRESS,
63  self::LC_TELEPHONE,
64  self::LC_MEASUREMENT,
65  self::LC_IDENTIFICATION,
66  );
67  foreach ($categories as $category) {
68  $this->locales[$category] = "en_US";
69  }
70  $this->component = $component;
71  }
72 
73  public static function nameToCategory($name)
74  {
75  $categories = array_flip(
76  array(
77  self::LC_CTYPE => 'LC_CTYPE',
78  self::LC_NUMERIC => 'LC_NUMERIC',
79  self::LC_TIME => 'LC_TIME',
80  self::LC_COLLATE => 'LC_COLLATE',
81  self::LC_MONETARY => 'LC_MONETARY',
82  self::LC_MESSAGES => 'LC_MESSAGES',
83  self::LC_PAPER => 'LC_PAPER',
84  self::LC_NAME => 'LC_NAME',
85  self::LC_ADDRESS => 'LC_ADDRESS',
86  self::LC_TELEPHONE => 'LC_TELEPHONE',
87  self::LC_MEASUREMENT => 'LC_MEASUREMENT',
88  self::LC_IDENTIFICATION => 'LC_IDENTIFICATION',
89  )
90  );
91  if (!isset($categories[$name])) {
92  throw new \InvalidArgumentException('Invalid category name');
93  }
94  return $categories[$name];
95  }
96 
97  public static function categoryToName($category)
98  {
99  $categories = array(
100  self::LC_CTYPE => 'LC_CTYPE',
101  self::LC_NUMERIC => 'LC_NUMERIC',
102  self::LC_TIME => 'LC_TIME',
103  self::LC_COLLATE => 'LC_COLLATE',
104  self::LC_MONETARY => 'LC_MONETARY',
105  self::LC_MESSAGES => 'LC_MESSAGES',
106  self::LC_PAPER => 'LC_PAPER',
107  self::LC_NAME => 'LC_NAME',
108  self::LC_ADDRESS => 'LC_ADDRESS',
109  self::LC_TELEPHONE => 'LC_TELEPHONE',
110  self::LC_MEASUREMENT => 'LC_MEASUREMENT',
111  self::LC_IDENTIFICATION => 'LC_IDENTIFICATION',
112  );
113  if (!isset($categories[$category])) {
114  throw new \InvalidArgumentException('Invalid category');
115  }
116  return $categories[$category];
117  }
118 
119  public function getLocale($category)
120  {
121  if (!isset($this->locales[$category])) {
122  throw new \InvalidArgumentException('Invalid category');
123  }
124  return $this->locales[$category];
125  }
126 
127  private function getBaseDir($component)
128  {
129  $reflector = new \ReflectionClass($component);
130  $parts = explode(DIRECTORY_SEPARATOR, $reflector->getFileName());
131  do {
132  $last = array_pop($parts);
133  } while ($last !== 'src' && count($parts));
134  $parts[] = 'data';
135  $parts[] = 'i18n';
136  $base = implode(DIRECTORY_SEPARATOR, $parts);
137  return $base;
138  }
139 
140  public function setLocale($category, $candidates)
141  {
142  $categoryName = self::categoryToName($category);
143  if (!is_array($candidates)) {
144  $candidates = array($candidates);
145  }
146  if (!count($candidates)) {
147  throw new \InvalidArgumentException('Invalid locale');
148  }
149 
150  $base = $this->getBaseDir($this->component);
151  $newLocale = null;
152  foreach ($candidates as $candidate) {
153  if (!is_string($candidate)) {
154  throw new \InvalidArgumentException('Invalid locale');
155  }
156 
157  $locale = \Locale::parseLocale($candidate);
158  if (!is_array($locale) || !isset($locale['language'])) {
159  throw new \InvalidArgumentException('Invalid locale');
160  }
161 
162  // For anything else than LC_MESSAGES,
163  // we take the first candidate as is.
164  if ($categoryName != 'LC_MESSAGES') {
165  $newLocale = $candidate;
166  }
167 
168  if ($newLocale !== null) {
169  continue;
170  }
171 
172  $catalog = str_replace('\\', '_', ltrim($this->component, '\\'));
173  if (isset($locale['region'])) {
174  $normLocale = $locale['language'] . '_' . $locale['region'];
175  $file = $base .
176  DIRECTORY_SEPARATOR . $normLocale .
177  DIRECTORY_SEPARATOR . $categoryName .
178  DIRECTORY_SEPARATOR . $catalog;
179 
180  if (file_exists($file . '.mo')) {
181  $newLocale = $normLocale;
182  continue;
183  }
184 
185  if (file_exists($file . '.po')) {
186  $newLocale = $normLocale;
187  continue;
188  }
189  }
190 
191  $file = $base .
192  DIRECTORY_SEPARATOR . $locale['language'] .
193  DIRECTORY_SEPARATOR . $categoryName .
194  DIRECTORY_SEPARATOR . $catalog;
195 
196  if (file_exists($file . '.mo')) {
197  $newLocale = $locale['language'];
198  continue;
199  }
200 
201  if (file_exists($file . '.po')) {
202  $newLocale = $locale['language'];
203  continue;
204  }
205  }
206 
207  if ($newLocale === null) {
208  $newLocale = 'en_US';
209  }
210  $this->locales[$category] = $newLocale;
211  return $newLocale;
212  }
213 
241  protected function getTranslation($component, $message)
242  {
243  $time = time();
244  $locale = $this->locales[self::LC_MESSAGES];
245  if (!isset(self::$cache[$component][$locale]) ||
246  $time > (self::$cache[$component][$locale]['added'] + self::EXPIRE_CACHE)) {
247  if (isset(self::$cache[$component][$locale]['file'])) {
248  $file = self::$cache[$component][$locale]['file'];
249  } else {
250  try {
251  $file = $this->getBaseDir($component);
252  } catch (Exception $e) {
253  return null;
254  }
255 
256  $catalog = str_replace('\\', '_', ltrim($component, '\\'));
257  $file .= DIRECTORY_SEPARATOR . $locale .
258  DIRECTORY_SEPARATOR . 'LC_MESSAGES' .
259  DIRECTORY_SEPARATOR . $catalog . '.mo';
260 
261  if (!file_exists($file)) {
262  $file = substr($file, 0, -3) . '.po';
263  }
264 
265  if (!file_exists($file)) {
266  return null;
267  }
268  }
269 
276  $oldErrorReporting = error_reporting(E_ERROR);
277 
278  if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
279  clearstatcache(false, $file);
280  } else {
281  clearstatcache();
282  }
283 
284  $mtime = false;
285  if ($file !== false) {
286  $mtime = filemtime($file);
287  }
288 
289  if ($mtime === false) {
290  // We also cache failures to avoid
291  // harassing the CPU too much.
292  self::$cache[$component][$locale] = array(
293  'mtime' => $time,
294  'string' => array(),
295  'added' => $time,
296  'file' => false,
297  );
298  } elseif (!isset(self::$cache[$component][$locale]) ||
299  $mtime !== self::$cache[$component][$locale]['mtime']) {
300  $parser = \File_Gettext::factory(substr($file, -2), $file);
301  $parser->load();
302  self::$cache[$component][$locale] = array(
303  'mtime' => $mtime,
304  'strings' => $parser->strings,
305  'added' => $time,
306  'file' => $file,
307  );
308  }
309  error_reporting($oldErrorReporting);
310  }
311 
312  if (isset(self::$cache[$component][$locale]['strings'][$message])) {
313  return self::$cache[$component][$locale]['strings'][$message];
314  }
315  return null;
316  }
317 
335  protected function reallyGetText($message, $component)
336  {
337  $translation = $this->getTranslation(
338  $component,
339  $message
340  );
341  return (!strlen($translation)) ? $message : $translation;
342  }
343 
344  public function gettext($message)
345  {
346  return $this->reallyGetText($message, $this->component);
347  }
348 
349  public function _($message)
350  {
351  return $this->reallyGetText($message, $this->component);
352  }
353 
360  public static function clearCache()
361  {
362  self::$cache = array();
363  }
364 }
Interface to provide internationalization.
static clearCache()
Definition: Intl.php:360
Base class for other (Erebot-related) exceptions.
Definition: Exception.php:27
Definition: CLI.php:21
$locales
The actual locales used for i18n.
Definition: Intl.php:37
A class which provides translations for messages used by the core and modules.
Definition: Intl.php:28
getLocale($category)
Definition: Intl.php:119
static categoryToName($category)
Definition: Intl.php:97
static nameToCategory($name)
Definition: Intl.php:73
reallyGetText($message, $component)
Definition: Intl.php:335
gettext($message)
Definition: Intl.php:344
setLocale($category, $candidates)
Definition: Intl.php:140
__construct($component)
Definition: Intl.php:50
$component
The component to get translations from (a module name or "Erebot").
Definition: Intl.php:40
_($message)
Definition: Intl.php:349
getTranslation($component, $message)
Definition: Intl.php:241