['\"])" . // 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 |28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/test-resources/exploration-resources/test.vue: -------------------------------------------------------------------------------- 1 | 2 |13 |27 |14 | Laravel 15 |16 | 17 |18 | {{ __('Hello World') }} 19 | @trans('Hi There') 20 | News 21 | Blog 22 | Nova 23 | Forge 24 | GitHub 25 |26 |{{ $trans.get('My name is Wubadu') }}3 |{{$t("Who let the dogs out?")}}4 | 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 | --------------------------------------------------------------------------------