1:
<?php
2:
3: namespace DK\Translator;
4:
5: use DK\Translator\Loaders\Loader;
6: use Tester\Dumper;
7:
8: /**
9: *
10: * @author David Kudera
11: */
12: class Translator
13: {
14:
15:
16: /** @var \DK\Translator\Loaders\Loader */
17: private $loader;
18:
19: /** @var string */
20: private $language;
21:
22: /** @var array */
23: private $plurals = array();
24:
25: /** @var array */
26: private $replacements = array();
27:
28: /** @var array */
29: private $filters = array();
30:
31: /** @var array */
32: private $data = array();
33:
34:
35: /**
36: * @param string|\DK\Translator\Loaders\Loader $pathOrLoader
37: * @throws \Exception
38: */
39: public function __construct($pathOrLoader)
40: {
41: if (!is_string($pathOrLoader) && !$pathOrLoader instanceof Loader) {
42: throw new \Exception('Argument passed to translator must be string or Loader.');
43: }
44:
45: if (is_string($pathOrLoader)) {
46: $config = array(
47: 'path' => $pathOrLoader,
48: 'loader' => 'Json'
49: );
50:
51: if (preg_match('/\.json$/', $pathOrLoader)) {
52: $_config = json_decode(file_get_contents($pathOrLoader));
53: if (isset($_config->path)) {
54: $config['path'] = $_config->path;
55: }
56: if (isset($_config->loader)) {
57: $config['loader'] = $_config->loader;
58: }
59: if ($config['path'][0] === '.') {
60: $config['path'] = $this->joinPaths(dirname($pathOrLoader), $config['path']);
61: }
62: }
63:
64: $loader = "\\DK\\Translator\\Loaders\\$config[loader]";
65: $pathOrLoader = new $loader($config['path']);
66: }
67:
68: $this->setLoader($pathOrLoader);
69:
70: $plurals = json_decode(file_get_contents(__DIR__. '/pluralForms.json'), true);
71: foreach ($plurals as $language => $data) {
72: $this->addPluralForm($language, $data['count'], $data['form']);
73: }
74: }
75:
76:
77: /**
78: * @param string $left
79: * @param string $right
80: * @return string
81: */
82: private function joinPaths($left, $right)
83: {
84: $paths = array();
85: foreach (func_get_args() as $arg) {
86: if ($arg !== '') {
87: $paths[] = $arg;
88: }
89: }
90: $path = preg_replace('#/+#','/',join('/', $paths));
91: return realpath($path);
92: }
93:
94:
95: /**
96: * @return \DK\Translator\Loaders\Loader
97: */
98: public function getLoader()
99: {
100: return $this->loader;
101: }
102:
103:
104: /**
105: * @param \DK\Translator\Loaders\Loader $loader
106: * @return \DK\Translator\Translator
107: */
108: public function setLoader(Loader $loader)
109: {
110: $this->loader = $loader;
111: return $this;
112: }
113:
114:
115: /**
116: * @return \DK\Translator\Translator
117: */
118: public function invalidate()
119: {
120: $this->data = array();
121: return $this;
122: }
123:
124:
125: /**
126: * @return array
127: */
128: public function getData()
129: {
130: return $this->data;
131: }
132:
133:
134: /**
135: * @return string
136: */
137: public function getLanguage()
138: {
139: return $this->language;
140: }
141:
142:
143: /**
144: * @param string $language
145: * @return \DK\Translator\Translator
146: */
147: public function setLanguage($language)
148: {
149: $this->language = $language;
150: return $this;
151: }
152:
153:
154: /**
155: * @param string $language
156: * @param int $count
157: * @param string $form
158: * @return \DK\Translator\Translator
159: */
160: public function addPluralForm($language, $count, $form)
161: {
162: $this->plurals[$language] = array(
163: 'count' => $count,
164: 'form' => $form
165: );
166: return $this;
167: }
168:
169:
170: /**
171: * @return array
172: */
173: public function getPluralForms()
174: {
175: return $this->plurals;
176: }
177:
178:
179: /**
180: * @param string $search
181: * @param string $replacement
182: * @return \DK\Translator\Translator
183: */
184: public function addReplacement($search, $replacement)
185: {
186: $this->replacements[$search] = $replacement;
187: return $this;
188: }
189:
190:
191: /**
192: * @param string $search
193: * @return \DK\Translator\Translator
194: * @throws \Exception
195: */
196: public function removeReplacement($search)
197: {
198: if (!isset($this->replacements[$search])) {
199: throw new \Exception("Replacement '$search' was not found.");
200: }
201:
202: unset($this->replacements[$search]);
203: return $this;
204: }
205:
206:
207: /**
208: * @return array
209: */
210: public function getReplacements()
211: {
212: return $this->replacements;
213: }
214:
215:
216: /**
217: * @param callable $fn
218: * @return \DK\Translator\Translator
219: */
220: public function addFilter($fn)
221: {
222: $this->filters[] = $fn;
223: return $this;
224: }
225:
226:
227: /**
228: * @private public because of php 5.3
229: * @param string|array $translation
230: * @return array
231: */
232: public function _applyFilters($translation)
233: {
234: if (is_array($translation)) {
235: $_this = $this;
236: return array_map(function($t) use($_this) {
237: return $_this->_applyFilters($t);
238: }, $translation);
239: }
240:
241: foreach ($this->filters as $filter) {
242: $translation = $filter($translation);
243: }
244:
245: return $translation;
246: }
247:
248:
249: /**
250: * @param string $path
251: * @param string $name
252: * @param string|null $language
253: * @return array
254: */
255: public function _loadCategory($path, $name, $language = null)
256: {
257: if ($language === null) {
258: $language = $this->getLanguage();
259: }
260:
261: $categoryName = $path. '/'. $name;
262: if (!isset($this->data[$categoryName])) {
263: $data = $this->loader->load($path, $name, $language);
264: $data = $this->_normalizeTranslations($data);
265:
266: $this->data[$categoryName] = $data;
267: }
268:
269: return $this->data[$categoryName];
270: }
271:
272:
273: /**
274: * @param array $translations
275: * @return array
276: */
277: private function _normalizeTranslations($translations)
278: {
279: $result = array();
280: foreach ($translations as $name => $translation) {
281: $list = false;
282: if (preg_match('~^--\s(.*)~', $name, $match)) {
283: $name = $match[1];
284: $list = true;
285: }
286:
287: if (is_string($translation)) {
288: $result[$name] = array($translation);
289: } elseif (is_array($translation)) {
290: $result[$name] = array();
291: foreach ($translation as $t) {
292: if (is_array($t)) {
293: $buf = array();
294: foreach ($t as $sub) {
295: if (!preg_match('~^\#.*\#$~', $sub)) {
296: $buf[] = $sub;
297: }
298: }
299: $result[$name][] = $buf;
300: } else {
301: if (!preg_match('~^\#.*\#$~', $t)) {
302: if ($list === true && !is_array($t)) {
303: $t = array($t);
304: }
305: $result[$name][] = $t;
306: }
307: }
308: }
309: }
310: }
311:
312: return $result;
313: }
314:
315:
316: /**
317: * @param string $message
318: * @param null|string $language
319: * @return bool
320: */
321: public function hasTranslation($message, $language = null)
322: {
323: if ($language === null) {
324: $language = $this->getLanguage();
325: }
326:
327: return $this->findTranslation($message, $language) !== null;
328: }
329:
330:
331: /**
332: * @param string $message
333: * @param null|string $language
334: * @return array|null
335: */
336: private function findTranslation($message, $language = null)
337: {
338: if ($language === null) {
339: $language = $this->getLanguage();
340: }
341:
342: $info = $this->getMessageInfo($message);
343: $data = $this->_loadCategory($info['path'], $info['category'], $language);
344: return isset($data[$info['name']]) ? $data[$info['name']] : null;
345: }
346:
347:
348: /**
349: * @param string $message
350: * @param int|null $count
351: * @param array $args
352: * @return array|string
353: * @throws \Exception
354: */
355: public function translate($message, $count = null, array $args = array())
356: {
357: if (!is_string($message)) {
358: return $message;
359: }
360:
361: if (is_array($count)) {
362: $args = $count;
363: $count = null;
364: }
365:
366: if ($count !== null) {
367: $args['count'] = $count;
368: }
369:
370: $language = $this->getLanguage();
371: $found = false;
372:
373: if (preg_match('~^\:(.*)\:$~', $message, $match)) {
374: $message = $match[1];
375: if (preg_match('/^[a-z]+\|(.*)$/', $message, $match)) {
376: $message = $match[1];
377: }
378: } else {
379: if (preg_match('/^([a-z]+)\|(.*)$/', $message, $match)) {
380: $language = $match[1];
381: $message = $match[2];
382: }
383:
384: if ($language === null) {
385: throw new \Exception('You have to set language.');
386: }
387:
388: $num = null;
389: if (preg_match('~(.+)\[(\d+)\]$~', $message, $match)) {
390: $message = $match[1];
391: $num = (int) $match[2];
392: }
393:
394: $message = $this->applyReplacements($message, $args);
395: $translation = $this->findTranslation($message, $language);
396:
397: $found = $this->hasTranslation($message);
398:
399: if ($num !== null) {
400: if (!$this->isList($translation)) {
401: throw new \Exception('Translation '. $message. ' is not a list.');
402: }
403:
404: if (!isset($translation[$num])) {
405: throw new \Exception('Item '. $num. ' was not found in '. $message. ' translation.');
406: }
407:
408: $translation = $translation[$num];
409: }
410:
411: if ($translation !== null) {
412: $message = $this->pluralize($message, $translation, $count, $language);
413: }
414: }
415:
416: $message = $this->prepareTranslation($message, $args);
417:
418: if ($found) {
419: $message = $this->_applyFilters($message);
420: }
421:
422: return $message;
423: }
424:
425:
426: /**
427: * @param string $message
428: * @param string $key
429: * @param string $value
430: * @param int|null $count
431: * @param array $args
432: * @return array
433: * @throws \Exception
434: */
435: public function translatePairs($message, $key, $value, $count = null, array $args = array())
436: {
437: $key = "$message.$key";
438: $value = "$message.$value";
439:
440: $key = $this->translate($key, $count, $args);
441: $value = $this->translate($value, $count, $args);
442:
443: if (!is_array($key) || !is_array($value)) {
444: throw new \Exception('Translations are not arrays.');
445: }
446:
447: if (count($key) !== count($value)) {
448: throw new \Exception('Keys and values translations have not got the same length.');
449: }
450:
451: return array_combine($key, $value);
452: }
453:
454:
455: /**
456: * @param array $list
457: * @param int|null $count
458: * @param array $args
459: * @param string|null $base
460: * @return array
461: */
462: public function translateMap(array $list, $count = null, array $args = null, $base = null)
463: {
464: if ($args === null) {
465: $args = array();
466: }
467:
468: $base = $base === null ? '' : $base. '.';
469:
470: $_this = $this;
471: return array_map(function($a) use($_this, $count, $args, $base) {
472: return $_this->translate($base. $a, $count, $args);
473: }, $list);
474: }
475:
476:
477: /**
478: * @param array $translation
479: * @return bool
480: */
481: private function isList($translation)
482: {
483: return is_array($translation[0]);
484: }
485:
486:
487: /**
488: * @param string $message
489: * @param array $translation
490: * @param int|null $count
491: * @param string|null $language
492: * @return array|string
493: */
494: private function pluralize($message, array $translation, $count = null, $language = null)
495: {
496: if ($language === null) {
497: $language = $this->getLanguage();
498: }
499:
500: if ($count !== null) {
501: if (is_string($translation[0])) {
502: $pluralForm = 'n='. $count. ';plural=+('. $this->plurals[$language]['form']. ');';
503: $pluralForm = preg_replace('/([a-z]+)/', '$$1', $pluralForm);
504:
505: $n = null;
506: $plural = null;
507:
508: eval($pluralForm);
509:
510: $message = $plural !== null && isset($translation[$plural]) ? $translation[$plural] : $translation[0];
511: } else {
512: $result = array();
513: foreach ($translation as $t) {
514: $result[] = $this->pluralize($message, $t, $count, $language);
515: }
516: $message = $result;
517: }
518: } else {
519: if (is_string($translation[0])) {
520: $message = $translation[0];
521: } else {
522: $message = array();
523: foreach ($translation as $t) {
524: $message[] = $t[0];
525: }
526: }
527: }
528:
529: return $message;
530: }
531:
532:
533: /**
534: * @param string|array $message
535: * @param array $args
536: * @return array|string
537: */
538: private function prepareTranslation($message, array $args = array())
539: {
540: if (is_string($message)) {
541: $message = $this->applyReplacements($message, $args);
542: } else {
543: $result = array();
544: foreach ($message as $m) {
545: $result[] = $this->prepareTranslation($m, $args);
546: }
547: $message = $result;
548: }
549:
550: return $message;
551: }
552:
553:
554: /**
555: * @param string $message
556: * @param array $args
557: * @return string
558: */
559: private function applyReplacements($message, array $args = array())
560: {
561: $replacements = $this->replacements;
562:
563: foreach ($args as $name => $value) {
564: $replacements[$name] = $value;
565: }
566:
567: foreach ($replacements as $name => $value) {
568: if ($value !== false) {
569: $message = preg_replace('~%'. $name. '%~', $value, $message);
570: }
571: }
572:
573: return $message;
574: }
575:
576:
577: /**
578: * @param string $message
579: * @return array
580: */
581: private function getMessageInfo($message)
582: {
583: $num = strrpos($message, '.');
584: $path = substr($message, 0, $num);
585: $name = substr($message, $num + 1);
586: $num = strrpos($path, '.');
587: if ($num !== false) {
588: $category = substr($path, $num + 1);
589: } else {
590: $category = $path;
591: }
592: $path = substr($path, 0, $num);
593: $path = preg_replace('/\./', '/', $path);
594:
595: return array(
596: 'path' => $path,
597: 'category' => $category,
598: 'name' => $name
599: );
600: }
601: