diff --git a/LICENSE b/LICENSE
index 02b6daf..47fe37a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,6 +2,7 @@ The MIT License (MIT)
Copyright (c) 2014 UFirst Group
Copyright (c) 2017 HighSolutions
+Copyright (c) 2019 Aidas Klimas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/README.md b/README.md
index 95d1be9..156c681 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,14 @@
-
-Laravel-Lang-Import-Export
+
+
+
+
+Laravel-Lang-Import-Export **(Refactored)**
==========================
-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.
+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. In practice, CSV format is supper easy to work with for any translator in Fiverr or for any other freelancer. Personally, I have tried every other format such as **php, yaml, docx, pod, txt** and all of them has too complex syntax and requires custom software to work with (Not to mention all those problems with file encodings). CSV solves it all! (Now supports **xls, xlsx, ods** file types too!)
+
+# How It Works?
It turns some navigation.php file...
@@ -41,23 +46,17 @@ navigation.tips.play,"Autoplay the slide show"
Installation
------------
-Add the following line to the `require` section of your Laravel webapp's `composer.json` file:
-
-```javascript
- "require": {
- "HighSolutions/laravel-lang-import-export": "5.4.*"
- }
+```sh
+ composer require aidask/laravel-lang-import-export
```
-Run `composer update` to install the package.
-
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(
/* ... */
- 'HighSolutions\LangImportExport\LangImportExportServiceProvider'
+ 'LangImportExport\LangImportExportServiceProvider'
)
```
@@ -69,46 +68,53 @@ The package currently provides two commands, one for exporting the files and one
### Export
```bash
-php artisan lang:export
-php artisan lang:export en * path/to/export
-php artisan lang:export en auth -A -X
+php artisan lang:export --locale en
+php artisan lang:export --locale en --target fr,de,pt # export en translations only missing in fr,de,pt locales. Each in separate files
+php artisan lang:export -l fr,de,pt -z all.zip # archive all the files
+php artisan lang:export --locale en -g pagination,validation # export only cretain groups
+php artisan lang:export --locale en --exclude pagination,validation # export all files except pagination and validation
+php artisan lang:export --locale en --ext xls # supported extensions: Xls, Xlsx, Ods, Csv, Html, Tcpdf, Mpdf, Dompdf
```
-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:
-
-| 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
-
+```bash
+php artisan lang:import es.csv # localed autodetected from file name
+php artisan lang:import espaniol.csv -l es
+php artisan lang:import espaniol.csv -l es -g pagination,validation # import only cretain groups
+php artisan lang:import es.csv -p --html # validate imported translations for missing placeholders and bad html (see below)
+php artisan lang:import es.xls -p --column-map A,B,D # import translations from different column. E.g. C column was left with base language
```
-php artisan lang:import
-php artisan lang:import en * path/to/import
-php artisan lang:import en auth -X
+
+### Validate
+```bash
+php artisan lang:validate ar -m --html -v # find missing keys, bad html and placeholders
```
+
-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 | \ |
+### Config
+
+You can export package config if you want to set defaults for the commands:
+```bash
+php artisan vendor:publish
+```
+
Changelog
------------
+6.4.0
+* Added support to export to Xls, Xlsx, Ods, Csv, Html, Tcpdf, Mpdf, Dompdf file types
+* You can now import translations from a zip file
+
+6.2.0
+* Validate HTML feature. Usually HTML tags are translated with random spaces such as "< /b>", which makes entire paragraph bold.
+* Added support to import from xls, ods, xlsx, csv file types (PhpOffice integration)
+
+6.1.0
+* Validate placeholders feature
+
+6.0.0
+* refactor whole repository
5.4.10
* Laravel 5.7 support
@@ -142,17 +148,10 @@ Changelog
- 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.
+Currently is developed by [Aidas Klimas](https://klimas.lt/), software house from Lithuania
diff --git a/composer.json b/composer.json
index 0425a76..4989e11 100644
--- a/composer.json
+++ b/composer.json
@@ -1,8 +1,12 @@
{
- "name": "highsolutions/laravel-lang-import-export",
+ "name": "aidask/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"],
"authors": [
+ {
+ "name": "Aidas Klimas",
+ "email": "aidaskk@gmail.com"
+ },
{
"name": "Michael Ruoss",
"email": "michael.ruoss@UFirstgroup.com"
@@ -14,25 +18,29 @@
],
"license": "MIT",
"require": {
- "php": ">=5.6.4",
- "illuminate/support": "5.x"
+ "php": ">=7",
+ "illuminate/support": ">=5",
+ "phpoffice/phpspreadsheet": "*"
},
"autoload": {
"psr-4": {
- "HighSolutions\\LangImportExport\\": "src/"
+ "LangImportExport\\": "src/"
},
"files": [
"src/Support/helpers.php"
]
},
+ "suggest": {
+ "ext-zip": "zip exported files"
+ },
"extra": {
"component": "package",
- "frameworks": ["Laravel 5.7"],
+ "frameworks": ["Laravel"],
"laravel": {
"providers": [
- "HighSolutions\\LangImportExport\\LangImportExportServiceProvider"
- ]
+ "LangImportExport\\LangImportExportServiceProvider"
+ ]
}
},
- "minimum-stability": "stable"
+ "minimum-stability": "dev"
}
diff --git a/intro.jpg b/intro.jpg
deleted file mode 100644
index da06549..0000000
Binary files a/intro.jpg and /dev/null differ
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..9a73a9d
Binary files /dev/null and b/logo.png differ
diff --git a/src/Console/ExportToCsvCommand.php b/src/Console/ExportToCsvCommand.php
index c6f337c..da470b5 100644
--- a/src/Console/ExportToCsvCommand.php
+++ b/src/Console/ExportToCsvCommand.php
@@ -1,13 +1,16 @@
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);
- }
- }
- }
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Exports language files to CSV file";
- /**
- * 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']);
- }
+ /**
+ * List of files created by the export
+ * @var array
+ */
+ protected $files = [];
- /**
- * Close output file and check if adjust file to Excel format.
- *
- * @param FilePointerResource $output
- * @return void
- */
- private function closeFile($output)
- {
- fclose($output);
+ /**
+ * Execute the console command.
+ *
+ * @return void
+ * @throws \Exception
+ */
+ public function handle()
+ {
+ $exportLocales = $this->option('locale') ?: config('lang_import_export.export_locale');
+ $targetLocales = $this->option('target') ?: config('lang_import_export.export_target');
+ $fileExtensions = $this->option('ext') ?: config('lang_import_export.export_default_extension');
+ foreach ($this->strToArray($exportLocales) as $exportLocale) {
+ foreach ($this->strToArray($targetLocales, [null]) as $targetLocale) {
+ $translations = $this->getTranslations($exportLocale, $targetLocale);
+ $wordCount = $this->getTranslatableWordCount($translations);
+ $fileName = $this->getOutputFileName($exportLocale, $wordCount, $fileExtensions, $targetLocale);
+ $this->saveTranslations($translations, $fileName, $fileExtensions);
+ $this->info(strtoupper($exportLocale) . strtoupper($targetLocale ?: '') . ' Translations saved to: ' . $this->getOutputFileName($exportLocale, $wordCount, $fileExtensions, $targetLocale));
+ }
+ }
+ if ($zipName = $this->option('zip')) {
+ $this->info('Creating archive...');
+ $zip = new \ZipArchive;
+ if (!$zip->open($zipName, \ZipArchive::CREATE)) {
+ throw new \Exception("Failed to open $zipName");
+ }
+ foreach ($this->files as $file) {
+ $zip->addFile($file, basename($file));
+ }
+ $zip->close();
+ $this->info('Cleaning up the files...');
+ foreach ($this->files as $file) {
+ unlink($file);
+ }
+ }
+ }
+
+ private function strToArray($string, $fallback = [])
+ {
+ if (!$string) {
+ return $fallback;
+ }
+ return array_filter(array_map('trim', explode(',', $string)));
+ }
- if($this->parameters['excel'])
- $this->adjustToExcel();
- }
+ /**
+ * Get translations from localization files.
+ *
+ * @param $locale
+ * @param null $target
+ * @return array
+ */
+ private function getTranslations($locale, $target = null)
+ {
+ $group = $this->option('group') ?: config('lang_import_export.groups');
+ $exclude = $this->option('exclude') ?: config('lang_import_export.exclude_groups');
+ $from = LangListService::loadLangList($locale, $group, $exclude);
+ if ($target) {
+ $targetList = LangListService::loadLangList($target, $group, $exclude);
+ foreach ($targetList as $group => $translations) {
+ foreach ($translations as $key => $v) {
+ unset($from[$group][$key]);
+ }
+ }
+ }
+ return $from;
+ }
- /**
- * 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'));
- }
+ /**
+ * @param $locale
+ * @param null $target
+ * @return mixed
+ */
+ private function getOutputFileName($locale, $wordCount, $fileExtension, $target = null)
+ {
+ $fileName = $this->option('output') ?: config('lang_import_export.export_path');
+ $fileName = str_replace(':locale', $locale, $fileName);
+ $fileName = str_replace(':target', $target, $fileName);
+ $fileName = str_replace(':wordcount', $wordCount, $fileName);
+ $fileName = str_replace(':ext', $fileExtension, $fileName);
+ return $fileName;
+ }
- /**
- * 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);
- }
+ /**
+ * @param $translations
+ */
+ private function getTranslatableWordCount($translations)
+ {
+ $wordCount = 0;
+ foreach ($translations as $group => $files) {
+ foreach ($files as $key => $value) {
+ if (is_array($value)) {
+ continue;
+ }
+ $wordCount += str_word_count($value);
+ }
+ }
+ return $wordCount;
+ }
+ /**
+ * @param $translations
+ * @param $fileName
+ * @param $fileExtension
+ */
+ private function saveTranslations($translations, $fileName, $fileExtension)
+ {
+ $data = [];
+ foreach ($translations as $group => $files) {
+ foreach ($files as $key => $value) {
+ if (is_array($value)) {
+ continue;
+ }
+ $data[] = [$group, $key, $value];
+ }
+ }
+ $spreadsheet = new Spreadsheet();
+ $spreadsheet->getActiveSheet()->fromArray($data);
+ $writer = IOFactory::createWriter($spreadsheet, ucfirst(strtolower($fileExtension)));
+ $writer->save($fileName);
+ $this->files[] = $fileName;
+ }
}
diff --git a/src/Console/ImportFromCsvCommand.php b/src/Console/ImportFromCsvCommand.php
index a4e341b..75629e7 100644
--- a/src/Console/ImportFromCsvCommand.php
+++ b/src/Console/ImportFromCsvCommand.php
@@ -1,13 +1,14 @@
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);
- }
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Imports the CSV file and write content into language files.";
+
+ public function handle()
+ {
+ $files = $this->argument('input');
+ if ($zipName = $this->option('zip')) {
+ $directory = 'extracted_language_files/';
+ $zip = new \ZipArchive();
+ $zip->open($zipName);
+ $zip->extractTo($directory);
+ $zip->close();
+ $files = array_merge($files, array_map(function($val) use ($directory) {
+ return $directory.$val;
+ }, preg_grep('/^([^.])/', scandir($directory))));
+ }
+ $locale = $this->option('locale');
+ foreach ($files as $fileName) {
+ try {
+ if (!$locale) {
+ preg_match('#\((.*?)\)#', pathinfo($fileName, PATHINFO_FILENAME), $localeCode);
+ $locale = $localeCode[1] ?? pathinfo($fileName, PATHINFO_FILENAME);
+ if (file_exists(lang_path($locale))) {
+ $this->info("Detected locale $locale");
+ } else {
+ $this->error("Could not detect locale of $fileName");
+ continue;
+ }
+ }
+ if (!file_exists($fileName)) {
+ $this->error("File $fileName does not exist");
+ continue;
+ }
+ $translations = $this->readTranslations($fileName);
+ $group = $this->option('group');
+ LangListService::writeLangList($locale, $group, $translations);
+ if ($this->option('placeholders') || config('lang_import_export.import_validate_placeholders')) {
+ $baseTranslations = LangListService::loadLangList(config('lang_import_export.base_locale'), $group);
+ foreach (LangListService::validatePlaceholders($translations, $baseTranslations) as $errors) {
+ $this->warn("lang/$locale/{$errors['group']}.php {$errors['key']} is missing \"{$errors['placeholder']}\".");
+ $this->info($errors['translation'], 'v');
+ $this->info($errors['baseTranslation'], 'vv');
+ }
+ }
+ if ($this->option('html') || config('lang_import_export.import_validate_html')) {
+ $baseTranslations = LangListService::loadLangList(config('lang_import_export.base_locale'), $group);
+ foreach (LangListService::validateHTML($translations, $baseTranslations) as $errors) {
+ $this->warn("lang/$locale/{$errors['group']}.php {$errors['key']} is missing `{$errors['tag']}` html tag.");
+ $this->info($errors['translation'], 'v');
+ $this->info($errors['baseTranslation'], 'vv');
+ }
+ }
+ $locale = $this->option('locale');
+ } catch (\Throwable $t) {
+ $this->error('Error occurred: '. $t->getMessage());
+ continue;
+ }
+ }
+ if (!empty($directory)) {
+ \File::deleteDirectory($directory);
+ }
+ }
- /**
- * 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
+ /**
+ * Get translations from CSV file.
+ *
+ * @return array
+ * @throws \Exception
+ */
+ private function readTranslations($fileName)
+ {
+ $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($fileName);
+ $rows = $spreadsheet->getActiveSheet()->toArray(null, false, false, true);
+
+ $translations = [];
+ $map = explode(',', $this->option('column-map') ?: 'A,B,C');
+ foreach ($rows as $data) {
+ if (isset($translations[$data[$map[0]]]) == false) {
+ $translations[$data[$map[0]]] = [];
+ }
+ $columns = count($data);
+ if ($columns < 3) {
+ throw new \Exception("File $fileName has only $columns column/s");
+ }
+ if ($columns > 3 && !$this->option('column-map')) {
+ $map = ['A', 'B', $spreadsheet->getActiveSheet()->getHighestDataColumn()];
+ }
+ $translations[$data[$map[0]]][$data[$map[1]]] = $data[$map[2]];
+ }
+
+ return $translations;
+ }
+}
diff --git a/src/Console/ValidationCommand.php b/src/Console/ValidationCommand.php
new file mode 100644
index 0000000..9d8be38
--- /dev/null
+++ b/src/Console/ValidationCommand.php
@@ -0,0 +1,115 @@
+option('locale') ?: config('lang_import_export.base_locale');
+ $groups = $this->option('group') ?: config('lang_import_export.groups');
+ $exclude = $this->option('exclude') ?: config('lang_import_export.exclude_groups');
+ $baseTranslations = LangListService::loadLangList($baseLocale, $groups, $exclude);
+ $target = $this->argument('target');
+ if (empty($target)) {
+ $this->error('--target is required');
+ }
+ foreach ($this->strToArray($target) as $locale) {
+ $targetTranslations = LangListService::loadLangList($locale, $groups, $exclude);
+ $this->validatePlaceholders($targetTranslations, $baseTranslations, $locale);
+ if ($this->option('html')) {
+ $this->validateHTML($targetTranslations, $baseTranslations, $locale);
+ }
+ if ($this->option('missing')) {
+ $this->showMissing($targetTranslations, $baseTranslations, $locale);
+ }
+ }
+ }
+
+ private function strToArray($string, $fallback = [])
+ {
+ if (!$string) {
+ return $fallback;
+ }
+ return array_filter(array_map('trim', explode(',', $string)));
+ }
+
+ /**
+ * @param $targetTranslations
+ * @param $baseTranslations
+ * @param $locale
+ */
+ private function validatePlaceholders($targetTranslations, $baseTranslations, $locale)
+ {
+ $this->info('Searching for missing placeholers...');
+ foreach (LangListService::validatePlaceholders($targetTranslations, $baseTranslations) as $errors) {
+ $this->warn("lang/$locale/{$errors['group']}.php {$errors['key']} is missing \"{$errors['placeholder']}\".");
+ $this->info($errors['translation'], 'v');
+ $this->info($errors['baseTranslation'], 'vv');
+ }
+ }
+
+ /**
+ * @param $targetTranslations
+ * @param $baseTranslations
+ * @param $locale
+ */
+ private function validateHTML($targetTranslations, $baseTranslations, $locale)
+ {
+ $this->info('Searching for HTML differences...');
+ foreach (LangListService::validateHTML($targetTranslations, $baseTranslations) as $errors) {
+ $this->warn("lang/$locale/{$errors['group']}.php {$errors['key']} is missing \"{$errors['tag']}\" html tag.");
+ $this->info($errors['translation'], 'v');
+ $this->info($errors['baseTranslation'], 'vv');
+ }
+ }
+
+ private function showMissing($targetTranslations, $baseTranslations, $locale)
+ {
+ $this->info('Searching for missing keys...');
+ foreach ($baseTranslations as $group => $translations) {
+ if (!isset($targetTranslations[$group])) {
+ $this->warn("lang/$locale/$group.php entire group is missing");
+ continue;
+ }
+ foreach ($translations as $key => $translation) {
+ if (!empty($baseTranslations[$group][$key]) && !isset($targetTranslations[$group][$key])) {
+ $this->warn("lang/$locale/$group.php $key is missing");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Facades/LangListService.php b/src/Facades/LangListService.php
index 855e4bf..9fce171 100644
--- a/src/Facades/LangListService.php
+++ b/src/Facades/LangListService.php
@@ -1,14 +1,15 @@
registerExportToCsvCommand();
- $this->registerImportFromCsvCommand();
- }
-
- /**
- * Register the service provider.
- *
- * @return void
- */
- public function register()
- {
- $this->app->singleton('LangImportExportLangListService', function() {
- return new LangListService;
- });
- }
-
- /**
- * Get the services provided by the provider.
- *
- * @return array
- */
- public function provides()
- {
- return [
- 'lang-export.csv', 'lang-import.csv'
- ];
- }
-
- private function registerExportToCsvCommand()
- {
- $this->app->singleton('lang-export.csv', function($app) {
- return new ExportToCsvCommand();
- });
-
- $this->commands('lang-export.csv');
- }
-
- private function registerImportFromCsvCommand()
- {
- $this->app->singleton('lang-import.csv', function($app) {
- return new ImportFromCsvCommand();
- });
-
- $this->commands('lang-import.csv');
- }
-
+ /**
+ * Bootstrap the application events.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ $this->publishes([
+ __DIR__ . '/config/lang_import_export.php' => config_path('lang_import_export.php'),
+ ]);
+ if ($this->app->runningInConsole()) {
+ $this->commands([
+ ImportFromCsvCommand::class,
+ ExportToCsvCommand::class,
+ ValidationCommand::class,
+ ]);
+ }
+ }
+
+ /**
+ * Register the service provider.
+ *
+ * @return void
+ */
+ public function register()
+ {
+ $this->mergeConfigFrom(__DIR__ . '/config/lang_import_export.php', 'lang_import_export');
+ $this->app->singleton('LangImportExportLangListService', function () {
+ return new LangListService;
+ });
+ }
}
diff --git a/src/LangListService.php b/src/LangListService.php
index 5ba5b99..7592c53 100644
--- a/src/LangListService.php
+++ b/src/LangListService.php
@@ -1,147 +1,190 @@
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 array_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) {
- array_set($translations, $key, $value);
- }
-
- if(in_array($group, $this->dotFiles)) {
- $translations = array_dot($translations);
- }
-
- return $translations;
- }
-
+ protected $dotFiles = ['routes'];
+
+ /**
+ * Load localization file or files for specified locale.
+ *
+ * @param string $locale
+ * @param string $group
+ * @param string $excludeGroups
+ * @return array
+ */
+ public function loadLangList($locale, $group, $excludeGroups = '')
+ {
+ $exclude = explode(',', $excludeGroups);
+ $result = [];
+ if ($this->isGroupList($group)) {
+ $groups = explode(',', $group);
+ } else {
+ $path = lang_path($locale . '/');
+ $files = $this->getAllFiles($path);
+ $groups = [];
+ foreach ($files as $file) {
+ $groups[] = substr($file->getRealPath(), strlen($path), -4);
+ }
+ }
+ foreach ($groups as $group) {
+ if (!in_array($group, $exclude)) {
+ $result[$group] = $this->getGroup($locale, $group);
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Check if $group is one file only.
+ *
+ * @param string $group
+ * @return bool
+ */
+ private function isGroupList($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->isGroupList($group)) {
+ $groups = explode(',', $group);
+ $new_translations = array_intersect_key($new_translations, array_flip($groups));
+ }
+
+ 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
+ * @return void
+ * @throws \Exception
+ */
+ 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;
+ }
+
+ public function validatePlaceholders($targetTranslations, $baseTranslations)
+ {
+ foreach ($targetTranslations as $group => $translations) {
+ foreach ($translations as $key => $translation) {
+ if (isset($baseTranslations[$group][$key]) && is_string($baseTranslations[$group][$key])) {
+ $baseTranslation = $baseTranslations[$group][$key];
+ $placeholders = $this->matchPlaceholders($baseTranslation);
+ foreach ($placeholders as $placeholder) {
+ if (strpos($translation, $placeholder) === false) {
+ yield compact('group', 'key', 'placeholder', 'translation', 'baseTranslation');
+ }
+ }
+ }
+ }
+ }
+ }
+
+ public function validateHTML($targetTranslations, $baseTranslations)
+ {
+ foreach ($targetTranslations as $group => $translations) {
+ foreach ($translations as $key => $translation) {
+ if (isset($baseTranslations[$group][$key]) && is_string($baseTranslations[$group][$key])) {
+ $baseTranslation = $baseTranslations[$group][$key];
+ preg_match_all('~(?[a-z]+[^>]*?>)~i', $baseTranslation, $m);
+ $tags = $m[1];
+ foreach ($tags as $tag) {
+ if (strpos($translation, $tag) === false) {
+ yield compact('group', 'key', 'translation', 'baseTranslation', 'tag');
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private function matchPlaceholders($translation)
+ {
+ preg_match_all('~(:[a-zA-Z0-9_]+)~', $translation, $m);
+ return $m[1] ?? [];
+ }
}
diff --git a/src/Support/helpers.php b/src/Support/helpers.php
index c18a3b8..145d576 100644
--- a/src/Support/helpers.php
+++ b/src/Support/helpers.php
@@ -1,11 +1,10 @@
'en',
+ 'groups' => null, // all groups or comma separated list (e.g. translation file names)
+ 'exclude_groups' => null, // comma separated list
+ 'export_locale' => 'en',// locale list separated by comma
+ 'export_target' => null,// target locale list separated by comma
+ 'export_path' => storage_path(':locale:target.:ext'),
+ 'export_default_extension' => 'csv',
+ 'import_validate_placeholders' => false, // validate placeholders by default
+ 'import_validate_html' => false, // validate html by default
+];
diff --git a/validation.png b/validation.png
new file mode 100644
index 0000000..8a8dece
Binary files /dev/null and b/validation.png differ