Skip to content

Commit

Permalink
Merge pull request #2 from Elhebert/main
Browse files Browse the repository at this point in the history
Add DeepL as translation service
  • Loading branch information
Bottelet authored Sep 19, 2024
2 parents 985a78a + ff79f85 commit e58f202
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 7 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.idea
vendor
.phpunit.result.cache
.php-cs-fixer.cache
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"php": "^8.1",
"nikic/php-parser": "^v5.2",
"google/cloud-translate": "^1.17",
"openai-php/client": "^0.10.1"
"openai-php/client": "^0.10.1",
"deeplcom/deepl-php": "^1.9"
},
"config": {
"allow-plugins": {
Expand Down
67 changes: 65 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion config/translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| using this translation library. This service is used when another is
| not explicitly specified when executing a given translation function.
|
| Supported: "google", "openai"
| Supported: "google", "openai", "deepl"
|
*/
'default' => env('DEFAULT_TRANSLATOR_SERVICE', 'openai'),
Expand All @@ -29,6 +29,10 @@
'api_key' => env('OPENAI_API_KEY'),
'organization_id' => env('OPENAI_ORGANIZATION'),
],
'deepl' => [
'driver' => Bottelet\TranslationChecker\Translator\DeeplTranslator::class,
'api_key' => env('DEEPL_API_KEY'),
],
],
'source_paths' => [
base_path('app/'),
Expand Down
9 changes: 8 additions & 1 deletion docs/translation-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ GOOGLE_TRANSLATE_CLIENT_CERT_URL=your_client_cert_url
```
See more here: [Google Translate Setup](https://cloud.google.com/translate/docs/setup)

For DeepL, you need to set the following environment variable
```bash
DEEPL_API_KEY=your_api_key
```
See more here: [DeepL API Authentication](https://developers.deepl.com/docs/getting-started/auth#authentication)

### Default Service
The `default` configuration option specifies the class to be used for automatic translation. This service will be used for translating strings.

There are currently two built-in translation services:
There are currently three built-in translation services:
1. **GoogleTranslateService** - Translates strings using Google Translate
2. **OpenAIService** - Translates strings using OpenAI's API
3. **DeepLService** - Translates strings using DeepL's API


You can create your own translation service by implementing the `Bottelet\TranslationChecker\Translator\TranslatorContract` and overwriting the `default` configuration option.
9 changes: 9 additions & 0 deletions src/TranslationCheckerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

use Bottelet\TranslationChecker\Sort\AlphabeticSort;
use Bottelet\TranslationChecker\Sort\SorterContract;
use Bottelet\TranslationChecker\Translator\DeeplTranslator;
use Bottelet\TranslationChecker\Translator\GoogleTranslator;
use Bottelet\TranslationChecker\Translator\OpenAiTranslator;
use Bottelet\TranslationChecker\Translator\TranslatorContract;
use Bottelet\TranslationChecker\Translator\VariableHandlers\VariableRegexHandler;
use DeepL\Translator;
use Google\Cloud\Translate\V2\TranslateClient;
use Illuminate\Support\ServiceProvider;
use OpenAI;
Expand Down Expand Up @@ -48,6 +50,13 @@ public function register(): void
$factory->withHttpHeader('OpenAI-Beta', 'assistants=v2')->make()
);
});

$this->app->bind(DeeplTranslator::class, function ($app) {
return new DeeplTranslator(
$app->make(VariableRegexHandler::class),
new Translator($app->config['translator.translators.deepl.api_key'])
);
});
}

public function boot(): void
Expand Down
57 changes: 57 additions & 0 deletions src/Translator/DeeplTranslator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Bottelet\TranslationChecker\Translator;

use Bottelet\TranslationChecker\Translator\VariableHandlers\VariableRegexHandler;
use DeepL\Translator;

class DeeplTranslator implements TranslatorContract
{
public function __construct(
protected VariableRegexHandler $variableHandler,
protected Translator $translateClient,
) {
}

/**
* @throws \DeepL\DeepLException
*/
public function translate(string $text, string $targetLanguage, string $sourceLanguage = 'en'): string
{
$replaceVariablesText = $this->variableHandler->replacePlaceholders($text);

$translation = $this->translateClient->translateText($replaceVariablesText, $sourceLanguage, $targetLanguage);

return $this->variableHandler->restorePlaceholders($translation->text);
}

/**
* @param array<string> $texts
* @return array<string>
*
* @throws \DeepL\DeepLException
*/
public function translateBatch(array $texts, string $targetLanguage, string $sourceLanguage = 'en'): array
{
$textsToTranslate = array_map([$this->variableHandler, 'replacePlaceholders'], $texts);

$translations = $this->translateClient->translateText($textsToTranslate, $sourceLanguage, $targetLanguage);

$translatedKeys = [];
foreach ($translations as $index => $translation) {
$translatedKeys[$texts[$index]] = $this->variableHandler->restorePlaceholders($translation->text);
}

return $translatedKeys;
}

public function isConfigured(): bool
{
/** @var array<string, null|string> $deeplConfig */
$deeplConfig = config('translator.translators.deepl');

return !in_array(null, $deeplConfig, true);
}
}
2 changes: 0 additions & 2 deletions tests/Finder/MissingKeysFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ public function findMissingTranslationsFindsPersistentKeysFromConfig(): void
$this->assertArrayHasKey('A sentence that should be added to the translation file', $foundStrings);
}


#[Test]
public function findMissingTranslatableStringUseNullAsDefaultValue(): void
{
Expand All @@ -145,6 +144,5 @@ public function findMissingTranslatableStringUseNullAsDefaultValue(): void

$foundStrings = $translationFinder->findMissingTranslatableStrings([$multiFunctionFile], []);
$this->assertSame(['da.key.test' => null, 'a long string' => null], $foundStrings);

}
}
103 changes: 103 additions & 0 deletions tests/Translator/DeeplTranslatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace Bottelet\TranslationChecker\Tests\Translator;

use Bottelet\TranslationChecker\Translator\DeeplTranslator;
use Bottelet\TranslationChecker\Translator\VariableHandlers\VariableRegexHandler;
use DeepL\TextResult;
use DeepL\Translator;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;

class DeeplTranslatorTest extends \Bottelet\TranslationChecker\Tests\TestCase
{
/** @var VariableRegexHandler|MockObject */
private $variableHandlerMock;

/** @var Translator|MockObject */
private $translateClientMock;

/** @var DeeplTranslator */
private $deeplTranslator;

protected function setUp(): void
{
parent::setUp();

$this->app['config']->set('translator.translators.deepl', [
'api_key' => 'test-key',
]);

$this->translateClientMock = $this->createMock(Translator::class);
$this->variableHandlerMock = $this->createMock(VariableRegexHandler::class);
$this->deeplTranslator = new DeeplTranslator($this->variableHandlerMock, $this->translateClientMock);
}

#[Test]
public function translate(): void
{
$text = 'Hello, world!';
$translatedText = 'Bonjour le monde!';
$targetLanguage = 'fr';

$this->variableHandlerMock->method('replacePlaceholders')
->willReturn($text);

$this->variableHandlerMock->method('restorePlaceholders')
->willReturn($translatedText);

$this->translateClientMock->method('translateText')
->willReturn(new TextResult($translatedText, 'en', 0));

$result = $this->deeplTranslator->translate($text, $targetLanguage);

$this->assertSame($translatedText, $result);
}

#[Test]
public function translateBatch(): void
{
$texts = ['Hello, world!', 'Good morning'];
$translatedTexts = ['Bonjour le monde!', 'Bonjour'];
$targetLanguage = 'fr';

$translations = array_map(fn ($text) => new TextResult($text, 'en', 0), $translatedTexts);

$this->variableHandlerMock->expects($this->exactly(count($texts)))
->method('replacePlaceholders')
->willReturnOnConsecutiveCalls(...$texts);

$this->variableHandlerMock->expects($this->exactly(count($texts)))
->method('restorePlaceholders')
->willReturnOnConsecutiveCalls(...$translatedTexts);

$this->translateClientMock->expects($this->once())
->method('translateText')
->willReturn($translations);

$result = $this->deeplTranslator->translateBatch($texts, $targetLanguage);

$this->assertSame(['Hello, world!' => 'Bonjour le monde!', 'Good morning' => 'Bonjour'], $result);
}

#[Test]
public function testDeeplTranslatorBinding(): void
{
$this->assertInstanceOf(DeeplTranslator::class, app(DeeplTranslator::class));
}

#[Test]
public function testDeeplTranslatorHasValidCredentials(): void
{
$this->assertTrue($this->deeplTranslator->isConfigured());
}

#[Test]
public function testDeeplTranslatorHasInvalidCredentials(): void
{
$this->app['config']->set('translator.translators.deepl', [
'api_key' => null,
]);
$this->assertFalse($this->deeplTranslator->isConfigured());
}
}

0 comments on commit e58f202

Please sign in to comment.