47 | */
48 | public function functions(): array;
49 | }
50 |
--------------------------------------------------------------------------------
/src/Translator/Exception/InvalidDirectoriesConfiguration.php:
--------------------------------------------------------------------------------
1 | key = $key;
13 | $this->value = $value;
14 | }
15 |
16 | public function getKey(): string
17 | {
18 | return $this->key;
19 | }
20 |
21 | public function getValue(): string
22 | {
23 | return $this->value;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Translator/TranslationRepository.php:
--------------------------------------------------------------------------------
1 | scanDirectory($directory, $ext, $functions)
42 | );
43 | },
44 | []
45 | );
46 | }
47 |
48 | /**
49 | * @param string[] $functions
50 | * @return Translation[]
51 | */
52 | private function scanDirectory(string $path, string $extensions, array $functions): array
53 | {
54 | $files = glob_recursive("{$path}/*.{{$extensions}}", GLOB_BRACE);
55 |
56 | return array_reduce($files, function (array $keys, $file) use ($functions): array {
57 | $content = $this->getFileContent($file);
58 |
59 | $keysFromFunctions = array_reduce(
60 | $functions,
61 | function (array $keys, string $function) use ($content): array {
62 | return array_merge($keys, $this->getKeysFromFunction($function, $content));
63 | },
64 | []
65 | );
66 |
67 | return array_merge(
68 | $keys,
69 | $keysFromFunctions
70 | );
71 | }, []);
72 | }
73 |
74 | private function getFileContent(string $filePath): string
75 | {
76 | $content = (string) file_get_contents($filePath) ?? '';
77 |
78 | return str_replace("\n", ' ', $content);
79 | }
80 |
81 | /**
82 | * @return string[]
83 | */
84 | private function getKeysFromFunction(string $functionName, string $content): array
85 | {
86 | preg_match_all("#{$functionName} *\( *((['\"])((?:\\\\\\2|.)*?)\\2)#", $content, $matches);
87 |
88 | $matches = $matches[1] ?? [];
89 |
90 | return array_reduce($matches, function (array $keys, string $match) {
91 | $quote = $match[0];
92 | $match = trim($match, $quote);
93 | $key = ($quote === '"') ? stripcslashes($match) : str_replace(["\\'", "\\\\"], ["'", "\\"], $match);
94 |
95 | return $key ?
96 | array_merge($keys, [$key => new Translation($key, '')]) :
97 | $keys;
98 | }, []);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Translator/TranslationService.php:
--------------------------------------------------------------------------------
1 | config = $config;
17 | $this->scanner = $scanner;
18 | $this->repository = $repository;
19 | }
20 |
21 | /**
22 | * @throws InvalidDirectoriesConfiguration
23 | * @throws InvalidExtensionsConfiguration
24 | */
25 | public function scanAndSaveNewKeys(): void
26 | {
27 | $directories = $this->config->directories();
28 | $extensions = $this->config->extensions();
29 | $functions = $this->config->functions();
30 |
31 | $translations = $this->scanner->scan($extensions, $directories, $functions);
32 |
33 | $this->storeTranslations($translations);
34 | }
35 |
36 | /**
37 | * @param Translation[] $translations
38 | */
39 | private function storeTranslations(array $translations): void
40 | {
41 | array_map(function (Translation $translation): void {
42 | $this->storeTranslation($translation);
43 | }, $translations);
44 | }
45 |
46 | private function storeTranslation(Translation $translation): void
47 | {
48 | $languages = $this->config->languages();
49 |
50 | array_map(function (string $language) use ($translation): void {
51 | if ($this->repository->exists($translation, $language)) {
52 | return;
53 | }
54 |
55 | $this->repository->save($translation, $language);
56 | }, $languages);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Fixtures/App/Functions/Lang/LangTranslation.php:
--------------------------------------------------------------------------------
1 | foo = $foo;
13 | $this->bar = $bar;
14 | }
15 |
16 | public function getFoo(): string
17 | {
18 | return $this->foo;
19 | }
20 |
21 | public function getBar(): string
22 | {
23 | return $this->bar;
24 | }
25 |
26 | public function __toString(): string
27 | {
28 | return __("Lang: :foo, :bar", [':foo' => $this->foo, ':bar' => $this->bar]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Fixtures/App/Functions/UnderscoreUnderscore/UnderscoreUnderscoreTranslation.php:
--------------------------------------------------------------------------------
1 | foo = $foo;
13 | $this->bar = $bar;
14 | }
15 |
16 | public function getFoo(): string
17 | {
18 | return $this->foo;
19 | }
20 |
21 | public function getBar(): string
22 | {
23 | return $this->bar;
24 | }
25 |
26 | public function __toString(): string
27 | {
28 | return __("Underscore: :foo, :bar", [':foo' => $this->foo, ':bar' => $this->bar]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Fixtures/App/View/Component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ __('This is a vue component') }}
4 |
5 |
6 |
7 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/Fixtures/App/View/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Laravel Translator
4 |
5 |
6 |
7 | @lang('Welcome, :name', [':name' => 'Arthur Dent'])
8 |
9 |
10 |
11 | {{ lang('Trip to :planet, check-in opens :time', [':place' => 'Argabuthon', ':time' => '9 days']) }}
12 |
13 |
14 |
15 | {{ __('Check offers to :planet', [':place' => 'Damogran']) }}
16 |
17 |
18 |
19 | {{ __("Translations should also work with double quotes.") }}
20 |
21 |
22 |
23 | {{ __('Shouldn\'t escaped quotes within strings also be correctly added?') }}
24 |
25 |
26 |
27 | {{ __("Same goes for \"double quotes\".") }}
28 |
29 |
30 |
31 | {{ __('String using (parentheses).') }}
32 |
33 |
34 |
35 | {{ __("Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\") }}
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/SubDir/SubDir2/SubDir2file1.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/SubDir/SubDir2/SubDir2file1.txt
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/SubDir/SubDir2/SubDir2file2.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/SubDir/SubDir2/SubDir2file2.txt
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/SubDir/SubDirFile1.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/SubDir/SubDirFile1.txt
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/SubDir/SubDirFile2.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/SubDir/SubDirFile2.txt
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/file1.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/file1.txt
--------------------------------------------------------------------------------
/tests/Fixtures/Glob/file2.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/Glob/file2.txt
--------------------------------------------------------------------------------
/tests/Fixtures/translations/bg.json:
--------------------------------------------------------------------------------
1 | {
2 | "I'll be back": ""
3 | }
--------------------------------------------------------------------------------
/tests/Fixtures/translations/de.json:
--------------------------------------------------------------------------------
1 | You shall not pass: Du kannst nicht vorbei
--------------------------------------------------------------------------------
/tests/Fixtures/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "I'll be back": "I'll be back"
3 | }
--------------------------------------------------------------------------------
/tests/Fixtures/translations/es.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/tests/Fixtures/translations/fr.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/tests/Fixtures/translations/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "You shall not pass": "Não passarás"
3 | }
4 |
--------------------------------------------------------------------------------
/tests/Fixtures/translations/ru.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thiagocordeiro/laravel-translator/89aec3e3aa3217800180547472261c7411408d60/tests/Fixtures/translations/ru.json
--------------------------------------------------------------------------------
/tests/Unit/Framework/HelperTest.php:
--------------------------------------------------------------------------------
1 | testDir = realpath(__DIR__ . '/../..') ?? '';
14 | }
15 |
16 | public function testGlobRecursive(): void
17 | {
18 | $fixturesDir = realpath("{$this->testDir}/Fixtures/Glob");
19 |
20 | $files = glob_recursive("{$fixturesDir}/*.txt", GLOB_BRACE);
21 |
22 | $this->assertEquals([
23 | '/Fixtures/Glob/file1.txt',
24 | '/Fixtures/Glob/file2.txt',
25 | '/Fixtures/Glob/SubDir/SubDirFile1.txt',
26 | '/Fixtures/Glob/SubDir/SubDirFile2.txt',
27 | '/Fixtures/Glob/SubDir/SubDir2/SubDir2file1.txt',
28 | '/Fixtures/Glob/SubDir/SubDir2/SubDir2file2.txt',
29 | ], $this->replaceDirectorySeparators($this->removeRelativePath($files)));
30 | }
31 |
32 | /**
33 | * @param string[] $files
34 | * @return string[]
35 | */
36 | private function removeRelativePath(array $files): array
37 | {
38 | return array_map(function (string $file): string {
39 | return str_replace($this->testDir, '', $file);
40 | }, $files);
41 | }
42 |
43 | /**
44 | * @param string[] $files
45 | * @return string[]
46 | */
47 | private function replaceDirectorySeparators(array $files): array
48 | {
49 | return array_map(function (string $file): string {
50 | return str_replace('\\', '/', $file);
51 | }, $files);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Unit/Infra/LaravelJsonTranslationRepositoryTest.php:
--------------------------------------------------------------------------------
1 | translationPath = realpath(__DIR__ . '/../../Fixtures/translations');
21 |
22 | $configLoader = $this->setupConfigLoader();
23 |
24 | file_put_contents("{$this->translationPath}/fr.json", '{}');
25 |
26 | $this->repository = new LaravelJsonTranslationRepository($configLoader);
27 | }
28 |
29 | /**
30 | * @return \PHPUnit\Framework\MockObject\MockObject|ConfigLoader
31 | */
32 | protected function setupConfigLoader()
33 | {
34 | $configLoader = $this->createMock(ConfigLoader::class);
35 | $configLoader
36 | ->method('output')
37 | ->willReturn($this->translationPath);
38 |
39 | return $configLoader;
40 | }
41 |
42 | public function testWhenFileForGivenLanguageDoesNotExistThenThrowException(): void
43 | {
44 | $translation = new Translation('', '');
45 | $language = 'nl';
46 |
47 | $this->expectException(TranslationFileDoesNotExistForLanguage::class);
48 |
49 | $this->repository->exists($translation, $language);
50 | }
51 |
52 | public function testWhenFileForGivenLanguageDoesNotContainAValidJsonContentThenThrowException(): void
53 | {
54 | $translation = new Translation('', '');
55 | $language = 'de';
56 |
57 | $this->expectException(InvalidTranslationFile::class);
58 |
59 | $this->repository->exists($translation, $language);
60 | }
61 |
62 | public function testWhenGivenANewKeyThenExistsIsFalse(): void
63 | {
64 | $translation = new Translation('You shall not pass', '');
65 | $language = 'es';
66 |
67 | $exists = $this->repository->exists($translation, $language);
68 |
69 | $this->assertFalse($exists);
70 | }
71 |
72 | public function testWhenGivenARegisteredKeyThenExistsIsTrue(): void
73 | {
74 | $translation = new Translation('You shall not pass', '');
75 | $language = 'pt';
76 |
77 | $exists = $this->repository->exists($translation, $language);
78 |
79 | $this->assertTrue($exists);
80 | }
81 |
82 | public function testWhenTryingToSaveAKeyWhichAlreadyExistsThenThrowException(): void
83 | {
84 | $translation = new Translation("I'll be back", '');
85 | $this->repository->save($translation, 'fr');
86 |
87 | $this->expectException(UnableToSaveTranslationKeyAlreadyExists::class);
88 |
89 | $this->repository->save($translation, 'fr');
90 | }
91 |
92 | public function testSavingATranslationThenUpdateFile(): void
93 | {
94 | $translation = new Translation("I'll be back", '');
95 |
96 | $this->repository->save($translation, 'fr');
97 |
98 | $json = json_decode(file_get_contents("$this->translationPath/fr.json"), true);
99 | $this->assertEquals(['I\'ll be back' => ''], $json);
100 | }
101 |
102 | public function testWhenTryingToLoadAnInvalidJsonFileThenThrowException(): void
103 | {
104 | $translation = new Translation("I'll be back", '');
105 |
106 | $this->expectException(InvalidTranslationFile::class);
107 |
108 | $this->repository->save($translation, 'ru');
109 | }
110 |
111 | public function testSettingDefaultLanguageKeyAsValue(): void
112 | {
113 | $configLoader = $this->setupConfigLoader();
114 | $configLoader->method('languages')->willReturn(['en', 'bg']);
115 | $configLoader->method('defaultLanguage')->willReturn('en');
116 | $configLoader->method('useKeysAsDefaultValue')->willReturn(true);
117 |
118 | file_put_contents("{$this->translationPath}/en.json", '{}');
119 | file_put_contents("{$this->translationPath}/bg.json", '{}');
120 |
121 | $repository = new LaravelJsonTranslationRepository($configLoader);
122 |
123 | $translation = new Translation("I'll be back", '');
124 | $repository->save($translation, 'en');
125 |
126 | $translation = new Translation("I'll be back", '');
127 | $repository->save($translation, 'bg');
128 |
129 | $json = json_decode(file_get_contents("$this->translationPath/en.json"), true);
130 | $this->assertEquals(['I\'ll be back' => 'I\'ll be back'], $json);
131 |
132 | $json = json_decode(file_get_contents("$this->translationPath/bg.json"), true);
133 | $this->assertEquals(['I\'ll be back' => ''], $json);
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/tests/Unit/Translator/TranslationScannerTest.php:
--------------------------------------------------------------------------------
1 | fixturesDir = realpath(__DIR__ . '/../../Fixtures');
19 | $this->scanner = new TranslationScanner();
20 | }
21 |
22 | public function testWhenDirectoriesToScanAreNotSetThenThrowException(): void
23 | {
24 | $directories = [];
25 |
26 | $this->expectException(InvalidDirectoriesConfiguration::class);
27 |
28 | $this->scanner->scan(['php'], $directories, ['lang', '__']);
29 | }
30 |
31 | public function testWhenFunctionsToScanAreNotSetThenThrowException(): void
32 | {
33 | $functions = [];
34 |
35 | $this->expectException(InvalidFunctionsConfiguration::class);
36 |
37 | $this->scanner->scan(['php'], ['App'], $functions);
38 | }
39 |
40 | public function testShouldFindTranslationsForUnderscoreFunctions(): void
41 | {
42 | $__dir = $this->fixturesDir . '/App/Functions/UnderscoreUnderscore';
43 |
44 | $translations = $this->scanner->scan(['php'], [$__dir], ['lang', '__']);
45 |
46 | $this->assertEquals(
47 | [
48 | 'Underscore: :foo, :bar' => new Translation('Underscore: :foo, :bar', ''),
49 | ],
50 | $translations
51 | );
52 | }
53 |
54 | public function testShouldFindTranslationsForLangFunctions(): void
55 | {
56 | $langDir = $this->fixturesDir . '/App/Functions/Lang';
57 |
58 | $translations = $this->scanner->scan(['php'], [$langDir], ['lang', '__']);
59 |
60 | $this->assertEquals(
61 | [
62 | 'Lang: :foo, :bar' => new Translation('Lang: :foo, :bar', ''),
63 | ],
64 | $translations
65 | );
66 | }
67 |
68 | public function testShouldFindTranslationsForDifferentFileExtensions(): void
69 | {
70 | $langDir = $this->fixturesDir . '/App/View';
71 |
72 | $translations = $this->scanner->scan(['vue'], [$langDir], ['lang', '__']);
73 |
74 | $this->assertEquals(
75 | [
76 | 'This is a vue component' => new Translation('This is a vue component', ''),
77 | 'Vue Component Title' => new Translation('Vue Component Title', ''),
78 | ],
79 | $translations
80 | );
81 | }
82 |
83 | public function testShouldFindTranslationsForBladeTemplates(): void
84 | {
85 | $viewDir = $this->fixturesDir . '/App/View';
86 |
87 | $translations = $this->scanner->scan(['php'], [$viewDir], ['lang', '__']);
88 |
89 | $this->assertEquals(
90 | [
91 | 'Welcome, :name' => new Translation('Welcome, :name', ''),
92 | 'Trip to :planet, check-in opens :time' => new Translation('Trip to :planet, check-in opens :time', ''),
93 | 'Check offers to :planet' => new Translation('Check offers to :planet', ''),
94 | 'Translations should also work with double quotes.' => new Translation(
95 | 'Translations should also work with double quotes.',
96 | ''
97 | ),
98 | 'Shouldn\'t escaped quotes within strings also be correctly added?' => new Translation(
99 | 'Shouldn\'t escaped quotes within strings also be correctly added?',
100 | ''
101 | ),
102 | 'Same goes for "double quotes".' => new Translation('Same goes for "double quotes".', ''),
103 | 'String using (parentheses).' => new Translation('String using (parentheses).', ''),
104 | "Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\" => new Translation(
105 | "Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\",
106 | ''
107 | ),
108 | ],
109 | $translations
110 | );
111 | }
112 |
113 | public function testShouldFindMultipleTranslationForDifferentFunctionsAndFiles(): void
114 | {
115 | $appDir = $this->fixturesDir . '/App';
116 |
117 | $translations = $this->scanner->scan(['php'], [$appDir], ['lang', '__']);
118 |
119 | $this->assertEquals(
120 | [
121 | 'Welcome, :name' => new Translation('Welcome, :name', ''),
122 | 'Trip to :planet, check-in opens :time' => new Translation('Trip to :planet, check-in opens :time', ''),
123 | 'Check offers to :planet' => new Translation('Check offers to :planet', ''),
124 | 'Translations should also work with double quotes.' => new Translation(
125 | 'Translations should also work with double quotes.',
126 | ''
127 | ),
128 | 'Shouldn\'t escaped quotes within strings also be correctly added?' => new Translation(
129 | 'Shouldn\'t escaped quotes within strings also be correctly added?',
130 | ''
131 | ),
132 | 'Same goes for "double quotes".' => new Translation('Same goes for "double quotes".', ''),
133 | 'String using (parentheses).' => new Translation('String using (parentheses).', ''),
134 | 'Underscore: :foo, :bar' => new Translation('Underscore: :foo, :bar', ''),
135 | 'Lang: :foo, :bar' => new Translation('Lang: :foo, :bar', ''),
136 | "Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\" => new Translation(
137 | "Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\",
138 | ''
139 | ),
140 | ],
141 | $translations
142 | );
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/tests/Unit/Translator/TranslationServiceTest.php:
--------------------------------------------------------------------------------
1 | fixturesDir = realpath(__DIR__ . '/../../Fixtures');
28 |
29 | $this->configLoader = $this->createMock(ConfigLoader::class);
30 | $this->configLoader
31 | ->method('languages')
32 | ->willReturn(['pt']);
33 |
34 | $scanner = new TranslationScanner();
35 | $this->repository = $this->createMock(TranslationRepository::class);
36 |
37 | $this->service = new TranslationService($this->configLoader, $scanner, $this->repository);
38 | }
39 |
40 | public function testShouldScanAndSaveKeys(): void
41 | {
42 | $this->configLoader
43 | ->method('extensions')
44 | ->willReturn(['php']);
45 | $this->configLoader
46 | ->method('directories')
47 | ->willReturn([$this->fixturesDir . '/App/View']);
48 | $this->configLoader
49 | ->method('functions')
50 | ->willReturn(['lang', '__']);
51 |
52 | $translations = [
53 | [new Translation('Welcome, :name', '')],
54 | [new Translation('Trip to :planet, check-in opens :time', '')],
55 | [new Translation('Check offers to :planet', '')],
56 | [new Translation('Translations should also work with double quotes.', '')],
57 | [new Translation('Shouldn\'t escaped quotes within strings also be correctly added?', '')],
58 | [new Translation('Same goes for "double quotes".', '')],
59 | [new Translation('String using (parentheses).', '')],
60 | [new Translation("Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\", '')],
61 | ];
62 |
63 | $this->repository
64 | ->expects($this->exactly(8))
65 | ->method('save')
66 | ->withConsecutive(...$translations);
67 |
68 | $this->service->scanAndSaveNewKeys();
69 | }
70 |
71 | public function testWhenGivenTranslationAlreadyExistsThenDoNotOverride(): void
72 | {
73 | $this->configLoader
74 | ->method('directories')
75 | ->willReturn([$this->fixturesDir . '/App/Functions/Lang']);
76 | $this->configLoader
77 | ->method('functions')
78 | ->willReturn(['lang', '__']);
79 | $this->configLoader
80 | ->method('extensions')
81 | ->willReturn(['php']);
82 |
83 | $this->repository
84 | ->method('exists')
85 | ->with(new Translation('Lang: :foo, :bar', ''))
86 | ->willReturn(true);
87 |
88 | $this->repository
89 | ->expects($this->never())
90 | ->method('save');
91 |
92 | $this->service->scanAndSaveNewKeys();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/integration.php:
--------------------------------------------------------------------------------
1 | '',
13 | 'Lang: :foo, :bar' => '',
14 | 'Welcome, :name' => '',
15 | 'Trip to :planet, check-in opens :time' => '',
16 | 'Check offers to :planet' => '',
17 | 'Translations should also work with double quotes.' => '',
18 | 'Shouldn\'t escaped quotes within strings also be correctly added?' => '',
19 | 'Same goes for "double quotes".' => '',
20 | 'String using (parentheses).' => '',
21 | "Double quoted string using \"double quotes\", and C-style escape sequences.\n\t\\" => '',
22 | ],
23 | json_decode(file_get_contents("resources/lang/pt-br.json"), true)
24 | );
25 |
26 | if (!empty($diff)) {
27 | throw new Exception(
28 | sprintf("Keys scanned does not match by the diff:\n%s\n", print_r($diff, true))
29 | );
30 | }
31 |
32 | echo 'Integration works :)';
33 |
--------------------------------------------------------------------------------