From fc72aae01160c9fbbe02f9a926b63c402f181fcc Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Wed, 5 May 2021 23:04:19 +0200 Subject: [PATCH 01/11] - index.php converted to blades - improved translation finding regex, while finding logs place and possible parameters - improved translation editing view, focus on one translation and move to another by arrows - bit of code prettifying - possibility to ignore JSON translations entirely - log URL where key was found --- config/translation-manager.php | 5 + ..._create_ltm_translations_sources_table.php | 36 ++ ...011_create_ltm_translations_urls_table.php | 35 ++ ...reate_ltm_translations_variables_table.php | 35 ++ readme.md | 1 + .../views/components/locales_list.blade.php | 45 ++ .../views/components/post_import.blade.php | 22 + .../views/components/post_publish.blade.php | 5 + .../components/translation_detail.blade.php | 78 +++ .../components/translations_list.blade.php | 99 ++++ resources/views/index.blade.php | 483 ++++++++++++++++++ resources/views/index.php | 326 ------------ screenshot2.png | Bin 0 -> 92243 bytes src/Controller.php | 142 +++-- src/Manager.php | 214 +++++--- src/ManagerServiceProvider.php | 67 +-- src/Models/Translation.php | 33 +- src/Translator.php | 2 +- src/routes.php | 35 +- 19 files changed, 1185 insertions(+), 478 deletions(-) create mode 100644 database/migrations/2021_05_04_201011_create_ltm_translations_sources_table.php create mode 100644 database/migrations/2021_05_04_201011_create_ltm_translations_urls_table.php create mode 100644 database/migrations/2021_05_04_201011_create_ltm_translations_variables_table.php create mode 100644 resources/views/components/locales_list.blade.php create mode 100644 resources/views/components/post_import.blade.php create mode 100644 resources/views/components/post_publish.blade.php create mode 100644 resources/views/components/translation_detail.blade.php create mode 100644 resources/views/components/translations_list.blade.php create mode 100644 resources/views/index.blade.php delete mode 100644 resources/views/index.php create mode 100644 screenshot2.png diff --git a/config/translation-manager.php b/config/translation-manager.php index 6411f6d4..c37d5fad 100644 --- a/config/translation-manager.php +++ b/config/translation-manager.php @@ -66,4 +66,9 @@ '$trans.get', ], + + 'ignore_new_trans' => false, + 'ignore_json' => true, + 'warn_in_code' => true, + ]; 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..ace53a16 --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_sources_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string( 'file_path' ); + $table->integer( 'file_line' ); + }); + } + + /** + * 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..ada92743 --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_urls_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string( 'url' ); + }); + } + + /** + * 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..5ad933f7 --- /dev/null +++ b/database/migrations/2021_05_04_201011_create_ltm_translations_variables_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + + $table->string('group'); + $table->text('key'); + + $table->string( 'attribute' ); + }); + } + + /** + * 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..4a37951a --- /dev/null +++ b/resources/views/components/locales_list.blade.php @@ -0,0 +1,45 @@ +
+ Supported locales +

+ Current supported locales: +

+
+ +
    + +
  • +
    + + + +
    +
  • + +
+
+
+ +
+

+ Enter new locale key: +

+
+
+ +
+
+ +
+
+
+
+
+
+ Export all translations +
+ + +
+
\ 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..290577e6 --- /dev/null +++ b/resources/views/components/post_import.blade.php @@ -0,0 +1,22 @@ +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
\ 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..106726cf --- /dev/null +++ b/resources/views/components/post_publish.blade.php @@ -0,0 +1,5 @@ +
+ + + Back +
\ 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..64e63aad --- /dev/null +++ b/resources/views/components/translation_detail.blade.php @@ -0,0 +1,78 @@ + +
+
$group, "translationKey" => $key]) }}"> + @csrf + +
+ + +
+
+ + +
+ @foreach($locales as $locale) + +
+ + +
+ @endforeach + +
+
+@if( $_translation != null ) +
+
+ Variables +
    + @foreach( $_translation->getPossibleVariables() as $entry ) +
  • {{ $entry->attribute }}
  • + @endforeach +
+
+
+ URLs +
    + @foreach( $_translation->getUrls() as $entry ) +
  • {{ $entry->url }}
  • + @endforeach +
+
+
+ Source Locations +
    + @foreach( $_translation->getSourceLocations() as $entry ) +
  • {{ $entry->file_path }}:{{ $entry->file_line }}
  • + @endforeach +
+
+
+@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..1e1fae92 --- /dev/null +++ b/resources/views/components/translations_list.blade.php @@ -0,0 +1,99 @@ +
+ +
+ + +
+
+ +
+
+
+
+ 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) : '' ?> + + +
\ 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..e6d2b7ff --- /dev/null +++ b/resources/views/index.blade.php @@ -0,0 +1,483 @@ + + + + + + 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( !isset( $group ) ) + @include( 'translation-manager::components.post_import' ) + @else + @include( 'translation-manager::components.post_publish' ) + @endif +

+ @if($group) + @if($key) + @include( 'translation-manager::components.translation_detail' ) + @else +
+ +
+

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.translations_list' ) + @endif + @else +
+ +
+

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.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 0000000000000000000000000000000000000000..25bbfcbd2815a03995ae57b1e660629512baf2d5 GIT binary patch literal 92243 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 literal 0 HcmV?d00001 diff --git a/src/Controller.php b/src/Controller.php index 31ab43c0..754b3710 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -1,14 +1,15 @@ manager = $manager; } - public function getIndex($group = null) + public function getIndex($groupKey = null, $translationKey = 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 +30,73 @@ 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; + $numChanged = Translation::where('group', $groupKey)->where('status', Translation::STATUS_CHANGED)->count(); + + + $allTranslations = Translation::where('group', $groupKey); + $allTranslations->orderBy('key', 'asc'); + $numTranslations = $allTranslations->count(); + /** @var \Illuminate\Database\Eloquent\Collection $allTranslations */ + $allTranslations = $allTranslations->get(); - $allTranslations = Translation::where('group', $group)->orderBy('key', 'asc')->get(); - $numTranslations = count($allTranslations); $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('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($groupKey); + } + + public function getDetail($groupKey = null, $translationKey = null) { - return $this->getIndex($group); + return $this->getIndex($groupKey, $translationKey); } protected function loadLocales() @@ -71,29 +114,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 +146,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(); + DB::table('ltm_translation_sources')->where('group', $groupKey)->where('key', $key)->delete(); + DB::table('ltm_translation_variables')->where('group', $groupKey)->where('key', $key)->delete(); + DB::table('ltm_translation_urls')->where('group', $groupKey)->where('key', $key)->delete(); return ['status' => 'ok']; } } @@ -126,15 +196,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 +212,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 +238,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 +255,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', diff --git a/src/Manager.php b/src/Manager.php index 7e478f5e..a0bf6867 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -2,14 +2,15 @@ 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\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 { @@ -43,7 +44,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)); @@ -93,7 +94,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; @@ -115,8 +116,7 @@ public function importTranslations($replace = false, $base = null, $import_group } $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); @@ -138,8 +138,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 +149,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; } @@ -165,17 +165,19 @@ public function findTranslations($path = null) $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 + $groupPattern = // See https://regex101.com/r/Mxr50T/1 + "[\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 one or more items/keys + ')'. // 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 @@ -193,60 +195,156 @@ public function findTranslations($path = null) /** @var \Symfony\Component\Finder\SplFileInfo $file */ foreach ($finder as $file) { // 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(",", 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'] != false) { + 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); + + //clean variables and sources + \Illuminate\Support\Facades\DB::statement('TRUNCATE TABLE `ltm_translation_sources`'); + \Illuminate\Support\Facades\DB::statement('TRUNCATE TABLE `ltm_translation_variables`'); // Add the translations to the database, if not existing. - foreach ($groupKeys as $key) { + foreach ($groupKeys as $key => $data) { // Split the group and item list($group, $item) = explode('.', $key, 2); $this->missingKey('', $group, $item); + + // 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, + ]); + } + + // save possible variables + $items = array_unique($data['variables']); + foreach ($items as $item) { + \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->insert([ + "group" => $group, + "key" => $item, + "attribute" => $item, + ]); + } + + $counter++; } - foreach ($stringKeys as $key) { - $group = self::JSON_GROUP; - $item = $key; - $this->missingKey('', $group, $item); + if ($this->config['ignore_json'] != false) { + $stringKeys = array_unique($stringKeys); + foreach ($stringKeys as $key) { + $group = self::JSON_GROUP; + $item = $key; + $this->missingKey('', $group, $item); + } } // Return the number of found translations return count($groupKeys + $stringKeys); } + /** + * 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; + } + public function missingKey($namespace, $group, $key) { - if (! in_array($group, $this->config['exclude_groups'])) { + 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, + 'group' => $group, + 'key' => $key, ]); + + 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, + ]); + } + } + } } } @@ -254,8 +352,8 @@ public function exportTranslations($group = null, $json = false) { $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,8 +364,8 @@ 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])) { @@ -287,7 +385,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,8 +402,8 @@ 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])) { @@ -399,7 +497,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 +511,7 @@ protected function saveIgnoredLocales() public function removeLocale($locale) { - if (! $locale) { + if (!$locale) { return false; } $this->ignoreLocales = array_merge($this->ignoreLocales, [$locale]); 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..92b86388 100644 --- a/src/Models/Translation.php +++ b/src/Models/Translation.php @@ -1,21 +1,22 @@ where('group', $group)->whereNotNull('value'); } - public function scopeOrderByGroupKeys($query, $ordered) { + public function scopeOrderByGroupKeys($query, $ordered) + { if ($ordered) { $query->orderBy('group')->orderBy('key'); } @@ -40,7 +42,7 @@ public function scopeSelectDistinctGroup($query) { $select = ''; - switch (DB::getDriverName()){ + switch (DB::getDriverName()) { case 'mysql': $select = 'DISTINCT `group`'; break; @@ -52,4 +54,19 @@ public function scopeSelectDistinctGroup($query) return $query->select(DB::raw($select)); } + public function getSourceLocations() + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->where('group', $this->group)->where('key', $this->key)->get(); + } + + public function getUrls() + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_urls')->where('group', $this->group)->where('key', $this->key)->get(); + } + + public function getPossibleVariables() + { + return \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->where('group', $this->group)->where('key', $this->key)->get(); + } + } diff --git a/src/Translator.php b/src/Translator.php index 28ed72f6..d1249c1d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -20,7 +20,7 @@ public function get($key, array $replace = array(), $locale = null, $fallback = { // Get without fallback $result = parent::get($key, $replace, $locale, false); - if($result === $key){ + if($result === $key && config( 'translation-manager.ignore_new_trans', false )){ $this->notifyMissingKey($key); // Reget with fallback diff --git a/src/routes.php b/src/routes.php index fefb399d..01de5bba 100644 --- a/src/routes.php +++ b/src/routes.php @@ -2,19 +2,24 @@ 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('/detail/{groupKey}/{translationKey}', [Controller::class, 'getDetail'])->name( 'translation-manager.translation' ); + $router->get('/{groupKey?}', [Controller::class, 'getIndex'])->where('groupKey', '.*'); + + + $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']); }); From f9a00764b9cc26674e3eb9a81e5ff031935c552b Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Thu, 6 May 2021 16:20:45 +0200 Subject: [PATCH 02/11] - index.php converted to blades - improved translation finding regex, while finding logs place and possible parameters - improved translation editing view, focus on one translation and move to another by arrows - bit of code prettifying - possibility to ignore JSON translations entirely - log URL where key was found --- config/translation-manager.php | 14 +++++++-- .../components/translation_detail.blade.php | 6 ++-- .../components/translations_list.blade.php | 4 +-- src/Controller.php | 6 ++-- src/Models/Translation.php | 31 +++++++++++++++---- src/Translator.php | 3 ++ 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/config/translation-manager.php b/config/translation-manager.php index c37d5fad..7c8c7b6f 100644 --- a/config/translation-manager.php +++ b/config/translation-manager.php @@ -66,9 +66,19 @@ '$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, - 'warn_in_code' => true, + + /** + * translations without source position will be marked as red + */ + 'warn_in_code' => false, ]; diff --git a/resources/views/components/translation_detail.blade.php b/resources/views/components/translation_detail.blade.php index 64e63aad..613c1f1d 100644 --- a/resources/views/components/translation_detail.blade.php +++ b/resources/views/components/translation_detail.blade.php @@ -53,7 +53,7 @@ class="glyphicon glyphicon-chevron-right">
Variables
    - @foreach( $_translation->getPossibleVariables() as $entry ) + @foreach( \Barryvdh\TranslationManager\Models\Translation::possibleVariables( $group, $key)->get() as $entry )
  • {{ $entry->attribute }}
  • @endforeach
@@ -61,7 +61,7 @@ class="glyphicon glyphicon-chevron-right">
URLs
    - @foreach( $_translation->getUrls() as $entry ) + @foreach( \Barryvdh\TranslationManager\Models\Translation::urls( $group, $key)->get() as $entry )
  • {{ $entry->url }}
  • @endforeach
@@ -69,7 +69,7 @@ class="glyphicon glyphicon-chevron-right">
Source Locations
    - @foreach( $_translation->getSourceLocations() as $entry ) + @foreach( \Barryvdh\TranslationManager\Models\Translation::sourceLocations( $group, $key)->get() as $entry )
  • {{ $entry->file_path }}:{{ $entry->file_line }}
  • @endforeach
diff --git a/resources/views/components/translations_list.blade.php b/resources/views/components/translations_list.blade.php index 1e1fae92..db998e2a 100644 --- a/resources/views/components/translations_list.blade.php +++ b/resources/views/components/translations_list.blade.php @@ -67,8 +67,8 @@ $translation): ?> - - + count() == 0 ) class="bg-danger" @endif> + $group, "translationKey" => $key ]) ?>" > diff --git a/src/Controller.php b/src/Controller.php index 754b3710..901735e1 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -174,9 +174,9 @@ public function postDelete($groupKey, $key) { if (!in_array($groupKey, $this->manager->getConfig('exclude_groups')) && $this->manager->getConfig('delete_enabled')) { Translation::where('group', $groupKey)->where('key', $key)->delete(); - DB::table('ltm_translation_sources')->where('group', $groupKey)->where('key', $key)->delete(); - DB::table('ltm_translation_variables')->where('group', $groupKey)->where('key', $key)->delete(); - DB::table('ltm_translation_urls')->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']; } } diff --git a/src/Models/Translation.php b/src/Models/Translation.php index 92b86388..246ab26e 100644 --- a/src/Models/Translation.php +++ b/src/Models/Translation.php @@ -1,6 +1,7 @@ select(DB::raw($select)); } - public function getSourceLocations() + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function sourceLocations( $group, $key ) { - return \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->where('group', $this->group)->where('key', $this->key)->get(); + return \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->where('group', $group)->where('key', $key); } - public function getUrls() + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function urls( $group, $key ) { - return \Illuminate\Support\Facades\DB::table('ltm_translation_urls')->where('group', $this->group)->where('key', $this->key)->get(); + return \Illuminate\Support\Facades\DB::table('ltm_translation_urls')->where('group', $group)->where('key', $key); } - public function getPossibleVariables() + /** + * @param $group + * @param $key + * + * @return Builder + */ + public static function possibleVariables( $group, $key ) { - return \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->where('group', $this->group)->where('key', $this->key)->get(); + 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 d1249c1d..1d396466 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -38,6 +38,9 @@ public function setTranslationManager(Manager $manager) protected function notifyMissingKey($key) { + 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); From dacb775d5c9ca222820b5e580db2e637de8f2edf Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Fri, 7 May 2021 21:42:38 +0200 Subject: [PATCH 03/11] Upgrade parameters catching --- src/Manager.php | 101 +++++++++++++++++++++++---------------------- src/Translator.php | 6 +-- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/Manager.php b/src/Manager.php index a0bf6867..cb3e4310 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -36,7 +36,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(); @@ -58,7 +58,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; } @@ -76,17 +76,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); @@ -110,7 +110,7 @@ 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; } @@ -130,7 +130,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; @@ -163,7 +162,7 @@ public function findTranslations($path = null) $path = $path ?: base_path(); $groupKeys = []; $stringKeys = []; - $functions = $this->config['trans_functions']; + $functions = $this->config[ 'trans_functions' ]; $groupPattern = // See https://regex101.com/r/Mxr50T/1 "[\W]". // Must not have an alphanum or _ or > before real method @@ -197,28 +196,28 @@ public function findTranslations($path = null) // Search the current file for the pattern if (preg_match_all("/$groupPattern/si", $file->getContents(), $matches)) { // Get all matches - foreach ($matches[2] as $i => $key) { + foreach ($matches[ 2 ] as $i => $key) { $found++; - if (!isset($groupKeys[$key])) { - $groupKeys[$key] = [ + 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(",", str_strip_whitespace($matches[5][$i])); + $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(",", str_strip_whitespace($matches[ 5 ][ $i ])); foreach ($attributes as $attribute) { list($item, $_rest) = explode("=", $attribute, 2); - $groupKeys[$key]["variables"][] = str_replace(['"', "'"], "", $item); + $groupKeys[ $key ][ "variables" ][] = str_replace(['"', "'"], "", $item); } } } } - if ($this->config['ignore_json'] != false) { + if ($this->config[ 'ignore_json' ] != false) { if (preg_match_all("/$stringPattern/siU", $file->getContents(), $matches)) { - foreach ($matches['string'] as $key) { + 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 @@ -241,16 +240,15 @@ public function findTranslations($path = null) //clean variables and sources \Illuminate\Support\Facades\DB::statement('TRUNCATE TABLE `ltm_translation_sources`'); - \Illuminate\Support\Facades\DB::statement('TRUNCATE TABLE `ltm_translation_variables`'); // Add the translations to the database, if not existing. foreach ($groupKeys as $key => $data) { // 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']); + $files = array_unique($data[ 'sources' ]); foreach ($files as $file) { list($path, $line) = explode(':', $file); \Illuminate\Support\Facades\DB::table('ltm_translation_sources')->insert([ @@ -261,20 +259,10 @@ public function findTranslations($path = null) ]); } - // save possible variables - $items = array_unique($data['variables']); - foreach ($items as $item) { - \Illuminate\Support\Facades\DB::table('ltm_translation_variables')->insert([ - "group" => $group, - "key" => $item, - "attribute" => $item, - ]); - } - $counter++; } - if ($this->config['ignore_json'] != false) { + if ($this->config[ 'ignore_json' ] != false) { $stringKeys = array_unique($stringKeys); foreach ($stringKeys as $key) { $group = self::JSON_GROUP; @@ -289,8 +277,10 @@ public function findTranslations($path = null) /** * return list of line_numbers + * * @param \Symfony\Component\Finder\SplFileInfo $file * @param $search + * * @return array */ private function findLineNumber(\Symfony\Component\Finder\SplFileInfo $file, $search) @@ -307,29 +297,42 @@ private function findLineNumber(\Symfony\Component\Finder\SplFileInfo $file, $se return $line_numbers; } - public function missingKey($namespace, $group, $key) + public function missingKey($namespace, $group, $key, $parameters = []) { - if (!in_array($group, $this->config['exclude_groups'])) { - if ($this->config['ignore_json']) { + 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) { + if ($groupMatches[ 0 ] != $key) { return; } } } Translation::firstOrCreate([ - 'locale' => $this->app['config']['app.locale'], + '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'])) { + if (!Str::contains($url, $this->config[ 'route' ][ 'prefix' ])) { // save URL with translation key $_testUrl = DB::table('ltm_translation_urls') ->where('group', $group) @@ -350,10 +353,10 @@ public function missingKey($namespace, $group, $key) 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 (!in_array($group, $this->config[ 'exclude_groups' ])) { $vendor = false; if ($group == '*') { return $this->exportAllTranslations(); @@ -368,9 +371,9 @@ public function exportTranslations($group = null, $json = 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) { @@ -406,9 +409,9 @@ public function exportTranslations($group = null, $json = 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); } @@ -440,10 +443,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, + Arr::set($array[ $translation->locale ][ $translation->group ], $translation->key, $translation->value); } } @@ -456,7 +459,7 @@ public function jsonSet(&$array, $key, $value) if (is_null($key)) { return $array = $value; } - $array[$key] = $value; + $array[ $key ] = $value; return $array; } @@ -526,7 +529,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/Translator.php b/src/Translator.php index 1d396466..c038ead9 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -21,7 +21,7 @@ public function get($key, array $replace = array(), $locale = null, $fallback = // Get without fallback $result = parent::get($key, $replace, $locale, false); if($result === $key && config( 'translation-manager.ignore_new_trans', false )){ - $this->notifyMissingKey($key); + $this->notifyMissingKey($key, array_keys( $replace )); // Reget with fallback $result = parent::get($key, $replace, $locale, $fallback); @@ -36,14 +36,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); } } From b9117b8dc274c8a57596a68994d7282e9c02d16b Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Sun, 9 May 2021 10:04:22 +0200 Subject: [PATCH 04/11] - Regex update to ignroe non-alphanumberic at the end --- src/Manager.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Manager.php b/src/Manager.php index cb3e4310..348e76fd 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -164,7 +164,7 @@ public function findTranslations($path = null) $stringKeys = []; $functions = $this->config[ 'trans_functions' ]; - $groupPattern = // See https://regex101.com/r/Mxr50T/1 + $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 @@ -172,15 +172,16 @@ public function findTranslations($path = null) '('. // 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 one or more items/keys + "([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 From 979c898788e96e9a902805571d1dd1a8cdfaa319 Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Sun, 9 May 2021 10:06:34 +0200 Subject: [PATCH 05/11] - Table fix --- resources/views/components/translations_list.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/components/translations_list.blade.php b/resources/views/components/translations_list.blade.php index db998e2a..702f0402 100644 --- a/resources/views/components/translations_list.blade.php +++ b/resources/views/components/translations_list.blade.php @@ -51,7 +51,7 @@

Total: , changed:

- +
From 6a66bddb0decc70f0187659f68a485f7fab6fa8b Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Thu, 13 May 2021 14:26:40 +0200 Subject: [PATCH 06/11] - add progress bar in translation:find - fix missing method - fix ignore_json conditions - add indexes to ltm_translations_* --- ...04_02_193005_create_translations_table.php | 45 +++++++----- ..._create_ltm_translations_sources_table.php | 16 ++-- ...011_create_ltm_translations_urls_table.php | 14 ++-- ...reate_ltm_translations_variables_table.php | 14 ++-- src/Console/FindCommand.php | 6 +- src/Manager.php | 73 +++++++++++++++++-- 6 files changed, 126 insertions(+), 42 deletions(-) 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 index ace53a16..78f6e760 100644 --- 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 @@ -1,10 +1,11 @@ bigIncrements('id'); $table->string('group'); $table->text('key'); - $table->string( 'file_path' ); - $table->integer( 'file_line' ); + $table->string('file_path'); + $table->integer('file_line'); + + $table->index(['group', 'key']); }); } 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 index ada92743..eecf23be 100644 --- 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 @@ -1,10 +1,11 @@ bigIncrements('id'); $table->string('group'); $table->text('key'); - $table->string( 'url' ); + $table->string('url'); + + $table->index(['group', 'key']); }); } 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 index 5ad933f7..9ec7cba7 100644 --- 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 @@ -1,10 +1,11 @@ bigIncrements('id'); $table->string('group'); $table->text('key'); - $table->string( 'attribute' ); + $table->string('attribute'); + + $table->index(['group', 'key']); }); } 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/Manager.php b/src/Manager.php index 348e76fd..55ac2384 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -10,6 +10,8 @@ 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; class Manager @@ -157,6 +159,7 @@ public function importTranslation($key, $value, $locale, $group, $replace = fals return true; } + public function findTranslations($path = null) { $path = $path ?: base_path(); @@ -192,8 +195,23 @@ 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/si", $file->getContents(), $matches)) { // Get all matches @@ -207,7 +225,7 @@ public function findTranslations($path = null) } $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(",", str_strip_whitespace($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); @@ -216,7 +234,7 @@ public function findTranslations($path = null) } } - if ($this->config[ 'ignore_json' ] != false) { + 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)) { @@ -239,14 +257,27 @@ public function findTranslations($path = null) // Remove duplicates 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 => $data) { + if ($section != null) { + $bar2->advance(); + } + // Split the group and item list($group, $item) = explode('.', $key, 2); - $this->missingKey('', $group, $item, array_unique( $data[ 'variables' ] ) ); + $this->missingKey('', $group, $item, array_unique($data[ 'variables' ])); // save location in strings $files = array_unique($data[ 'sources' ]); @@ -263,13 +294,33 @@ public function findTranslations($path = null) $counter++; } - if ($this->config[ 'ignore_json' ] != false) { + 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) { - $group = self::JSON_GROUP; + 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 @@ -298,6 +349,18 @@ private function findLineNumber(\Symfony\Component\Finder\SplFileInfo $file, $se return $line_numbers; } + /** + * Strp all whitespaces inside of string + * + * @param $string + * + * @return string|string[]|null + */ + public static function str_strip_whitespace($string) + { + return preg_replace('/\s+/', '', $string); + } + public function missingKey($namespace, $group, $key, $parameters = []) { if (!in_array($group, $this->config[ 'exclude_groups' ])) { From 76f5bdf96752dc7777c9fbf1efc6977e8e124eee Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Fri, 11 Jun 2021 15:02:08 +0200 Subject: [PATCH 07/11] - search - debug mode for visible translation keys - blades beautifying --- config/translation-manager.php | 14 +- .../views/components/locales_list.blade.php | 20 +-- .../views/components/post_import.blade.php | 8 +- .../views/components/post_publish.blade.php | 6 +- .../components/translation_detail.blade.php | 9 +- .../components/translations_list.blade.php | 103 ++++---------- resources/views/index.blade.php | 131 +++++++++++++----- src/Controller.php | 39 ++++-- src/Translator.php | 6 + src/routes.php | 3 +- 10 files changed, 194 insertions(+), 145 deletions(-) diff --git a/config/translation-manager.php b/config/translation-manager.php index 7c8c7b6f..a552a080 100644 --- a/config/translation-manager.php +++ b/config/translation-manager.php @@ -77,8 +77,20 @@ 'ignore_json' => true, /** - * translations without source position will be marked as red + * 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/resources/views/components/locales_list.blade.php b/resources/views/components/locales_list.blade.php index 4a37951a..d62bed86 100644 --- a/resources/views/components/locales_list.blade.php +++ b/resources/views/components/locales_list.blade.php @@ -3,24 +3,24 @@

Current supported locales:

-
- + + @csrf
    - + @foreach($locales as $locale)
  • - - + {{ $locale }}
  • - + @endforeach
-
- + + @csrf

Enter new locale key: @@ -38,8 +38,8 @@

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 index 290577e6..543012ec 100644 --- a/resources/views/components/post_import.blade.php +++ b/resources/views/components/post_import.blade.php @@ -1,5 +1,5 @@ -
- + + @csrf
@@ -14,9 +14,9 @@
-
+
- + @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 index 106726cf..ae020fb4 100644 --- a/resources/views/components/post_publish.blade.php +++ b/resources/views/components/post_publish.blade.php @@ -1,5 +1,5 @@ -
- + + @csrf - Back + Back \ No newline at end of file diff --git a/resources/views/components/translation_detail.blade.php b/resources/views/components/translation_detail.blade.php index 613c1f1d..dd973162 100644 --- a/resources/views/components/translation_detail.blade.php +++ b/resources/views/components/translation_detail.blade.php @@ -14,8 +14,8 @@
@foreach($locales as $locale)
Key
+

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

+
- - - - + @foreach ($locales as $locale) + + @endforeach + @if ($deleteEnabled) - + @endif - $translation): ?> - - + - + @foreach ($locales as $locale) - - + @endforeach + @if ($deleteEnabled) - + @endif - + @endforeach
Key{{ $locale }} 
count() == 0 ) class="bg-danger" @endif> - $group, "translationKey" => $key ]) ?>" > + @foreach ($translations as $key => $translation) +
count() == 0 ) class="danger" @endif>{!! htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!} + $group, "translationKey" => $key ]) }}" > " - id="username" data-type="textarea" data-pk="id : 0 ?>" - data-url="" - data-title="Enter translation">value, ENT_QUOTES, 'UTF-8', - false) : '' ?> + class="editable status-{{ $t ? $t->status : 0 }} locale-{{ $locale }}" + data-locale="{{ $locale }}" + data-name="{!! $locale."|".htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!}" + id="username" data-type="textarea" data-pk="{{ $t ? $t->id : 0 }}" + data-url="{{ $editUrl }}" + 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 index e6d2b7ff..c139ee64 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -316,7 +316,7 @@ $.ajaxSetup({ beforeSend: function (xhr, settings) { console.log('beforesend'); - settings.data += "&_token="; + settings.data += "&_token={{ csrf_token() }}"; } }); @@ -336,9 +336,9 @@ $('.group-select').on('change', function () { var group = $(this).val(); if (group) { - window.location.href = '/' + $(this).val(); + window.location.href = '{{ action('\Barryvdh\TranslationManager\Controller@getView') }}/' + $(this).val(); } else { - window.location.href = ''; + window.location.href = '{{ action('\Barryvdh\TranslationManager\Controller@getIndex') }}'; } }); @@ -400,7 +400,7 @@ - + Translation Manager @@ -417,53 +417,116 @@

Done searching for translations, found N items!

- + @if(Session::has('successPublish'))
- + {{ Session::get('successPublish') }}
- -

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

- @if($group) + @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 -
- -
-

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

- + @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
- + action="{{ action('\Barryvdh\TranslationManager\Controller@postAddGroup') }}"> + @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.

@@ -475,6 +538,8 @@
+ @include( 'translation-manager::components.search' ) + @include( 'translation-manager::components.locales_list' ) @endif
diff --git a/src/Controller.php b/src/Controller.php index 901735e1..f7b1b12e 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -1,6 +1,7 @@ manager = $manager; } - public function getIndex($groupKey = null, $translationKey = null) + public function getIndex($groupKey = null, $translationKey = null, $q = null) { $locales = $this->manager->getLocales(); $groups = Translation::groupBy('group'); @@ -31,16 +32,23 @@ public function getIndex($groupKey = null, $translationKey = null) $groups = $groups->all(); } $groups = ['' => 'Choose a group'] + $groups; - $numChanged = Translation::where('group', $groupKey)->where('status', Translation::STATUS_CHANGED)->count(); + /** @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); + } - $allTranslations = Translation::where('group', $groupKey); - $allTranslations->orderBy('key', 'asc'); + $numChanged = (clone $translationQuery)->where('status', Translation::STATUS_CHANGED)->count(); + $translationQuery->orderBy('key', 'asc'); + $numTranslations = $translationQuery->count(); - $numTranslations = $allTranslations->count(); /** @var \Illuminate\Database\Eloquent\Collection $allTranslations */ - $allTranslations = $allTranslations->get(); - + $allTranslations = $translationQuery->get(); $translations = []; foreach ($allTranslations as $translation) { $translations[$translation->key][$translation->locale] = $translation; @@ -77,6 +85,7 @@ public function getIndex($groupKey = null, $translationKey = null) return view('translation-manager::index') ->with('translations', $translations) + ->with('q', $q) ->with('locales', $locales) ->with('groups', $groups) ->with('group', $groupKey) @@ -167,16 +176,16 @@ public function postEditAll(Request $request, $groupKey, $translationKey) } } - return back( )->with( 'successPublish', 'Saved!'); + return back()->with('successPublish', 'Saved!'); } public function postDelete($groupKey, $key) { 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(); + Translation::possibleVariables($groupKey, $key)->delete(); + Translation::sourceLocations($groupKey, $key)->delete(); + Translation::urls($groupKey, $key)->delete(); return ['status' => 'ok']; } } @@ -213,7 +222,7 @@ public function postAddGroup(Request $request) { $group = str_replace(".", '', $request->input('new-group')); if ($group) { - return redirect()->route('translation-manager.group.list', [ "groupKey" => $group ]); + return redirect()->route('translation-manager.group.list', ["groupKey" => $group]); } else { return redirect()->back(); } @@ -268,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/Translator.php b/src/Translator.php index c038ead9..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. * @@ -28,6 +31,9 @@ public function get($key, array $replace = array(), $locale = null, $fallback = } + if( config( 'translation-manager.debug', false ) && $result != $key ) + $result .= " [" . $key . "]"; + return $result; } diff --git a/src/routes.php b/src/routes.php index 01de5bba..99883868 100644 --- a/src/routes.php +++ b/src/routes.php @@ -6,8 +6,9 @@ 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', '.*'); + $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'); From 4449e70c8d9800e5b3cf818f9c045e61fd0aafa5 Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Wed, 16 Jun 2021 11:58:21 +0200 Subject: [PATCH 08/11] - search - debug mode for visible translation keys - blades beautifying --- resources/views/components/search.blade.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 resources/views/components/search.blade.php 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 From f34d96ede3654b1cbae4ab2deb2463a991b242c1 Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Sun, 25 Jul 2021 21:41:37 +0200 Subject: [PATCH 09/11] - Unit Test to check if keys are not arrays. - Visual differences in detail view between empty and filled locales. --- .../components/translation_detail.blade.php | 14 ++--- .../components/translations_list.blade.php | 63 +++++++++++-------- src/Console/ExportCommand.php | 2 +- tests/Unit/ValidateTranslationKeysTest.php | 32 ++++++++++ 4 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 tests/Unit/ValidateTranslationKeysTest.php diff --git a/resources/views/components/translation_detail.blade.php b/resources/views/components/translation_detail.blade.php index dd973162..6061baac 100644 --- a/resources/views/components/translation_detail.blade.php +++ b/resources/views/components/translation_detail.blade.php @@ -6,32 +6,32 @@
- +
- +
- @foreach($locales as $locale) + @foreach($locales as $localeKey => $locale) -
- +
+
@endforeach
-
+ -
+
@if( $prevTranslation != null ) $prevTranslation['group'], "translationKey" => $prevTranslation['key'] ] ) }}" diff --git a/resources/views/components/translations_list.blade.php b/resources/views/components/translations_list.blade.php index 954b1f98..161b77cf 100644 --- a/resources/views/components/translations_list.blade.php +++ b/resources/views/components/translations_list.blade.php @@ -5,44 +5,53 @@ Key @foreach ($locales as $locale) - {{ $locale }} + {{ $locale }} @endforeach @if ($deleteEnabled) -   +   @endif @foreach ($translations as $key => $translation) - - count() == 0 ) class="danger" @endif>{!! htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!} - $group, "translationKey" => $key ]) }}" > - - @foreach ($locales as $locale) - + group; + } + } + ?> + + count() == 0 ) class="danger" @endif>{!! htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!} + $group, "translationKey" => $key ]) }}"> + + @foreach ($locales as $locale) + - - {!! $t ? htmlentities($t->value, ENT_QUOTES, 'UTF-8', + + $group]) }}" + data-title="Enter translation">{!! $t ? htmlentities($t->value, ENT_QUOTES, 'UTF-8', false) : '' !!} - - @endforeach - @if ($deleteEnabled) - - - - @endif - + class="glyphicon glyphicon-trash"> + + @endif + @endforeach \ No newline at end of file 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/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!" ); + } + } +} From c94d427395b7744f3ace69d9a74fe39343d59440 Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Fri, 30 Jul 2021 11:19:11 +0200 Subject: [PATCH 10/11] - ignore empty translations while export --- resources/views/components/translation_detail.blade.php | 2 +- src/Manager.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/components/translation_detail.blade.php b/resources/views/components/translation_detail.blade.php index 6061baac..4568444e 100644 --- a/resources/views/components/translation_detail.blade.php +++ b/resources/views/components/translation_detail.blade.php @@ -18,7 +18,7 @@ $_translation = $translations[ $key ][ $locale ]; } ?> -
+
diff --git a/src/Manager.php b/src/Manager.php index 55ac2384..48a13b9e 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -509,7 +509,7 @@ protected function makeTree($translations, $json = false) if ($json) { $this->jsonSet($array[ $translation->locale ][ $translation->group ], $translation->key, $translation->value); - } else { + } else if( isset($translation->value) && $translation->value != "" ){ Arr::set($array[ $translation->locale ][ $translation->group ], $translation->key, $translation->value); } From c261284bbfc24dec5531a5bc0870dde9325a9fdb Mon Sep 17 00:00:00 2001 From: MimoGraphix Date: Mon, 14 Mar 2022 16:21:41 +0100 Subject: [PATCH 11/11] - mark empty lines --- resources/views/components/translations_list.blade.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/views/components/translations_list.blade.php b/resources/views/components/translations_list.blade.php index 161b77cf..e35b2f99 100644 --- a/resources/views/components/translations_list.blade.php +++ b/resources/views/components/translations_list.blade.php @@ -16,14 +16,18 @@ @foreach ($translations as $key => $translation) group; + } else { + $isEmpty = true; + } } } ?> - + count() == 0 ) class="danger" @endif>{!! htmlentities($key, ENT_QUOTES, 'UTF-8', false) !!} $group, "translationKey" => $key ]) }}">