Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (translation): add ability to ignore keys in translations #199

Merged
merged 4 commits into from Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 94 additions & 11 deletions src/GoogleTranslate.php
Expand Up @@ -37,6 +37,11 @@ class GoogleTranslate
*/
protected ?string $target;

/*
* @var string|null Regex pattern to match replaceable parts in a string, defualts to "words"
*/
protected string $pattern = '/:(\w+)/';

/**
* @var string|null Last detected source language.
*/
Expand Down Expand Up @@ -112,7 +117,7 @@ class GoogleTranslate
public function __construct(string $target = 'en', string $source = null, array $options = [], TokenProviderInterface $tokenProvider = null)
{
$this->client = new Client();
$this->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
$this->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator())
->setOptions($options) // Options are already set in client constructor tho.
->setSource($source)
->setTarget($target);
Expand Down Expand Up @@ -190,6 +195,18 @@ public function setTokenProvider(TokenProviderInterface $tokenProvider): self
return $this;
}

/**
* Set the regex pattern to match replaceable parts in a string
*
* @param string $pattern
* @return self
*/
public function setPattern(string $pattern): self
{
$this->pattern = $pattern;
return $this;
}

/**
* Get last detected source language
*
Expand All @@ -216,8 +233,8 @@ public function getLastDetectedSource(): ?string
*/
public static function trans(string $string, string $target = 'en', string $source = null, array $options = [], TokenProviderInterface $tokenProvider = null): ?string
{
return (new self)
->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator)
return (new self())
->setTokenProvider($tokenProvider ?? new GoogleTokenGenerator())
->setOptions($options) // Options are already set in client constructor tho.
->setSource($source)
->setTarget($target)
Expand All @@ -244,7 +261,11 @@ public function translate(string $string): ?string
return $string;
}

$responseArray = $this->getResponse($string);
// Extract replaceable keywords from string and transform to array for use later
$replacements = $this->getReplacements($string);

// Reaplce replaceable keywords with ${\d} for replacement later
$responseArray = $this->getResponse($this->extract($string));

// Check if translation exists
if (empty($responseArray[0])) {
Expand Down Expand Up @@ -278,18 +299,80 @@ public function translate(string $string): ?string
}

// The response sometime can be a translated string.
$output = '';
if (is_string($responseArray)) {
return $responseArray;
}

if (is_array($responseArray[0])) {
return (string) array_reduce($responseArray[0], static function ($carry, $item) {
$output = $this->inject($responseArray, $replacements);
} elseif (is_array($responseArray[0])) {
$output = (string) $this->inject(array_reduce($responseArray[0], static function ($carry, $item) {
$carry .= $item[0];
return $carry;
});
}), $replacements);
} else {
$output = (string) $this->inject($responseArray[0], $replacements);
}

return (string) $responseArray[0];
return $this->inject($this->sanitize($output), $replacements);
}

/**
* Extract replaceable keywords from string using the supplied pattern
*
* @param string $string
* @return string
*/
public function extract(string $string): string
{
return preg_replace_callback(
$this->pattern,
function ($matches) {
static $index = -1;

$index++;

return '${' . $index . '}';
},
$string
);
}

/**
* Inject the replacements back into the translated string
*
* @param string $string
* @param array<string> $replacements
* @return string
*/
public function inject(string $string, array $replacements): string
{
return preg_replace_callback(
'/\${(\d+)}/',
fn($matches) => ':' . $replacements[$matches[1]],
$string
);
}

/**
* Extract an array of replaceable parts to be injected into the translated string
* at a later time
*
* @return array<string>
*/
public function getReplacements(string $string): array
{
$matches = [];
preg_match_all($this->pattern, $string, $matches);
return $matches[1];
}

/**
* Cleans up weird spaces returned from Google Translate.
*
* @param string $string
* @return string
*/
protected function sanitize(string $string): string
{
return preg_replace('/\xc2\xa0/', ' ', $string);
}

/**
Expand Down
49 changes: 49 additions & 0 deletions tests/TranslationTest.php
Expand Up @@ -29,6 +29,13 @@ public function testTranslationEquality(): void
$this->assertEqualsIgnoringCase($resultOne, $resultTwo, 'Static and instance methods should return same result.');
}

public function testTranslationKeyExtraction(): void
{
$result = $this->tr->setSource('en')->setTarget('fr')->translate('Hello :name');

$this->assertEquals('Bonjour :name', $result, 'Translation should be correct with proper key extraction.');
}

public function testNewerLanguageTranslation(): void
{
$result = $this->tr->setSource('en')->setTarget('tk')->translate('Hello');
Expand Down Expand Up @@ -60,4 +67,46 @@ public function testRawResponse(): void

$this->assertIsArray($rawResult, 'Method getResponse() should return an array');
}

public function testGetReplacements(): void
{
$replacements = $this->tr->getReplacements('Hello :name are you :some_greeting?');

$this->assertEquals(['name', 'some_greeting'], $replacements, 'Replacements should be extracted from string');
}

public function testGetEmptyReplacements(): void
{
$replacements = $this->tr->getReplacements('Hello');

$this->assertEquals([], $replacements, 'Replacements should be empty');
}

public function testExtract(): void
{
$extracted = $this->tr->extract('Hello :name are you :some_greeting?');

$this->assertEquals('Hello ${0} are you ${1}?', $extracted, 'Extraction should change strings to placeholder tokens');
}

public function testEmptyExtract(): void
{
$extracted = $this->tr->extract('Hello');

$this->assertEquals('Hello', $extracted, 'Extraction should not change strings');
}

public function testInject(): void
{
$replaced = $this->tr->inject('Hello ${0} are you ${1}?', ['name', 'some_greeting']);

$this->assertEquals('Hello :name are you :some_greeting?', $replaced, 'Replacement should change placeholder tokens to strings');
}

public function testEmptyInject(): void
{
$replaced = $this->tr->inject('Hello', []);

$this->assertEquals('Hello', $replaced, 'Replacement should not change strings');
}
}