├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Api │ ├── GoogleApiTranslate.php │ ├── StichozaApiTranslate.php │ └── YandexApiTranslate.php ├── Commands │ └── TranslateFilesCommand.php ├── Contracts │ ├── ApiTranslatorContract.php │ └── FileTranslatorContract.php ├── Helpers │ ├── ApiLimitHelper.php │ ├── ConfigHelper.php │ ├── ConsoleHelper.php │ └── FileHelper.php ├── LaravelGoogleTranslateServiceProvider.php ├── TranslationFileTranslators │ ├── JsonArrayFileTranslator.php │ └── PhpArrayFileTranslator.php ├── Translators │ ├── ApiTranslate.php │ └── ApiTranslateWithAttribute.php └── config │ └── laravel_google_translate.php └── tests ├── TestCase.php ├── Unit ├── TranslateFilesCommandTest.php └── TranslateTest.php └── test-resources ├── exploration-resources ├── test.blade.php └── test.vue └── resources └── lang └── sv └── tests.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: tanmuhittin 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | .idea/ 4 | docker-compose.yml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 muhittin tan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-google-translate 2 | 3 | * Translate translation files (under /resources/lang) or lang.json files 4 | * Provide extra facade functions Str::apiTranslate and Str::apiTranslateWithAttributes 5 | 6 | by using stichoza/google-translate-php or Google Translate API https://cloud.google.com/translate/ or Yandex Translatin API https://tech.yandex.com/translate/ 7 | 8 | ## Str facade api-translation helpers 9 | This package provides two translation methods for Laravel helper Str 10 | * `Illuminate\Support\Str::apiTranslate` -> Translates texts using your selected api in config 11 | * `Illuminate\Support\Str::apiTranslateWithAttributes` -> Again translates texts using your selected api in config 12 | in addition to that this function ***respects Laravel translation text attributes*** like :name 13 | 14 | ## how to use your own translation api 15 | 16 | * Create your own translation api class by implementing Tanmuhittin\LaravelGoogleTranslate\Contracts\ApiTranslatorContract 17 | * Write your classname in config laravel_google_translate.custom_api_translator . Example : Myclass::class 18 | * Write your custom apikey for your custom class in laravel_google_translate.custom_api_translator_key 19 | 20 | Now all translations will use your custom api. 21 | 22 | ## installation 23 | ```console 24 | composer require tanmuhittin/laravel-google-translate 25 | php artisan vendor:publish --provider="Tanmuhittin\LaravelGoogleTranslate\LaravelGoogleTranslateServiceProvider" 26 | ``` 27 | 28 | If you would like to use stichoza/google-translate-php you do not need an API key. If you would like to use Google Translate API, edit config/laravel_google_translate.php and add your Google Translate API key. 29 | 30 | ```console 31 | php artisan config:cache 32 | ``` 33 | 34 | Then you can run 35 | 36 | ```console 37 | php artisan translate:files 38 | ``` 39 | 40 | See it on action:
41 | 42 | laravel-google-translate 43 | 44 | ## potential issues 45 | 46 | ### SSL certificate problem: unable to get local issuer certificate 47 | https://stackoverflow.com/a/31830614 48 | 49 | ## suggested packages 50 | This package can be used with https://github.com/andrey-helldar/laravel-lang-publisher. 51 | 52 | * Add base Laravel translation files using https://github.com/andrey-helldar/laravel-lang-publisher 53 | * Translate your custom files using this package 54 | 55 | Done 56 | 57 | ## finally 58 | Thank you for using laravel-google-translate :) 59 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tanmuhittin/laravel-google-translate", 3 | "description": "Translate translation files to other languages using google translate api", 4 | "license": "MIT", 5 | "require": { 6 | "illuminate/console": ">=5.1", 7 | "php": ">=8.2", 8 | "illuminate/support": "^5.5|^6|^7|^8|^9|^10|^11|^12", 9 | "illuminate/translation": "^5.5|^6|^7|^8|^9|^10|^11|^12", 10 | "stichoza/google-translate-php": "^5.0.1", 11 | "google/cloud-translate": "^1.7.4", 12 | "yandex/translate-api": "^1.5.2", 13 | "ext-json": "*" 14 | }, 15 | "extra": { 16 | "laravel": { 17 | "providers": [ 18 | "Tanmuhittin\\LaravelGoogleTranslate\\LaravelGoogleTranslateServiceProvider" 19 | ] 20 | } 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Tanmuhittin\\LaravelGoogleTranslate\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tanmuhittin\\LaravelGoogleTranslateTests\\": "tests" 30 | } 31 | }, 32 | "authors": [ 33 | { 34 | "name": "Muhittin Tan", 35 | "email": "tanmuhittin@gmail.com" 36 | } 37 | ], 38 | "require-dev": { 39 | "phpunit/phpunit": "^10.5", 40 | "orchestra/testbench": "5.x-dev|6.x-dev|9.x-dev" 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Unit 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Api/GoogleApiTranslate.php: -------------------------------------------------------------------------------- 1 | handle = new TranslateClient([ 15 | 'key' => $api_key 16 | ]); 17 | 18 | } 19 | 20 | public function translate(string $text, string $locale, string $base_locale = null): string 21 | { 22 | if ($base_locale === null) 23 | $result = $this->handle->translate($text, [ 24 | 'target' => $locale 25 | ]); 26 | else 27 | $result = $this->handle->translate($text, [ 28 | 'source' => $base_locale, 29 | 'target' => $locale 30 | ]); 31 | 32 | return $result['text']; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Api/StichozaApiTranslate.php: -------------------------------------------------------------------------------- 1 | handle = new GoogleTranslate(); 20 | } 21 | 22 | public function translate(string $text, string $locale, string $base_locale = null): string 23 | { 24 | if ($base_locale === null) 25 | $this->handle->setSource(); 26 | else 27 | $this->handle->setSource($base_locale); 28 | $this->handle->setTarget($locale); 29 | try { 30 | return $this->handle->translate($text); 31 | } catch (\ErrorException $e) { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Api/YandexApiTranslate.php: -------------------------------------------------------------------------------- 1 | handle = new \Yandex\Translate\Translator($api_key); 16 | 17 | } 18 | 19 | public function translate(string $text, string $locale, string $base_locale = null): string 20 | { 21 | try { 22 | $translation = $this->handle->translate($text, $base_locale . '-' . $locale); 23 | } catch (\Exception $e) { 24 | return false; 25 | } 26 | return $translation['text'][0]; //todo test if works Yandex code is old 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/TranslateFilesCommand.php: -------------------------------------------------------------------------------- 1 | base_locale = $base_locale; 50 | $this->locales = array_filter(explode(",", $locales)); 51 | $this->target_files = array_filter(explode(",", $target_files)); 52 | $this->force = $force; 53 | $this->json = $json; 54 | $this->verbose = $verbose; 55 | $this->excluded_files = array_filter(explode(",", $excluded_files)); 56 | } 57 | 58 | /** 59 | * @throws \Exception 60 | */ 61 | public function handle() 62 | { 63 | //Collect input 64 | $this->base_locale = $this->ask('What is base locale?', config('app.locale', 'en')); 65 | $this->locales = array_filter(explode(",", $this->ask('What are the target locales? Comma seperate each lang key', config('laravel_google_translate.default_target_locales','tr,it')))); 66 | $should_force = $this->choice('Force overwrite existing translations?', ['No', 'Yes'], 'No'); 67 | $this->force = false; 68 | if ($should_force === 'Yes') { 69 | $this->force = true; 70 | } 71 | $should_verbose = $this->choice('Verbose each translation?', ['No', 'Yes'], 'Yes'); 72 | $this->verbose = false; 73 | if ($should_verbose === 'Yes') { 74 | $this->verbose = true; 75 | } 76 | $mode = $this->choice('Use text exploration and json translation or php files?', ['json', 'php'], 'php'); 77 | $this->json = false; 78 | if ($mode === 'json') { 79 | $this->json = true; 80 | $file_translator = new JsonArrayFileTranslator($this->base_locale, $this->verbose, $this->force); 81 | } 82 | else { 83 | $file_translator = new PhpArrayFileTranslator($this->base_locale, $this->verbose, $this->force); 84 | $this->target_files = array_filter(explode(",", $this->ask('Are there specific target files to translate only? ex: file1,file2', ''))); 85 | foreach ($this->target_files as $key => $target_file) { 86 | $this->target_files[$key] = $target_file; 87 | } 88 | $file_translator->setTargetFiles($this->target_files); 89 | $this->excluded_files = array_filter(explode(",", $this->ask('Are there specific files to exclude?', 'auth,pagination,validation,passwords'))); 90 | $file_translator->setExcludedFiles($this->excluded_files); 91 | } 92 | //Start Translating 93 | $bar = $this->output->createProgressBar(count($this->locales)); 94 | $bar->start(); 95 | $this->line(""); 96 | // loop target locales 97 | foreach ($this->locales as $locale) { 98 | if ($locale == $this->base_locale) { 99 | continue; 100 | } 101 | $this->line($this->base_locale . " -> " . $locale . " translating..."); 102 | $file_translator->handle($locale); 103 | $this->line($this->base_locale . " -> " . $locale . " translated."); 104 | $bar->advance(); 105 | $this->line(""); 106 | } 107 | $bar->finish(); 108 | $this->line(""); 109 | $this->line("Translations Completed."); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Contracts/ApiTranslatorContract.php: -------------------------------------------------------------------------------- 1 | request_count >= $this->request_per_sec) { 20 | sleep($this->sleep_for_sec); 21 | $this->request_count = 0; 22 | } 23 | $this->request_count++; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Helpers/ConfigHelper.php: -------------------------------------------------------------------------------- 1 | get('laravel_google_translate'); 10 | if(!isset($config['api_limit_settings'])){ 11 | $config['api_limit_settings'] = [ 12 | 'no_requests_per_batch' => 5, 13 | 'sleep_time_between_batches' => 1 14 | ]; 15 | } 16 | return $config; 17 | } 18 | 19 | public static function getBaseLocale($base_locale){ 20 | if ($base_locale === null) { 21 | $config = resolve('config'); 22 | if ($config['locale'] !== null) { 23 | $base_locale = $config['locale']; 24 | } 25 | } 26 | return $base_locale; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Helpers/ConsoleHelper.php: -------------------------------------------------------------------------------- 1 | verbose) || (isset($this->verbose) && $this->verbose)) 11 | echo $text . "\n"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Helpers/FileHelper.php: -------------------------------------------------------------------------------- 1 | commands([ 26 | TranslateFilesCommand::class 27 | ]); 28 | $this->publishes([ 29 | __DIR__ . '/config/laravel_google_translate.php' => config_path('laravel_google_translate.php'), 30 | ]); 31 | } 32 | 33 | /** 34 | * Register services. 35 | * 36 | * @return void 37 | */ 38 | public function register() 39 | { 40 | $config = ConfigHelper::getLaravelGoogleTranslateConfig(); 41 | 42 | $this->app->singleton(ApiTranslatorContract::class, function ($app) use ($config) { 43 | if (isset($config['custom_api_translator']) && $config['custom_api_translator']!==null){ 44 | $custom_translator = new $config['custom_api_translator']($config['custom_api_translator_key']); 45 | if($custom_translator instanceof ApiTranslatorContract) 46 | return $custom_translator; 47 | else 48 | throw new \Exception($config['custom_api_translator'].' must implement '.ApiTranslatorContract::class); 49 | } 50 | elseif (isset($config['google_translate_api_key']) && $config['google_translate_api_key'] !== null) { 51 | return new GoogleApiTranslate($config['google_translate_api_key']); 52 | } elseif (isset($config['yandex_translate_api_key']) && $config['yandex_translate_api_key'] !== null) { 53 | return new YandexApiTranslate($config['yandex_translate_api_key']); 54 | } else { 55 | return new StichozaApiTranslate(null); 56 | } 57 | }); 58 | 59 | $this->app->singleton(ApiTranslate::class,function ($app) use ($config){ 60 | return new ApiTranslate( 61 | resolve(ApiTranslatorContract::class), 62 | $config['api_limit_settings']['no_requests_per_batch'], 63 | $config['api_limit_settings']['sleep_time_between_batches'] 64 | ); 65 | }); 66 | 67 | 68 | $this->app->singleton(ApiTranslateWithAttribute::class,function ($app) use ($config){ 69 | return new ApiTranslateWithAttribute( 70 | resolve(ApiTranslatorContract::class), 71 | $config['api_limit_settings']['no_requests_per_batch'], 72 | $config['api_limit_settings']['sleep_time_between_batches'] 73 | ); 74 | }); 75 | 76 | Str::macro('apiTranslate', function (string $text, string $locale, ?string $base_locale = null) { 77 | ConfigHelper::getBaseLocale($base_locale); 78 | $translator = resolve(ApiTranslate::class); 79 | return $translator->translate($text, $locale, $base_locale); 80 | }); 81 | Str::macro('apiTranslateWithAttributes', function (string $text, string $locale, ?string $base_locale = null) { 82 | ConfigHelper::getBaseLocale($base_locale); 83 | $translator = resolve(ApiTranslateWithAttribute::class); 84 | return $translator->translateWithAttributes($text, $locale, $base_locale); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/TranslationFileTranslators/JsonArrayFileTranslator.php: -------------------------------------------------------------------------------- 1 | base_locale = $base_locale; 21 | $this->verbose = $verbose; 22 | $this->force = $force; 23 | } 24 | 25 | public function handle($target_locale) : void 26 | { 27 | $stringKeys = $this->explore_strings(); 28 | $existing_translations = $this->fetch_existing_translations($target_locale); 29 | $translated_strings = []; 30 | foreach ($stringKeys as $to_be_translated) { 31 | //check existing translations 32 | if (isset($existing_translations[$to_be_translated]) && 33 | $existing_translations[$to_be_translated] != '' && 34 | !$this->force) { 35 | $translated_strings[$to_be_translated] = $existing_translations[$to_be_translated]; 36 | $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $translated_strings[$to_be_translated]); 37 | continue; 38 | } 39 | $translated_strings[$to_be_translated] = addslashes(Str::apiTranslateWithAttributes($to_be_translated, $target_locale, $this->base_locale)); 40 | $this->line($to_be_translated . ' : ' . $translated_strings[$to_be_translated]); 41 | } 42 | $this->write_translated_strings_to_file($translated_strings, $target_locale); 43 | return; 44 | } 45 | 46 | private function write_translated_strings_to_file($translated_strings,$target_locale){ 47 | $file = fopen(FileHelper::getFile($target_locale . '.json'), "w+"); 48 | $write_text = json_encode($translated_strings, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 49 | fwrite($file, $write_text); 50 | fclose($file); 51 | } 52 | 53 | private function fetch_existing_translations($target_locale){ 54 | $existing_translations = []; 55 | if (file_exists(FileHelper::getFile($target_locale . '.json'))) { 56 | $json_translations_string = file_get_contents(FileHelper::getFile($target_locale . '.json')); 57 | $existing_translations = json_decode($json_translations_string, true); 58 | } 59 | return $existing_translations; 60 | } 61 | 62 | /** 63 | * copied from Barryvdh\TranslationManager\Manager findTranslations 64 | * @return array 65 | */ 66 | private function explore_strings() 67 | { 68 | $groupKeys = []; 69 | $stringKeys = []; 70 | $functions = config('laravel_google_translate.trans_functions', [ 71 | 'trans', 72 | 'trans_choice', 73 | 'Lang::get', 74 | 'Lang::choice', 75 | 'Lang::trans', 76 | 'Lang::transChoice', 77 | '@lang', 78 | '@choice', 79 | '__', 80 | '\$trans.get', 81 | '\$t' 82 | ]); 83 | $groupPattern = // See https://regex101.com/r/WEJqdL/6 84 | "[^\w|>]" . // Must not have an alphanum or _ or > before real method 85 | '(' . implode('|', $functions) . ')' . // Must start with one of the functions 86 | "\(" . // Match opening parenthesis 87 | "[\'\"]" . // Match " or ' 88 | '(' . // Start a new group to match: 89 | '[a-zA-Z0-9_-]+' . // Must start with group 90 | "([.](?! )[^\1)]+)+" . // Be followed by one or more items/keys 91 | ')' . // Close group 92 | "[\'\"]" . // Closing quote 93 | "[\),]"; // Close parentheses or new parameter 94 | $stringPattern = 95 | "[^\w]" . // Must not have an alphanum before real method 96 | '(' . implode('|', $functions) . ')' . // Must start with one of the functions 97 | "\(" . // Match opening parenthesis 98 | "(?P['\"])" . // Match " or ' and store in {quote} 99 | "(?P(?:\\\k{quote}|(?!\k{quote}).)*)" . // Match any string that can be {quote} escaped 100 | "\k{quote}" . // Match " or ' previously matched 101 | "[\),]"; // Close parentheses or new parameter 102 | $finder = new Finder(); 103 | $finder->in(base_path())->exclude('storage')->exclude('vendor')->name('*.php')->name('*.twig')->name('*.vue')->files(); 104 | /** @var \Symfony\Component\Finder\SplFileInfo $file */ 105 | foreach ($finder as $file) { 106 | // Search the current file for the pattern 107 | if (preg_match_all("/$groupPattern/siU", $file->getContents(), $matches)) { 108 | // Get all matches 109 | foreach ($matches[2] as $key) { 110 | $groupKeys[] = $key; 111 | } 112 | } 113 | if (preg_match_all("/$stringPattern/siU", $file->getContents(), $matches)) { 114 | foreach ($matches['string'] as $key) { 115 | if (preg_match("/(^[a-zA-Z0-9_-]+([.][^\1)\ ]+)+$)/siU", $key, $groupMatches)) { 116 | // group{.group}.key format, already in $groupKeys but also matched here 117 | // do nothing, it has to be treated as a group 118 | continue; 119 | } 120 | //TODO: This can probably be done in the regex, but I couldn't do it. 121 | //skip keys which contain namespacing characters, unless they also contain a 122 | //space, which makes it JSON. 123 | if (!(mb_strpos($key, '::') !== FALSE && mb_strpos($key, '.') !== FALSE) 124 | || mb_strpos($key, ' ') !== FALSE) { 125 | $stringKeys[] = $key; 126 | $this->line('Found : ' . $key); 127 | } 128 | } 129 | } 130 | } 131 | // Remove duplicates 132 | $groupKeys = array_unique($groupKeys); // todo: not supporting group keys for now add this feature! 133 | $stringKeys = array_unique($stringKeys); 134 | return $stringKeys; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/TranslationFileTranslators/PhpArrayFileTranslator.php: -------------------------------------------------------------------------------- 1 | base_locale = $base_locale; 22 | $this->verbose = $verbose; 23 | $this->force = $force; 24 | } 25 | 26 | public function handle($target_locale) : void 27 | { 28 | $files = $this->get_translation_files(); 29 | $this->create_missing_target_folders($target_locale, $files); 30 | foreach ($files as $file) { 31 | $existing_translations = []; 32 | $file_address = $this->get_language_file_address($target_locale, $file.'.php'); 33 | $this->line($file_address.' is preparing'); 34 | if (file_exists($file_address)) { 35 | $this->line('File already exists'); 36 | $existing_translations = trans($file, [], $target_locale); 37 | $this->line('Existing translations collected'); 38 | } 39 | $to_be_translateds = trans($file, [], $this->base_locale); 40 | $this->line('Source text collected'); 41 | $translations = []; 42 | if (is_array($to_be_translateds)) { 43 | $translations = $this->handleTranslations($to_be_translateds, $existing_translations, $target_locale); 44 | } 45 | $this->write_translations_to_file($target_locale, $file, $translations); 46 | } 47 | return; 48 | } 49 | 50 | // file, folder operations: 51 | 52 | private function create_missing_target_folders($target_locale, $files) 53 | { 54 | $target_locale_folder = $this->get_language_file_address($target_locale); 55 | if(!is_dir($target_locale_folder)){ 56 | mkdir($target_locale_folder); 57 | } 58 | foreach ($files as $file){ 59 | if(Str::contains($file, '/')){ 60 | $folder_address = $this->get_language_file_address($target_locale, dirname($file)); 61 | if(!is_dir($folder_address)){ 62 | mkdir($folder_address, 0777, true); 63 | } 64 | } 65 | } 66 | } 67 | 68 | private function write_translations_to_file($target_locale, $file, $translations){ 69 | $file = fopen($this->get_language_file_address($target_locale, $file.'.php'), "w+"); 70 | $export = var_export($translations, true); 71 | 72 | //use [] notation instead of array() 73 | $patterns = [ 74 | "/array \(/" => '[', 75 | "/^([ ]*)\)(,?)$/m" => '$1]$2', 76 | "/=>[ ]?\n[ ]+\[/" => '=> [', 77 | "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$2 => $3', 78 | ]; 79 | $export = preg_replace(array_keys($patterns), array_values($patterns), $export); 80 | 81 | 82 | $write_text = "target_files) > 0) { 103 | $files = $this->target_files; 104 | } 105 | else{ 106 | $files = []; 107 | $dir_contents = preg_grep('/^([^.])/', scandir($this->get_language_file_address($this->base_locale, $folder))); 108 | foreach ($dir_contents as $dir_content){ 109 | if(!is_null($folder)) 110 | $dir_content = $folder.'/'.$dir_content; 111 | if (in_array($this->strip_php_extension($dir_content), $this->excluded_files)) { 112 | continue; 113 | } 114 | if(is_dir($this->get_language_file_address($this->base_locale, $dir_content))){ 115 | $files = array_merge($files,$this->get_translation_files($dir_content)); 116 | } 117 | else{ 118 | $files[] = $this->strip_php_extension($dir_content); 119 | } 120 | } 121 | } 122 | return $files; 123 | } 124 | 125 | 126 | // in file operations : 127 | 128 | /** 129 | * Walks array recursively to find and translate strings 130 | * 131 | * @param array $to_be_translateds 132 | * @param array $existing_translations 133 | * @param String $target_locale 134 | * 135 | * @return array 136 | */ 137 | private function handleTranslations($to_be_translateds, $existing_translations, $target_locale) 138 | { 139 | $translations = []; 140 | foreach ($to_be_translateds as $key => $to_be_translated) { 141 | if (is_array($to_be_translated)) { 142 | if (!isset($existing_translations[$key])) { 143 | $existing_translations[$key] = []; 144 | } 145 | $translations[$key] = $this->handleTranslations($to_be_translated, $existing_translations[$key], $target_locale); 146 | } else { 147 | if (isset($existing_translations[$key]) && $existing_translations[$key] != '' && !$this->force) { 148 | $translations[$key] = $existing_translations[$key]; 149 | $this->line('Exists Skipping -> ' . $to_be_translated . ' : ' . $translations[$key]); 150 | continue; 151 | } else { 152 | $translations[$key] = Str::apiTranslateWithAttributes($to_be_translated, $target_locale, $this->base_locale); 153 | $this->line($to_be_translated . ' : ' . $translations[$key]); 154 | } 155 | } 156 | } 157 | return $translations; 158 | } 159 | 160 | // others 161 | 162 | public function setTargetFiles($target_files) 163 | { 164 | $this->target_files = $target_files; 165 | } 166 | 167 | public function setExcludedFiles($excluded_files) 168 | { 169 | $this->excluded_files = $excluded_files; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Translators/ApiTranslate.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 18 | $this->request_per_sec = $request_per_second; 19 | $this->sleep_for_sec = $sleep_for_sec; 20 | } 21 | 22 | public function translate($text, $locale, $base_locale = null) : string 23 | { 24 | $this->api_limit_check(); 25 | return $this->translator->translate($text, $locale, $base_locale); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Translators/ApiTranslateWithAttribute.php: -------------------------------------------------------------------------------- 1 | api_limit_check(); 28 | 29 | $text = $this->pre_handle_parameters($text); 30 | 31 | $translated = $this->translator->translate($text, $locale, $base_locale); 32 | 33 | $translated = $this->post_handle_parameters($translated); 34 | 35 | return $translated; 36 | } 37 | 38 | 39 | private function find_parameters($text) 40 | { 41 | preg_match_all("/(^:|([\s|\:])\:)([a-zA-z])+/", $text, $matches); 42 | return $matches[0]; 43 | } 44 | 45 | 46 | private function replace_parameters_with_placeholders($text, $parameters) 47 | { 48 | $parameter_map = []; 49 | $i = 1; 50 | foreach ($parameters as $match) { 51 | $parameter_map ["x" . $i] = $match; 52 | $text = str_replace($match, " x" . $i, $text); 53 | $i++; 54 | } 55 | return ['parameter_map' => $parameter_map, 'text' => $text]; 56 | } 57 | 58 | private function pre_handle_parameters($text) 59 | { 60 | $parameters = $this->find_parameters($text); 61 | $replaced_text_and_parameter_map = $this->replace_parameters_with_placeholders($text, $parameters); 62 | $this->parameter_map = $replaced_text_and_parameter_map['parameter_map']; 63 | return $replaced_text_and_parameter_map['text']; 64 | } 65 | 66 | /** 67 | * Put back parameters to translated text 68 | * @param $text 69 | * @return mixed 70 | */ 71 | private function post_handle_parameters($text) 72 | { 73 | foreach ($this->parameter_map as $key => $attribute) { 74 | $combinations = [ 75 | $key, 76 | substr($key, 0, 1) . " " . substr($key, 1), 77 | strtoupper(substr($key, 0, 1)) . " " . substr($key, 1), 78 | strtoupper(substr($key, 0, 1)) . substr($key, 1) 79 | ]; 80 | foreach ($combinations as $combination) { 81 | $text = str_replace($combination, $attribute, $text, $count); 82 | if ($count > 0) 83 | break; 84 | } 85 | } 86 | return str_replace(" :", " :", $text); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config/laravel_google_translate.php: -------------------------------------------------------------------------------- 1 | env('GOOGLE_TRANSLATE_API_KEY', null), 4 | 'yandex_translate_api_key'=>env('YANDEX_TRANSLATE_API_KEY', null), 5 | 'custom_api_translator' => env('CUSTOM_API_TRANSLATOR', null), 6 | 'custom_api_translator_key' => env('CUSTOM_API_TRANSLATOR_KEY', null), 7 | 'api_limit_settings'=>[ 8 | 'no_requests_per_batch' => env('NO_REQUESTS_PER_BATCH', 5), 9 | 'sleep_time_between_batches' => env('SLEEP_TIME_BETWEEN_BATCHES', 1) 10 | ], 11 | 'default_target_locales'=>'tr,it', 12 | 'trans_functions' => [ 13 | 'trans', 14 | 'trans_choice', 15 | 'Lang::get', 16 | 'Lang::choice', 17 | 'Lang::trans', 18 | 'Lang::transChoice', 19 | '@lang', 20 | '@choice', 21 | '__', 22 | '\$trans.get', 23 | '\$t' 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app->setBasePath(__DIR__.'/../test-resources'); 13 | $this->artisan('translate:files') 14 | ->expectsQuestion('What is base locale?', 'sv') 15 | ->expectsQuestion('What are the target locales? Comma seperate each lang key', 'tr') 16 | ->expectsQuestion('Force overwrite existing translations?','1') 17 | ->expectsQuestion('Verbose each translation?','1') 18 | ->expectsQuestion('Use text exploration and json translation or php files?','php') 19 | ->expectsQuestion('Are there specific target files to translate only? ex: file1,file2','') 20 | ->expectsQuestion('Are there specific files to exclude?','') 21 | ->assertExitCode(0); 22 | $this->assertFileExists(resource_path('lang/tr/tests.php')); 23 | unlink(resource_path('lang/tr/tests.php')); 24 | rmdir(resource_path('lang/tr')); 25 | } 26 | 27 | public function testTranslateJsonFilesCommand() 28 | { 29 | $this->app->setBasePath(__DIR__.'/../test-resources'); 30 | $this->artisan('translate:files') 31 | ->expectsQuestion('What is base locale?', 'sv') 32 | ->expectsQuestion('What are the target locales? Comma seperate each lang key', 'tr') 33 | ->expectsQuestion('Force overwrite existing translations?','Yes') 34 | ->expectsQuestion('Verbose each translation?','Yes') 35 | ->expectsQuestion('Use text exploration and json translation or php files?','json') 36 | ->assertExitCode(0); 37 | $this->assertFileExists(resource_path('lang/tr.json')); 38 | unlink(resource_path('lang/tr.json')); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /tests/Unit/TranslateTest.php: -------------------------------------------------------------------------------- 1 | assertStringContainsStringIgnoringCase('Dünya', $translated_test_text); 15 | } 16 | 17 | public function testTranslateWithAttributes(){ 18 | $test_text = 'My name is :attribute'; 19 | $translated_test_text = Str::apiTranslateWithAttributes($test_text, 'tr', 'en'); 20 | $this->assertStringContainsString(':attribute', $translated_test_text); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /tests/test-resources/exploration-resources/test.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Laravel 8 | 9 | 10 | 11 |
12 |
13 |
14 | Laravel 15 |
16 | 17 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/test-resources/exploration-resources/test.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | -------------------------------------------------------------------------------- /tests/test-resources/resources/lang/sv/tests.php: -------------------------------------------------------------------------------- 1 | "Tidpunkterna måste vara i formatet hh:mm! ", 4 | "text2" => ":name har redan attesterat denna månad!", 5 | "text3" => "Detta krockar med en registrering som :name har gjort mellan klockan :from och :to samma dag!", 6 | "text4" => "Grattis, du hade rätt på :percent% av frågorna på första försöket!", 7 | "text5" => "(Ange :alternatives alternativ)", 8 | ]; 9 | --------------------------------------------------------------------------------