Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
],
"require": {
"php": "^8.1",
"illuminate/console": "^9.0|^10.0|^11.0",
"illuminate/support": "^9.0|^10.0|^11.0",
"illuminate/translation": "^9.0|^10.0|^11.0"
},
Expand Down
23 changes: 23 additions & 0 deletions src/Console/FindTranslationsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types=1);

namespace Bambamboole\LaravelTranslationDumper\Console;

use Bambamboole\LaravelTranslationDumper\TranslationDumperInterface;
use Bambamboole\LaravelTranslationDumper\TranslationFinder;
use Illuminate\Console\Command;

class FindTranslationsCommand extends Command
{
protected $signature = 'translations:find';

protected $description = 'Find translations in PHP code and template files';

public function handle(TranslationFinder $finder, TranslationDumperInterface $dumper): int

Check warning on line 15 in src/Console/FindTranslationsCommand.php

View check run for this annotation

Codecov / codecov/patch

src/Console/FindTranslationsCommand.php#L15

Added line #L15 was not covered by tests
{
$translations = $finder->findTranslations();
$this->info(sprintf('Found %s translations', count($translations)));
$dumper->dump($translations);

Check warning on line 19 in src/Console/FindTranslationsCommand.php

View check run for this annotation

Codecov / codecov/patch

src/Console/FindTranslationsCommand.php#L17-L19

Added lines #L17 - L19 were not covered by tests

return self::SUCCESS;

Check warning on line 21 in src/Console/FindTranslationsCommand.php

View check run for this annotation

Codecov / codecov/patch

src/Console/FindTranslationsCommand.php#L21

Added line #L21 was not covered by tests
}
}
39 changes: 24 additions & 15 deletions src/LaravelTranslationDumperServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Bambamboole\LaravelTranslationDumper;

use Bambamboole\LaravelTranslationDumper\Console\FindTranslationsCommand;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\Filesystem;
Expand All @@ -13,6 +14,9 @@
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
FindTranslationsCommand::class,
]);
$this->publishes([
dirname(__DIR__).'/config/config.php' => $this->app->configPath('translation.php'),
], 'config');
Expand All @@ -23,23 +27,28 @@
{
$this->mergeConfigFrom(__DIR__.'/../config/config.php', 'translation');

if ($this->app->make(Repository::class)->get('translation.dump_translations')) {
$this->app->singleton(
TranslationDumper::class,
static fn (Application $app) => new TranslationDumper(
new Filesystem(),
new ArrayExporter(),
$app->langPath(),
$app->make(Repository::class)->get('app.locale'),
$app->make(Repository::class)->get('translation.dump_prefix'),
),
);
$this->app->singleton(
TranslationDumper::class,
static fn (Application $app) => new TranslationDumper(
new Filesystem(),
new ArrayExporter(),
$app->langPath(),
$app->make(Repository::class)->get('app.locale'),
$app->make(Repository::class)->get('translation.dump_prefix'),
),
);

Check warning on line 39 in src/LaravelTranslationDumperServiceProvider.php

View check run for this annotation

Codecov / codecov/patch

src/LaravelTranslationDumperServiceProvider.php#L30-L39

Added lines #L30 - L39 were not covered by tests

$this->app->bind(
TranslationDumperInterface::class,
static fn (Application $app) => $app->make($app->make(Repository::class)->get('translation.dumper'))
);
$this->app->bind(
TranslationDumperInterface::class,
static fn (Application $app) => $app->make($app->make(Repository::class)->get('translation.dumper'))
);

Check warning on line 44 in src/LaravelTranslationDumperServiceProvider.php

View check run for this annotation

Codecov / codecov/patch

src/LaravelTranslationDumperServiceProvider.php#L41-L44

Added lines #L41 - L44 were not covered by tests

$this->app->singleton(
TranslationFinder::class,
static fn (Application $app) => new TranslationFinder($app->basePath()),
);

Check warning on line 49 in src/LaravelTranslationDumperServiceProvider.php

View check run for this annotation

Codecov / codecov/patch

src/LaravelTranslationDumperServiceProvider.php#L46-L49

Added lines #L46 - L49 were not covered by tests

if ($this->app->make(Repository::class)->get('translation.dump_translations')) {

Check warning on line 51 in src/LaravelTranslationDumperServiceProvider.php

View check run for this annotation

Codecov / codecov/patch

src/LaravelTranslationDumperServiceProvider.php#L51

Added line #L51 was not covered by tests
$this->app->extend(
'translator',
static fn (Translator $translator, $app) => new DumpingTranslator(
Expand Down
98 changes: 98 additions & 0 deletions src/TranslationFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types=1);

namespace Bambamboole\LaravelTranslationDumper;

use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;

class TranslationFinder
{
public function __construct(private readonly string $basePath)
{
}

public function findTranslations(): array
{
$groupKeys = [];
$stringKeys = [];
$functions = [
'trans',
'trans_choice',
'Lang::get',
'Lang::choice',
'Lang::trans',
'Lang::transChoice',
'@lang',
'@choice',
'__',
];

// The translation search part is based on the work of the awesome barryvdh in
// https://github.com/barryvdh/laravel-translation-manager/blob/master/src/Manager.php
$groupPattern = // See https://regex101.com/r/WEJqdL/6
'[^\w|>]'. // Must not have an alphanum or _ or > before real method
'('.implode('|', $functions).')'. // Must start with one of the functions
'\('. // Match opening parenthesis
"[\'\"]". // Match " or '
'('. // Start a new group to match:
'[\/a-zA-Z0-9_-]+'. // Must start with group
"([.](?! )[^\1)]+)+". // Be followed by one or more items/keys
')'. // Close group
"[\'\"]". // Closing quote
'[\),]'; // Close parentheses or new parameter

$stringPattern =
'[^\w]'. // Must not have an alphanum before real method
'('.implode('|', $functions).')'. // Must start with one of the functions
'\(\s*'. // Match opening parenthesis
"(?P<quote>['\"])". // Match " or ' and store in {quote}
"(?P<string>(?:\\\k{quote}|(?!\k{quote}).)*)". // Match any string that can be {quote} escaped
'\k{quote}'. // Match " or ' previously matched
'\s*[\),]'; // Close parentheses or new parameter

// Find all PHP + Twig files in the app folder, except for storage
$finder = new Finder();
$files = $finder->in($this->basePath)
->exclude('storage')
->exclude('vendor')
->name('*.php')
->files();

/** @var \Symfony\Component\Finder\SplFileInfo $file */
foreach ($files as $file) {
// Search the current file for the pattern
if (preg_match_all("/$groupPattern/siU", $file->getContents(), $matches)) {
// Get all matches
foreach ($matches[2] as $key) {
$groupKeys[] = $key;
}
}

if (preg_match_all("/$stringPattern/siU", $file->getContents(), $matches)) {
foreach ($matches['string'] as $key) {
if (preg_match("/(^[\/a-zA-Z0-9_-]+([.][^\1)\ ]+)+$)/siU", $key, $groupMatches)) {
// group{.group}.key format, already in $groupKeys but also matched here
// do nothing, it has to be treated as a group
continue;
}

//TODO: This can probably be done in the regex, but I couldn't do it.
//skip keys which contain namespacing characters, unless they also contain a
//space, which makes it JSON.
if (! (Str::contains($key, '::') && Str::contains($key, '.'))
|| Str::contains($key, ' ')) {
$stringKeys[] = $key;
}
}
}
}
// Remove duplicates
$groupKeys = array_unique($groupKeys);
$stringKeys = array_unique($stringKeys);
// Merge and order them
$keys = array_merge($groupKeys, $stringKeys);
ksort($keys, SORT_NATURAL | SORT_FLAG_CASE);

return $keys;
}
}
30 changes: 30 additions & 0 deletions tests/Feature/TranslationFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);

namespace Bambamboole\LaravelTranslationDumper\Tests\Feature;

use Bambamboole\LaravelTranslationDumper\TranslationFinder;
use PHPUnit\Framework\TestCase;

class TranslationFinderTest extends TestCase
{
public function testItCanFindTranslationsInCode(): void
{
$translations = $this->createSubject()->findTranslations();

$expected = [
'test.dotted',
'test.dotted.additional',
'This is just a sentence',
'test undotted key',
'test',
];
foreach ($expected as $item) {
self::assertContains($item, $translations);
}
}

private function createSubject(): TranslationFinder
{
return new TranslationFinder(__DIR__.'/fixtures/code');
}
}
15 changes: 15 additions & 0 deletions tests/Feature/fixtures/code/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace Bambamboole\LaravelTranslationDumper\Tests\Feature\fixtures\code;

class TestClass
{
public function get(): array
{
return [
__('test.dotted.additional'),
trans('test undotted key'),
trans_choice('test'),
];
}
}
2 changes: 2 additions & 0 deletions tests/Feature/fixtures/code/template.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div>{{ __('test.dotted') }}</div>
<div>@lang('This is just a sentence')</div>