Skip to content

Commit 7d4d17d

Browse files
committed
extract and harden converter and add extra tests
1 parent 45c3c48 commit 7d4d17d

File tree

5 files changed

+144
-97
lines changed

5 files changed

+144
-97
lines changed

src/LokaliseClient.php

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ public function getTranslations(?string $fileName = null): Collection
6565
$key = Str::replace('::', '.', $data['key_name']['web']);
6666
$translations = $translations->merge(
6767
array_map(
68-
fn (array $translation) => $this->prepareTranslation($translation['language_iso'], $key, $translation['translation']),
68+
fn (array $translation) => new Translation(
69+
$translation['language_iso'],
70+
$key,
71+
TranslationConverter::fromLokalise($translation['translation']),
72+
),
6973
$data['translations'] ?? [],
7074
)
7175
);
@@ -121,26 +125,4 @@ public function deleteKeys(array $keys): void
121125
],
122126
);
123127
}
124-
125-
private function prepareTranslation(string $locale, string $key, ?string $translation = null): ?Translation
126-
{
127-
if (empty($translation)) {
128-
return null;
129-
}
130-
131-
// Check if the translation is a plural translation and map it to a Laravel compatible format
132-
$json = json_decode($translation, true);
133-
if ($json && isset($json['one'], $json['other'])) {
134-
if (empty($json['one']) && empty($json['other'])) {
135-
return null;
136-
}
137-
$translation = $json['one'].'|'.$json['other'];
138-
}
139-
// I get these strings and need to convert it to colon prefix variable names:
140-
// The [%1$s:attribute] field must be present when [%1$s:values] are present.
141-
// The :attribute field must be present when :values are present.
142-
$translation = Str::of($translation)->replaceMatches('/\[\%1\$s:(\w+)\]/', ':$1')->__toString();
143-
144-
return new Translation($locale, $key, $translation);
145-
}
146128
}

src/LokaliseService.php

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,7 @@ public function prepare(array $translations): array
102102
// For keys, we can use the simple regex
103103
$lokaliseKey = preg_replace("/:([\w\d]+)/", '{{$1}}', $key);
104104

105-
// For translation values, we need more sophisticated pattern matching
106-
// This regex avoids replacing:
107-
// 1. Variables already in curly braces like {VARIABLE}
108-
// 2. Variables inside HTML attributes like style="width:100%"
109-
$translationWithReplacedVariableSyntax = preg_replace(
110-
'/(?<![\{\w]):([\w\d]+)(?!\}|%|[^\s\.,;!\?<>\(\)\[\]\{\}\'"])/m',
111-
'{{$1}}',
112-
$value
113-
);
114-
115-
if (Str::contains($translationWithReplacedVariableSyntax, '|')) {
116-
[$singular, $plural] = explode('|', $translationWithReplacedVariableSyntax, 2);
117-
$translationWithReplacedVariableSyntax = json_encode([
118-
'one' => $singular,
119-
'other' => $plural,
120-
], JSON_UNESCAPED_UNICODE);
121-
}
122-
$lokaliseTranslations[$lokaliseKey] = $translationWithReplacedVariableSyntax;
105+
$lokaliseTranslations[$lokaliseKey] = TranslationConverter::toLokalise($value);
123106
}
124107

125108
return $lokaliseTranslations;

src/TranslationConverter.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bambamboole\LaravelLokalise;
4+
5+
use Illuminate\Support\Str;
6+
7+
class TranslationConverter
8+
{
9+
public static function toLokalise(string $translation): string
10+
{
11+
// For translation values, we need more sophisticated pattern matching
12+
// This regex avoids replacing:
13+
// 1. Variables already in curly braces like {VARIABLE}
14+
// 2. Variables inside HTML attributes like style="width:100%"
15+
$translationWithReplacedVariableSyntax = preg_replace(
16+
'/(?<![\{\w]):([\w\d]+)(?!\}|%|[^\s\.,;!\?<>\(\)\[\]\{\}\'"])/m',
17+
'{{$1}}',
18+
$translation
19+
);
20+
21+
if (Str::contains($translationWithReplacedVariableSyntax, '|')) {
22+
[$singular, $plural] = explode('|', $translationWithReplacedVariableSyntax, 2);
23+
$translationWithReplacedVariableSyntax = json_encode([
24+
'one' => $singular,
25+
'other' => $plural,
26+
], JSON_UNESCAPED_UNICODE);
27+
}
28+
29+
return $translationWithReplacedVariableSyntax;
30+
}
31+
32+
public static function fromLokalise(string $translation): string
33+
{
34+
// Check if the translation is a plural translation and map it to a Laravel compatible format
35+
$json = json_decode($translation, true);
36+
if ($json && isset($json['one'], $json['other'])) {
37+
$translation = $json['one'].'|'.$json['other'];
38+
}
39+
40+
// I get these strings and need to convert it to colon prefix variable names:
41+
// The [%1$s:attribute] field must be present when [%1$s:values] are present.
42+
// The :attribute field must be present when :values are present.
43+
return Str::of($translation)->replaceMatches('/\[\%1\$s:(\w+)\]/', ':$1')->__toString();
44+
}
45+
}

tests/Unit/LokaliseServiceTest.php

Lines changed: 19 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,90 +7,53 @@
77
use Bambamboole\LaravelLokalise\LokaliseService;
88
use Illuminate\Filesystem\Filesystem;
99
use Illuminate\Support\Str;
10+
use PHPUnit\Framework\MockObject\MockObject;
1011
use PHPUnit\Framework\TestCase;
1112

1213
class LokaliseServiceTest extends TestCase
1314
{
14-
public function test_it_preserves_curly_braces_formats()
15-
{
16-
$client = $this->createMock(LokaliseClient::class);
17-
$repository = $this->createMock(LocalTranslationRepository::class);
18-
$service = new LokaliseService($client, $repository, __DIR__);
19-
20-
$result = $service->prepare([
21-
// Basic Laravel placeholders
22-
'accepted' => 'The :attribute must be accepted.',
23-
24-
// Variable within curly braces - should not be converted
25-
'page_page_nb' => 'Page :page of {nb}',
26-
27-
// Complex variables in curly braces - should not be converted
28-
'eg_discount' => 'e.g. discount {ZAHLUNGSZIELSKONTO}% within {ZAHLUNGSZIELTAGESKONTO} days.',
29-
30-
// Variable in curly braces with text explanation - should not convert the curly brace content
31-
'content_password' => 'The content can be accessed via the variable {PASSWORT}.',
32-
33-
// HTML with styling that contains colons - should not convert CSS attributes
34-
'table_style' => '<table style="width:100%;"><tr><td width="100%">:prefix should convert</td></tr></table>',
35-
36-
// Mixed example with both HTML styling and regular Laravel variables
37-
'mixed_html' => '<div style="color:red; width:50%;">The :attribute field is :status.</div>',
38-
]);
15+
private string $basePath;
3916

40-
$this->assertEquals('The {{attribute}} must be accepted.', $result['accepted']);
41-
// Verify basic Laravel placeholders are converted
42-
$this->assertEquals('The {{attribute}} must be accepted.', $result['accepted']);
17+
private MockObject|LokaliseClient $client;
4318

44-
// Verify variable within curly braces are not converted
45-
$this->assertEquals('Page {{page}} of {nb}', $result['page_page_nb']);
19+
private LocalTranslationRepository $repo;
4620

47-
// Verify complex variables in curly braces are not converted
48-
$this->assertEquals('e.g. discount {ZAHLUNGSZIELSKONTO}% within {ZAHLUNGSZIELTAGESKONTO} days.', $result['eg_discount']);
49-
50-
// Verify variable in curly braces with text explanation is not converted
51-
$this->assertEquals('The content can be accessed via the variable {PASSWORT}.', $result['content_password']);
52-
53-
// Verify HTML with styling that contains colons is not converted in attributes
54-
$this->assertEquals('<table style="width:100%;"><tr><td width="100%">{{prefix}} should convert</td></tr></table>', $result['table_style']);
55-
56-
// Verify mixed example works correctly
57-
$this->assertEquals('<div style="color:red; width:50%;">The {{attribute}} field is {{status}}.</div>', $result['mixed_html']);
21+
protected function setUp(): void
22+
{
23+
$this->basePath = dirname(__DIR__).'/fixtures';
24+
$this->client = $this->createMock(LokaliseClient::class);
25+
$this->repo = new LocalTranslationRepository(new Filesystem, $this->basePath);
5826
}
5927

6028
public function test_it_skips_json_files_if_configured()
6129
{
62-
$basePath = dirname(__DIR__).'/fixtures';
63-
$repo = new LocalTranslationRepository(new Filesystem, $basePath);
64-
$client = $this->createMock(LokaliseClient::class);
65-
$client->expects(self::once())
30+
$this->client->expects(self::once())
6631
->method('getLocales')
6732
->willReturn(['en', 'de']);
68-
$client->expects(self::exactly(2))
33+
$this->client->expects(self::exactly(2))
6934
->method('uploadFile')
7035
->with(self::anything(), self::callback(fn ($file) => Str::endsWith($file, '.php')));
7136

72-
$service = new LokaliseService($client, $repo, dirname(__DIR__).'/fixtures');
73-
74-
$service->uploadTranslations();
37+
$this->createSubject()->uploadTranslations();
7538
}
7639

7740
public function test_it_includes_json_files_if_configured()
7841
{
79-
$basePath = dirname(__DIR__).'/fixtures';
80-
$repo = new LocalTranslationRepository(new Filesystem, $basePath);
81-
$client = $this->createMock(LokaliseClient::class);
82-
$client->expects(self::once())
42+
$this->client->expects(self::once())
8343
->method('getLocales')
8444
->willReturn(['en', 'de']);
85-
$client->expects(self::exactly(4))
45+
$this->client->expects(self::exactly(4))
8646
->method('uploadFile')
8747
->with(
8848
self::anything(),
8949
self::callback(fn ($file) => Str::endsWith($file, '.php') || Str::endsWith($file, '.json')),
9050
);
9151

92-
$service = new LokaliseService($client, $repo, dirname(__DIR__).'/fixtures', false);
52+
$this->createSubject(false)->uploadTranslations();
53+
}
9354

94-
$service->uploadTranslations();
55+
private function createSubject(bool $skipJsonFiles = true): LokaliseService
56+
{
57+
return new LokaliseService($this->client, $this->repo, $this->basePath, $skipJsonFiles);
9558
}
9659
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bambamboole\LaravelLokalise\Tests\Unit;
4+
5+
use Bambamboole\LaravelLokalise\TranslationConverter;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class TranslationConverterTest extends TestCase
10+
{
11+
#[DataProvider('provideTranslationsFromLokalise')]
12+
public function test_from_lokalise(string $in, string $out)
13+
{
14+
$this->assertEquals($out, TranslationConverter::fromLokalise($in));
15+
}
16+
17+
public static function provideTranslationsFromLokalise(): array
18+
{
19+
return [
20+
[
21+
'in' => 'https://help.xentral.com/hc/de/articles/16164850902428-Arbeiten-mit-dem-%C3%9Cbertragungen-Modul',
22+
'out' => 'https://help.xentral.com/hc/de/articles/16164850902428-Arbeiten-mit-dem-%C3%9Cbertragungen-Modul',
23+
],
24+
[
25+
'in' => 'https://community.xentral.com/hc/de/articles/360019652739-St%C3%BCckliste-verwenden',
26+
'out' => 'https://community.xentral.com/hc/de/articles/360019652739-St%C3%BCckliste-verwenden',
27+
],
28+
[
29+
'in' => 'The [%1$s:attribute] field must be present & valid.',
30+
'out' => 'The :attribute field must be present & valid.',
31+
],
32+
[
33+
'in' => 'Visit https://example.com/%C3%BCber and check the [%1$s:field] value.',
34+
'out' => 'Visit https://example.com/%C3%BCber and check the :field value.',
35+
],
36+
];
37+
}
38+
39+
#[DataProvider('provideTranslationsToLokalise')]
40+
public function test_to_lokalise(string $in, string $out)
41+
{
42+
$this->assertEquals($out, TranslationConverter::toLokalise($in));
43+
}
44+
45+
public static function provideTranslationsToLokalise(): array
46+
{
47+
return [
48+
[
49+
'in' => 'The :attribute must be accepted.',
50+
'out' => 'The {{attribute}} must be accepted.',
51+
],
52+
[
53+
'in' => 'Page :page of {nb}',
54+
'out' => 'Page {{page}} of {nb}',
55+
],
56+
[
57+
'in' => 'e.g. discount {ZAHLUNGSZIELSKONTO}% within {ZAHLUNGSZIELTAGESKONTO} days.',
58+
'out' => 'e.g. discount {ZAHLUNGSZIELSKONTO}% within {ZAHLUNGSZIELTAGESKONTO} days.',
59+
],
60+
[
61+
'in' => 'The content can be accessed via the variable {PASSWORT}.',
62+
'out' => 'The content can be accessed via the variable {PASSWORT}.',
63+
],
64+
[
65+
'in' => '<table style="width:100%;"><tr><td width="100%">:prefix should convert</td></tr></table>',
66+
'out' => '<table style="width:100%;"><tr><td width="100%">{{prefix}} should convert</td></tr></table>',
67+
],
68+
[
69+
'in' => '<div style="color:red; width:50%;">The :attribute field is :status.</div>',
70+
'out' => '<div style="color:red; width:50%;">The {{attribute}} field is {{status}}.</div>',
71+
],
72+
];
73+
}
74+
}

0 commit comments

Comments
 (0)