├── .gitignore ├── LICENSE ├── composer.json ├── config └── translation-manager.php ├── database └── migrations │ ├── .gitkeep │ └── 2014_04_02_193005_create_translations_table.php ├── intro.jpg ├── readme.md ├── resources ├── lang │ └── en │ │ └── panel.php └── views │ ├── .gitkeep │ ├── files_list.blade.php │ ├── index.blade.php │ ├── index_manage.blade.php │ ├── key_row.blade.php │ ├── layout.blade.php │ ├── menu_actions.blade.php │ ├── show.blade.php │ ├── show_group.blade.php │ └── super_layout.blade.php └── src ├── Console ├── CleanCommand.php ├── CloneCommand.php ├── ExportCommand.php ├── FindCommand.php ├── ImportCommand.php ├── ResetCommand.php └── SuffixCommand.php ├── Controller.php ├── Manager.php ├── ManagerServiceProvider.php ├── Models └── Translation.php ├── Service.php ├── TranslationServiceProvider.php └── Translator.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Barry vd. Heuvel 2 | Copyright (C) 2017 HighSolutions 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "highsolutions/laravel-translation-manager", 3 | "description": "Manage Laravel Translations", 4 | "keywords": ["laravel", "translations", "translator", "language"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Barry vd. Heuvel", 9 | "email": "barryvdh@gmail.com" 10 | }, 11 | { 12 | "name": "HighSolutions", 13 | "email": "adam@highsolutions.pl" 14 | } 15 | ], 16 | "require": { 17 | "php": "^5.4|^7.3|^8.0.2", 18 | "illuminate/console": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 19 | "illuminate/support": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 20 | "symfony/finder": "*" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "HighSolutions\\TranslationManager\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "component": "package", 29 | "frameworks": ["Laravel 5.7", "Laravel 5.8", "Laravel 6.x", "Laravel 7.x", "Laravel 8.x", "Laravel 9.x", "Laravel 10.x", "Laravel 11.x", "Laravel 12.x"], 30 | "laravel": { 31 | "providers": [ 32 | "HighSolutions\\TranslationManager\\ManagerServiceProvider", 33 | "HighSolutions\\TranslationManager\\TranslationServiceProvider" 34 | ] 35 | } 36 | }, 37 | "minimum-stability": "stable" 38 | } 39 | -------------------------------------------------------------------------------- /config/translation-manager.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'prefix' => 'translations', 15 | 'namespace' => 'HighSolutions\TranslationManager', 16 | 'middleware' => [ 17 | 'web', 18 | 'auth', 19 | ], 20 | ], 21 | 22 | /** 23 | * Enable deletion of translations 24 | * 25 | * @type boolean 26 | */ 27 | 'delete_enabled' => true, 28 | 29 | /** 30 | * Exclude specific groups from Laravel Translation Manager. 31 | * This is useful if, for example, you want to avoid editing the official Laravel language files. 32 | * 33 | * @type array 34 | * 35 | * array( 36 | * 'pagination', 37 | * 'reminders', 38 | * 'validation', 39 | * ) 40 | */ 41 | 'exclude_groups' => array(), 42 | 43 | /** 44 | * Exclude specific langs from Laravel Translation Manager. 45 | * This is useful if, for example, you want to avoid editing spare lang files or vendor catalog. 46 | * 47 | * @type array 48 | * 49 | * array( 50 | * 'en', 51 | * 'vendor', 52 | * ) 53 | */ 54 | 'exclude_langs' => array( 55 | 'vendor', 56 | // ... 57 | ), 58 | 59 | /** 60 | * Basic language used by translator. 61 | */ 62 | 'basic_lang' => 'en', 63 | 64 | /** 65 | * Export translations with keys output alphabetically. 66 | */ 67 | 'sort_keys ' => false, 68 | 69 | /** 70 | * Highlight lines with locale marked (e.g. EN at the end of text) 71 | */ 72 | 'highlight_locale_marked' => false, 73 | 74 | /** 75 | * Enable live translation of content. 76 | */ 77 | 'live_translation_enabled' => false, 78 | 79 | /** 80 | * Position of live translation popup. 81 | */ 82 | 'popup_placement' => 'top', 83 | 84 | /** 85 | * Define who and when can manage module. 86 | * 87 | * @return bool 88 | */ 89 | 'permissions' => env('APP_ENV') == 'local', 90 | 91 | ); 92 | -------------------------------------------------------------------------------- /database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highsolutions/laravel-translation-manager/7ec9fd953fa16dd27515d5691f7925a1c583952e/database/migrations/.gitkeep -------------------------------------------------------------------------------- /database/migrations/2014_04_02_193005_create_translations_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->integer('status')->default(0); 19 | $table->string('locale'); 20 | $table->string('group'); 21 | $table->string('key'); 22 | $table->text('value')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('ltm_translations'); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /intro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highsolutions/laravel-translation-manager/7ec9fd953fa16dd27515d5691f7925a1c583952e/intro.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Laravel Translation Manager 2 | ============================= 3 | 4 | Easy management of translations in Laravel. 5 | 6 | ![Laravel-Translation-Manager by HighSolutions](https://raw.githubusercontent.com/highsolutions/laravel-translation-manager/master/intro.jpg) 7 | 8 | Installation 9 | ------------ 10 | 11 | Add the following line to the `require` section of your Laravel webapp's `composer.json` file: 12 | 13 | ```javascript 14 | "require": { 15 | "highsolutions/laravel-translation-manager": "^1.0" 16 | } 17 | ``` 18 | 19 | Run `composer update` to install the package. 20 | 21 | Then, update `config/app.php` by adding an entry for the service provider: 22 | 23 | ```php 24 | 'providers' => [ 25 | // ... 26 | HighSolutions\TranslationManager\ManagerServiceProvider::class, 27 | ]; 28 | ``` 29 | 30 | Next, publish all package resources: 31 | 32 | ```bash 33 | php artisan vendor:publish --provider="HighSolutions\TranslationManager\ManagerServiceProvider" 34 | ``` 35 | 36 | This will add to your project: 37 | 38 | - migration - database table for storing translations 39 | - configuration - package configurations 40 | - views - configurable views for translation management 41 | - translations - translations for webinterface 42 | 43 | Remember to launch migration: 44 | 45 | ```bash 46 | php artisan migrate 47 | ``` 48 | 49 | Workflow 50 | ------------ 51 | 52 | This package doesn't replace the Translation system, only import/export PHP files to a database and make them editable in browser. 53 | Package contains helper for live editing content on website. 54 | 55 | The workflow would be: 56 | 57 | - Import translations: Read all translation files and save them in the database 58 | - Find all translations in php/twig sources 59 | - Optionally: Listen to missing translation with the custom Translator 60 | - Optionally: Mark not-translated records as not-translated (suffix command) 61 | - Translate all keys through the webinterface 62 | - Export: Write all translations back to the translation files. 63 | 64 | Usage 65 | ------ 66 | 67 | You can access package in `http://yourdomain.com/translations` in default configuration. You can change as you pleased. 68 | 69 | Configuration 70 | ------------- 71 | 72 | | Setting name | Description | Default value | 73 | |--------------------------|-------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| 74 | | route | Route declaration (prefix, namespace, middlewares etc.) | [,'prefix' => 'translations', 'namespace' => 'HighSolutions\TranslationManager', 'middleware' => [,'web', 'auth',],] | 75 | | delete_enabled | Enable deletion of translations | true | 76 | | exclude_groups | Exclude specific file groups (like validation, pagination, routes etc.) | [] | 77 | | exclude_langs | Exclude specific langs and directories (like vendor and en, etc.) | [] | 78 | | basic_lang | Basic language used by translator. | 'en' | 79 | | sort_keys | Export translations with keys output alphabetically. | false | 80 | | highlight_locale_marked | Highlight lines with locale marked as not translated. | false | 81 | | live_translation_enabled | Enable live translation of content. | false | 82 | | popup_placement | Position of live translation popup. | top | 83 | | permissions | Define whow and when can edit translations. | env('APP_ENV') == 'local' | 84 | 85 | 86 | Commands 87 | --------- 88 | 89 | ### Import command 90 | 91 | The import command will search through app/lang and load all strings in the database, so you can easily manage them. 92 | 93 | ```bash 94 | php artisan translations:import 95 | ``` 96 | 97 | Note: By default, only new strings are added. Translations already in the DB are kept the same. If you want to replace all values with the ones from the files, 98 | add the `--replace` (or `-R`) option: `php artisan translations:import --replace` 99 | 100 | ### Find translations in source 101 | 102 | The Find command/button will look search for all php/twig files in the app directory, to see if they contain translation functions, and will try to extract the group/item names. 103 | The found keys will be added to the database, so they can be easily translated. 104 | This can be done through the webinterface, or via an Artisan command. 105 | 106 | ```bash 107 | php artisan translations:find 108 | ``` 109 | 110 | ### Export command 111 | 112 | The export command will write the contents of the database back to resources/lang php files. 113 | This will overwrite existing translations and remove all comments, so make sure to backup your data before using. 114 | Supply the group name to define which groups you want to publish. 115 | If you want to export all groups, provide `*` as name of group. 116 | 117 | ```bash 118 | php artisan translations:export 119 | ``` 120 | 121 | For example, `php artisan translations:export reminders` when you have 2 locales (en/pl), will write to `resources/lang/en/reminders.php` and `resources/lang/pl/reminders.php` 122 | 123 | ### Clean command 124 | 125 | The clean command will search for all translation that are NULL and delete them, so your interface is a bit cleaner. Note: empty translations are never exported. 126 | 127 | ```bash 128 | php artisan translations:clean 129 | ``` 130 | 131 | ### Reset command 132 | 133 | The reset command simply clears all translation in the database, so you can start fresh (by a new import). Make sure to export your work if needed before doing this. 134 | 135 | ```bash 136 | php artisan translations:reset 137 | ``` 138 | 139 | ### Clone command 140 | 141 | The clone command copy directory of basic language (langFrom parameter) and saves as new language (langTo parameter). After this operation you will need to launch import command. 142 | 143 | ```bash 144 | php artisan translations:clone langFrom langTo 145 | ``` 146 | 147 | ### Suffix command 148 | 149 | The suffix command analyzes all translations from new locale (langNew parameter) and if the value is the same as in original language (langOriginal parameter) then adds suffix to the end of value of new locale translations to mark that this translation needs to be translated. The suffix is locale code (e.g. EN) upper-cased. 150 | 151 | ```bash 152 | php artisan translations:sufix langOriginal langNew 153 | ``` 154 | 155 | ### Detect missing translations 156 | 157 | Most translations can be found by using the Find command (see above), but in case you have dynamic keys (variables/automatic forms etc), it can be helpful to 'listen' to the missing translations. 158 | To detect missing translations, we can swap the Laravel TranslationServicepProvider with a custom provider. 159 | In your config/app.php, comment out the original TranslationServiceProvider and add the one from this package: 160 | 161 | ```php 162 | //'Illuminate\Translation\TranslationServiceProvider', 163 | 'HighSolutions\TranslationManager\TranslationServiceProvider', 164 | ``` 165 | 166 | This will extend the Translator and will create a new database entry, whenever a key is not found, so you have to visit the pages that use them. 167 | This way it shows up in the webinterface and can be edited and later exported. 168 | You shouldn't use this in production, just in production to translate your views, then just switch back. 169 | 170 | Live editing 171 | --------- 172 | 173 | When you have translations in database, you can use `transEditable` method instead of `trans` whenever it's suitable. To do this, you have to make few steps: 174 | 175 | Update `config/app.php` by adding an entry for the service provider (another one): 176 | 177 | ```php 178 | 'providers' => [ 179 | // ... 180 | HighSolutions\TranslationManager\TranslationServiceProvider::class, 181 | ]; 182 | ``` 183 | 184 | Add these two methods to `app\helpers.php` file. 185 | 186 | ```php 187 | if (!function_exists('transEditable')) { 188 | /** 189 | * Translate the given message and wraps it in .editable container to allow editing 190 | * 191 | * @param string $id 192 | * @param array $parameters 193 | * @param string $domain 194 | * @param string $locale 195 | * @return \Symfony\Component\Translation\TranslatorInterface|string 196 | */ 197 | function transEditable($id = null, $parameters = [], $domain = 'messages', $locale = null) { 198 | return app('translator')->transEditable($id, $parameters, $locale); 199 | } 200 | } 201 | 202 | if (!function_exists('isLiveTranslationEnabled')) { 203 | /** 204 | * Return true if live translation enabled 205 | * 206 | * @return bool 207 | */ 208 | function isLiveTranslationEnabled() { 209 | return Request::cookie('live-translation-enabled') || config('translation-manager.live_translation_enabled'); 210 | } 211 | } 212 | ``` 213 | 214 | In your layout view add this scripts and style (see Layout customization section): 215 | 216 | ```html 217 | 218 | 247 | // ... 248 | 249 | ``` 250 | 251 | If you want to change all links into non-links, add this little script: 252 | 253 | ``` 254 | 262 | ``` 263 | 264 | Last step is to add this JS file: 265 | 266 | ``` 267 | jQuery(document).ready(function($){ 268 | 269 | $.ajaxSetup({ 270 | beforeSend: function(xhr, settings) { 271 | settings.data += "&_token=" + $(':hidden[name="_token"]').val(); 272 | } 273 | }); 274 | 275 | $('.editable').editable().on('hidden', function(e, reason){ 276 | var locale = $(this).data('locale'); 277 | if(reason === 'save'){ 278 | $(this).removeClass('status-0').addClass('status-1'); 279 | } 280 | if(reason === 'save' || reason === 'nochange') { 281 | var $next = $(this).closest('tr').next().find('.editable.locale-'+locale); 282 | setTimeout(function() { 283 | $next.editable('show'); 284 | }, 300); 285 | } 286 | }); 287 | 288 | $(document).on('click', 'a', function(event) { 289 | if (event.target.localName !== 'a' && event.target.className.indexOf('editable-submit') !== -1) { 290 | event.preventDefault(); 291 | return false; 292 | } 293 | }); 294 | }); 295 | 296 | ``` 297 | 298 | And now you are able to use `transEditable` helper and when live editing is active (checked through `isLiveTranslationEnabled`), user is able to click on text, popup will show and text can be changed. Saving changes will cause saving to the database and exporting this text to translation file. If live editing is not active, user will see standard text. 299 | 300 | You can use this helper like this: 301 | 302 | ```php 303 |
{!! transEditable('auth.failed') !!}
304 | ``` 305 | 306 | Do not use this inside of non-clickable elements (title attribute, alt attributes etc.). To launch popup inside link, click on border, not text. 307 | 308 | Changelog 309 | --------- 310 | 311 | 1.4.0 312 | * Support Laravel 12.x 313 | 314 | 1.3.0 315 | * Support Laravel 11.x 316 | 317 | 1.2.0 318 | * Support Laravel 9.x and 10.x 319 | 320 | 1.1.0 321 | * Support Laravel 7.x and 8.x 322 | 323 | 1.0.0 324 | * Support Laravel 6.0 325 | 326 | 0.6.0 327 | * No STRICT_MODE needed anymore 328 | 329 | 0.5.1 330 | * Fix searching translations 331 | 332 | 0.5.0 333 | * Change the views path 334 | 335 | 0.4.7 336 | * remove closures in config file 337 | 338 | 0.4.5 339 | * Laravel 5.6 support 340 | 341 | 0.4.4 342 | * Fix translation title popup 343 | 344 | 0.4.3 345 | * New configurations (popup placement and basic language) 346 | * Update documentation about necessary JS scripts 347 | 348 | 0.4.0 349 | 350 | * New commands: clone and suffix 351 | * Improve export command 352 | 353 | 0.3.7 354 | 355 | * New configuration option to exclude langs 356 | 357 | 0.3.6 358 | 359 | * Support auto-discovery and Laravel 5.5 360 | 361 | 0.3.0 362 | 363 | * Support for subdirectories 364 | * Support for array translations 365 | * New design 366 | * Permission management 367 | * Translations for view 368 | * Live editing 369 | 370 | 0.2.0 371 | 372 | * Barryvdh version of package 373 | 374 | Roadmap 375 | ------- 376 | 377 | * Duplicate translations of one locale to another with locale suffix. 378 | * Detection of incorrect files. 379 | * Support vendor translations files. 380 | * Unit tests! 381 | 382 | Credits 383 | ------- 384 | 385 | This package was originally created by [Barry vd. Heuvel](https://github.com/barryvdh) and is available here: [laravel-feed](https://github.com/barryvdh/laravel-translation-manager). 386 | 387 | Currently is developed by [HighSolutions](http://highsolutions.pl), software house from Poland in love in Laravel. 388 | -------------------------------------------------------------------------------- /resources/lang/en/panel.php: -------------------------------------------------------------------------------- 1 | array( 5 | 'add' => 'Add new keys to file', 6 | 'append' => 'Add only new translations', 7 | 'back' => 'Go back to main site', 8 | 'clean' => 'Remove all empty translations', 9 | 'cleanOrReset' => 'Clean translations', 10 | 'delete' => 'Delete this translation', 11 | 'export' => 'Export all changes to files', 12 | 'find' => 'Find missing translations', 13 | 'import' => 'Import all files to database', 14 | 'publish' => 'Publish this file', 15 | 'replace' => 'Replace all translations', 16 | 'reset' => 'Clear translations database', 17 | ), 18 | 'group' => array( 19 | 'add-lines' => 'Write new keys for file, one in line', 20 | ), 21 | 'rows' => array( 22 | 'confirm-delete' => 'Are you sure to delete this translations?', 23 | 'edit' => 'Save changes', 24 | 'key' => 'Key', 25 | ), 26 | 'welcome' => array( 27 | 'availableFiles' => 'Available files', 28 | 'chooseGroup' => 'Choose group to edit files.', 29 | 'doImport' => 'There is no translation files imported to database. Please launch import operation.', 30 | 'title' => 'Welcome in Laravel Translation Manager', 31 | ), 32 | ); -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/highsolutions/laravel-translation-manager/7ec9fd953fa16dd27515d5691f7925a1c583952e/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/files_list.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation-manager::layout') 2 | 3 | @section('translate_section') 4 | @if($canManage) 5 | @include('translation-manager::index_manage') 6 | @endif 7 | @endsection -------------------------------------------------------------------------------- /resources/views/index_manage.blade.php: -------------------------------------------------------------------------------- 1 | @if(!isset($group)) 2 |
3 |
{{ trans('translation-manager::panel.welcome.title') }}
4 |
5 | @if(sizeof($groups) == 0) 6 |

{{ trans('translation-manager::panel.welcome.doImport') }}

7 | @else 8 |

{{ trans('translation-manager::panel.welcome.chooseGroup') }}

9 | @endif 10 |
11 |
12 | @else 13 |
14 |
{{ $group }}
15 |
16 | @yield('translate_section') 17 |
18 |
19 | @endif -------------------------------------------------------------------------------- /resources/views/key_row.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $isCatalog = true; 3 | foreach($locales as $locale) { 4 | if(isset($translation[$locale])) { 5 | $isCatalog = false; 6 | break; 7 | } 8 | } 9 | @endphp 10 | @if(is_array($translation) && $isCatalog) 11 | 12 | {{ str_repeat(" ", $indent * 4) . $key }} 13 | 14 | @foreach($translation as $key2 => $value2) 15 | @include('translation-manager::key_row', [ 16 | 'key' => $key2, 17 | 'translation' => $value2, 18 | 'indent' => $indent + 1, 19 | 'parent_key' => $parent_key . $key .'.' 20 | ]) 21 | @endforeach 22 | @else 23 | 24 | {{ str_repeat(" ", $indent * 4) . $key }} 25 | @foreach($locales as $locale) 26 | @php $t = isset($translation[$locale]) ? $translation[$locale] : null; @endphp 27 | 28 | {{ $t ? htmlentities($t->value, ENT_QUOTES, 'UTF-8', false) : '' }} 38 | 39 | @endforeach 40 | @if($deleteEnabled) 41 | 42 | 48 | 49 | @endif 50 | 51 | @endif -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation-manager::super_layout') 2 | 3 | @section('css') 4 | 48 | @endsection 49 | 50 | @section('content') 51 | 52 |
53 | 68 |
69 | @include('translation-manager::index_manage') 70 |
71 |
72 |
73 | 74 | @endsection -------------------------------------------------------------------------------- /resources/views/menu_actions.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(sizeof($groups) == 0) 3 | 4 | 5 | 6 | @else 7 | 16 | @endif 17 |
18 |
19 | {{ csrf_field() }} 20 | 21 |
22 |
23 | 32 |
33 | 37 |
-------------------------------------------------------------------------------- /resources/views/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation-manager::layout') 2 | 3 | @section('translate_section') 4 | @include('translation-manager::show_group') 5 | @endsection -------------------------------------------------------------------------------- /resources/views/show_group.blade.php: -------------------------------------------------------------------------------- 1 | @if($canManage) 2 |
3 | {{ csrf_field() }} 4 | 5 | 6 |
7 |
8 |
9 | @endif 10 |
11 | 12 | 13 | 14 | 15 | @foreach($locales as $locale) 16 | 17 | @endforeach 18 | @if($deleteEnabled) 19 | 20 | @endif 21 | 22 | 23 | 24 | @foreach($translations as $key => $translation) 25 | @include('translation-manager::key_row', [ 26 | 'key' => $key, 27 | 'translation' => $translation, 28 | 'indent' => 0, 29 | 'parent_key' => '' 30 | ]) 31 | @endforeach 32 | 33 |
{{ trans('translation-manager::panel.rows.key') }}{{ $locale }} 
34 |
35 | 36 |
37 |
38 | {{ csrf_field() }} 39 | 40 | 41 | 42 | 43 |
44 |
-------------------------------------------------------------------------------- /resources/views/super_layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Translation Manager 7 | 8 | 9 | 74 | @yield('css') 75 | 76 | 77 | 78 | @yield('content') 79 | 80 | 81 | 83 | 84 | 95 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /src/Console/CleanCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 31 | parent::__construct(); 32 | } 33 | 34 | /** 35 | * Execute the console command. 36 | * 37 | * @return void 38 | */ 39 | public function handle() 40 | { 41 | $this->manager->cleanTranslations(); 42 | $this->info("Done cleaning translations"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/CloneCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 31 | parent::__construct(); 32 | } 33 | 34 | /** 35 | * Execute the console command. 36 | * 37 | * @return void 38 | */ 39 | public function handle() 40 | { 41 | $this->manager->cloneTranslations($this->argument('from'), $this->argument('to')); 42 | $this->info("Done cloning translations"); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Console/ExportCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 32 | parent::__construct(); 33 | } 34 | 35 | /** 36 | * Execute the console command. 37 | * 38 | * @return void 39 | */ 40 | public function handle() 41 | { 42 | $group = $this->argument('group'); 43 | 44 | $this->manager->exportTranslations($group); 45 | 46 | $this->info("Done writing language files for " . (($group == '*') ? 'ALL groups' : $group . " group") ); 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/FindCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 32 | parent::__construct(); 33 | } 34 | 35 | /** 36 | * Execute the console command. 37 | * 38 | * @return void 39 | */ 40 | public function handle() 41 | { 42 | $counter = $this->manager->findTranslations(); 43 | $this->info('Done importing, processed '.$counter. ' items!'); 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Console/ImportCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 32 | parent::__construct(); 33 | } 34 | 35 | /** 36 | * Execute the console command. 37 | * 38 | * @return void 39 | */ 40 | public function handle() 41 | { 42 | $replace = $this->option('replace'); 43 | $counter = $this->manager->importTranslations($replace); 44 | $this->info('Done importing, processed '.$counter. ' items!'); 45 | 46 | } 47 | 48 | /** 49 | * Get the console command options. 50 | * 51 | * @return array 52 | */ 53 | protected function getOptions() 54 | { 55 | return array( 56 | array('replace', "R", InputOption::VALUE_NONE, 'Replace existing keys'), 57 | ); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/Console/ResetCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 30 | parent::__construct(); 31 | } 32 | 33 | /** 34 | * Execute the console command. 35 | * 36 | * @return void 37 | */ 38 | public function handle() 39 | { 40 | $this->manager->truncateTranslations(); 41 | $this->info("All translations are deleted"); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/SuffixCommand.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 31 | parent::__construct(); 32 | } 33 | 34 | /** 35 | * Execute the console command. 36 | * 37 | * @return void 38 | */ 39 | public function handle() 40 | { 41 | $count = $this->manager->suffixTranslations($this->argument('basic'), $this->argument('new')); 42 | $this->info("Done suffixing translations for {$count} records."); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | service = new Service; 18 | } 19 | 20 | public function getIndex() 21 | { 22 | return view('translation-manager::index', [ 23 | 'locales' => $this->service->loadLocales(), 24 | 'groups' => $this->service->getGroups(), 25 | 'highlighted' => $this->service->getHighlighted(), 26 | 'canManage' => $this->service->canManage(), 27 | 'group' => null, 28 | ]); 29 | } 30 | 31 | public function getView($group = null) 32 | { 33 | $group = $this->service->getGroup('translations/view/'); 34 | 35 | return view('translation-manager::show', [ 36 | 'translations' => $this->service->getTranslations($group), 37 | 'locales' => $this->service->loadLocales(), 38 | 'groups' => $this->service->getGroups(), 39 | 'highlighted' => $this->service->getHighlighted(), 40 | 'canManage' => $this->service->canManage(), 41 | 'group' => $group, 42 | 'currentLocale' => config('app.locale'), 43 | 'deleteEnabled' => $this->service->getConfig('delete_enabled'), 44 | ]); 45 | } 46 | 47 | public function postEditAndExport(Request $request) 48 | { 49 | $this->service->update($request->input('name'), $request->input('value')); 50 | 51 | return [ 52 | 'success' => true, 53 | ]; 54 | } 55 | 56 | public function postAdd(Request $request) 57 | { 58 | $group = $this->service->getGroup('translations/add/'); 59 | 60 | $this->service->add($group, $request->input('keys')); 61 | 62 | return redirect()->back(); 63 | } 64 | 65 | public function postEdit(Request $request) 66 | { 67 | $group = $this->service->getGroup('translations/edit/'); 68 | 69 | if(in_array($group, $this->service->getConfig('exclude_groups'))) 70 | return [ 71 | 'success' => false, 72 | 'msg' => 'File is excluded', 73 | ]; 74 | 75 | $this->service->edit($group, $request->input('name'), $request->input('value')); 76 | 77 | return [ 78 | 'success' => true, 79 | ]; 80 | } 81 | 82 | public function postDelete() 83 | { 84 | list($key, $group) = $this->service->getDeleteParams(func_get_args()); 85 | 86 | if(in_array($group, $this->service->getConfig('exclude_groups')) || 87 | !$this->service->getConfig('delete_enabled')) 88 | return [ 89 | 'success' => false, 90 | 'msg' => 'Removing key from this file is forbidden.', 91 | ]; 92 | 93 | $this->service->remove($group, $key); 94 | 95 | return [ 96 | 'success' => true, 97 | ]; 98 | } 99 | 100 | public function postImport(Request $request) 101 | { 102 | $this->service->import($request->input('replace')); 103 | 104 | return [ 105 | 'success' => true, 106 | ]; 107 | } 108 | 109 | public function postFind() 110 | { 111 | $this->service->find(); 112 | 113 | return [ 114 | 'success' => true, 115 | ]; 116 | } 117 | 118 | public function postClean(Request $request) 119 | { 120 | if($request->input('reset') == false) 121 | $this->service->cleanEmpty(); 122 | else 123 | $this->service->removeAll(); 124 | 125 | return [ 126 | 'success' => true, 127 | ]; 128 | } 129 | 130 | public function postPublish() 131 | { 132 | $group = $this->service->getGroup('translations/publish/'); 133 | 134 | $this->service->publish($group); 135 | 136 | return [ 137 | 'success' => true, 138 | ]; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | $this->files = $files; 27 | $this->config = app()['config']['translation-manager']; 28 | } 29 | 30 | public function missingKey($namespace, $group, $key) 31 | { 32 | $locales = in_array($namespace, ['*', '']) ? $this->loadLocales() : $namespace; 33 | 34 | if(!in_array($group, $this->config['exclude_groups'])) { 35 | foreach($locales as $locale) { 36 | Translation::firstOrCreate([ 37 | 'locale' => $locale, 38 | 'group' => $group, 39 | 'key' => $key, 40 | ]); 41 | } 42 | } 43 | } 44 | 45 | public function importTranslations($replace = false) 46 | { 47 | $counter = 0; 48 | foreach($this->files->directories($this->app['path.lang']) as $langPath) { 49 | $locale = basename($langPath); 50 | if(in_array($locale, $this->config['exclude_langs'])) 51 | continue; 52 | 53 | foreach($this->files->allfiles($langPath) as $file) { 54 | $info = pathinfo($file); 55 | $group = $info['filename']; 56 | 57 | if(in_array($group, $this->config['exclude_groups'])) 58 | continue; 59 | 60 | $subLangPath = str_replace($langPath . DIRECTORY_SEPARATOR, "", $info['dirname']); 61 | if ($subLangPath != $langPath) { 62 | $group = $subLangPath . "/" . $group; 63 | } 64 | $group = str_replace('\\', '/', $group); 65 | 66 | $translations = \Lang::getLoader()->load($locale, $group); 67 | if (!$translations || !is_array($translations)) 68 | continue; 69 | 70 | foreach(Arr::dot($translations) as $key => $value) { 71 | if(is_array($value)) // process only string values 72 | continue; 73 | 74 | $value = (string) $value; 75 | $translation = Translation::firstOrNew([ 76 | 'locale' => $locale, 77 | 'group' => $group, 78 | 'key' => $key, 79 | ]); 80 | 81 | // Check if the database is different then the files 82 | $newStatus = $translation->value === $value || !$translation->value ? Translation::STATUS_SAVED : Translation::STATUS_CHANGED; 83 | if($newStatus !== (int) $translation->status) 84 | $translation->status = $newStatus; 85 | 86 | // Only replace when empty, or explicitly told so 87 | if($replace || !$translation->value) 88 | $translation->value = $value; 89 | 90 | $translation->save(); 91 | 92 | $counter++; 93 | } 94 | } 95 | } 96 | return $counter; 97 | } 98 | 99 | public function findTranslations($path = null) 100 | { 101 | $path = $path ?: base_path(); 102 | $keys = array(); 103 | $functions = array('trans', 'trans_choice', 'Lang::get', 'Lang::choice', 'Lang::trans', 'Lang::transChoice', '@lang', '@choice', 'transEditable'); 104 | $pattern = // See http://regexr.com/392hu 105 | "[^\w|>]". // Must not have an alphanum or _ or > before real method 106 | "(".implode('|', $functions) .")". // Must start with one of the functions 107 | "\(". // Match opening parenthese 108 | "[\'\"]". // Match " or ' 109 | "(". // Start a new group to match: 110 | "[a-zA-Z0-9_-]+". // Must start with group 111 | "([.][^\1)]+)+". // Be followed by one or more items/keys 112 | ")". // Close group 113 | "[\'\"]". // Closing quote 114 | "[\),]"; // Close parentheses or new parameter 115 | 116 | // Find all PHP + Twig files in the app folder, except for storage 117 | $finder = new Finder(); 118 | $finder->in($path)->exclude('storage')->name('*.php')->name('*.twig')->files(); 119 | 120 | /** @var \Symfony\Component\Finder\SplFileInfo $file */ 121 | foreach ($finder as $file) { 122 | // Search the current file for the pattern 123 | if(preg_match_all("/$pattern/siU", $file->getContents(), $matches)) { 124 | // Get all matches 125 | foreach ($matches[2] as $key) { 126 | $keys[] = $key; 127 | } 128 | } 129 | } 130 | // Remove duplicates 131 | $keys = array_unique($keys); 132 | 133 | // Add the translations to the database, if not existing. 134 | foreach($keys as $key) { 135 | // Split the group and item 136 | list($group, $item) = explode('.', $key, 2); 137 | $this->missingKey('', $group, $item); 138 | } 139 | 140 | // Return the number of found translations 141 | return count($keys); 142 | } 143 | 144 | public function exportTranslations($group) 145 | { 146 | if(in_array($group, $this->config['exclude_groups'])) 147 | return; 148 | 149 | if ($group == '*') { 150 | return $this->exportAllTranslations(); 151 | } 152 | 153 | $tree = $this->makeTree(Translation::where('group', $group)->whereNotNull('value')->get()); 154 | 155 | foreach($tree as $locale => $groups) { 156 | if(!isset($groups[$group])) 157 | continue; 158 | 159 | $translations = $groups[$group]; 160 | ksort($translations); 161 | 162 | $path = $this->app->langPath() . '/' . $locale . '/' . $group . '.php'; 163 | $output = "files->put($path, preg_replace("/ \R/", "\n", $output)); // get rid of trailing spaces before line breaks and store in file 166 | } 167 | 168 | Translation::where('group', $group) 169 | ->whereNotNull('value') 170 | ->where('status', '=', Translation::STATUS_CHANGED) 171 | ->update(array('status' => Translation::STATUS_SAVED)); 172 | } 173 | 174 | public function exportAllTranslations() 175 | { 176 | $groups = Translation::whereNotNull('value')->selectDistinctGroup()->get('group'); 177 | 178 | foreach($groups as $group){ 179 | $this->exportTranslations($group->group); 180 | } 181 | } 182 | 183 | public function cleanTranslations() 184 | { 185 | Translation::whereNull('value')->delete(); 186 | } 187 | 188 | public function truncateTranslations() 189 | { 190 | Translation::truncate(); 191 | } 192 | 193 | protected function makeTree($translations) 194 | { 195 | $array = array(); 196 | foreach($translations as $translation) { 197 | Arr::set( 198 | $array[$translation->locale][$translation->group], 199 | $translation->key, 200 | $translation->value 201 | ); 202 | } 203 | return $array; 204 | } 205 | 206 | public function getConfig($key = null) 207 | { 208 | if($key == null) 209 | return $this->config; 210 | 211 | return $this->config[$key]; 212 | } 213 | 214 | public function loadLocales() 215 | { 216 | $locales = Translation::groupBy('locale') 217 | ->select('locale') 218 | ->get() 219 | ->pluck('locale') 220 | ->all(); 221 | 222 | $locales = array_merge([config('app.locale')], $locales); 223 | return array_unique($locales); 224 | } 225 | 226 | public function cloneTranslations($from, $to) 227 | { 228 | $fromDir = $this->app['path.lang'] . DIRECTORY_SEPARATOR . $from; 229 | $toDir = $this->app['path.lang'] . DIRECTORY_SEPARATOR . $to; 230 | 231 | if(File::isDirectory($fromDir)) 232 | File::copyDirectory($fromDir, $toDir); 233 | } 234 | 235 | public function suffixTranslations($original, $locale) 236 | { 237 | $langOriginal = Translation::where('locale', $original) 238 | ->whereNotNull('value') 239 | ->where('status', '=', Translation::STATUS_SAVED) 240 | ->get(); 241 | 242 | $langLocale = Translation::where('locale', $locale) 243 | ->whereNotNull('value') 244 | ->where('status', '=', Translation::STATUS_SAVED) 245 | ->get(); 246 | 247 | $suffix = strtoupper($locale); 248 | 249 | return $langLocale->filter(function ($lang) use ($langOriginal) { 250 | $toCompare = $langOriginal->where('group', $lang->group) 251 | ->where('key', $lang->key) 252 | ->first(); 253 | 254 | return $toCompare != null && $toCompare->value == $lang->value; 255 | })->each(function ($lang) use ($suffix) { 256 | if(substr($lang->value, -2) == $suffix) 257 | return; 258 | 259 | $lang->update([ 260 | 'value' => $lang->value . ' '. $suffix, 261 | ]); 262 | })->count(); 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/ManagerServiceProvider.php: -------------------------------------------------------------------------------- 1 | _basicRegister(); 31 | 32 | $this->_commandsRegister(); 33 | 34 | $this->_managerRegister(); 35 | } 36 | 37 | private function _basicRegister() 38 | { 39 | $configPath = __DIR__ . '/../config/translation-manager.php'; 40 | $this->mergeConfigFrom($configPath, 'translation-manager'); 41 | $this->publishes([ 42 | $configPath => config_path('translation-manager.php') 43 | ], 'config'); 44 | } 45 | 46 | private function _commandsRegister() 47 | { 48 | foreach($this->commandsList() as $name => $class) { 49 | $this->initCommand($name, $class); 50 | } 51 | } 52 | 53 | protected function commandsList() 54 | { 55 | return [ 56 | 'reset' => ResetCommand::class, 57 | 'import' => ImportCommand::class, 58 | 'find' => FindCommand::class, 59 | 'export' => ExportCommand::class, 60 | 'clean' => CleanCommand::class, 61 | 'clone' => CloneCommand::class, 62 | 'suffix' => SuffixCommand::class, 63 | ]; 64 | } 65 | 66 | private function initCommand($name, $class) 67 | { 68 | $this->app->singleton("command.translation-manager.{$name}", function($app) use ($class) { 69 | return new $class($app['translation-manager']); 70 | }); 71 | 72 | $this->commands("command.translation-manager.{$name}"); 73 | } 74 | 75 | private function _managerRegister() 76 | { 77 | $this->app->singleton('translation-manager', function($app) { 78 | return $app->make(Manager::class); 79 | }); 80 | } 81 | 82 | /** 83 | * Bootstrap the application events. 84 | * 85 | * @param \Illuminate\Routing\Router $router 86 | * @return void 87 | */ 88 | public function boot(Router $router) 89 | { 90 | $this->loadViews(); 91 | $this->loadMigrations(); 92 | $this->loadTranslations(); 93 | $this->loadRoutes($router); 94 | } 95 | 96 | protected function loadViews() 97 | { 98 | $viewPath = __DIR__.'/../resources/views'; 99 | $this->loadViewsFrom($viewPath, 'translation-manager'); 100 | $this->publishes([ 101 | $viewPath => resource_path('views/vendor/translation-manager'), 102 | ], 'views'); 103 | } 104 | 105 | protected function loadMigrations() 106 | { 107 | $migrationPath = __DIR__.'/../database/migrations'; 108 | $this->publishes([ 109 | $migrationPath => base_path('database/migrations'), 110 | ], 'migrations'); 111 | } 112 | 113 | protected function loadTranslations() 114 | { 115 | $translationPath = __DIR__.'/../resources/lang'; 116 | $this->loadTranslationsFrom($translationPath, 'translation-manager'); 117 | 118 | $this->publishes([ 119 | $translationPath => resource_path('lang/vendor/translation-manager'), 120 | ], 'translations'); 121 | } 122 | 123 | public function loadRoutes($router) { 124 | $config = $this->routeConfig(); 125 | 126 | $router->group($config, function($router) { 127 | $router->get('/', 'Controller@getIndex')->name('translation-manager.index'); 128 | $router->get('/view/{group?}/{group2?}/{group3?}/{group4?}/{group5?}', 'Controller@getView')->name('translation-manager.view'); 129 | $router->post('/add/{group}/{group2?}/{group3?}/{group4?}/{group5?}', 'Controller@postAdd')->name('translation-manager.add'); 130 | $router->post('/edit/{group}/{group2?}/{group3?}/{group4?}/{group5?}', 'Controller@postEdit')->name('translation-manager.edit'); 131 | $router->post('/delete/{key}/{group}/{group2?}/{group3?}/{group4?}/{group5?}', 'Controller@postDelete')->name('translation-manager.delete'); 132 | $router->post('/publish/{group}/{group2?}/{group3?}/{group4?}/{group5?}', 'Controller@postPublish')->name('translation-manager.publish'); 133 | $router->post('/import', 'Controller@postImport')->name('translation-manager.import'); 134 | $router->post('/clean', 'Controller@postClean')->name('translation-manager.clean'); 135 | $router->post('/find', 'Controller@postFind')->name('translation-manager.find'); 136 | 137 | $router->post('custom-update', 'Controller@postEditAndExport')->name('translation-manager.update'); 138 | }); 139 | } 140 | 141 | private function routeConfig() { 142 | return $this->app['config']->get('translation-manager.route', []); 143 | } 144 | 145 | /** 146 | * Get the services provided by the provider. 147 | * 148 | * @return array 149 | */ 150 | public function provides() 151 | { 152 | return [ 153 | 'translation-manager', 154 | 'command.translation-manager.reset', 155 | 'command.translation-manager.import', 156 | 'command.translation-manager.find', 157 | 'command.translation-manager.export', 158 | 'command.translation-manager.clean', 159 | 'command.translation-manager.clone', 160 | 'command.translation-manager.suffix', 161 | ]; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/Models/Translation.php: -------------------------------------------------------------------------------- 1 | where('group', $group)->whereNotNull('value'); 31 | } 32 | 33 | public function scopeOrderByGroupKeys($query, $ordered) 34 | { 35 | if ($ordered) { 36 | $query->orderBy('group')->orderBy('key'); 37 | } 38 | 39 | return $query; 40 | } 41 | 42 | public function scopeSelectDistinctGroup($query) 43 | { 44 | $select = ''; 45 | 46 | switch (DB::getDriverName()) { 47 | case 'mysql': 48 | $select = 'DISTINCT `group`'; 49 | break; 50 | default: 51 | $select = 'DISTINCT "group"'; 52 | break; 53 | } 54 | 55 | return $query->select(DB::raw($select)); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | manager = app()->make(Manager::class); 16 | } 17 | 18 | public function getGroup($route) 19 | { 20 | $groups = \Request::path(); 21 | return str_replace($route, '', $groups); 22 | } 23 | 24 | public function loadLocales() 25 | { 26 | return $this->manager->loadLocales(); 27 | } 28 | 29 | public function getGroups() 30 | { 31 | $groups = Translation::select('group'); 32 | $excludedGroups = $this->manager->getConfig('exclude_groups'); 33 | if($excludedGroups) { 34 | $groups->whereNotIn('group', $excludedGroups); 35 | } 36 | 37 | $groups = $groups->pluck('group', 'group')->all(); 38 | return $this->treeGroups($groups); 39 | } 40 | 41 | protected function treeGroups($groups, $delimiter = '/') 42 | { 43 | $list = []; 44 | 45 | foreach($groups as $group) { 46 | $this->stringToArray($list, $group, $delimiter); 47 | } 48 | 49 | return $list; 50 | } 51 | 52 | protected function stringToArray(&$arr, $path, $separator = '.') 53 | { 54 | $keys = explode($separator, $path); 55 | 56 | foreach ($keys as $key) { 57 | if(isset($arr[$key]) && !is_array($arr[$key])) // remove key if it's a string and the same key has array 58 | unset($arr[$key]); 59 | 60 | $arr = &$arr[$key]; 61 | } 62 | 63 | $arr = $path; 64 | } 65 | 66 | public function getHighlighted() 67 | { 68 | $locale = config('app.locale'); 69 | 70 | $notPublished = Translation::where('status', Translation::STATUS_CHANGED) 71 | ->select('group') 72 | ->get() 73 | ->pluck('group') 74 | ->all(); 75 | 76 | $empty = Translation::whereNull('value') 77 | ->select('group') 78 | ->get() 79 | ->pluck('group') 80 | ->all(); 81 | 82 | $notTranslated = []; 83 | if(config('translation-manager.highlight_locale_marked')) 84 | $notTranslated = Translation::where('value', 'like binary', '%'. strtoupper($locale)) 85 | ->where('locale', $locale) 86 | ->select('group') 87 | ->get() 88 | ->pluck('group') 89 | ->all(); 90 | 91 | return array_merge($notPublished, $empty, $notTranslated); 92 | } 93 | 94 | public function getTranslations($group) 95 | { 96 | $allTranslations = Translation::where('group', $group)->orderBy('key', 'asc')->get(); 97 | $translations = []; 98 | foreach($allTranslations as $translation) { 99 | $translations[$translation->key][$translation->locale] = $translation; 100 | } 101 | 102 | return $this->treeKeys($translations); 103 | } 104 | 105 | protected function treeKeys($keys, $delimiter = '.') { 106 | $list = []; 107 | $array = []; 108 | foreach($keys as $key => $value) 109 | $array[$key] = $key; 110 | 111 | foreach($array as $key) { 112 | $this->stringToArray($array, $key, $delimiter); 113 | } 114 | 115 | foreach($array as $key => $value) { 116 | if(strpos($key, '.') > -1) 117 | continue; 118 | $list[$key] = $value; 119 | } 120 | unset($array); 121 | 122 | array_walk_recursive($list, function(&$value, $key, $original) { 123 | $value = $original[$value]; 124 | }, $keys); 125 | 126 | ksort($list); 127 | 128 | return $list; 129 | } 130 | 131 | public function getConfig($key) 132 | { 133 | return $this->manager->getConfig($key); 134 | } 135 | 136 | public function update($name, $value) 137 | { 138 | list($locale, $key) = explode('|', $name, 2); 139 | $segments = explode('.', $key); 140 | $group = $segments[0]; 141 | unset($segments[0]); 142 | $key = implode('.', $segments); 143 | 144 | if(in_array($group, $this->manager->getConfig('exclude_groups'))) 145 | return; 146 | 147 | // update translation 148 | $translation = Translation::firstOrNew([ 149 | 'locale' => $locale, 150 | 'group' => $group, 151 | 'key' => $key, 152 | ]); 153 | $translation->value = (string) $value ?: null; 154 | $translation->status = Translation::STATUS_CHANGED; 155 | $translation->save(); 156 | 157 | // export translations automatically 158 | $this->manager->exportTranslations($group); 159 | } 160 | 161 | public function add($group, $keys) 162 | { 163 | $keys = explode("\n", $keys); 164 | 165 | foreach($keys as $key) { 166 | $key = trim($key); 167 | if($group && $key) { 168 | $this->manager->missingKey('*', $group, $key); 169 | } 170 | } 171 | } 172 | 173 | public function edit($group, $name, $value) 174 | { 175 | list($locale, $key) = explode('|', $name, 2); 176 | $translation = Translation::firstOrNew([ 177 | 'locale' => $locale, 178 | 'group' => $group, 179 | 'key' => $key, 180 | ]); 181 | $translation->value = (string) $value ?: null; 182 | $translation->status = Translation::STATUS_CHANGED; 183 | $translation->save(); 184 | } 185 | 186 | public function getDeleteParams($groups) 187 | { 188 | return [ 189 | array_shift($groups), 190 | implode('/', $groups) 191 | ]; 192 | } 193 | 194 | public function remove($group, $key) 195 | { 196 | Translation::where('group', $group)->where('key', $key)->delete(); 197 | } 198 | 199 | public function import($replace) 200 | { 201 | return $this->manager->importTranslations($replace); 202 | } 203 | 204 | public function find() 205 | { 206 | return $this->manager->findTranslations(); 207 | } 208 | 209 | public function cleanEmpty() 210 | { 211 | return $this->manager->cleanTranslations(); 212 | } 213 | 214 | public function removeAll() 215 | { 216 | return $this->manager->truncateTranslations(); 217 | } 218 | 219 | public function publish($group) 220 | { 221 | return $this->manager->exportTranslations($group); 222 | } 223 | 224 | public function canManage() 225 | { 226 | $func = $this->manager->getConfig('permissions'); 227 | if(is_callable($func)) 228 | return $func(); 229 | 230 | return true; 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/TranslationServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerLoader(); 18 | 19 | $this->app->singleton('translator', function ($app) { 20 | $loader = $app['translation.loader']; 21 | 22 | $locale = $app['config']['app.locale']; 23 | 24 | $trans = new Translator($loader, $locale); 25 | 26 | $trans->setFallback($app['config']['app.fallback_locale']); 27 | 28 | return $trans; 29 | }); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Translator.php: -------------------------------------------------------------------------------- 1 | notifyMissingKey($key); 30 | 31 | // Reget with fallback 32 | $result = parent::get($key, $replace, $locale, $fallback); 33 | } 34 | 35 | return $result; 36 | } 37 | 38 | public function setTranslationManager(Manager $manager) 39 | { 40 | $this->manager = $manager; 41 | } 42 | 43 | protected function notifyMissingKey($key) 44 | { 45 | list($namespace, $group, $item) = $this->parseKey($key); 46 | if($this->manager && $namespace === '*' && $group && $item ){ 47 | $this->manager->missingKey($namespace, $group, $item); 48 | } 49 | } 50 | 51 | /** 52 | * Get the translation for the given key and wrap it in a span element 53 | * (it allowins translators to edit translations in-place) 54 | * 55 | * @param string $key 56 | * @param array $replace 57 | * @param string|null $locale 58 | * @param bool $fallback 59 | * 60 | * @return string|array|null 61 | * 62 | * @note do not escape translations in views! 63 | */ 64 | public function transEditable($key, array $replace = [], $locale = null, $fallback = true) 65 | { 66 | $translation = parent::get($key, $replace, $locale, $fallback); 67 | if (isLiveTranslationEnabled() == false) { 68 | // user is not logged or he hasn't enabled inline translations or we are on translations page 69 | return $translation; 70 | } 71 | 72 | // currently logged user has enabled inline translations - return translation wrapped in span element 73 | 74 | if (!$locale) { 75 | $locale = \App::getLocale(); 76 | } 77 | $basicLocale = config('translation-manager.basic_lang', 'en'); 78 | $basicTranslation = parent::get($key, $replace, $basicLocale, $fallback); 79 | 80 | /** 81 | * we need escaped versions in order to show them as title and value; 82 | * real translation is displayed unescaped 83 | */ 84 | $escapedBasicTranslation = htmlspecialchars($basicTranslation, ENT_QUOTES, 'UTF-8', false); 85 | $escapedCurrentTranslation = htmlspecialchars($translation, ENT_QUOTES, 'UTF-8', false); 86 | 87 | return ' ' . $locale . ')\'>' . 96 | $translation . ''; 97 | } 98 | 99 | } 100 | --------------------------------------------------------------------------------