diff --git a/config/translation-manager.php b/config/translation-manager.php index 6411f6d4..a552a080 100644 --- a/config/translation-manager.php +++ b/config/translation-manager.php @@ -66,4 +66,31 @@ '$trans.get', ], + /** + * if true finding new translations will be disabled while users browse application + */ + 'ignore_new_trans' => false, + + /** + * When project does not user JSON alternative it can be ignored + */ + 'ignore_json' => true, + + /** + * Translations without source position will be marked as red + */ + 'warn_in_code' => false, + + /* + |-------------------------------------------------------------------------- + | DEBUG + |-------------------------------------------------------------------------- + | + | After every translation will be placed original key in square brackets + | e.g.: trans('auth.login') -> Login [auth.login] + | NOTE: only when translation exists! + | + */ + 'debug' => false, + ]; diff --git a/database/migrations/2014_04_02_193005_create_translations_table.php b/database/migrations/2014_04_02_193005_create_translations_table.php index 053d09c2..3f8d92a6 100644 --- a/database/migrations/2014_04_02_193005_create_translations_table.php +++ b/database/migrations/2014_04_02_193005_create_translations_table.php @@ -1,20 +1,22 @@ collation = 'utf8mb4_bin'; + $table->collation = 'utf8mb4_bin'; $table->bigIncrements('id'); $table->integer('status')->default(0); $table->string('locale'); @@ -22,17 +24,20 @@ public function up() $table->text('key'); $table->text('value')->nullable(); $table->timestamps(); + + $table->index(['group']); + $table->index(['locale']); }); - } + } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { Schema::drop('ltm_translations'); - } + } } diff --git a/database/migrations/2021_05_04_201011_create_ltm_translations_sources_table.php b/database/migrations/2021_05_04_201011_create_ltm_translations_sources_table.php new file mode 100644 index 00000000..78f6e760 --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_sources_table.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string('file_path'); + $table->integer('file_line'); + + $table->index(['group', 'key']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ltm_translation_sources'); + } +} diff --git a/database/migrations/2021_05_04_201011_create_ltm_translations_urls_table.php b/database/migrations/2021_05_04_201011_create_ltm_translations_urls_table.php new file mode 100644 index 00000000..eecf23be --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_urls_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string('url'); + + $table->index(['group', 'key']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ltm_translation_urls'); + } +} diff --git a/database/migrations/2021_05_04_201011_create_ltm_translations_variables_table.php b/database/migrations/2021_05_04_201011_create_ltm_translations_variables_table.php new file mode 100644 index 00000000..9ec7cba7 --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_variables_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string('attribute'); + + $table->index(['group', 'key']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ltm_translation_variables'); + } +} diff --git a/readme.md b/readme.md index 3de3adc1..cbc919c6 100644 --- a/readme.md +++ b/readme.md @@ -15,6 +15,7 @@ The workflow would be: This way, translations can be saved in git history and no overhead is introduced in production. ![Screenshot](http://i.imgur.com/4th2krf.png) +![Screenshot](screenshot2.png) ## Installation diff --git a/resources/views/components/locales_list.blade.php b/resources/views/components/locales_list.blade.php new file mode 100644 index 00000000..d62bed86 --- /dev/null +++ b/resources/views/components/locales_list.blade.php @@ -0,0 +1,45 @@ +
+ Supported locales +

+ Current supported locales: +

+
+ @csrf + +
+
+ @csrf +
+

+ Enter new locale key: +

+
+
+ +
+
+ +
+
+
+
+
+
+ Export all translations +
+ @csrf + +
+
\ No newline at end of file diff --git a/resources/views/components/post_import.blade.php b/resources/views/components/post_import.blade.php new file mode 100644 index 00000000..543012ec --- /dev/null +++ b/resources/views/components/post_import.blade.php @@ -0,0 +1,22 @@ +
+ @csrf +
+
+
+ +
+
+ +
+
+
+
+
+
+ @csrf + +
+
\ No newline at end of file diff --git a/resources/views/components/post_publish.blade.php b/resources/views/components/post_publish.blade.php new file mode 100644 index 00000000..ae020fb4 --- /dev/null +++ b/resources/views/components/post_publish.blade.php @@ -0,0 +1,5 @@ +
+ @csrf + + Back +
\ No newline at end of file diff --git a/resources/views/components/search.blade.php b/resources/views/components/search.blade.php new file mode 100644 index 00000000..8e07bee8 --- /dev/null +++ b/resources/views/components/search.blade.php @@ -0,0 +1,14 @@ +
+ Search +
+
+

Search for translation text

+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/resources/views/components/translation_detail.blade.php b/resources/views/components/translation_detail.blade.php new file mode 100644 index 00000000..4568444e --- /dev/null +++ b/resources/views/components/translation_detail.blade.php @@ -0,0 +1,79 @@ + +
+
$group, "translationKey" => $key]) }}"> + @csrf + +
+ + +
+
+ + +
+ @foreach($locales as $localeKey => $locale) + +
+ + +
+ @endforeach + +
+
+@if( $_translation != null ) +
+
+ Variables + +
+
+ URLs + +
+
+ Source Locations + +
+
+@endif \ No newline at end of file diff --git a/resources/views/components/translations_list.blade.php b/resources/views/components/translations_list.blade.php new file mode 100644 index 00000000..e35b2f99 --- /dev/null +++ b/resources/views/components/translations_list.blade.php @@ -0,0 +1,61 @@ +
+

Total: {{ $numTranslations }}, changed: {{ $numChanged }}

+ + + + + @foreach ($locales as $locale) + + @endforeach + @if ($deleteEnabled) + + @endif + + + + + @foreach ($translations as $key => $translation) + group; + } else { + $isEmpty = true; + } + } + } + ?> + + + @foreach ($locales as $locale) + + + + @endforeach + @if ($deleteEnabled) + + @endif + + @endforeach + +
Key{{ $locale }} 
count() == 0 ) class="danger" @endif>{!! htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!} + $group, "translationKey" => $key ]) }}"> + + $group]) }}" + data-title="Enter translation">{!! $t ? htmlentities($t->value, ENT_QUOTES, 'UTF-8', + false) : '' !!} + + +
\ No newline at end of file diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php new file mode 100644 index 00000000..c139ee64 --- /dev/null +++ b/resources/views/index.blade.php @@ -0,0 +1,548 @@ + + + + + + Translation Manager + + + + + + + + + + + + +
+

Warning, translations are not visible until they are exported back to the app/lang file, using php artisan + translation:export command or publish button.

+ + + + + @if(Session::has('successPublish')) +
+ {{ Session::get('successPublish') }} +
+ @endif + @if( !$q ) +

+ @if($group) + @include( 'translation-manager::components.post_publish' ) + @else + @include( 'translation-manager::components.post_import' ) + @endif +

+ @else + Back + @endif + @if($group || $q) + @if($key) + @include( 'translation-manager::components.translation_detail' ) + @else + @if($q) + @include( 'translation-manager::components.search' ) + @else +
+ @csrf +
+

Choose a group to display the group translations. If no groups are visisble, make sure you + have run + the migrations and imported the translations.

+ +
+
+
+ @csrf +
+ + +
+
+ +
+
+
+
+ Use Auto Translate +
+
+ + @endif + @include( 'translation-manager::components.translations_list' ) + @endif + @else +
+ @csrf +
+

Choose a group to display the group translations. If no groups are visisble, make sure you have run + the migrations and imported the translations.

+ +
+
+ + +
+
+ +
+
+ + @include( 'translation-manager::components.search' ) + + @include( 'translation-manager::components.locales_list' ) + @endif +
+ + + diff --git a/resources/views/index.php b/resources/views/index.php deleted file mode 100644 index 65034bdc..00000000 --- a/resources/views/index.php +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - Translation Manager - - - - - - - - - - - - -
-

Warning, translations are not visible until they are exported back to the app/lang file, using php artisan translation:export command or publish button.

- - - - - -
- -
- -

- -

- -
-
-
- -
-
- -
-
-
-
-
-
- - -
-
- - -
- - - Back -
- -

-
- -
-

Choose a group to display the group translations. If no groups are visisble, make sure you have run the migrations and imported the translations.

- -
-
- - -
-
- -
-
- -
- -
- - -
-
- -
-
-
-
- Use Auto Translate -
-
- -
-

Total: , changed:

- - - - - - - - - - - - - - - $translation): ?> - - - - - - - - - - - - - -
Key 
- " - id="username" data-type="textarea" data-pk="id : 0 ?>" - data-url="" - data-title="Enter translation">value, ENT_QUOTES, 'UTF-8', false) : '' ?> - - -
- -
- Supported locales -

- Current supported locales: -

-
- -
    - -
  • -
    - - - -
    -
  • - -
-
-
- -
-

- Enter new locale key: -

-
-
- -
-
- -
-
-
-
-
-
- Export all translations -
- - -
-
- - -
- - - diff --git a/screenshot2.png b/screenshot2.png new file mode 100644 index 00000000..25bbfcbd Binary files /dev/null and b/screenshot2.png differ diff --git a/src/Console/ExportCommand.php b/src/Console/ExportCommand.php index ee0e63db..38fa0f42 100644 --- a/src/Console/ExportCommand.php +++ b/src/Console/ExportCommand.php @@ -14,7 +14,7 @@ class ExportCommand extends Command * * @var string */ - protected $name = 'translations:export {group}'; + protected $name = 'translations:export {group?}'; /** * The console command description. diff --git a/src/Console/FindCommand.php b/src/Console/FindCommand.php index 1c49d421..b26653a1 100644 --- a/src/Console/FindCommand.php +++ b/src/Console/FindCommand.php @@ -5,7 +5,8 @@ use Barryvdh\TranslationManager\Manager; use Illuminate\Console\Command; -class FindCommand extends Command +class FindCommand + extends Command { /** * The console command name. @@ -24,6 +25,8 @@ class FindCommand extends Command /** @var \Barryvdh\TranslationManager\Manager */ protected $manager; + protected $config; + public function __construct(Manager $manager) { $this->manager = $manager; @@ -36,6 +39,7 @@ public function __construct(Manager $manager) public function handle() { $counter = $this->manager->findTranslations(null); + $this->config = config('translation-manager'); $this->info('Done importing, processed '.$counter.' items!'); } } diff --git a/src/Controller.php b/src/Controller.php index 31ab43c0..f7b1b12e 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -1,14 +1,16 @@ manager = $manager; } - public function getIndex($group = null) + public function getIndex($groupKey = null, $translationKey = null, $q = null) { $locales = $this->manager->getLocales(); $groups = Translation::groupBy('group'); $excludedGroups = $this->manager->getConfig('exclude_groups'); - if($excludedGroups){ + if ($excludedGroups) { $groups->whereNotIn('group', $excludedGroups); } @@ -29,31 +31,81 @@ public function getIndex($group = null) if ($groups instanceof Collection) { $groups = $groups->all(); } - $groups = [''=>'Choose a group'] + $groups; - $numChanged = Translation::where('group', $group)->where('status', Translation::STATUS_CHANGED)->count(); + $groups = ['' => 'Choose a group'] + $groups; + + /** @var Builder $translationQuery */ + if ($groupKey == null && $q != null) { + $translationQuery = Translation::where(function ($query) use ($q) { + $query->where('key', 'LIKE', "%".$q."%") + ->orWhere('value', 'LIKE', "%".$q."%"); + }); + } else { + $translationQuery = Translation::where('group', $groupKey); + } + $numChanged = (clone $translationQuery)->where('status', Translation::STATUS_CHANGED)->count(); + $translationQuery->orderBy('key', 'asc'); + $numTranslations = $translationQuery->count(); - $allTranslations = Translation::where('group', $group)->orderBy('key', 'asc')->get(); - $numTranslations = count($allTranslations); + /** @var \Illuminate\Database\Eloquent\Collection $allTranslations */ + $allTranslations = $translationQuery->get(); $translations = []; - foreach($allTranslations as $translation){ + foreach ($allTranslations as $translation) { $translations[$translation->key][$translation->locale] = $translation; } - return view('translation-manager::index') + $prevTranslation = null; + $nextTranslation = null; + if ($translationKey) { + $translationArrayKeys = array_keys($translations); + + $_index = array_search($translationKey, $translationArrayKeys); + + // find previous item + if ($_index > 0) { + $prevTranslation = [ + "group" => $groupKey, + "key" => $translationArrayKeys[$_index - 1], + ]; + } + + // find next item + if ($_index < count($translationArrayKeys)) { + $nextTranslation = [ + "group" => $groupKey, + "key" => $translationArrayKeys[$_index + 1], + ]; + } + + // replace array with one key only + $newTranslations = []; + $newTranslations[$translationKey] = $translations[$translationKey]; + $translations = $newTranslations; + } + + return view('translation-manager::index') ->with('translations', $translations) + ->with('q', $q) ->with('locales', $locales) ->with('groups', $groups) - ->with('group', $group) + ->with('group', $groupKey) + ->with('key', $translationKey) + ->with('nextTranslation', $nextTranslation) + ->with('prevTranslation', $prevTranslation) ->with('numTranslations', $numTranslations) ->with('numChanged', $numChanged) - ->with('editUrl', $group ? action('\Barryvdh\TranslationManager\Controller@postEdit', [$group]) : null) + ->with('editUrl', $groupKey ? route('translation-manager.translation.edit', ["groupKey" => $groupKey]) : null) ->with('deleteEnabled', $this->manager->getConfig('delete_enabled')); } - public function getView($group = null) + public function getView($groupKey = null) { - return $this->getIndex($group); + return $this->getIndex($groupKey); + } + + public function getDetail($groupKey = null, $translationKey = null) + { + return $this->getIndex($groupKey, $translationKey); } protected function loadLocales() @@ -71,29 +123,29 @@ protected function loadLocales() return array_unique($locales); } - public function postAdd($group = null) + public function postAdd($groupKey = null) { $keys = explode("\n", request()->get('keys')); - foreach($keys as $key){ + foreach ($keys as $key) { $key = trim($key); - if($group && $key){ - $this->manager->missingKey('*', $group, $key); + if ($groupKey && $key) { + $this->manager->missingKey('*', $groupKey, $key); } } return redirect()->back(); } - public function postEdit($group = null) + public function postEdit($groupKey = null) { - if(!in_array($group, $this->manager->getConfig('exclude_groups'))) { + if (!in_array($groupKey, $this->manager->getConfig('exclude_groups'))) { $name = request()->get('name'); $value = request()->get('value'); list($locale, $key) = explode('|', $name, 2); $translation = Translation::firstOrNew([ 'locale' => $locale, - 'group' => $group, + 'group' => $groupKey, 'key' => $key, ]); $translation->value = (string) $value ?: null; @@ -103,10 +155,37 @@ public function postEdit($group = null) } } - public function postDelete($group, $key) + public function postEditAll(Request $request, $groupKey, $translationKey) + { + if (!in_array($groupKey, $this->manager->getConfig('exclude_groups'))) { + $values = request()->get('value'); + + foreach ($values as $locale => $value) { + $translation = Translation::firstOrNew([ + 'locale' => $locale, + 'group' => $groupKey, + 'key' => $translationKey, + ]); + + if ((string) $translation->value != (string) $value) { + $translation->status = Translation::STATUS_CHANGED; + } + + $translation->value = (string) $value ?? null; + $translation->save(); + } + } + + return back()->with('successPublish', 'Saved!'); + } + + public function postDelete($groupKey, $key) { - if(!in_array($group, $this->manager->getConfig('exclude_groups')) && $this->manager->getConfig('delete_enabled')) { - Translation::where('group', $group)->where('key', $key)->delete(); + if (!in_array($groupKey, $this->manager->getConfig('exclude_groups')) && $this->manager->getConfig('delete_enabled')) { + Translation::where('group', $groupKey)->where('key', $key)->delete(); + Translation::possibleVariables($groupKey, $key)->delete(); + Translation::sourceLocations($groupKey, $key)->delete(); + Translation::urls($groupKey, $key)->delete(); return ['status' => 'ok']; } } @@ -126,15 +205,15 @@ public function postFind() return ['status' => 'ok', 'counter' => (int) $numFound]; } - public function postPublish($group = null) + public function postPublish($groupKey = null) { - $json = false; + $json = false; - if($group === '_json'){ + if ($groupKey === '_json') { $json = true; } - $this->manager->exportTranslations($group, $json); + $this->manager->exportTranslations($groupKey, $json); return ['status' => 'ok']; } @@ -142,12 +221,9 @@ public function postPublish($group = null) public function postAddGroup(Request $request) { $group = str_replace(".", '', $request->input('new-group')); - if ($group) - { - return redirect()->action('\Barryvdh\TranslationManager\Controller@getView',$group); - } - else - { + if ($group) { + return redirect()->route('translation-manager.group.list', ["groupKey" => $group]); + } else { return redirect()->back(); } } @@ -171,10 +247,11 @@ public function postRemoveLocale(Request $request) return redirect()->back(); } - public function postTranslateMissing(Request $request){ + public function postTranslateMissing(Request $request) + { $locales = $this->manager->getLocales(); $newLocale = str_replace([], '-', trim($request->input('new-locale'))); - if($request->has('with-translations') && $request->has('base-locale') && in_array($request->input('base-locale'),$locales) && $request->has('file') && in_array($newLocale, $locales)){ + if ($request->has('with-translations') && $request->has('base-locale') && in_array($request->input('base-locale'), $locales) && $request->has('file') && in_array($newLocale, $locales)) { $base_locale = $request->get('base-locale'); $group = $request->get('file'); $base_strings = Translation::where('group', $group)->where('locale', $base_locale)->get(); @@ -187,7 +264,7 @@ public function postTranslateMissing(Request $request){ $translated_text = Str::apiTranslateWithAttributes($base_string->value, $newLocale, $base_locale); request()->replace([ 'value' => $translated_text, - 'name' => $newLocale . '|' . $base_string->key, + 'name' => $newLocale.'|'.$base_string->key, ]); app()->call( 'Barryvdh\TranslationManager\Controller@postEdit', @@ -200,4 +277,10 @@ public function postTranslateMissing(Request $request){ } return redirect()->back(); } + + + public function getSearchResults(Request $request) + { + return $this->getIndex(null, null, trim($request->get('q'))); + } } diff --git a/src/Manager.php b/src/Manager.php index 7e478f5e..48a13b9e 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -2,14 +2,17 @@ namespace Barryvdh\TranslationManager; +use Barryvdh\TranslationManager\Events\TranslationsExportedEvent; +use Barryvdh\TranslationManager\Models\Translation; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Finder\Finder; -use Illuminate\Filesystem\Filesystem; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Foundation\Application; -use Barryvdh\TranslationManager\Models\Translation; -use Barryvdh\TranslationManager\Events\TranslationsExportedEvent; class Manager { @@ -35,7 +38,7 @@ public function __construct(Application $app, Filesystem $files, Dispatcher $eve $this->app = $app; $this->files = $files; $this->events = $events; - $this->config = $app['config']['translation-manager']; + $this->config = $app[ 'config' ][ 'translation-manager' ]; $this->ignoreFilePath = storage_path('.ignore_locales'); $this->locales = []; $this->ignoreLocales = $this->getIgnoredLocales(); @@ -43,7 +46,7 @@ public function __construct(Application $app, Filesystem $files, Dispatcher $eve protected function getIgnoredLocales() { - if (! $this->files->exists($this->ignoreFilePath)) { + if (!$this->files->exists($this->ignoreFilePath)) { return []; } $result = json_decode($this->files->get($this->ignoreFilePath)); @@ -57,7 +60,7 @@ public function importTranslations($replace = false, $base = null, $import_group //allows for vendor lang files to be properly recorded through recursion. $vendor = true; if ($base == null) { - $base = $this->app['path.lang']; + $base = $this->app[ 'path.lang' ]; $vendor = false; } @@ -75,17 +78,17 @@ public function importTranslations($replace = false, $base = null, $import_group $vendorName = $this->files->name($this->files->dirname($langPath)); foreach ($this->files->allfiles($langPath) as $file) { $info = pathinfo($file); - $group = $info['filename']; + $group = $info[ 'filename' ]; if ($import_group) { if ($import_group !== $group) { continue; } } - if (in_array($group, $this->config['exclude_groups'])) { + if (in_array($group, $this->config[ 'exclude_groups' ])) { continue; } - $subLangPath = str_replace($langPath.DIRECTORY_SEPARATOR, '', $info['dirname']); + $subLangPath = str_replace($langPath.DIRECTORY_SEPARATOR, '', $info[ 'dirname' ]); $subLangPath = str_replace(DIRECTORY_SEPARATOR, '/', $subLangPath); $langPath = str_replace(DIRECTORY_SEPARATOR, '/', $langPath); @@ -93,7 +96,7 @@ public function importTranslations($replace = false, $base = null, $import_group $group = $subLangPath.'/'.$group; } - if (! $vendor) { + if (!$vendor) { $translations = \Lang::getLoader()->load($locale, $group); } else { $translations = include $file; @@ -109,14 +112,13 @@ public function importTranslations($replace = false, $base = null, $import_group } } - foreach ($this->files->files($this->app['path.lang']) as $jsonTranslationFile) { + foreach ($this->files->files($this->app[ 'path.lang' ]) as $jsonTranslationFile) { if (strpos($jsonTranslationFile, '.json') === false) { continue; } $locale = basename($jsonTranslationFile, '.json'); $group = self::JSON_GROUP; - $translations = - \Lang::getLoader()->load($locale, '*', '*'); // Retrieves JSON entries of the given locale only + $translations = \Lang::getLoader()->load($locale, '*', '*'); // Retrieves JSON entries of the given locale only if ($translations && is_array($translations)) { foreach ($translations as $key => $value) { $importedTranslation = $this->importTranslation($key, $value, $locale, $group, $replace); @@ -130,7 +132,6 @@ public function importTranslations($replace = false, $base = null, $import_group public function importTranslation($key, $value, $locale, $group, $replace = false) { - // process only string values if (is_array($value)) { return false; @@ -138,8 +139,8 @@ public function importTranslation($key, $value, $locale, $group, $replace = fals $value = (string) $value; $translation = Translation::firstOrNew([ 'locale' => $locale, - 'group' => $group, - 'key' => $key, + 'group' => $group, + 'key' => $key, ]); // Check if the database is different then the files @@ -149,7 +150,7 @@ public function importTranslation($key, $value, $locale, $group, $replace = fals } // Only replace when empty, or explicitly told so - if ($replace || ! $translation->value) { + if ($replace || !$translation->value) { $translation->value = $value; } @@ -158,28 +159,32 @@ public function importTranslation($key, $value, $locale, $group, $replace = fals return true; } + public function findTranslations($path = null) { $path = $path ?: base_path(); $groupKeys = []; $stringKeys = []; - $functions = $this->config['trans_functions']; - - $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 + $functions = $this->config[ 'trans_functions' ]; + + $groupPattern = // See https://regex101.com/r/Mxr50T/2 + "[\W]". // Must not have an alphanum or _ or > before real method + '('.implode('|', $functions).')'. // Must start with one of the functions + "\(\s?". // Match opening parenthesis + "[\'\"]". // Match " or ' + '('. // Start a new group to match: + '[a-zA-Z0-9_-]+'. // Must start with group + '[\.]'. // Group ends with dot + "([a-zA-Z0-9_\-\.]*)". // Be followed by zero or more items/keys + '[a-zA-Z0-9]'. // Must end with a number or letter + ')'. // Close group + "[\'\"]\s?". // Closing quote + "[\),\s]{1,3}". // Close parentheses or new parameter + "(\[([^\]]*)\])?"; // take atributes if exists $stringPattern = - "[^\w]". // Must not have an alphanum before real method - '('.implode('|', $functions).')'. // Must start with one of the functions + "[^\w]". // Must not have an alphanum before real method + '('.implode('|', $functions).')'. // Must start with one of the functions "\(\s*". // Match opening parenthesis "(?P['\"])". // Match " or ' and store in {quote} "(?P(?:\\\k{quote}|(?!\k{quote}).)*)". // Match any string that can be {quote} escaped @@ -190,72 +195,232 @@ public function findTranslations($path = null) $finder = new Finder(); $finder->in($path)->exclude('storage')->exclude('vendor')->name('*.php')->name('*.twig')->name('*.vue')->files(); + $section = null; + if (app()->runningInConsole()) { + $output = new ConsoleOutput(); + $section = $output->section(); + + $bar = new ProgressBar($section); + $bar->setFormat("%message%\n %current%/%max% [%bar%] %percent:3s%%"); + $bar->setMessage('Files'); + $bar->start(count($finder)); + } + /** @var \Symfony\Component\Finder\SplFileInfo $file */ foreach ($finder as $file) { + if ($section != null) { + $bar->advance(); + } + // Search the current file for the pattern - if (preg_match_all("/$groupPattern/siU", $file->getContents(), $matches)) { + if (preg_match_all("/$groupPattern/si", $file->getContents(), $matches)) { // Get all matches - foreach ($matches[2] as $key) { - $groupKeys[] = $key; + foreach ($matches[ 2 ] as $i => $key) { + $found++; + if (!isset($groupKeys[ $key ])) { + $groupKeys[ $key ] = [ + "sources" => [], + "variables" => [], + ]; + } + $groupKeys[ $key ][ "sources" ] = array_merge($groupKeys[ $key ][ "sources" ], $this->findLineNumber($file, $key)); + if (isset($matches[ 5 ]) && isset($matches[ 5 ][ $i ]) && $matches[ 5 ][ $i ] != "") { + $attributes = explode(",", static::str_strip_whitespace($matches[ 5 ][ $i ])); + foreach ($attributes as $attribute) { + list($item, $_rest) = explode("=", $attribute, 2); + $groupKeys[ $key ][ "variables" ][] = str_replace(['"', "'"], "", $item); + } + } } } - 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; - } + if (!$this->config[ 'ignore_json' ]) { + 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; + //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); + ksort($groupKeys); + + if ($section != null) { + $bar->finish(); + + $bar2 = new ProgressBar($section); + $bar2->setFormat("%message%\n %current%/%max% [%bar%] %percent:3s%%"); + $bar2->setMessage("Keys"); + $bar2->start(count($groupKeys)); + } + + //clean variables and sources + \Illuminate\Support\Facades\DB::statement('TRUNCATE TABLE `ltm_translation_sources`'); // Add the translations to the database, if not existing. - foreach ($groupKeys as $key) { + foreach ($groupKeys as $key => $data) { + if ($section != null) { + $bar2->advance(); + } + // Split the group and item list($group, $item) = explode('.', $key, 2); - $this->missingKey('', $group, $item); + $this->missingKey('', $group, $item, array_unique($data[ 'variables' ])); + + // save location in strings + $files = array_unique($data[ 'sources' ]); + foreach ($files as $file) { + list($path, $line) = explode(':', $file); + \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->insert([ + "group" => $group, + "key" => $item, + "file_path" => $path, + "file_line" => $line, + ]); + } + + $counter++; } - foreach ($stringKeys as $key) { - $group = self::JSON_GROUP; - $item = $key; - $this->missingKey('', $group, $item); + if ($section != null) { + $bar2->finish(); + } + + if (!$this->config[ 'ignore_json' ]) { + $stringKeys = array_unique($stringKeys); + + if ($section != null) { + $bar3 = new ProgressBar($section); + $bar3->setFormat("%message%\n %current%/%max% [%bar%] %percent:3s%%"); + $bar3->setMessage("JSON"); + $bar3->start(count($groupKeys)); + } + + foreach ($stringKeys as $key) { + if ($bar3 != null) { + $bar3->advance(); + } + + $group = Manager::JSON_GROUP; + $item = $key; + $this->missingKey('', $group, $item); + } + + if ($section != null) { + $bar3->finish(); + } } // Return the number of found translations return count($groupKeys + $stringKeys); } - public function missingKey($namespace, $group, $key) + /** + * return list of line_numbers + * + * @param \Symfony\Component\Finder\SplFileInfo $file + * @param $search + * + * @return array + */ + private function findLineNumber(\Symfony\Component\Finder\SplFileInfo $file, $search) + { + $lines = file($file->getRealPath()); + $line_numbers = []; + + foreach ($lines as $key => $line) { + if (strpos($line, $search) !== false) { + $line_numbers[] = $file->getRelativePath()."/".$file->getFilename().":".($key + 1); + } + } + + return $line_numbers; + } + + /** + * Strp all whitespaces inside of string + * + * @param $string + * + * @return string|string[]|null + */ + public static function str_strip_whitespace($string) { - if (! in_array($group, $this->config['exclude_groups'])) { + return preg_replace('/\s+/', '', $string); + } + + public function missingKey($namespace, $group, $key, $parameters = []) + { + if (!in_array($group, $this->config[ 'exclude_groups' ])) { + if ($this->config[ 'ignore_json' ]) { + //ignore all non alphanumeric strings + if (preg_match("/[a-zA-Z0-9-_\.]*/", $key, $groupMatches)) { + if ($groupMatches[ 0 ] != $key) { + return; + } + } + } + Translation::firstOrCreate([ - 'locale' => $this->app['config']['app.locale'], - 'group' => $group, - 'key' => $key, + 'locale' => $this->app[ 'config' ][ 'app.locale' ], + 'group' => $group, + 'key' => $key, ]); + + if (count($parameters) > 0) { + Translation::possibleVariables($group, $key)->delete(); + + // save possible variables + foreach ($parameters as $parameter) { + \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->insert([ + "group" => $group, + "key" => $key, + "attribute" => $parameter, + ]); + } + } + + if (!app()->runningInConsole()) { + $url = request()->getRequestUri(); + + // ignore url when part of config->route->prefix + if (!Str::contains($url, $this->config[ 'route' ][ 'prefix' ])) { + // save URL with translation key + $_testUrl = DB::table('ltm_translation_urls') + ->where('group', $group) + ->where('key', $key) + ->where('url', $url); + + if ($_testUrl->count() == 0) { + DB::table('ltm_translation_urls')->insert([ + 'group' => $group, + 'key' => $key, + 'url' => $url, + ]); + } + } + } } } public function exportTranslations($group = null, $json = false) { - $basePath = $this->app['path.lang']; + $basePath = $this->app[ 'path.lang' ]; - if (! is_null($group) && ! $json) { - if (! in_array($group, $this->config['exclude_groups'])) { + if (!is_null($group) && !$json) { + if (!in_array($group, $this->config[ 'exclude_groups' ])) { $vendor = false; if ($group == '*') { return $this->exportAllTranslations(); @@ -266,13 +431,13 @@ public function exportTranslations($group = null, $json = false) } $tree = $this->makeTree(Translation::ofTranslatedGroup($group) - ->orderByGroupKeys(Arr::get($this->config, 'sort_keys', false)) - ->get()); + ->orderByGroupKeys(Arr::get($this->config, 'sort_keys', false)) + ->get()); foreach ($tree as $locale => $groups) { - if (isset($groups[$group])) { - $translations = $groups[$group]; - $path = $this->app['path.lang']; + if (isset($groups[ $group ])) { + $translations = $groups[ $group ]; + $path = $this->app[ 'path.lang' ]; $locale_path = $locale.DIRECTORY_SEPARATOR.$group; if ($vendor) { @@ -287,7 +452,7 @@ public function exportTranslations($group = null, $json = false) $subfolder_level = $subfolder_level.$subfolder.DIRECTORY_SEPARATOR; $temp_path = rtrim($path.DIRECTORY_SEPARATOR.$subfolder_level, DIRECTORY_SEPARATOR); - if (! is_dir($temp_path)) { + if (!is_dir($temp_path)) { mkdir($temp_path, 0777, true); } } @@ -304,13 +469,13 @@ public function exportTranslations($group = null, $json = false) if ($json) { $tree = $this->makeTree(Translation::ofTranslatedGroup(self::JSON_GROUP) - ->orderByGroupKeys(Arr::get($this->config, 'sort_keys', false)) - ->get(), true); + ->orderByGroupKeys(Arr::get($this->config, 'sort_keys', false)) + ->get(), true); foreach ($tree as $locale => $groups) { - if (isset($groups[self::JSON_GROUP])) { - $translations = $groups[self::JSON_GROUP]; - $path = $this->app['path.lang'].'/'.$locale.'.json'; + if (isset($groups[ self::JSON_GROUP ])) { + $translations = $groups[ self::JSON_GROUP ]; + $path = $this->app[ 'path.lang' ].'/'.$locale.'.json'; $output = json_encode($translations, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE); $this->files->put($path, $output); } @@ -342,10 +507,10 @@ protected function makeTree($translations, $json = false) $array = []; foreach ($translations as $translation) { if ($json) { - $this->jsonSet($array[$translation->locale][$translation->group], $translation->key, + $this->jsonSet($array[ $translation->locale ][ $translation->group ], $translation->key, $translation->value); - } else { - Arr::set($array[$translation->locale][$translation->group], $translation->key, + } else if( isset($translation->value) && $translation->value != "" ){ + Arr::set($array[ $translation->locale ][ $translation->group ], $translation->key, $translation->value); } } @@ -358,7 +523,7 @@ public function jsonSet(&$array, $key, $value) if (is_null($key)) { return $array = $value; } - $array[$key] = $value; + $array[ $key ] = $value; return $array; } @@ -399,7 +564,7 @@ public function addLocale($locale) $this->saveIgnoredLocales(); $this->ignoreLocales = $this->getIgnoredLocales(); - if (! $this->files->exists($localeDir) || ! $this->files->isDirectory($localeDir)) { + if (!$this->files->exists($localeDir) || !$this->files->isDirectory($localeDir)) { return $this->files->makeDirectory($localeDir); } @@ -413,7 +578,7 @@ protected function saveIgnoredLocales() public function removeLocale($locale) { - if (! $locale) { + if (!$locale) { return false; } $this->ignoreLocales = array_merge($this->ignoreLocales, [$locale]); @@ -428,7 +593,7 @@ public function getConfig($key = null) if ($key == null) { return $this->config; } else { - return $this->config[$key]; + return $this->config[ $key ]; } } } diff --git a/src/ManagerServiceProvider.php b/src/ManagerServiceProvider.php index daef4bd4..c4d92702 100644 --- a/src/ManagerServiceProvider.php +++ b/src/ManagerServiceProvider.php @@ -1,25 +1,25 @@ mergeConfigFrom($configPath, 'translation-manager'); $this->publishes([$configPath => config_path('translation-manager.php')], 'config'); @@ -52,15 +52,15 @@ public function register() return new Console\CleanCommand($app['translation-manager']); }); $this->commands('command.translation-manager.clean'); - } + } /** - * Bootstrap the application events. - * - * @return void - */ - public function boot() - { + * Bootstrap the application events. + * + * @return void + */ + public function boot() + { $viewPath = __DIR__.'/../resources/views'; $this->loadViewsFrom($viewPath, 'translation-manager'); $this->publishes([ @@ -73,22 +73,23 @@ public function boot() ], 'migrations'); $this->loadRoutesFrom(__DIR__.'/routes.php'); - } + } - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array('translation-manager', + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return array( + 'translation-manager', 'command.translation-manager.reset', 'command.translation-manager.import', 'command.translation-manager.find', 'command.translation-manager.export', 'command.translation-manager.clean' ); - } + } } diff --git a/src/Models/Translation.php b/src/Models/Translation.php index 60a744a4..246ab26e 100644 --- a/src/Models/Translation.php +++ b/src/Models/Translation.php @@ -1,21 +1,23 @@ where('group', $group)->whereNotNull('value'); } - public function scopeOrderByGroupKeys($query, $ordered) { + public function scopeOrderByGroupKeys($query, $ordered) + { if ($ordered) { $query->orderBy('group')->orderBy('key'); } @@ -40,7 +43,7 @@ public function scopeSelectDistinctGroup($query) { $select = ''; - switch (DB::getDriverName()){ + switch (DB::getDriverName()) { case 'mysql': $select = 'DISTINCT `group`'; break; @@ -52,4 +55,37 @@ public function scopeSelectDistinctGroup($query) return $query->select(DB::raw($select)); } + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function sourceLocations( $group, $key ) + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->where('group', $group)->where('key', $key); + } + + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function urls( $group, $key ) + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_urls')->where('group', $group)->where('key', $key); + } + + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function possibleVariables( $group, $key ) + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->where('group', $group)->where('key', $key); + } + } diff --git a/src/Translator.php b/src/Translator.php index 28ed72f6..a3cb0160 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -8,6 +8,9 @@ class Translator extends LaravelTranslator { /** @var Dispatcher */ protected $events; + /** @var Manager */ + protected $manager; + /** * Get the translation for the given key. * @@ -20,14 +23,17 @@ public function get($key, array $replace = array(), $locale = null, $fallback = { // Get without fallback $result = parent::get($key, $replace, $locale, false); - if($result === $key){ - $this->notifyMissingKey($key); + if($result === $key && config( 'translation-manager.ignore_new_trans', false )){ + $this->notifyMissingKey($key, array_keys( $replace )); // Reget with fallback $result = parent::get($key, $replace, $locale, $fallback); } + if( config( 'translation-manager.debug', false ) && $result != $key ) + $result .= " [" . $key . "]"; + return $result; } @@ -36,11 +42,14 @@ public function setTranslationManager(Manager $manager) $this->manager = $manager; } - protected function notifyMissingKey($key) + protected function notifyMissingKey($key, $parameters) { + if( config('translation-manager.ignore_new_trans', false ) ) + return ; + list($namespace, $group, $item) = $this->parseKey($key); if($this->manager && $namespace === '*' && $group && $item ){ - $this->manager->missingKey($namespace, $group, $item); + $this->manager->missingKey($namespace, $group, $item, $parameters); } } diff --git a/src/routes.php b/src/routes.php index fefb399d..99883868 100644 --- a/src/routes.php +++ b/src/routes.php @@ -2,19 +2,25 @@ declare(strict_types=1); -$config = array_merge(config('translation-manager.route'), ['namespace' => 'Barryvdh\TranslationManager']); -Route::group($config, function($router) -{ - $router->get('view/{groupKey?}', 'Controller@getView')->where('groupKey', '.*'); - $router->get('/{groupKey?}', 'Controller@getIndex')->where('groupKey', '.*'); - $router->post('/add/{groupKey}', 'Controller@postAdd')->where('groupKey', '.*'); - $router->post('/edit/{groupKey}', 'Controller@postEdit')->where('groupKey', '.*'); - $router->post('/groups/add', 'Controller@postAddGroup'); - $router->post('/delete/{groupKey}/{translationKey}', 'Controller@postDelete')->where('groupKey', '.*'); - $router->post('/import', 'Controller@postImport'); - $router->post('/find', 'Controller@postFind'); - $router->post('/locales/add', 'Controller@postAddLocale'); - $router->post('/locales/remove', 'Controller@postRemoveLocale'); - $router->post('/publish/{groupKey}', 'Controller@postPublish')->where('groupKey', '.*'); - $router->post('/translate-missing', 'Controller@postTranslateMissing'); +use Barryvdh\TranslationManager\Controller; + +Route::group(config('translation-manager.route'), function ($router) { + $router->get('/view/{groupKey?}', [Controller::class, 'getView'])->where('groupKey', '.*')->name( 'translation-manager.group.list' ); + $router->get('/search', [Controller::class, 'getSearchResults'])->name( 'translation-manager.search' ); + $router->get('/detail/{groupKey}/{translationKey}', [Controller::class, 'getDetail'])->name( 'translation-manager.translation' ); + $router->get('/{groupKey?}', [Controller::class, 'getIndex'])->where('groupKey', '.*')->name( 'translation-manager.index'); + + + $router->post('/add/{groupKey}', [Controller::class, 'postAdd'])->where('groupKey', '.*')->name('translation-manager.translation.add'); + $router->post('/edit/{groupKey}', [Controller::class, 'postEdit'])->where('groupKey', '.*')->name('translation-manager.translation.edit'); + $router->post('/edit-all/{groupKey}/{translationKey}', [Controller::class, 'postEditAll'])->name('translation-manager.translation.edit-all'); + + $router->post('/groups/add', [Controller::class, 'postAddGroup']); + $router->post('/delete/{groupKey}/{translationKey}', [Controller::class, 'postDelete'])->where('groupKey', '.*'); + $router->post('/import', [Controller::class, 'postImport']); + $router->post('/find', [Controller::class, 'postFind']); + $router->post('/locales/add', [Controller::class, 'postAddLocale']); + $router->post('/locales/remove', [Controller::class, 'postRemoveLocale']); + $router->post('/publish/{groupKey}', [Controller::class, 'postPublish'])->where('groupKey', '.*'); + $router->post('/translate-missing', [Controller::class, 'postTranslateMissing']); }); diff --git a/tests/Unit/ValidateTranslationKeysTest.php b/tests/Unit/ValidateTranslationKeysTest.php new file mode 100644 index 00000000..d8ffc307 --- /dev/null +++ b/tests/Unit/ValidateTranslationKeysTest.php @@ -0,0 +1,32 @@ +truncate(); + $this->artisan( 'translations:find' ); + + DB::table( 'ltm_translations' )->whereNotNull( 'key' )->update( [ 'value' => 'test' ] ); + + Artisan::command( 'translations:export --all', function () {} ); + + $translations = DB::table( 'ltm_translations' )->whereNotNull( 'value' )->get(); + foreach ( $translations as $translation ){ + $this->assertIsString( trans( $translation->group . "." . $translation->key ), "Key[" . $translation->group . "." . $translation->key . "] has more than one result!" ); + } + } +}