diff --git a/.gitignore b/.gitignore
index dfd6caa..0de6d4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/vendor
-composer.lock
\ No newline at end of file
+composer.lock
+.idea/
+docker-compose.yml
diff --git a/README.md b/README.md
index 1fdf6ca..2537c7b 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,30 @@
# laravel-google-translate
-Translate translation files (under /resources/lang) or lang.json files from specified base locale to other languages using stichoza/google-translate-php or Google Translate API https://cloud.google.com/translate/
+
+* Translate translation files (under /resources/lang) or lang.json files
+* Provide extra facade functions Str::apiTranslate and Str::apiTranslateWithAttributes
+
+by using stichoza/google-translate-php or Google Translate API https://cloud.google.com/translate/ or Yandex Translatin API https://tech.yandex.com/translate/
+
+## Str facade api-translation helpers
+This package provides two translation methods for Laravel helper Str
+* `Illuminate\Support\Str::apiTranslate` -> Translates texts using your selected api in config
+* `Illuminate\Support\Str::apiTranslateWithAttributes` -> Again translates texts using your selected api in config
+ in addition to that this function ***respects Laravel translation text attributes*** like :name
+
+## how to use your own translation api
+
+* Create your own translation api class by implementing Tanmuhittin\LaravelGoogleTranslate\Contracts\ApiTranslatorContract
+* Write your classname in config laravel_google_translate.custom_api_translator . Example : Myclass::class
+* Write your custom apikey for your custom class in laravel_google_translate.custom_api_translator_key
+
+Now all translations will use your custom api.
## this project needs refactor
Project code needs to be designed much better. Refactoring is in progress in refactor-code branch
## installation
```console
-composer require tanmuhittin/laravel-google-translate --dev
+composer require tanmuhittin/laravel-google-translate
php artisan vendor:publish --provider="Tanmuhittin\LaravelGoogleTranslate\LaravelGoogleTranslateServiceProvider"
```
@@ -37,12 +55,7 @@ This package can be used with https://github.com/andrey-helldar/laravel-lang-pub
* Add base Laravel translation files using https://github.com/andrey-helldar/laravel-lang-publisher
* Translate your custom files using this package
-Done
-
-## todo
-* Handle vendor translations too
-* Prepare Web Interface
-* Add other translation API support (Bing, Yandex...)
+Done
## finally
Thank you for using laravel-google-translate :)
diff --git a/composer.json b/composer.json
index 9e92086..f601624 100644
--- a/composer.json
+++ b/composer.json
@@ -6,7 +6,10 @@
"php": ">=7.0.0",
"illuminate/support": "^5.5|^6|^7",
"illuminate/translation": "^5.5|^6|^7",
- "stichoza/google-translate-php": "^4.0"
+ "stichoza/google-translate-php": "^4.0",
+ "google/cloud-translate": "dev-master",
+ "yandex/translate-api": "dev-master",
+ "ext-json": "*"
},
"extra": {
"laravel": {
@@ -31,9 +34,10 @@
"email": "tanmuhittin@gmail.com"
}
],
- "minimum-stability": "dev",
"require-dev": {
- "phpunit/phpunit": "^8.3@dev",
- "ext-json": "*"
- }
+ "phpunit/phpunit": "^8.3",
+ "orchestra/testbench": "5.x-dev"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
diff --git a/src/Api/GoogleApiTranslate.php b/src/Api/GoogleApiTranslate.php
new file mode 100644
index 0000000..a507d9c
--- /dev/null
+++ b/src/Api/GoogleApiTranslate.php
@@ -0,0 +1,34 @@
+handle = new TranslateClient([
+ 'key' => $api_key
+ ]);
+
+ }
+
+ public function translate(string $text, string $locale, string $base_locale): string
+ {
+ if (is_null($base_locale))
+ $result = $this->handle->translate($text, [
+ 'target' => $locale
+ ]);
+ else
+ $result = $this->handle->translate($text, [
+ 'source' => $base_locale,
+ 'target' => $locale
+ ]);
+
+ return $result['text'];
+ }
+}
diff --git a/src/Api/StichozaApiTranslate.php b/src/Api/StichozaApiTranslate.php
new file mode 100644
index 0000000..aed9277
--- /dev/null
+++ b/src/Api/StichozaApiTranslate.php
@@ -0,0 +1,35 @@
+handle = new GoogleTranslate();
+ }
+
+ public function translate(string $text, string $locale, string $base_locale): string
+ {
+ if (is_null($base_locale))
+ $this->handle->setSource();
+ else
+ $this->handle->setSource($base_locale);
+ $this->handle->setTarget($locale);
+ try {
+ return $this->handle->translate($text);
+ } catch (\ErrorException $e) {
+ return false;
+ }
+ }
+}
diff --git a/src/Api/YandexApiTranslate.php b/src/Api/YandexApiTranslate.php
new file mode 100644
index 0000000..ca3cdd1
--- /dev/null
+++ b/src/Api/YandexApiTranslate.php
@@ -0,0 +1,28 @@
+handle = new \Yandex\Translate\Translator($api_key);
+
+ }
+
+ public function translate(string $text, string $locale, string $base_locale): string
+ {
+ try {
+ $translation = $this->handle->translate($text, $base_locale . '-' . $locale);
+ } catch (\Exception $e) {
+ return false;
+ }
+ return $translation['text'][0]; //todo test if works Yandex code is old
+ }
+}
diff --git a/src/ApiTranslateWithAttribute.php b/src/ApiTranslateWithAttribute.php
new file mode 100644
index 0000000..5b8ccae
--- /dev/null
+++ b/src/ApiTranslateWithAttribute.php
@@ -0,0 +1,106 @@
+translator = resolve(ApiTranslatorContract::class);
+ }
+
+ /**
+ * Holds the logic for replacing laravel translation attributes like :attribute
+ * @param $base_locale
+ * @param $locale
+ * @param $text
+ * @return mixed|string
+ */
+ public function translate($text, $locale, $base_locale = null)
+ {
+ $this->api_limit_check();
+
+ $text = $this->pre_handle_parameters($text);
+
+ $translated = $this->translator->translate($text, $locale, $base_locale);
+
+ $translated = $this->post_handle_parameters($translated);
+
+ return $translated;
+ }
+
+ /**
+ * Check if the API request limit reached.
+ */
+ private function api_limit_check()
+ {
+ if ($this->request_count >= $this->request_per_sec) {
+ sleep($this->sleep_for_sec);
+ $this->request_count = 0;
+ }
+ $this->request_count++;
+ }
+
+
+ private function find_parameters($text)
+ {
+ preg_match_all("/(^:|([\s|\:])\:)([a-zA-z])+/", $text, $matches);
+ return $matches[0];
+ }
+
+
+ private function replace_parameters_with_placeholders($text, $parameters)
+ {
+ $parameter_map = [];
+ $i = 1;
+ foreach ($parameters as $match) {
+ $parameter_map ["x" . $i] = $match;
+ $text = str_replace($match, " x" . $i, $text);
+ $i++;
+ }
+ return ['parameter_map' => $parameter_map, 'text' => $text];
+ }
+
+ private function pre_handle_parameters($text)
+ {
+ $parameters = $this->find_parameters($text);
+ $replaced_text_and_parameter_map = $this->replace_parameters_with_placeholders($text, $parameters);
+ $this->parameter_map = $replaced_text_and_parameter_map['parameter_map'];
+ return $replaced_text_and_parameter_map['text'];
+ }
+
+ /**
+ * Put back parameters to translated text
+ * @param $text
+ * @return mixed
+ */
+ private function post_handle_parameters($text)
+ {
+ foreach ($this->parameter_map as $key => $attribute) {
+ $combinations = [
+ $key,
+ substr($key, 0, 1) . " " . substr($key, 1),
+ strtoupper(substr($key, 0, 1)) . " " . substr($key, 1),
+ strtoupper(substr($key, 0, 1)) . substr($key, 1)
+ ];
+ foreach ($combinations as $combination) {
+ $text = str_replace($combination, $attribute, $text, $count);
+ if ($count > 0)
+ break;
+ }
+ }
+ return str_replace(" :", " :", $text);
+ }
+}
diff --git a/src/Commands/TranslateFilesCommand.php b/src/Commands/TranslateFilesCommand.php
index 99788b4..8470f71 100644
--- a/src/Commands/TranslateFilesCommand.php
+++ b/src/Commands/TranslateFilesCommand.php
@@ -3,15 +3,12 @@
namespace Tanmuhittin\LaravelGoogleTranslate\Commands;
use Illuminate\Console\Command;
-use Stichoza\GoogleTranslate\GoogleTranslate;
-use Symfony\Component\Finder\Finder;
+use Tanmuhittin\LaravelGoogleTranslate\TranslationFileTranslators\JsonArrayFileTranslator;
+use Tanmuhittin\LaravelGoogleTranslate\TranslationFileTranslators\PhpArrayFileTranslator;
+use Tanmuhittin\LaravelGoogleTranslate\Contracts\ApiTranslatorContract;
class TranslateFilesCommand extends Command
{
- public static $request_count = 0;
- public static $request_per_sec = 5;
- public static $sleep_for_sec = 1;
-
public $base_locale;
public $locales;
public $excluded_files;
@@ -19,6 +16,8 @@ class TranslateFilesCommand extends Command
public $json;
public $force;
public $verbose;
+
+ protected $translator;
/**
* The name and signature of the console command.
*
@@ -35,13 +34,14 @@ class TranslateFilesCommand extends Command
/**
* TranslateFilesCommand constructor.
+ * @param ApiTranslatorContract $translator
* @param string $base_locale
* @param string $locales
+ * @param string $target_files
* @param bool $force
* @param bool $json
- * @param string $target_files
- * @param string $excluded_files
* @param bool $verbose
+ * @param string $excluded_files
*/
public function __construct($base_locale = 'en', $locales = 'tr,it', $target_files = '', $force = false, $json = false, $verbose = true, $excluded_files = 'auth,pagination,validation,passwords')
{
@@ -61,54 +61,46 @@ public function __construct($base_locale = 'en', $locales = 'tr,it', $target_fil
public function handle()
{
//Collect input
- $this->base_locale = $this->ask('What is base locale?',config('app.locale','en'));
- $this->locales = array_filter(explode(",", $this->ask('What are the target locales? Comma seperate each lang key','tr,it')));
- $should_force = $this->choice('Force overwrite existing translations?',['No','Yes'],'No');
+ $this->base_locale = $this->ask('What is base locale?', config('app.locale', 'en'));
+ $this->locales = array_filter(explode(",", $this->ask('What are the target locales? Comma seperate each lang key', 'tr,it')));
+ $should_force = $this->choice('Force overwrite existing translations?', ['No', 'Yes'], 'No');
$this->force = false;
- if($should_force === 'Yes'){
+ if ($should_force === 'Yes') {
$this->force = true;
}
- $mode = $this->choice('Use text exploration and json translation or php files?',['json','php'],'php');
+ $should_verbose = $this->choice('Verbose each translation?', ['No', 'Yes'], 'Yes');
+ $this->verbose = false;
+ if ($should_verbose === 'Yes') {
+ $this->verbose = true;
+ }
+ $mode = $this->choice('Use text exploration and json translation or php files?', ['json', 'php'], 'php');
$this->json = false;
- if($mode === 'json'){
+ if ($mode === 'json') {
$this->json = true;
+ $file_translator = new JsonArrayFileTranslator($this->base_locale, $this->verbose, $this->force);
}
- if(!$this->json){
- $this->target_files = array_filter(explode(",", $this->ask('Are there specific target files to translate only? ex: file1,file2','')));
- foreach ($this->target_files as $key=>$target_file){
- $this->target_files[$key] = $target_file.'.php';
+ else {
+ $file_translator = new PhpArrayFileTranslator($this->base_locale, $this->verbose, $this->force);
+ $this->target_files = array_filter(explode(",", $this->ask('Are there specific target files to translate only? ex: file1,file2', '')));
+ foreach ($this->target_files as $key => $target_file) {
+ $this->target_files[$key] = $target_file . '.php';
}
- $this->excluded_files = array_filter(explode(",", $this->ask('Are there specific files to exclude?','auth,pagination,validation,passwords')));
- }
- $should_verbose = $this->choice('Verbose each translation?',['No','Yes'],'Yes');
- $this->verbose = false;
- if($should_verbose === 'Yes'){
- $this->verbose = true;
+ $file_translator->setTargetFiles($this->target_files);
+ $this->excluded_files = array_filter(explode(",", $this->ask('Are there specific files to exclude?', 'auth,pagination,validation,passwords')));
+ $file_translator->setExcludedFiles($this->excluded_files);
}
//Start Translating
$bar = $this->output->createProgressBar(count($this->locales));
$bar->start();
$this->line("");
// loop target locales
- if($this->json){
- $this->line("Exploring strings...");
- $stringKeys = $this->explore_strings();
- $this->line('Exploration completed. Let\'s get started');
- }
foreach ($this->locales as $locale) {
if ($locale == $this->base_locale) {
continue;
}
$this->line($this->base_locale . " -> " . $locale . " translating...");
- if($this->json){
- $this->translate_json_array_file($locale,$stringKeys);
- }
- else if ($locale !== 'vendor') {
- if(!is_dir(resource_path('lang/' . $locale))){
- mkdir(resource_path('lang/' . $locale));
- }
- $this->translate_php_array_files($locale);
- }
+ $file_translator->handle($locale);
+ $this->line($this->base_locale . " -> " . $locale . " translated.");
$bar->advance();
$this->line("");
}
@@ -116,298 +108,4 @@ public function handle()
$this->line("");
$this->line("Translations Completed.");
}
-
- /**
- * @param $base_locale
- * @param $locale
- * @param $text
- * @return mixed|null|string
- * @throws \ErrorException
- * @throws \Exception
- */
- public static function translate($base_locale, $locale, $text)
- {
- preg_match_all("/(^:|([\s|\:])\:)([a-zA-z])+/",$text,$matches);
- $parameter_map = [];
- $i = 1;
- foreach($matches[0] as $match){
- $parameter_map ["x".$i]= $match;
- $text = str_replace($match," x".$i,$text);
- $i++;
- }
-
- // Check if the API request limit reached.
- if( self::$request_count >= self::$request_per_sec ){
- sleep(self::$sleep_for_sec);
- self::$request_count = 0; //Reset the $request_count
- }
- self::$request_count++; //Increase the request_count by 1
-
- if(config('laravel_google_translate.google_translate_api_key', false)){
- $translated = self::translate_via_api_key($base_locale, $locale, $text);
- }else{
- $translated = self::translate_via_stichoza($base_locale, $locale, $text);
- }
- foreach ($parameter_map as $key=>$attribute){
- $combinations = [
- $key,
- substr($key,0,1)." ".substr($key,1),
- strtoupper(substr($key,0,1))." ".substr($key,1),
- strtoupper(substr($key,0,1)).substr($key,1)
- ];
- foreach ($combinations as $combination){
- $translated = str_replace($combination,$attribute,$translated,$count);
- if($count > 0)
- break;
- }
- }
- $translated = str_replace(" :"," :",$translated);
- return $translated;
- }
-
- /**
- * @param $base_locale
- * @param $locale
- * @param $text
- * @return null|string
- * @throws \ErrorException
- */
- private static function translate_via_stichoza($base_locale, $locale, $text){
- $tr = new GoogleTranslate();
- $tr->setSource($base_locale);
- $tr->setTarget($locale);
- return $tr->translate($text);
- }
-
- /**
- * @param $base_locale
- * @param $locale
- * @param $text
- * @return mixed
- * @throws \Exception
- */
- private static function translate_via_api_key($base_locale, $locale, $text){
- $apiKey = config('laravel_google_translate.google_translate_api_key', false);
- $url = 'https://www.googleapis.com/language/translate/v2?key=' . $apiKey . '&q=' . rawurlencode($text) . '&source=' . substr($base_locale, 0, strpos($base_locale."_", "_")) . '&target=' . substr($locale, 0, strpos($locale."_", "_"));
- $handle = curl_init();
- curl_setopt($handle, CURLOPT_URL, $url);
- curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
- $response = curl_exec($handle);
- if ($response === false) {
- throw new \Exception(curl_error($handle), curl_errno($handle));
- }
- $responseDecoded = json_decode($response, true);
- curl_close($handle);
-
- if (isset($responseDecoded['error'])) {
- /*$this->error("Google Translate API returned error");
- if (isset($responseDecoded["error"]["message"])) {
- $this->error($responseDecoded["error"]["message"]);
- }*/
- var_dump($responseDecoded);
- exit;
- }
-
- return $responseDecoded['data']['translations'][0]['translatedText'];
- }
-
- /**
- * @param $locale
- * @throws \Exception
- */
- public function translate_php_array_files($locale)
- {
- $files = preg_grep('/^([^.])/', scandir(resource_path('lang/' . $this->base_locale)));
-
- if (count($this->target_files) > 0) {
- $files = $this->target_files;
- }
- foreach ($files as $file) {
- $file = substr($file, 0, -4);
- $already_translateds = [];
- if (file_exists(resource_path('lang/' . $locale . '/' . $file . '.php'))) {
- if($this->verbose)
- $this->line('File already exists: lang/' . $locale . '/' . $file . '.php. Checking missing translations');
- $already_translateds = trans($file, [], $locale);
- }
- if (in_array($file, $this->excluded_files)) {
- continue;
- }
- $to_be_translateds = trans($file, [], $this->base_locale);
- $new_lang = [];
- if(is_array($to_be_translateds)){
- $new_lang = $this->skipMultidensional($to_be_translateds, $already_translateds, $locale);
- }
- //save new lang to new file
- if(!file_exists(resource_path('lang/' . $locale ))){
- mkdir(resource_path('lang/' . $locale ));
- }
- $file = fopen(resource_path('lang/' . $locale . '/' . $file . '.php'), "w+");
- $write_text = "
- *
- * @param array $to_be_translateds
- * @param array $already_translateds
- * @param String $locale
- *
- * @return array
- */
- private function skipMultidensional($to_be_translateds, $already_translateds, $locale){
- $data = [];
- foreach($to_be_translateds as $key => $to_be_translated){
- if ( is_array($to_be_translateds[$key]) ) {
- if( !isset($already_translateds[$key]) ) {
- $already_translateds[$key] = [];
- }
- $data[$key] = $this->skipMultidensional($to_be_translateds[$key], $already_translateds[$key], $locale);
- } else {
- if ( isset($already_translateds[$key]) && $already_translateds[$key] != '' && !$this->force) {
- $data[$key] = $already_translateds[$key];
- if ($this->verbose) {
- $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $data[$key]);
- }
- continue;
- } else {
- $data[$key] = $this->translate_attribute($to_be_translated,$locale);
- }
- }
- }
- return $data;
- }
-
- private function translate_attribute($attribute,$locale){
- if(is_array($attribute)){
- $return = [];
- foreach ($attribute as $k => $t){
- $return[$k] = $this->translate_attribute($t,$locale);
- }
- return $return;
- }else{
- $translated = self::translate($this->base_locale, $locale, $attribute);
- if ($this->verbose) {
- $this->line($attribute . ' : ' . $translated);
- }
- return $translated;
- }
- }
-
- /**
- * @return array
- */
- public function explore_strings(){
- $groupKeys = [];
- $stringKeys = [];
- $functions = config('laravel_google_translate.trans_functions', [
- 'trans',
- 'trans_choice',
- 'Lang::get',
- 'Lang::choice',
- 'Lang::trans',
- 'Lang::transChoice',
- '@lang',
- '@choice',
- '__',
- '\$trans.get',
- '\$t'
- ]);
- $groupPattern = // See https://regex101.com/r/WEJqdL/6
- "[^\w|>]" . // Must not have an alphanum or _ or > before real method
- '(' . implode( '|', $functions ) . ')' . // Must start with one of the functions
- "\(" . // Match opening parenthesis
- "[\'\"]" . // Match " or '
- '(' . // Start a new group to match:
- '[a-zA-Z0-9_-]+' . // Must start with group
- "([.](?! )[^\1)]+)+" . // Be followed by one or more items/keys
- ')' . // Close group
- "[\'\"]" . // Closing quote
- "[\),]"; // Close parentheses or new parameter
- $stringPattern =
- "[^\w]" . // Must not have an alphanum before real method
- '(' . implode( '|', $functions ) . ')' . // Must start with one of the functions
- "\(" . // Match opening parenthesis
- "(?P['\"])" . // Match " or ' and store in {quote}
- "(?P(?:\\\k{quote}|(?!\k{quote}).)*)" . // Match any string that can be {quote} escaped
- "\k{quote}" . // Match " or ' previously matched
- "[\),]"; // Close parentheses or new parameter
- $finder = new Finder();
- $finder->in( base_path() )->exclude( 'storage' )->exclude( 'vendor' )->name( '*.php' )->name( '*.twig' )->name( '*.vue' )->files();
- /** @var \Symfony\Component\Finder\SplFileInfo $file */
- foreach ( $finder as $file ) {
- // Search the current file for the pattern
- if ( preg_match_all( "/$groupPattern/siU", $file->getContents(), $matches ) ) {
- // Get all matches
- foreach ( $matches[ 2 ] as $key ) {
- $groupKeys[] = $key;
- }
- }
- if ( preg_match_all( "/$stringPattern/siU", $file->getContents(), $matches ) ) {
- foreach ( $matches[ 'string' ] as $key ) {
- if ( preg_match( "/(^[a-zA-Z0-9_-]+([.][^\1)\ ]+)+$)/siU", $key, $groupMatches ) ) {
- // group{.group}.key format, already in $groupKeys but also matched here
- // do nothing, it has to be treated as a group
- continue;
- }
- //TODO: This can probably be done in the regex, but I couldn't do it.
- //skip keys which contain namespacing characters, unless they also contain a
- //space, which makes it JSON.
- if ( !( mb_strpos( $key, '::' ) !== FALSE && mb_strpos( $key, '.' ) !== FALSE )
- || mb_strpos( $key, ' ' ) !== FALSE ) {
- $stringKeys[] = $key;
- if($this->verbose){
- $this->line('Found : '.$key);
- }
- }
- }
- }
- }
- // Remove duplicates
- $groupKeys = array_unique( $groupKeys ); // todo: not supporting group keys for now add this feature!
- $stringKeys = array_unique( $stringKeys );
- return $stringKeys;
- }
-
- /**
- * @param $locale
- * @param $stringKeys
- * @throws \ErrorException
- * @throws \Exception
- */
- public function translate_json_array_file($locale,$stringKeys)
- {
- $new_lang = [];
- $json_existing_translations = [];
- if(file_exists(resource_path('lang/' . $locale . '.json'))){
- $json_translations_string = file_get_contents(resource_path('lang/' . $locale . '.json'));
- $json_existing_translations = json_decode($json_translations_string, true);
- }
- foreach ($stringKeys as $to_be_translated){
- //check existing translations
- if(isset($json_existing_translations[$to_be_translated]) &&
- $json_existing_translations[$to_be_translated]!='' &&
- !$this->force)
- {
- $new_lang[$to_be_translated] = $json_existing_translations[$to_be_translated];
- if($this->verbose)
- $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $new_lang[$to_be_translated]);
- continue;
- }
- $new_lang[$to_be_translated] = addslashes(self::translate($this->base_locale, $locale, $to_be_translated));
- if ($this->verbose) {
- $this->line($to_be_translated . ' : ' . $new_lang[$to_be_translated]);
- }
- }
- $file = fopen(resource_path('lang/' . $locale . '.json'), "w+");
- $write_text = json_encode($new_lang, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
- fwrite($file, $write_text);
- fclose($file);
- }
}
diff --git a/src/Contracts/ApiTranslatorContract.php b/src/Contracts/ApiTranslatorContract.php
new file mode 100644
index 0000000..9cb0e30
--- /dev/null
+++ b/src/Contracts/ApiTranslatorContract.php
@@ -0,0 +1,24 @@
+ $p) {
- deleteAll($p);
- }
- return @rmdir($path);
- }
-}
diff --git a/src/Helpers/ConsoleHelper.php b/src/Helpers/ConsoleHelper.php
new file mode 100644
index 0000000..dab869f
--- /dev/null
+++ b/src/Helpers/ConsoleHelper.php
@@ -0,0 +1,13 @@
+verbose) || (isset($this->verbose) && $this->verbose))
+ echo $text . "\n";
+ }
+}
diff --git a/src/LaravelGoogleTranslateServiceProvider.php b/src/LaravelGoogleTranslateServiceProvider.php
index f9d7026..7bcd01d 100644
--- a/src/LaravelGoogleTranslateServiceProvider.php
+++ b/src/LaravelGoogleTranslateServiceProvider.php
@@ -3,7 +3,12 @@
namespace Tanmuhittin\LaravelGoogleTranslate;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Str;
+use Tanmuhittin\LaravelGoogleTranslate\Api\GoogleApiTranslate;
+use Tanmuhittin\LaravelGoogleTranslate\Api\StichozaApiTranslate;
+use Tanmuhittin\LaravelGoogleTranslate\Api\YandexApiTranslate;
use Tanmuhittin\LaravelGoogleTranslate\Commands\TranslateFilesCommand;
+use Tanmuhittin\LaravelGoogleTranslate\Contracts\ApiTranslatorContract;
class LaravelGoogleTranslateServiceProvider extends ServiceProvider
{
@@ -18,7 +23,7 @@ public function boot()
TranslateFilesCommand::class
]);
$this->publishes([
- __DIR__.'/laravel_google_translate.php' => config_path('laravel_google_translate.php'),
+ __DIR__ . '/laravel_google_translate.php' => config_path('laravel_google_translate.php'),
]);
}
@@ -29,8 +34,43 @@ public function boot()
*/
public function register()
{
- if ($this->app['config']->get('laravel_google_translate') === null) {
- $this->app['config']->set('laravel_google_translate', require __DIR__.'/laravel_google_translate.php');
- }
+ $this->app->singleton(ApiTranslatorContract::class, function ($app) {
+ $config = $app->make('config')->get('laravel_google_translate');
+ if ($config['custom_api_translator']!==null){
+ $custom_translator = new $config['custom_api_translator']($config['custom_api_translator_key']);
+ if($custom_translator instanceof ApiTranslatorContract)
+ return $custom_translator;
+ else
+ throw new \Exception($config['custom_api_translator'].' must implement '.ApiTranslatorContract::class);
+ }
+ elseif ($config['google_translate_api_key'] !== null) {
+ return new GoogleApiTranslate($config['google_translate_api_key']);
+ } elseif ($config['yandex_translate_api_key'] !== null) {
+ return new YandexApiTranslate($config['yandex_translate_api_key']);
+ } else {
+ return new StichozaApiTranslate(null);
+ }
+ });
+
+ Str::macro('apiTranslate', function (string $text, string $locale, string $base_locale = null) {
+ if ($base_locale === null) {
+ $config = resolve('config')->get('app');
+ if (!is_null($config['locale'])) {
+ $base_locale = $config['locale'];
+ }
+ }
+ $translator = resolve(ApiTranslatorContract::class);
+ return $translator->translate($text, $locale, $base_locale);
+ });
+ Str::macro('apiTranslateWithAttributes', function (string $text, string $locale, string $base_locale = null) {
+ if ($base_locale === null) {
+ $config = resolve('config')->get('app');
+ if (!is_null($config['locale'])) {
+ $base_locale = $config['locale'];
+ }
+ }
+ $translator = new ApiTranslateWithAttribute;
+ return $translator->translate($text, $locale, $base_locale);
+ });
}
}
diff --git a/src/TranslationFileTranslators/JsonArrayFileTranslator.php b/src/TranslationFileTranslators/JsonArrayFileTranslator.php
new file mode 100644
index 0000000..02cf84a
--- /dev/null
+++ b/src/TranslationFileTranslators/JsonArrayFileTranslator.php
@@ -0,0 +1,135 @@
+base_locale = $base_locale;
+ $this->verbose = $verbose;
+ $this->force = $force;
+ }
+
+ public function handle($target_locale) : void
+ {
+ $stringKeys = $this->explore_strings();
+ $existing_translations = $this->fetch_existing_translations($target_locale);
+ $translated_strings = [];
+ foreach ($stringKeys as $to_be_translated) {
+ //check existing translations
+ if (isset($existing_translations[$to_be_translated]) &&
+ $existing_translations[$to_be_translated] != '' &&
+ !$this->force) {
+ $translated_strings[$to_be_translated] = $existing_translations[$to_be_translated];
+ $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $translated_strings[$to_be_translated]);
+ continue;
+ }
+ $translated_strings[$to_be_translated] = addslashes(Str::apiTranslateWithAttributes($to_be_translated, $target_locale, $this->base_locale));
+ $this->line($to_be_translated . ' : ' . $translated_strings[$to_be_translated]);
+ }
+ $this->write_translated_strings_to_file($translated_strings, $target_locale);
+ return;
+ }
+
+ private function write_translated_strings_to_file($translated_strings,$target_locale){
+ $file = fopen(resource_path('lang/' . $target_locale . '.json'), "w+");
+ $write_text = json_encode($translated_strings, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+ fwrite($file, $write_text);
+ fclose($file);
+ }
+
+ private function fetch_existing_translations($target_locale){
+ $existing_translations = [];
+ if (file_exists(resource_path('lang/' . $target_locale . '.json'))) {
+ $json_translations_string = file_get_contents(resource_path('lang/' . $target_locale . '.json'));
+ $existing_translations = json_decode($json_translations_string, true);
+ }
+ return $existing_translations;
+ }
+
+ /**
+ * copied from Barryvdh\TranslationManager\Manager findTranslations
+ * @return array
+ */
+ private function explore_strings()
+ {
+ $groupKeys = [];
+ $stringKeys = [];
+ $functions = config('laravel_google_translate.trans_functions', [
+ 'trans',
+ 'trans_choice',
+ 'Lang::get',
+ 'Lang::choice',
+ 'Lang::trans',
+ 'Lang::transChoice',
+ '@lang',
+ '@choice',
+ '__',
+ '\$trans.get',
+ '\$t'
+ ]);
+ $groupPattern = // See https://regex101.com/r/WEJqdL/6
+ "[^\w|>]" . // Must not have an alphanum or _ or > before real method
+ '(' . implode('|', $functions) . ')' . // Must start with one of the functions
+ "\(" . // Match opening parenthesis
+ "[\'\"]" . // Match " or '
+ '(' . // Start a new group to match:
+ '[a-zA-Z0-9_-]+' . // Must start with group
+ "([.](?! )[^\1)]+)+" . // Be followed by one or more items/keys
+ ')' . // Close group
+ "[\'\"]" . // Closing quote
+ "[\),]"; // Close parentheses or new parameter
+ $stringPattern =
+ "[^\w]" . // Must not have an alphanum before real method
+ '(' . implode('|', $functions) . ')' . // Must start with one of the functions
+ "\(" . // Match opening parenthesis
+ "(?P['\"])" . // Match " or ' and store in {quote}
+ "(?P(?:\\\k{quote}|(?!\k{quote}).)*)" . // Match any string that can be {quote} escaped
+ "\k{quote}" . // Match " or ' previously matched
+ "[\),]"; // Close parentheses or new parameter
+ $finder = new Finder();
+ $finder->in(base_path())->exclude('storage')->exclude('vendor')->name('*.php')->name('*.twig')->name('*.vue')->files();
+ /** @var \Symfony\Component\Finder\SplFileInfo $file */
+ foreach ($finder as $file) {
+ // Search the current file for the pattern
+ if (preg_match_all("/$groupPattern/siU", $file->getContents(), $matches)) {
+ // Get all matches
+ foreach ($matches[2] as $key) {
+ $groupKeys[] = $key;
+ }
+ }
+ if (preg_match_all("/$stringPattern/siU", $file->getContents(), $matches)) {
+ foreach ($matches['string'] as $key) {
+ if (preg_match("/(^[a-zA-Z0-9_-]+([.][^\1)\ ]+)+$)/siU", $key, $groupMatches)) {
+ // group{.group}.key format, already in $groupKeys but also matched here
+ // do nothing, it has to be treated as a group
+ continue;
+ }
+ //TODO: This can probably be done in the regex, but I couldn't do it.
+ //skip keys which contain namespacing characters, unless they also contain a
+ //space, which makes it JSON.
+ if (!(mb_strpos($key, '::') !== FALSE && mb_strpos($key, '.') !== FALSE)
+ || mb_strpos($key, ' ') !== FALSE) {
+ $stringKeys[] = $key;
+ $this->line('Found : ' . $key);
+ }
+ }
+ }
+ }
+ // Remove duplicates
+ $groupKeys = array_unique($groupKeys); // todo: not supporting group keys for now add this feature!
+ $stringKeys = array_unique($stringKeys);
+ return $stringKeys;
+ }
+}
diff --git a/src/TranslationFileTranslators/PhpArrayFileTranslator.php b/src/TranslationFileTranslators/PhpArrayFileTranslator.php
new file mode 100644
index 0000000..0baa0db
--- /dev/null
+++ b/src/TranslationFileTranslators/PhpArrayFileTranslator.php
@@ -0,0 +1,170 @@
+base_locale = $base_locale;
+ $this->verbose = $verbose;
+ $this->force = $force;
+ }
+
+ public function handle($target_locale) : void
+ {
+ $files = $this->get_translation_files();
+ $this->create_missing_target_folders($target_locale, $files);
+ foreach ($files as $file) {
+ $existing_translations = [];
+ $file_address = $this->get_language_file_address($target_locale, $file.'.php');
+ $this->line($file_address.' is preparing');
+ if (file_exists($file_address)) {
+ $this->line('File already exists');
+ $existing_translations = trans($file, [], $target_locale);
+ $this->line('Existing translations collected');
+ }
+ $to_be_translateds = trans($file, [], $this->base_locale);
+ $this->line('Source text collected');
+ $translations = [];
+ if (is_array($to_be_translateds)) {
+ $translations = $this->handleTranslations($to_be_translateds, $existing_translations, $target_locale);
+ }
+ $this->write_translations_to_file($target_locale, $file, $translations);
+ }
+ return;
+ }
+
+ // file, folder operations:
+
+ private function create_missing_target_folders($target_locale, $files)
+ {
+ $target_locale_folder = $this->get_language_file_address($target_locale);
+ if(!is_dir($target_locale_folder)){
+ mkdir($target_locale_folder);
+ }
+ foreach ($files as $file){
+ if(Str::contains($file, '/')){
+ $folder_address = $this->get_language_file_address($target_locale, Str::of($file)->dirname());
+ if(!is_dir($folder_address)){
+ mkdir($folder_address, 0777, true);
+ }
+ }
+ }
+ }
+
+ private function write_translations_to_file($target_locale, $file, $translations){
+ $file = fopen($this->get_language_file_address($target_locale, $file.'.php'), "w+");
+ $export = var_export($translations, true);
+
+ //use [] notation instead of array()
+ $patterns = [
+ "/array \(/" => '[',
+ "/^([ ]*)\)(,?)$/m" => '$1]$2',
+ "/=>[ ]?\n[ ]+\[/" => '=> [',
+ "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3',
+ ];
+ $export = preg_replace(array_keys($patterns), array_values($patterns), $export);
+
+
+ $write_text = "target_files) > 0) {
+ $files = $this->target_files;
+ }
+ else{
+ $files = [];
+ $dir_contents = preg_grep('/^([^.])/', scandir($this->get_language_file_address($this->base_locale, $folder)));
+ foreach ($dir_contents as $dir_content){
+ if(!is_null($folder))
+ $dir_content = $folder.'/'.$dir_content;
+ if (in_array($this->strip_php_extension($dir_content), $this->excluded_files)) {
+ continue;
+ }
+ if(is_dir($this->get_language_file_address($this->base_locale, $dir_content))){
+ $files = array_merge($files,$this->get_translation_files($dir_content));
+ }
+ else{
+ $files[] = $this->strip_php_extension($dir_content);
+ }
+ }
+ }
+ return $files;
+ }
+
+
+ // in file operations :
+
+ /**
+ * Walks array recursively to find and translate strings
+ *
+ * @param array $to_be_translateds
+ * @param array $existing_translations
+ * @param String $target_locale
+ *
+ * @return array
+ */
+ private function handleTranslations($to_be_translateds, $existing_translations, $target_locale)
+ {
+ $translations = [];
+ foreach ($to_be_translateds as $key => $to_be_translated) {
+ if (is_array($to_be_translated)) {
+ if (!isset($existing_translations[$key])) {
+ $existing_translations[$key] = [];
+ }
+ $translations[$key] = $this->handleTranslations($to_be_translated, $existing_translations[$key], $target_locale);
+ } else {
+ if (isset($existing_translations[$key]) && $existing_translations[$key] != '' && !$this->force) {
+ $translations[$key] = $existing_translations[$key];
+ $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $translations[$key]);
+ continue;
+ } else {
+ $translations[$key] = Str::apiTranslateWithAttributes($to_be_translated, $target_locale, $this->base_locale);
+ $this->line($to_be_translated . ' : ' . $translations[$key]);
+ }
+ }
+ }
+ return $translations;
+ }
+
+ // others
+
+ public function setTargetFiles($target_files)
+ {
+ $this->target_files = $target_files;
+ }
+
+ public function setExcludedFiles($excluded_files)
+ {
+ $this->excluded_files = $excluded_files;
+ }
+}
diff --git a/src/laravel_google_translate.php b/src/laravel_google_translate.php
index ef134c4..7df08e3 100644
--- a/src/laravel_google_translate.php
+++ b/src/laravel_google_translate.php
@@ -1,6 +1,9 @@
null,
+ 'yandex_translate_api_key'=>null,
+ 'custom_api_translator' => null,
+ 'custom_api_translator_key' => null,
'trans_functions' => [
'trans',
'trans_choice',
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 6d1c4c1..39707ad 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -2,11 +2,13 @@
namespace Tanmuhittin\LaravelGoogleTranslateTests;
-abstract class TestCase extends \PHPUnit\Framework\TestCase
+use Tanmuhittin\LaravelGoogleTranslate\LaravelGoogleTranslateServiceProvider;
+use Tests\Laravel\App;
+
+abstract class TestCase extends \Orchestra\Testbench\TestCase
{
- public function __construct()
+ protected function getPackageProviders($app)
{
- parent::__construct();
- include_once __DIR__ . './../src/DeclareFunctionsIfMissing.php';
+ return [LaravelGoogleTranslateServiceProvider::class];
}
}
diff --git a/tests/Unit/CommandTest.php b/tests/Unit/CommandTest.php
deleted file mode 100644
index 67ed5ff..0000000
--- a/tests/Unit/CommandTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-translate_php_array_files('tr');
- $command->translate_php_array_files('it');
- $translated_texts_tr = trans('tests', [], 'tr');
- $translated_texts_it = trans('tests', [], 'it');
- deleteAll(__DIR__ . '/../../test-resources/resources/lang/tr');
- deleteAll(__DIR__ . '/../../test-resources/resources/lang/it');
- $this->assertEquals(count($to_be_translated_texts_sv), count($translated_texts_tr));
- $this->assertEquals(count($to_be_translated_texts_sv), count($translated_texts_it));
- }
-
- public function testTextExplorationAndJsonTranslationsCommand()
- {
- $command = new TranslateFilesCommand('en', 'tr,it', '', $force = false, $json = true, $verbose = false, $excluded_files = 'auth,pagination,validation,passwords');
- $stringKeys = $command->explore_strings();
- $command->translate_json_array_file('tr', $stringKeys);
- $command->translate_json_array_file('it', $stringKeys);
- $tr_translations = json_decode(file_get_contents(__DIR__ . '/../../test-resources/resources/lang/tr.json'), true);
- $it_translations = json_decode(file_get_contents(__DIR__ . '/../../test-resources/resources/lang/it.json'), true);
- unlink(__DIR__ . '/../../test-resources/resources/lang/tr.json');
- unlink(__DIR__ . '/../../test-resources/resources/lang/it.json');
- $this->assertNotEquals(0, count($tr_translations));
- $this->assertNotEquals(0, count($it_translations));
- }
-}
diff --git a/tests/Unit/TranslateFilesCommandTest.php b/tests/Unit/TranslateFilesCommandTest.php
new file mode 100644
index 0000000..ec3b6b1
--- /dev/null
+++ b/tests/Unit/TranslateFilesCommandTest.php
@@ -0,0 +1,41 @@
+app->setBasePath(__DIR__.'/../test-resources');
+ $this->artisan('translate:files')
+ ->expectsQuestion('What is base locale?', 'sv')
+ ->expectsQuestion('What are the target locales? Comma seperate each lang key', 'tr')
+ ->expectsQuestion('Force overwrite existing translations?','1')
+ ->expectsQuestion('Verbose each translation?','1')
+ ->expectsQuestion('Use text exploration and json translation or php files?','php')
+ ->expectsQuestion('Are there specific target files to translate only? ex: file1,file2','')
+ ->expectsQuestion('Are there specific files to exclude?','')
+ ->assertExitCode(0);
+ $this->assertFileExists(resource_path('lang/tr/tests.php'));
+ unlink(resource_path('lang/tr/tests.php'));
+ rmdir(resource_path('lang/tr'));
+ }
+
+ public function testTranslateJsonFilesCommand()
+ {
+ $this->app->setBasePath(__DIR__.'/../test-resources');
+ $this->artisan('translate:files')
+ ->expectsQuestion('What is base locale?', 'sv')
+ ->expectsQuestion('What are the target locales? Comma seperate each lang key', 'tr')
+ ->expectsQuestion('Force overwrite existing translations?','Yes')
+ ->expectsQuestion('Verbose each translation?','Yes')
+ ->expectsQuestion('Use text exploration and json translation or php files?','json')
+ ->assertExitCode(0);
+ $this->assertFileExists(resource_path('lang/tr.json'));
+ unlink(resource_path('lang/tr.json'));
+ }
+}
+
diff --git a/tests/Unit/TranslateTest.php b/tests/Unit/TranslateTest.php
index 78f0f07..3ba5a69 100644
--- a/tests/Unit/TranslateTest.php
+++ b/tests/Unit/TranslateTest.php
@@ -2,15 +2,22 @@
namespace Tanmuhittin\LaravelGoogleTranslateTests\Unit;
-use Tanmuhittin\LaravelGoogleTranslate\Commands\TranslateFilesCommand;
+use Illuminate\Support\Str;
use Tanmuhittin\LaravelGoogleTranslateTests\TestCase;
class TranslateTest extends TestCase
{
public function testTranslate()
{
- $test_text = "Hello :yourname";
- $translated_test_text = TranslateFilesCommand::translate("en", "tr", $test_text);
- $this->assertStringContainsString(":yourname", $translated_test_text);
+ $test_text = 'Hello World';
+ $translated_test_text = Str::apiTranslate($test_text, 'tr', 'en');
+ $this->assertStringContainsStringIgnoringCase('Dünya', $translated_test_text);
+ }
+
+ public function testTranslateWithAttributes(){
+ $test_text = 'My name is :attribute';
+ $translated_test_text = Str::apiTranslateWithAttributes($test_text, 'tr', 'en');
+ $this->assertStringContainsString(':attribute', $translated_test_text);
}
}
+
diff --git a/test-resources/exploration_files/test.blade.php b/tests/test-resources/exploration-resources/test.blade.php
similarity index 100%
rename from test-resources/exploration_files/test.blade.php
rename to tests/test-resources/exploration-resources/test.blade.php
diff --git a/test-resources/exploration_files/test.vue b/tests/test-resources/exploration-resources/test.vue
similarity index 100%
rename from test-resources/exploration_files/test.vue
rename to tests/test-resources/exploration-resources/test.vue
diff --git a/test-resources/resources/lang/sv/tests.php b/tests/test-resources/resources/lang/sv/tests.php
similarity index 100%
rename from test-resources/resources/lang/sv/tests.php
rename to tests/test-resources/resources/lang/sv/tests.php