Skip to content

Commit

Permalink
restore i18n
Browse files Browse the repository at this point in the history
  • Loading branch information
axelrindle committed Sep 29, 2024
1 parent adfde30 commit 1c5f48e
Show file tree
Hide file tree
Showing 5 changed files with 495 additions and 0 deletions.
240 changes: 240 additions & 0 deletions app/Console/Commands/Translate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php

namespace App\Console\Commands;

use Closure;
use Exception;
use Google\Cloud\Translate\V2\TranslateClient;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;

use function Laravel\Prompts\confirm;

// TODO: Purge existing language

/**
* Translates the base english language files to another language using the Google Cloud Translation API.
*/
class Translate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:translate
{language : The target language code. See <https://cloud.google.com/translate/docs/languages> for reference}
{--force : Force the operation to run}
{--only= : Limit translation to a single component.}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Translates the base english language files to another language using the Google Cloud Translation API';

/**
* @var Collection<int, string>
*/
protected $skip;

/**
* @var Collection<string, Closure>
*/
protected $parts;

protected int $chunkSize = 128;

public function __construct()
{
parent::__construct();

// TODO: Configure this somewhere else
$this->skip = collect([
// Backend
'validation.custom.attribute-name.rule-name',

// Frontend
'Sidebar.creator-login',
]);

$this->parts = collect([
'backend' => fn () => $this->handleBackend(),
'frontend' => fn () => $this->handleFrontend(),
]);
}

/**
* Execute the console command.
*/
public function handle(): void
{
$only = $this->option('only');

if (is_string($only)) {
$handler = $this->parts->get($only);

if ($handler == null) {
$this->error('Invalid component! Specify one of the following: ' . $this->parts->keys()->join(', '));
} else {
$this->info("Processing {$only}");
$handler();
}
} else {
foreach ($this->parts as $part => $handler) {
$this->info("Processing {$part}");
$handler();
}
}
}

private function handleBackend(): void
{
$files = array_diff(scandir(lang_path('en')), ['..', '.']);
$targetLanguage = $this->argument('language');

$existingKeys = collect();
if (is_dir(lang_path($targetLanguage))) {
$filesTarget = array_diff(scandir(lang_path($targetLanguage)), ['..', '.']);
/** @var Collection<string, string> */
$existingKeys = collect($filesTarget)
->mapWithKeys(fn (string $file) => [basename($file, '.php') => require lang_path($targetLanguage . '/' . $file)])
->dot()
->filter(fn (mixed $value) => is_string($value))
->filter(fn (string $value, string $key) => $this->skip->doesntContain($key));
}

/** @var Collection<string, string> */
$data = collect($files)
->mapWithKeys(fn (string $file) => [basename($file, '.php') => require lang_path('en/' . $file)])
->dot()
->filter(fn (mixed $value) => is_string($value))
->filter(fn (string $value, string $key) => $this->skip->doesntContain($key))
->filter(fn (string $value, string $key) => ! $existingKeys->has($key));

$count = $data->count();
if ($count == 0) {
$this->info('There is nothing to translate :)');

return;
}

if (! $this->confirmTranslation($data)) {
return;
}

/** @var Collection<string, string> */
$translated = $data
->chunk($this->chunkSize)
->map(fn (Collection $value) => $this->translate($value->values()))
->flatten(1)
->map(fn (array $result) => $result['text'])
->keyBy(fn (string $item, int $key) => $data->keys()[$key])
->merge($existingKeys)
->undot();

@mkdir(lang_path($targetLanguage, 0750));

foreach ($translated as $file => $data) {
$targetFile = lang_path($targetLanguage . '/' . $file . '.php');
$dump = var_export($data, true);

$contents = <<<EOL
<?php
return {$dump};
EOL;

file_put_contents($targetFile, $contents);
}

$this->info("Successfully translated {$count} strings.");
$this->comment('Note that the content has been machine-translated. Make sure to manually review the translations.');
}

private function handleFrontend(): void
{
$targetLanguage = $this->argument('language');

$data = collect(
json_decode(file_get_contents(base_path('frontend/messages/en.json')), true)
)
->dot()
->filter(fn (string $value, string $key) => $this->skip->doesntContain($key));

$count = $data->count();
if ($count == 0) {
$this->info('There is nothing to translate :)');

return;
}

if (! $this->confirmTranslation($data)) {
return;
}

/** @var Collection<string, string> */
$translated = $data
->chunk($this->chunkSize)
->map(fn (Collection $value) => $this->translate($value->values()))
->flatten(1)
->map(fn (array $result) => $result['text'])
->keyBy(fn (string $item, int $key) => $data->keys()[$key])
->undot();

$options = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE;
file_put_contents(base_path('frontend/messages/' . $targetLanguage . '.json'), $translated->toJson($options));

$this->info("Successfully translated {$count} strings.");
$this->comment('Note that the content has been machine-translated. Make sure to manually review the translations.');
}

/**
* @param Collection<string, string> $data
*/
private function confirmTranslation(Collection $data): bool
{
$count = $data->count();
$apiCalls = ceil($count / $this->chunkSize);

if (! $this->option('force')) {
$proceed = confirm(
label: "Will process {$count} strings. Continue?",
default: true,
yes: 'Proceed',
no: 'Cancel',
hint: "Proceeding will make approx. {$apiCalls} API calls to the Google Cloud Translation API",
);

if (! $proceed) {
$this->error('Operation canceled by user.');

return false;
}
}

return true;
}

/**
* @param Collection<int, string> $messages
*/
private function translate(Collection $messages): ?array
{
$key = config('services.google.translate.api_key');
if (! $key) {
throw new Exception('Api key is missing! Did you specify the environment variable "GOOGLE_TRANSLATE_API_KEY" ?');
}

$client = new TranslateClient([
'key' => $key,
]);

return $client->translateBatch($messages->toArray(), [
'source' => 'en',
'target' => $this->argument('language'),
'format' => 'text',
]);
}
}
20 changes: 20 additions & 0 deletions lang/en/auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/

'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',

];
19 changes: 19 additions & 0 deletions lang/en/pagination.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/

'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',

];
22 changes: 22 additions & 0 deletions lang/en/passwords.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| outcome such as failure due to an invalid password / reset token.
|
*/

'reset' => 'Your password has been reset.',
'sent' => 'We have emailed your password reset link.',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that email address.",

];
Loading

0 comments on commit 1c5f48e

Please sign in to comment.