Skip to content

Commit 2578fa8

Browse files
committed
implement csv export/import
1 parent f45f94b commit 2578fa8

13 files changed

+369
-1
lines changed

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ Easily manage all your Laravel translation strings with powerful features:
5656
1. [Detecting Dead Translations](#detecting-dead-translations)
5757
- [CLI Usage](#cli-usage-2)
5858
- [Programmatic Usage](#programmatic-usage-2)
59+
1. [Export to a CSV](#export-to-a-csv)
60+
- [CLI Usage](#cli-usage-3)
61+
- [Programmatic Usage](#programmatic-usage-3)
62+
1. [Import from a CSV](#import-from-a-csv)
63+
- [CLI Usage](#cli-usage-4)
64+
- [Programmatic Usage](#programmatic-usage-4)
5965

6066
## How does it work?
6167

@@ -496,6 +502,50 @@ php artisan translator:dead en
496502
Translator::getDeadTranslations(locale: 'fr');
497503
```
498504

505+
## Export to a CSV
506+
507+
Service: `exporter`
508+
509+
Export all your translation strings to a CSV file in the following format:
510+
511+
| Key | English | French |
512+
| ------------------- | ------- | --------- |
513+
| messages.auth.login | Login | Connexion |
514+
515+
### CLI Usage
516+
517+
```bash
518+
php artisan translator:export /path/to/my/file.csv
519+
```
520+
521+
### Programmatic Usage
522+
523+
```php
524+
$path = Translator::exportTranslations('/path/to/my/file.csv');
525+
```
526+
527+
## Import from a CSV
528+
529+
Service: `exporter`
530+
531+
Import translation strings from a CSV file. Ensure your CSV follows the format below:
532+
533+
| Key | English | French |
534+
| ------------------- | ------- | --------- |
535+
| messages.auth.login | Login | Connexion |
536+
537+
### CLI Usage
538+
539+
```bash
540+
php artisan translator:import /path/to/my/file.csv
541+
```
542+
543+
### Programmatic Usage
544+
545+
```php
546+
$translations = Translator::importTranslations('/path/to/my/file.csv');
547+
```
548+
499549
## Testing
500550

501551
Run tests using:

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"nikic/php-parser": "^5.1",
2626
"openai-php/laravel": "^0.11",
2727
"spatie/laravel-package-tools": "^1.16",
28+
"spatie/simple-excel": "^3.7",
2829
"symfony/finder": "^6.0||^7.0",
2930
"symfony/intl": "^7.2"
3031
},

config/translator.php

+14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types=1);
44

55
use Elegantly\Translator\Drivers\PhpDriver;
6+
use Elegantly\Translator\Services\Exporter\CsvExporterService;
67
use Elegantly\Translator\Support\LocaleValidator;
78

89
return [
@@ -175,4 +176,17 @@
175176

176177
],
177178

179+
/*
180+
|--------------------------------------------------------------------------
181+
| Exporter/Importer Service
182+
|--------------------------------------------------------------------------
183+
|
184+
| These are the services that can be used to export and import your translations.
185+
| You can customize their behavior here, or you can define your own service.
186+
|
187+
*/
188+
'exporter' => [
189+
'service' => CsvExporterService::class,
190+
],
191+
178192
];

src/Commands/ExportCommand.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Elegantly\Translator\Commands;
6+
7+
use Illuminate\Contracts\Console\PromptsForMissingInput;
8+
9+
use function Laravel\Prompts\info;
10+
use function Laravel\Prompts\intro;
11+
12+
/**
13+
* Display translations strings found in codebase but not in a locale
14+
*/
15+
class ExportCommand extends TranslatorCommand implements PromptsForMissingInput
16+
{
17+
public $signature = 'translator:export {path} {--driver=}';
18+
19+
public $description = 'Export all the translations in a file.';
20+
21+
public function handle(): int
22+
{
23+
/** @var string $path */
24+
$path = $this->argument('path');
25+
26+
$translator = $this->getTranslator();
27+
28+
intro('Using driver: '.$translator->driver::class);
29+
30+
$translator->exportTranslations($path);
31+
32+
info("Translations sucessfully exported here: {$path}");
33+
34+
return self::SUCCESS;
35+
}
36+
}

src/Commands/ImportCommand.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Elegantly\Translator\Commands;
6+
7+
use Illuminate\Contracts\Console\PromptsForMissingInput;
8+
9+
use function Laravel\Prompts\info;
10+
use function Laravel\Prompts\intro;
11+
12+
/**
13+
* Display translations strings found in codebase but not in a locale
14+
*/
15+
class ImportCommand extends TranslatorCommand implements PromptsForMissingInput
16+
{
17+
public $signature = 'translator:import {path} {--driver=}';
18+
19+
public $description = 'Import all the translations from a file.';
20+
21+
public function handle(): int
22+
{
23+
/** @var string $path */
24+
$path = $this->argument('path');
25+
26+
$translator = $this->getTranslator();
27+
28+
intro('Using driver: '.$translator->driver::class);
29+
30+
$imported = $translator->importTranslations($path);
31+
32+
info('Translations sucessfully imported.');
33+
34+
return self::SUCCESS;
35+
}
36+
}

src/Exceptions/TranslatorServiceException.php

+5
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ public static function missingSearchcodeService(): self
2222
{
2323
return new self('The searchcode service is missing. Please define a searchcode service in configs.');
2424
}
25+
26+
public static function missingExporterService(): self
27+
{
28+
return new self('The exporter service is missing. Please define an exporter service in configs.');
29+
}
2530
}

src/Facades/Translator.php

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Elegantly\Translator\Collections\Translations;
88
use Elegantly\Translator\Drivers\Driver;
9+
use Elegantly\Translator\Services\Exporter\ExporterInterface;
910
use Illuminate\Support\Facades\Facade;
1011

1112
/**
@@ -21,6 +22,8 @@
2122
* @method static Translations deleteTranslations(string $locale, array<int, string> $keys)
2223
* @method static Translations sortTranslations(string $locale)
2324
* @method static Translations saveTranslations(string $locale, Translations $translations)
25+
* @method static string exportTranslations(string $path, ?ExporterInterface $exporter = null)
26+
* @method static array<string, array<int|string, scalar>> importTranslations(string $path, ?ExporterInterface $exporter = null)
2427
* @method static void clearCache()
2528
*
2629
* @see \Elegantly\Translator\Translator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Elegantly\Translator\Services\Exporter;
6+
7+
use Illuminate\Support\Arr;
8+
use Spatie\SimpleExcel\SimpleExcelReader;
9+
use Spatie\SimpleExcel\SimpleExcelWriter;
10+
11+
class CsvExporterService implements ExporterInterface
12+
{
13+
public function __construct()
14+
{
15+
//
16+
}
17+
18+
public static function make(): self
19+
{
20+
return new self;
21+
}
22+
23+
public function export(array $translationsByLocale, string $path): string
24+
{
25+
26+
$writer = SimpleExcelWriter::create($path);
27+
28+
/** @var string[] $locales */
29+
$locales = array_keys($translationsByLocale);
30+
31+
$writer->addHeader(['key', ...$locales]);
32+
33+
/** @var string[] $keys */
34+
$keys = collect($translationsByLocale)
35+
->flatMap(fn ($translations) => $translations->keys())
36+
->unique()
37+
->all();
38+
39+
foreach ($keys as $key) {
40+
$writer->addRow([
41+
'key' => $key,
42+
...array_map(fn ($locale) => $translationsByLocale[$locale]->get($key), $locales),
43+
]);
44+
}
45+
46+
$writer->close();
47+
48+
return $path;
49+
50+
}
51+
52+
public function import(string $path): array
53+
{
54+
55+
/**
56+
* @var array<string, array<string, scalar>> $translationsByLocale
57+
*/
58+
$translationsByLocale = [];
59+
60+
$rows = SimpleExcelReader::create($path)->getRows();
61+
62+
foreach ($rows as $row) {
63+
64+
$key = Arr::pull($row, 'key');
65+
66+
foreach ($row as $locale => $value) {
67+
68+
$translationsByLocale[$locale] = [
69+
...($translationsByLocale[$locale] ?? []),
70+
$key => $value,
71+
];
72+
73+
}
74+
75+
}
76+
77+
return $translationsByLocale;
78+
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Elegantly\Translator\Services\Exporter;
6+
7+
use Elegantly\Translator\Collections\Translations;
8+
9+
interface ExporterInterface
10+
{
11+
public static function make(): self;
12+
13+
/**
14+
* @param array<string, Translations> $translationsByLocale
15+
*/
16+
public function export(array $translationsByLocale, string $path): string;
17+
18+
/**
19+
* @return array<string, array<string, scalar>>
20+
*/
21+
public function import(string $path): array;
22+
}

src/Translator.php

+54-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Elegantly\Translator\Collections\Translations;
99
use Elegantly\Translator\Drivers\Driver;
1010
use Elegantly\Translator\Exceptions\TranslatorServiceException;
11+
use Elegantly\Translator\Services\Exporter\ExporterInterface;
1112
use Elegantly\Translator\Services\Proofread\ProofreadServiceInterface;
1213
use Elegantly\Translator\Services\SearchCode\SearchCodeServiceInterface;
1314
use Elegantly\Translator\Services\Translate\TranslateServiceInterface;
@@ -19,6 +20,7 @@ final public function __construct(
1920
public ?TranslateServiceInterface $translateService = null,
2021
public ?ProofreadServiceInterface $proofreadService = null,
2122
public ?SearchCodeServiceInterface $searchcodeService = null,
23+
public ?ExporterInterface $exporter = null,
2224
) {
2325
//
2426
}
@@ -29,7 +31,8 @@ public function driver(null|string|Driver $name): static
2931
driver: $name instanceof Driver ? $name : TranslatorServiceProvider::getDriverFromConfig($name),
3032
translateService: $this->translateService,
3133
proofreadService: $this->proofreadService,
32-
searchcodeService: $this->searchcodeService
34+
searchcodeService: $this->searchcodeService,
35+
exporter: $this->exporter,
3336
);
3437
}
3538

@@ -327,6 +330,56 @@ public function saveTranslations(
327330

328331
}
329332

333+
public function exportTranslations(
334+
string $path,
335+
?ExporterInterface $exporter = null
336+
): string {
337+
$exporter = $exporter ?? $this->exporter;
338+
339+
if (! $exporter) {
340+
throw TranslatorServiceException::missingExporterService();
341+
}
342+
343+
$locales = $this->getLocales();
344+
345+
$translationsByLocale = collect($locales)
346+
->mapWithKeys(fn ($locale) => [$locale => $this->getTranslations($locale)])
347+
->all();
348+
349+
return $exporter->export($translationsByLocale, $path);
350+
351+
}
352+
353+
/**
354+
* @return array<string, array<int|string, scalar>>
355+
*/
356+
public function importTranslations(
357+
string $path,
358+
?ExporterInterface $exporter = null
359+
): array {
360+
$exporter = $exporter ?? $this->exporter;
361+
362+
if (! $exporter) {
363+
throw TranslatorServiceException::missingExporterService();
364+
}
365+
366+
$translationsByLocale = $exporter->import($path);
367+
368+
foreach ($translationsByLocale as $locale => $values) {
369+
370+
$this->transformTranslations(
371+
locale: $locale,
372+
callback: function ($translations) use ($values) {
373+
return $translations->merge($values);
374+
},
375+
sort: config()->boolean('translator.sort_keys'),
376+
);
377+
378+
}
379+
380+
return $translationsByLocale;
381+
}
382+
330383
public function clearCache(): void
331384
{
332385
$this->searchcodeService?->getCache()?->flush();

0 commit comments

Comments
 (0)