Skip to content

Commit ff6e6fa

Browse files
committed
Add more generic PoTranslator
1 parent 970f6d8 commit ff6e6fa

12 files changed

+391
-157
lines changed

bin/potrans

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/usr/bin/env php
22
<?php
3-
require __DIR__ . '/../src/potrans.php';
3+
require __DIR__ . '/../src/commands/index.php';

readme.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
![Packagist License](https://img.shields.io/packagist/l/om/potrans?style=for-the-badge)
2+
![Packagist Version](https://img.shields.io/packagist/v/om/potrans?style=for-the-badge)
3+
![Packagist Downloads](https://img.shields.io/packagist/dm/om/potrans?style=for-the-badge)
4+
15
# PO file translator
26

37
Potrans it's PHP command line tool for automatic translation of [Gettext](https://www.gnu.org/software/gettext/) PO file with
@@ -23,7 +27,7 @@ Arguments:
2327
Options:
2428
--from=FROM Source language (default: en) [default: "en"]
2529
--to=TO Target language (default: cs) [default: "cs"]
26-
--all Re-translate including translated sentences
30+
--force Force re-translate including translated sentences
2731
--wait=WAIT Wait between translations in milliseconds [default: false]
2832
--credentials=CREDENTIALS Path to Google Credentials file [default: "./credentials.json"]
2933
--project=PROJECT Google Cloud Project ID [default: project_id from credentials.json]
@@ -95,7 +99,7 @@ Arguments:
9599
Options:
96100
--from=FROM Source language (default: en) [default: "en"]
97101
--to=TO Target language (default: cs) [default: "cs"]
98-
--all Re-translate including translated sentences
102+
--force Force re-translate including translated sentences
99103
--wait=WAIT Wait between translations in milliseconds [default: false]
100104
--apikey=APIKEY Deepl API Key
101105
--cache|--no-cache Load from cache or not
@@ -159,8 +163,8 @@ There is missing issuer certificate `cacert.pem` file and curl won't verify SSL
159163
3. Update your `php.ini` with following:
160164

161165
```ini
162-
curl.cainfo="/usr/local/etc/cacert.pem"
163-
openssl.cafile="/usr/local/etc/cacert.pem"
166+
curl.cainfo = "/usr/local/etc/cacert.pem"
167+
openssl.cafile = "/usr/local/etc/cacert.pem"
164168
```
165169

166170
You can verify it with `phpinfo()` or `php --info`. Read more detailed [instruction here](https://stackoverflow.com/a/32095378/355316).

src/PoTranslator.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace potrans;
4+
5+
use Gettext\Generator\MoGenerator;
6+
use Gettext\Generator\PoGenerator;
7+
use Gettext\Loader\PoLoader;
8+
use Gettext\Translations;
9+
use potrans\translator\Translator;
10+
use Symfony\Component\Cache\Adapter\AdapterInterface;
11+
12+
class PoTranslator {
13+
14+
/** @var \potrans\translator\Translator */
15+
private Translator $translator;
16+
/** @var \Symfony\Component\Cache\Adapter\AdapterInterface */
17+
private AdapterInterface $cache;
18+
/** @var \Gettext\Translations */
19+
private Translations $sentences;
20+
21+
public function __construct(Translator $translator, AdapterInterface $cache) {
22+
$this->translator = $translator;
23+
$this->cache = $cache;
24+
}
25+
26+
/**
27+
* Read PO file
28+
*
29+
* @param string $filename
30+
* @return \Gettext\Translations
31+
*/
32+
public function loadFile(string $filename): Translations {
33+
return $this->sentences = (new PoLoader())->loadFile($filename);
34+
}
35+
36+
/**
37+
* Save MO file
38+
*
39+
* @param string $filename
40+
* @return bool
41+
*/
42+
public function saveMoFile(string $filename): bool {
43+
return (new MoGenerator())->generateFile($this->sentences, $filename);
44+
}
45+
46+
/**
47+
* Save PO file
48+
*
49+
* @param string $filename
50+
* @return bool
51+
*/
52+
public function savePoFile(string $filename): bool {
53+
return (new PoGenerator())->generateFile($this->sentences, $filename);
54+
}
55+
56+
/**
57+
* Translate all sentences
58+
*
59+
* @param string $from
60+
* @param string $to
61+
* @param bool $force
62+
* @return \Generator
63+
* @throws \Psr\Cache\InvalidArgumentException
64+
*/
65+
public function translate(string $from, string $to, bool $force = false): \Generator {
66+
/** @var \Gettext\Translation $sentence */
67+
foreach ($this->sentences as $sentence) {
68+
69+
// sentence translation is missing or we re-translate everything
70+
if (!$sentence->isTranslated() || $force) {
71+
$key = md5($sentence->getOriginal() . $from . $to);
72+
$translation = $this->cache->getItem($key);
73+
74+
if (!$translation->isHit()) {
75+
76+
$translation->set(
77+
$this->translator
78+
->from($from)
79+
->to($to)
80+
->getTranslation($sentence)
81+
);
82+
83+
// save cache
84+
$this->cache->save($translation);
85+
}
86+
87+
$sentence->translate($translation->get());
88+
89+
yield $sentence;
90+
}
91+
}
92+
}
93+
}

src/TranslatorAbstract.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace potrans;
4+
5+
use Gettext\Loader\PoLoader;
6+
use Gettext\Translation;
7+
use Gettext\Translations;
8+
use potrans\translator\Translator;
9+
use Symfony\Component\Cache\Adapter\AdapterInterface;
10+
use Symfony\Component\Cache\Adapter\NullAdapter;
11+
use Symfony\Component\Console\Exception\RuntimeException;
12+
13+
abstract class TranslatorAbstract implements Translator {
14+
15+
/** @var bool $translateAll re-translate everything again */
16+
protected bool $translateAll = false;
17+
/** @var \Gettext\Translations */
18+
private Translations $sentences;
19+
private string $from;
20+
private string $to;
21+
22+
23+
24+
/**
25+
* @param string $filename
26+
* @return Translations
27+
*/
28+
public function loadFile(string $filename): Translations {
29+
return $this->sentences = (new PoLoader())->loadFile($filename);
30+
}
31+
32+
public function getTranslations(string $from, string $to, ?callable $callback = null): Translations {
33+
/** @var \Gettext\Translation $sentence */
34+
foreach ($this->sentences as $sentence) {
35+
36+
// sentence translation is missing or we re-translate everything
37+
if (!$sentence->isTranslated() || $this->translateAll) {
38+
$key = md5($sentence->getOriginal() . $from . $to);
39+
/** @var \Symfony\Component\Cache\CacheItem $translation */
40+
41+
$translation = $this->getCache()->get($key);
42+
43+
if (!$translation->isHit()) {
44+
// translate sentence
45+
$translation->set($this->getTranslation($from, $to, $sentence->getOriginal()));
46+
47+
// save cache
48+
$this->getCache()->save($translation);
49+
}
50+
51+
$sentence->translate($translation->get());
52+
53+
if ($callback) $callback($sentence);
54+
}
55+
56+
}
57+
58+
return $this->sentences;
59+
}
60+
61+
abstract protected function getCache(): AdapterInterface;
62+
63+
64+
65+
}

src/commands/DeepLTranslatorCommand.php

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
namespace potrans\commands;
44

55
use DeepL\Translator;
6-
use Gettext\Generator\MoGenerator;
7-
use Gettext\Generator\PoGenerator;
8-
use Gettext\Loader\PoLoader;
9-
use Gettext\Translation;
6+
use potrans\PoTranslator;
7+
use potrans\translator\DeepLTranslator;
108
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
9+
use Symfony\Component\Cache\Adapter\NullAdapter;
1110
use Symfony\Component\Console\Command\Command;
1211
use Symfony\Component\Console\Exception\InvalidOptionException;
1312
use Symfony\Component\Console\Exception\RuntimeException;
@@ -25,38 +24,46 @@ protected function configure(): void {
2524
->addArgument('output', InputArgument::OPTIONAL, 'Output PO, MO files directory', '~/Downloads')
2625
->addOption('from', null, InputOption::VALUE_REQUIRED, 'Source language (default: en)', 'en')
2726
->addOption('to', null, InputOption::VALUE_REQUIRED, 'Target language (default: cs)', 'cs')
28-
->addOption('all', null, InputOption::VALUE_NONE, 'Re-translate including translated sentences')
27+
->addOption('force', null, InputOption::VALUE_NONE, 'Force re-translate including translated sentences')
2928
->addOption('wait', null, InputOption::VALUE_REQUIRED, 'Wait between translations in milliseconds', false)
3029
->addOption('apikey', null, InputOption::VALUE_REQUIRED, 'Deepl API Key')
3130
->addOption('cache', null, InputOption::VALUE_NEGATABLE, 'Load from cache or not', true);
3231
}
3332

3433
protected function execute(InputInterface $input, OutputInterface $output): int {
35-
// input PO file loading
36-
$wait = $input->getOption('wait');
37-
$cache = new FilesystemAdapter('deepl', 3600, 'cache');
38-
3934
try {
4035

36+
// Input PO file
4137
$inputFile = $input->getArgument('input');
42-
if (!is_file($inputFile)) {
38+
if (!file_exists($inputFile)) {
4339
throw new RuntimeException(sprintf('Input file "%s" not found', $inputFile));
4440
}
4541

46-
$poLoader = new PoLoader();
47-
$translations = $poLoader->loadFile($inputFile);
48-
49-
// output directory
42+
// Output directory
5043
$outputDir = realpath($input->getArgument('output')) . DIRECTORY_SEPARATOR;
5144
if (!is_dir($outputDir)) {
5245
throw new InvalidOptionException('Invalid directory path: ' . $outputDir);
5346
}
5447

55-
$from = $input->getOption('from');
56-
$to = $input->getOption('to');
57-
$apikey = $input->getOption('apikey');
48+
// Crete new DeepL translator
49+
$apikey = (string) $input->getOption('apikey');
50+
$translator = new DeepLTranslator(
51+
new Translator($apikey),
52+
);
53+
54+
// Setup caching
55+
$cache = $input->getOption('cache') ?
56+
new FilesystemAdapter('deepl', 3600, 'cache') :
57+
new NullAdapter();
58+
59+
// Read params
60+
$force = (bool) $input->getOption('force');
61+
$to = (string) $input->getOption('to');
62+
$from = (string) $input->getOption('from');
63+
$wait = (int) $input->getOption('wait');
5864

59-
$translator = new Translator($apikey);
65+
$potrans = new PoTranslator($translator, $cache);
66+
$translations = $potrans->loadFile($inputFile);
6067

6168
// translator
6269
$output->writeln(
@@ -74,53 +81,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7481
$progress = new ProgressBar($output, count($translations));
7582

7683
$translated = 0; // counter
77-
/** @var Translation $sentence */
78-
foreach ($translations as $sentence) {
79-
80-
if (!$sentence->getTranslation() || $input->getOption('all')) {
81-
// translated counter
82-
$translated++;
83-
84-
$key = md5($sentence->getOriginal() . $from . $to);
85-
$translation = $cache->getItem($key);
86-
87-
if (!$translation->isHit() || !$input->getOption('cache')) {
88-
89-
// TODO add Text translation options
90-
// @see https://github.com/DeepLcom/deepl-php#text-translation-options
91-
$response = $translator->translateText(
92-
$sentence->getOriginal(),
93-
$from,
94-
$to,
95-
);
96-
97-
$translation->set($response->text); // set new translation
98-
99-
// save only successful translations
100-
if ($response->text && $input->getOption('cache')) {
101-
$cache->save($translation);
102-
}
103-
}
104-
105-
$sentence->translate($translation->get());
106-
107-
// verbose mode show everything
108-
if ($output->isVeryVerbose()) {
109-
$output->writeln(
110-
[
111-
'-------------------------------------------------------------------------',
112-
' > ' . $sentence->getOriginal(),
113-
' > ' . $sentence->getTranslation(),
114-
]
115-
);
116-
}
84+
foreach ($potrans->translate($from, $to, $force) as $sentence) {
85+
// verbose mode show everything
86+
if ($output->isVeryVerbose()) {
87+
$output->writeln(
88+
[
89+
'-------------------------------------------------------------------------',
90+
' > <info>' . $sentence->getOriginal() . '</info>',
91+
' > <comment>' . $sentence->getTranslation() . '</comment>',
92+
]
93+
);
11794
}
11895

11996
// progress
12097
if (!$output->isVeryVerbose()) {
12198
$progress->advance();
12299
}
123100

101+
$translated++;
102+
124103
if ($wait) usleep($wait);
125104
}
126105

@@ -132,20 +111,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
132111
$output->writeln('<comment>Translated :</comment> ' . $translated . ' sentences');
133112

134113
// MO file output
135-
$moGenerator = new MoGenerator();
136114
$moOutputFile = $outputDir . pathinfo($inputFile, PATHINFO_FILENAME) . '.mo';
137115
if ($output->isVeryVerbose()) {
138116
$output->writeln('<comment>Writing new MO File</comment>: ' . $moOutputFile);
139117
}
140-
$moGenerator->generateFile($translations, $moOutputFile);
118+
$potrans->saveMoFile($moOutputFile);
141119

142120
// PO file output
143-
$poGenerator = new PoGenerator();
144121
$poOutputFile = $outputDir . pathinfo($inputFile, PATHINFO_FILENAME) . '.po';
145122
if ($output->isVeryVerbose()) {
146123
$output->writeln('<comment>Writing new PO File</comment>: ' . $poOutputFile);
147124
}
148-
$poGenerator->generateFile($translations, $poOutputFile);
125+
$potrans->savePoFile($moOutputFile);
149126

150127
// done!
151128
$output->writeln('<info>DONE!</info>');

0 commit comments

Comments
 (0)