diff --git a/.travis.yml b/.travis.yml index aa14ee5..28e2914 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,17 @@ language: php php: - - 5.3 - - 5.4 - - 5.5 - 5.6 - - hhvm + - 7.0 + +matrix: + include: + - php: 5.6 + env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' before_script: - composer self-update - composer install --prefer-source --no-interaction --dev + - composer dump-autoload script: phpunit diff --git a/LICENSE b/LICENSE index 1cfd3c4..02b6daf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2014 UFirst Group +Copyright (c) 2017 HighSolutions Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index df0b50f..594a861 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -laravel-lang-import-export +![Laravel-Lang-Import-Export by HighSolutions](https://raw.githubusercontent.com/highsolutions/laravel-lang-import-export/master/intro.jpg) + +Laravel-Lang-Import-Export ========================== This package provides artisan commands to import and export language files from and to CSV. This can be used to send translations to agencies that normally work with Excel-like files. @@ -43,20 +45,19 @@ Add the following line to the `require` section of your Laravel webapp's `compos ```javascript "require": { - "ufirst/lang-import-export": "dev-master" + "HighSolutions/laravel-lang-import-export": "^6.0" } ``` - Run `composer update` to install the package. - -Finally add the following line to the `providers` array of your `app/config/app.php` file: +This package uses Laravel 5.5 Package Auto-Discovery. +For previous versions of Laravel, you need to update `config/app.php` by adding an entry for the service provider: ```php 'providers' => array( /* ... */ - 'UFirst\LangImportExport\LangImportExportServiceProvider' + 'HighSolutions\LangImportExport\LangImportExportServiceProvider' ) ``` @@ -68,19 +69,99 @@ The package currently provides two commands, one for exporting the files and one ### Export ```bash -php artisan lang-export:csv en_US navigation -php artisan lang-export:csv --output /some/file en_US navigation -php artisan lang-export:csv --delimiter=";" --enclosure='"' --output=/some/file en_US navigation +php artisan lang:export +php artisan lang:export en * path/to/export +php artisan lang:export en auth -A -X ``` -You have to pass the __locale__ and the __group__ as arguments. The group is the name of the langauge file without its extension. You may define options for your desired CSV format. +When you call command without parameters, export file will be generated for all localization files within default locale. But you can define **locale** explicitly. You can also export only one file (second parameter - **group**) and define where to store file (you can provide name with and without .csv extension). When you use **output** argument, default path is base_path() -> catalog of your whole project. +But there is few more useful parameters: -### Import +| name of parameter | description | is required? | default value | +|-------------------|-----------------------------------------|--------------|------------------------------------| +| locale | The locale to be exported | NO | default lang of application | +| group | The name of translation file to export | NO | \* - all files | +| output | Filename of exported translation files | NO | storage/app/lang-import-export.csv | +| -A / --append | Append name of group to the name of file | NO | empty | +| -X / --excel | Set file encoding (UTF-16) for Excel | NO | UTF-8 | +| -D / --delimiter | Field delimiter | NO | , | +| -E / --enclosure | Field enclosure | NO | " | +### Import ``` -php artisan lang-import:csv en_US navigation /some/file -php artisan lang-import:csv --delimiter=";" --enclosure='"' --escape='\\' en_US navigation /some/file +php artisan lang:import +php artisan lang:import en * path/to/import +php artisan lang:import en auth -X ``` -You have to pass the __locale__, the __group__ and the __path to the CSV file__ as arguments. The group is the name of the langauge file without its extension. You may define options to match the CSV format of your input file. +When you call command without parameters - it will try to read default file of export command without parameters for default locale and all localization files. You can of course specify all parameters (**locale**, **group**, **input**) and there is few more options: + +| name of parameter | description | is required? | default value | +|-------------------|----------------------------------------------|--------------|------------------------------------| +| locale | The locale to be imported | NO | default lang of application | +| group | The name of translation file to import | NO | * - all files | +| output | Filename of translation files to be imported | NO | storage/app/lang-import-export.csv | +| -X / --excel | Set file encoding from Excel | NO | UTF-8 | +| -D / --delimiter | Field delimiter | NO | , | +| -E / --enclosure | Field enclosure | NO | " | +| -C / --escape | Field escape | NO | \ | + +Changelog +------------ + +6.2.0 +* Support Laravel 9.x and 10.x + +6.1.0 +* Support Laravel 7.x and 8.x + +6.0.0 +* Support Laravel 6.0 + +5.4.10 +* Laravel 5.7 support + +5.4.9 +* Create new directory, when not exists before + +5.4.8 +* Fix UTF-8 encoding + +5.4.7 +* Handling empty keys + +5.4.6 +* Laravel 5.6 support + +5.4.3 +- support Package Auto-Discovery + +5.4.2 +- resolve problems with PSR-4 autoloading + +5.4.1 +- improved import command +- improved Excel support +- support of [LaravelLocalization](https://github.com/mcamara/laravel-localization) routes files + +5.4.0 +- refactor whole repository +- add support for Excel +- add support for export and import all localization files +- any arguments are not required + +Roadmap +------------ + +* Removing tabs from text +* Option for deleting export file after importing. +* Option for excluding certain files (and system ones). +* Unit tests! + +Credits +------------ + +This package was originally created by [UFirst](http://github.com/ufirstgroup) and is available here: [Laravel-lang-import-export](https://github.com/ufirstgroup/laravel-lang-import-export). + +Currently is developed by [HighSolutions](https://highsolutions.org), software house from Poland in love in Laravel. diff --git a/composer.json b/composer.json index e35b40f..3e8abf3 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,54 @@ { - "name": "ufirst/lang-import-export", + "name": "highsolutions/laravel-lang-import-export", "description": "A Laravel package providing artisan commands to import and export language files from and to CSV.", - "keywords": ["laravel", "localization", "translation", "messages", "import", "export", "CSV"], + "keywords": [ + "laravel", + "localization", + "translation", + "messages", + "import", + "export", + "CSV" + ], "authors": [ { "name": "Michael Ruoss", - "email": "michael.ruoss@ufirstgroup.com" + "email": "michael.ruoss@UFirstgroup.com" + }, + { + "name": "HighSolutions", + "email": "adam@highsolutions.pl" } ], "license": "MIT", "require": { - "php": ">=5.3.0", - "illuminate/support": "~5.1" + "php": ">=5.6.4", + "illuminate/support": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0" }, "autoload": { - "classmap": [ - "src/migrations" - ], - "psr-0": { - "UFirst\\LangImportExport\\": "src/" - } + "psr-4": { + "HighSolutions\\LangImportExport\\": "src/" + }, + "files": [ + "src/Support/helpers.php" + ] }, "extra": { "component": "package", - "frameworks": ["Laravel 5"] + "frameworks": [ + "Laravel 5.7", + "Laravel 5.8", + "Laravel 6.x", + "Laravel 7.x", + "Laravel 8.x", + "Laravel 9.x", + "Laravel 10.x" + ], + "laravel": { + "providers": [ + "HighSolutions\\LangImportExport\\LangImportExportServiceProvider" + ] + } }, "minimum-stability": "stable" } diff --git a/intro.jpg b/intro.jpg new file mode 100644 index 0000000..da06549 Binary files /dev/null and b/intro.jpg differ diff --git a/public/.gitkeep b/public/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Console/ExportToCsvCommand.php b/src/Console/ExportToCsvCommand.php new file mode 100644 index 0000000..c6f337c --- /dev/null +++ b/src/Console/ExportToCsvCommand.php @@ -0,0 +1,244 @@ +defaultPath = storage_path('app'. DIRECTORY_SEPARATOR .'lang-import-export') . $this->ext; + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $this->getParameters(); + + $this->sayItsBeginning(); + + $translations = $this->getTranslations(); + + $this->saveTranslations($translations); + + $this->sayItsFinish(); + } + + /** + * Fetch command parameters (arguments and options) and analyze them. + * + * @return void + */ + private function getParameters() + { + $this->parameters = [ + 'group' => $this->argument('group'), + 'locale' => $this->argument('locale') === null ? config('app.locale') : $this->argument('locale'), + 'output' => $this->argument('output') === null ? $this->defaultPath : base_path($this->argument('output')), + 'append' => $this->option('append') !== false, + 'excel' => $this->option('excel') !== false, + 'delimiter' => $this->option('delimiter'), + 'enclosure' => $this->option('enclosure'), + ]; + + $this->setDefaultPath(); + } + + /** + * Set possible file names. + * + * @return void + */ + private function setDefaultPath() + { + if($this->parameters['append']) { + $this->parameters['output'] .= '-'. $this->parameters['group']; + $this->defaultPath .= '-'. $this->parameters['group']; + } + } + + /** + * Display output that command has started and which groups are being exported. + * + * @return void + */ + private function sayItsBeginning() + { + $this->info(PHP_EOL + . 'Translations export of '. ($this->parameters['group'] === null ? 'all groups' : $this->parameters['group'] .' group') .' - started.'); + } + + /** + * Get translations from localization files. + * + * @return array + */ + private function getTranslations() + { + return LangListService::loadLangList($this->parameters['locale'], $this->parameters['group']); + } + + /** + * Save fetched translations to file. + * + * @return void + */ + private function saveTranslations($translations) + { + $output = $this->openFile(); + + $this->saveTranslationsToFile($output, $translations); + + $this->closeFile($output); + } + + /** + * Open specified file (if not possible, open default one). + * + * @return FilePointerResource + */ + private function openFile() + { + if(substr($this->parameters['output'], -4) != $this->ext) + $this->parameters['output'] .= $this->ext; + + if (!($output = fopen($this->parameters['output'], 'w'))) { + $output = fopen($this->defaultPath . $this->ext, 'w'); + } + + fputs($output, "\xEF\xBB\xBF"); + + return $output; + } + + /** + * Save content of translation files to specified file. + * + * @param FilePointerResource $output + * @param array $translations + * @return void + */ + private function saveTranslationsToFile($output, $translations) + { + foreach ($translations as $group => $files) { + foreach($files as $key => $value) { + if(is_array($value)) { + continue; + } + $this->writeFile($output, $group, $key, $value); + } + } + } + + /** + * Put content of file to specified file with CSV parameters. + * + * @param FilePointerResource $output + * @param string $group + * @param string $key + * @param string $value + * @return void + * + */ + private function writeFile() + { + $data = func_get_args(); + $output = array_shift($data); + fputcsv($output, $data, $this->parameters['delimiter'], $this->parameters['enclosure']); + } + + /** + * Close output file and check if adjust file to Excel format. + * + * @param FilePointerResource $output + * @return void + */ + private function closeFile($output) + { + fclose($output); + + if($this->parameters['excel']) + $this->adjustToExcel(); + } + + /** + * Adjust file to Excel format. + * + * @return void + * + */ + private function adjustToExcel() + { + $data = file_get_contents($this->parameters['output']); + file_put_contents($this->parameters['output'], chr(255) . chr(254) . mb_convert_encoding($data, 'UTF-16LE', 'UTF-8')); + } + + /** + * Display output that command is finished and where to find file. + * + * @return void + */ + private function sayItsFinish() + { + $this->info('Finished! Translations saved to: '. (substr($this->parameters['output'], strlen(base_path()) + 1)) + . PHP_EOL); + } + +} diff --git a/src/Console/ImportFromCsvCommand.php b/src/Console/ImportFromCsvCommand.php new file mode 100644 index 0000000..a4e341b --- /dev/null +++ b/src/Console/ImportFromCsvCommand.php @@ -0,0 +1,214 @@ +defaultPath = storage_path('app'. DIRECTORY_SEPARATOR .'lang-import-export') . $this->ext; + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $this->getParameters(); + + $this->sayItsBeginning(); + + $translations = $this->getTranslations(); + + $this->saveTranslations($translations); + + $this->sayItsFinish(); + } + + /** + * Fetch command parameters (arguments and options) and analyze them. + * + * @return void + */ + private function getParameters() + { + $this->parameters = [ + 'locale' => $this->argument('locale') === null ? config('app.locale') : $this->argument('locale'), + 'group' => $this->argument('group'), + 'input' => $this->argument('input') === null ? $this->defaultPath : base_path($this->argument('input')), + 'delimiter' => $this->option('delimiter'), + 'enclosure' => $this->option('enclosure'), + 'escape' => $this->option('escape'), + 'excel' => $this->option('excel') !== false, + ]; + + if(substr($this->parameters['input'], -4) != $this->ext) + $this->parameters['input'] .= $this->ext; + } + + /** + * Display output that command has started and which groups are being imported. + * + * @return void + */ + private function sayItsBeginning() + { + $this->info(PHP_EOL + . 'Translations import of '. ($this->parameters['group'] === false ? 'all groups' : $this->parameters['group'] .' group') .' has started.'); + } + + /** + * Get translations from CSV file. + * + * @return array + */ + private function getTranslations() + { + $input = $this->openFile(); + + $translations = $this->readFile($input); + + $this->closeFile($input); + + return $translations; + } + + /** + * Opens file to read content. + * + * @return FileInputPointer + */ + private function openFile() + { + if (($input = fopen($this->parameters['input'], 'r')) === false) { + $this->error('Can\'t open the input file!'); + } + + return $input; + } + + /** + * Read content of file. + * + * @param FilePointer $input + * @throws \Exception + * @return array + */ + private function readFile($input) + { + if($this->parameters['excel']) + $this->adjustFromExcel(); + + $translations = []; + while (($data = fgetcsv($input, 0, $this->parameters['delimiter'], $this->parameters['enclosure'], $this->parameters['escape'])) !== false) { + if(isset($translations[$data[0]]) == false) + $translations[$data[0]] = []; + + if(sizeof($data) != 3) + throw new \Exception("Wrong format of file. Try launch command with -X option if you use Excel for editing file."); + + $translations[$data[0]][$data[1]] = $data[2]; + } + + return $translations; + } + + /** + * Adjust file to Excel format. + * + * @return void + */ + private function adjustFromExcel() + { + $data = file_get_contents($this->parameters['input']); + file_put_contents($this->parameters['input'], mb_convert_encoding($data, 'UTF-8', 'UTF-16')); + } + + /** + * Close file. + * + * @return void + */ + private function closeFile($input) + { + fclose($input); + } + + /** + * Save fetched translations to file. + * + * @return void + */ + private function saveTranslations($translations) + { + LangListService::writeLangList($this->parameters['locale'], $this->parameters['group'], $translations); + } + + /** + * Display output that command is finished and where to find file. + * + * @return void + */ + private function sayItsFinish() + { + $this->info('Finished! Translations imported from: '. (substr($this->parameters['input'], strlen(base_path()) + 1)) + . PHP_EOL); + } + +} \ No newline at end of file diff --git a/src/UFirst/LangImportExport/Facades/LangListService.php b/src/Facades/LangListService.php similarity index 56% rename from src/UFirst/LangImportExport/Facades/LangListService.php rename to src/Facades/LangListService.php index 5ed0c36..855e4bf 100644 --- a/src/UFirst/LangImportExport/Facades/LangListService.php +++ b/src/Facades/LangListService.php @@ -1,11 +1,14 @@ registerExportToCsvCommand(); $this->registerImportFromCsvCommand(); } @@ -36,7 +34,9 @@ public function boot(DispatcherContract $events) */ public function register() { - require __DIR__.'/../../bindings.php'; + $this->app->singleton('LangImportExportLangListService', function() { + return new LangListService; + }); } /** @@ -51,18 +51,18 @@ public function provides() ]; } - private function registerExportToCsvCommand() { - $this->app['lang-export.csv'] = $this->app->share(function($app) - { + private function registerExportToCsvCommand() + { + $this->app->singleton('lang-export.csv', function($app) { return new ExportToCsvCommand(); }); $this->commands('lang-export.csv'); } - private function registerImportFromCsvCommand() { - $this->app['lang-import.csv'] = $this->app->share(function($app) - { + private function registerImportFromCsvCommand() + { + $this->app->singleton('lang-import.csv', function($app) { return new ImportFromCsvCommand(); }); diff --git a/src/LangListService.php b/src/LangListService.php new file mode 100644 index 0000000..d24a808 --- /dev/null +++ b/src/LangListService.php @@ -0,0 +1,148 @@ +isOneGroup($group)) { + $result[$group] = $this->getGroup($locale, $group); + return $result; + } + + $path = resource_path('lang/'. $locale.'/'); + $files = $this->getAllFiles($path); + foreach($files as $file) { + $file_path = substr($file->getRealPath(), strlen($path), -4); + $result[$file_path] = $this->getGroup($locale, $file_path); + } + return $result; + } + + /** + * Check if $group is one file only. + * + * @param string $group + * @return bool + */ + private function isOneGroup($group) + { + return $group != '*' && $group != ''; + } + + /** + * Fetch localization from file. + * + * @param string $locale + * @param string $group + * @return array + */ + private function getGroup($locale, $group) + { + $translations = Lang::getLoader()->load($locale, $group); + return Arr::dot($translations); + } + + /** + * Get list of all files from $path. + * + * @param string $path + * @return array + */ + private function getAllFiles($path) + { + return File::allFiles($path); + } + + /** + * Write translated content to localization file or files. + * + * @param string $locale + * @param string $group + * @param array $new_translations + * @return void + */ + public function writeLangList($locale, $group, $new_translations) + { + if($this->isOneGroup($group)) { + if(isset($new_translations[$group]) == false) + return; + + return $this->writeLangFile($locale, $group, $new_translations[$group]); + } + + foreach($new_translations as $group => $translations) + $this->writeLangFile($locale, $group, $translations); + } + + /** + * Write translated content to one file. + * + * @param string $locale + * @param string $group + * @param array $new_translations + * @throws \Exception + * @return void + */ + private function writeLangFile($locale, $group, $new_translations) + { + $translations = $this->getTranslations($locale, $group, $new_translations); + + $header = "load($locale, $group); + foreach($new_translations as $key => $value) { + Arr::set($translations, $key, $value); + } + + if(in_array($group, $this->dotFiles)) { + $translations = Arr::dot($translations); + } + + return $translations; + } + +} diff --git a/src/Support/helpers.php b/src/Support/helpers.php new file mode 100644 index 0000000..c18a3b8 --- /dev/null +++ b/src/Support/helpers.php @@ -0,0 +1,15 @@ +basePath() . DIRECTORY_SEPARATOR . 'resources' . ($path ? DIRECTORY_SEPARATOR . $path : $path); + } +} diff --git a/src/UFirst/LangImportExport/Console/ExportToCsvCommand.php b/src/UFirst/LangImportExport/Console/ExportToCsvCommand.php deleted file mode 100644 index 0b4236a..0000000 --- a/src/UFirst/LangImportExport/Console/ExportToCsvCommand.php +++ /dev/null @@ -1,81 +0,0 @@ -argument('locale'); - $group = $this->argument('group'); - - $delimiter = $this->option('delimiter'); - $enclosure = $this->option('enclosure'); - - $strings = LangListService::loadLangList($locale, $group); - - // Create output device and write CSV. - $output = $this->option('output'); - if (empty($output) || !($out = fopen($output, 'w'))) { - $out = fopen('php://output', 'w'); - } - - // Write CSV lintes - foreach ($strings as $key => $value) { - fputcsv($out, array($key, $value), $delimiter, $enclosure); - } - - fclose($out); - } -} \ No newline at end of file diff --git a/src/UFirst/LangImportExport/Console/ImportFromCsvCommand.php b/src/UFirst/LangImportExport/Console/ImportFromCsvCommand.php deleted file mode 100644 index be4bda1..0000000 --- a/src/UFirst/LangImportExport/Console/ImportFromCsvCommand.php +++ /dev/null @@ -1,84 +0,0 @@ -argument('locale'); - $group = $this->argument('group'); - $file = $this->argument('file'); - - $delimiter = $this->option('delimiter'); - $enclosure = $this->option('enclosure'); - $escape = $this->option('escape'); - - $strings = array(); - - // Create output device and write CSV. - if (($input_fp = fopen($file, 'r')) === FALSE) { - $this->error('Can\'t open the input file!'); - } - - // Write CSV lintes - while (($data = fgetcsv($input_fp, 0, $delimiter, $enclosure, $escape)) !== FALSE) { - $strings[$data[0]] = $data[1]; - } - - fclose($input_fp); - LangListService::writeLangList($locale, $group, $strings); - } -} \ No newline at end of file diff --git a/src/UFirst/LangImportExport/LangListService.php b/src/UFirst/LangImportExport/LangListService.php deleted file mode 100644 index 38972cf..0000000 --- a/src/UFirst/LangImportExport/LangListService.php +++ /dev/null @@ -1,32 +0,0 @@ -load($locale, $group); - $translations_with_prefix = array_dot(array($group => $translations)); - return $translations_with_prefix; - } - - public function writeLangList($locale, $group, $new_translations) { - $translations = Lang::getLoader()->load($locale, $group); - foreach($new_translations as $key => $value) { - array_set($translations, $key, $value); - } - $header = "