Code coverage 100 %

0 %
Loaders/Loader.php
1: <?php
2:
3:
namespace DK\Translator\Loaders;
4:
5:
/**
6:  *
7:  * @author David Kudera
8:  */
9:
interface Loader
10:
{
11:
12:
13:     
/**
14:      * @param string $parent
15:      * @param string $name
16:      * @param string $language
17:      * @return array
18:      */
19:     
public function load($parent$name$language);
20:
21:
22:     
/**
23:      * @param string $parent
24:      * @param string $name
25:      * @param string $language
26:      * @return string
27:      */
28:     
public function getFileSystemPath($parent$name$language);
29:
0 %
Loaders/Json.php
1: <?php
2:
3:
namespace DK\Translator\Loaders;
4:
5:
/**
6:  *
7:  * @author David Kudera
8:  */
9:
class Json implements Loader
10:
{
11:
12:
13:     
/** @var string  */
14:     
private $directory;
15:
16:
17:     
/**
18:      * @param string $directory
19:      */
20:     
public function __construct($directory)
21:     {
22:         
$this->directory $directory;
23:     }
24:
25:
26:     
/**
27:      * @return string
28:      */
29:     
public function getDirectory()
30:     {
31:         return 
$this->directory;
32:     }
33:
34:
35:     
/**
36:      * @param string $parent
37:      * @param string $name
38:      * @param string $language
39:      * @return array
40:      */
41:     
public function load($parent$name$language)
42:     {
43:         
$path $this->getFileSystemPath($parent$name$language);
44:         if (
is_file($path)) {
45:             return 
json_decode(file_get_contents($path));
46:         } else {
47:             return array();
48:         }
49:     }
50:
51:
52:     
/**
53:      * @param string $parent
54:      * @param string $name
55:      * @param string $language
56:      * @return string
57:      */
58:     
public function getFileSystemPath($parent$name$language)
59:     {
60:         return 
$this->directory. ($parent !== '' '/'$parent ''). "/$language.$name.json";
61:     }
62:
100 %
Translator.php
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($message0$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($path0$num);
593:         
$path preg_replace('/\./''/'$path);
594:
595:         return array(
596:             
'path' => $path,
597:             
'category' => $category,
598:             
'name' => $name
599:         
);
600:     }
601: