├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── laravel-chained-translator.php ├── documentation └── img │ └── banner-laravel-chained-translator.png └── src ├── BaseTranslationServiceProvider.php ├── ChainLoader.php ├── ChainedTranslationManager.php ├── Console └── Commands │ └── MergeTranslationsCommand.php ├── Exceptions └── SaveTranslationFileException.php ├── Helpers └── helpers.php ├── NonPackageFileLoader.php └── TranslationServiceProvider.php /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /vendor 3 | /.idea 4 | /.vscode 5 | /.vagrant 6 | npm-debug.log 7 | yarn-error.log 8 | .phpunit.result.cache 9 | composr.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the __Laravel Chained Translator__ will be documented in this file. 4 | 5 | ## Version 2.2.0 6 | ### Added 7 | - Added support for Laravel 10 8 | - Added return types 9 | ## Version 2.1.0 10 | ### Fixed 11 | - Editing translations in nested folders 12 | ### Added 13 | - Support for json translations 14 | - Config option 'group_keys_in_array' 15 | - Config option 'json_group' 16 | 17 | ## Version 2.0.0 18 | ### Updated 19 | - Added support for Laravel 9 and the moved lang dir 20 | 21 | ## Version 1.3.2 22 | ### Fixed 23 | - Merge (custom-lang into lang) command not deleting translation keys anymore. 24 | ### Added 25 | - Gitignore now generates with content 26 | 27 | ## Version 1.2.0 28 | ### Fixed 29 | - Nova does not load all dependencies on Nova tool routes. This fixes the dependencies so the injection can be deferred. 30 | 31 | ## Version 1.1.0 32 | ### Added 33 | - command to merge custom translations into the default translation files. 34 | 35 | ## Version 1.0.0 36 | ### Added 37 | Initial version 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Statik bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Laravel Chained Translator

2 | 3 | # Laravel Chained Translator 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/statikbe/laravel-chained-translator.svg?style=flat-square)](https://packagist.org/packages/statikbe/laravel-chained-translator) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/statikbe/laravel-chained-translator.svg?style=flat-square)](https://packagist.org/packages/statikbe/laravel-chained-translator) 7 | 8 | The chained translator can combine several translators that can override each others translations. Typically, at some 9 | point during the development phase, a content manager wants to translate or finetune the translation strings added by 10 | developers. This often results in merge and versioning issues, when developers and content managers are working on 11 | the translation files at the same time. 12 | 13 | The Chained Translator package allows translations created by developers to exist separately from translations edited by 14 | the content manager in separate `lang` directories. The library merges the translations of both language directories, 15 | where the translations of the content manager (the custom translations) override those of the developer (the default 16 | translations). 17 | 18 | For instance, the default translations created by developers are written in the default Laravel `lang` directory in 19 | `resources/lang`, and the translations by the content manager are added to `resources/lang-custom`. When a translation 20 | key exists in the `resources/lang-custom` directory, this is preferred, otherwise we fallback to the default 21 | translations. 22 | 23 | We offer two package that provide a UI to let content managers edit translations. 24 | - [Laravel Nova Chained Translation Manager](https://github.com/statikbe/laravel-nova-chained-translation-manager) for Laravel Nova 25 | - [Laravel Filament Chained Translation Manager](https://github.com/statikbe/laravel-filament-chained-translation-manager) for Laravel Filament 26 | 27 | ## Installation 28 | 29 | Via composer: 30 | ``` 31 | composer require statikbe/laravel-chained-translator 32 | ``` 33 | 34 | ## Commands 35 | 36 | ### Merge the custom translations back into the default translation files 37 | If you want to combine the translation files made in the current environment by the content manager with the default 38 | translation files, you can use the following command. You need to pass the locale as a parameter, since this library is 39 | agnostic of the locales supported by your Laravel application. Laravel sadly does not have a default supported locales 40 | list. So if you want to merge all files for all supported locales, run this command for each locale. 41 | 42 | For example, for French: 43 | 44 | ```shell script 45 | php artisan chainedtranslator:merge fr 46 | ``` 47 | 48 | This command can be useful to merge the translation work of a translator back into the default translation files. 49 | 50 | ## Configuration 51 | 52 | You can publish the configuration by running this command: 53 | ```bash 54 | php artisan vendor:publish --provider="Statikbe\LaravelChainedTranslator\TranslationServiceProvider" --tag=config 55 | ``` 56 | 57 | ### The following configuration fields are available: 58 | 59 | #### 1. Custom lang directory 60 | By default, the custom translations are saved in `resources/lang-custom`. This can be configured using `custom_lang_directory_name`. 61 | 62 | #### 2. .gitignore in custom lang directory 63 | If `add_gitignore_to_custom_lang_directory` is set to true, a .gitignore file is added to the custom 64 | language directory. 65 | 66 | #### 3. Group keys in nested arrays 67 | If `group_keys_in_array` is set to true, dotted translation keys will be mapped into arrays. 68 | 69 | Set to __true__: saved as nested arrays, f.e. 70 | ```php 71 | 'key' => [ 72 | 'detail' => 'translation', 73 | ] 74 | ``` 75 | 76 | Set to __false__: saved as dotted keys, f.e. 77 | 78 | ```php 79 | 'key.detail' => 'translation', 80 | ``` 81 | 82 | #### 4. Custom json group name 83 | You can edit the group name of all json translations with the `json_group`. 84 | 85 | ## TODO's & Ideas 86 | 87 | - option to overwrite the default `lang` directory. This could be useful on local and staging environments to manage the 88 | developer translations. 89 | 90 | ## Credits 91 | 92 | We used [Joe Dixon's](https://github.com/joedixon) translation libraries as a source of technical expertise and inspiration: 93 | - [Laravel Translation](https://github.com/joedixon/laravel-translation) 94 | 95 | Thanks a lot for the great work! 96 | 97 | ## License 98 | The MIT License (MIT). Please see [license file](LICENSE.md) for more information. 99 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statikbe/laravel-chained-translator", 3 | "description": "The Laravel Chained Translator can combine several translators that can override each others translations.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "statikbe", 7 | "laravel", 8 | "statik", 9 | "translations", 10 | "customer", 11 | "content manager", 12 | "custom", 13 | "chained", 14 | "chain" 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Statikbe\\LaravelChainedTranslator\\": "src/" 19 | }, 20 | "files": [ 21 | "src/Helpers/helpers.php" 22 | ] 23 | }, 24 | "authors": [ 25 | { 26 | "name": "Kobe Christiaensen", 27 | "email": "kobe@statik.be" 28 | }, 29 | { 30 | "name": "Sten Govaerts", 31 | "email": "sten@statik.be" 32 | }, 33 | { 34 | "name": "Johan Maes", 35 | "email": "johan@statik.be" 36 | }, 37 | { 38 | "name": "Kayalion" 39 | } 40 | ], 41 | "require": { 42 | "php": "^8.0|^8.1|^8.2|^8.3|^8.4", 43 | "brick/varexporter": "^0.6", 44 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0" 45 | }, 46 | "config": { 47 | "preferred-install": "dist", 48 | "sort-packages": true 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Statikbe\\LaravelChainedTranslator\\TranslationServiceProvider" 54 | ] 55 | } 56 | }, 57 | "minimum-stability": "dev", 58 | "prefer-stable": true 59 | } 60 | -------------------------------------------------------------------------------- /config/laravel-chained-translator.php: -------------------------------------------------------------------------------- 1 | 'lang-custom', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Adds a .gitignore file to the custom language directory 16 | |-------------------------------------------------------------------------- 17 | | If true, a .gitignore file is added to the custom language directory, if 18 | | the directory does not exist yet. 19 | */ 20 | 'add_gitignore_to_custom_lang_directory' => true, 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Group translation keys in to arrays 25 | |-------------------------------------------------------------------------- 26 | | You can choose if the translations keys are in dotted notation or grouped 27 | | using arrays. 28 | | 29 | | True: saved as nested arrays, f.e. 30 | | 'key' => [ 31 | | 'detail' => 'translation', 32 | | ] 33 | | 34 | | False: saved as dotted keys, f.e. 35 | | 'key.detail' => 'translation', 36 | | 37 | */ 38 | 'group_keys_in_array' => false, 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Json group name 43 | |-------------------------------------------------------------------------- 44 | | You can customize what group is used for all json translations. 45 | */ 46 | 'json_group' => 'json-file', 47 | ]; 48 | -------------------------------------------------------------------------------- /documentation/img/banner-laravel-chained-translator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/statikbe/laravel-chained-translator/d11760671ca27d41526b17762ba570183449e170/documentation/img/banner-laravel-chained-translator.png -------------------------------------------------------------------------------- /src/BaseTranslationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../config/laravel-chained-translator.php' => config_path('laravel-chained-translator.php'), 21 | ], 'config'); 22 | 23 | //register commands: 24 | if ($this->app->runningInConsole()) { 25 | $this->commands([ 26 | MergeTranslationsCommand::class, 27 | ]); 28 | } 29 | } 30 | 31 | /** 32 | * Register the service provider. 33 | * 34 | * @return void 35 | */ 36 | public function register(): void 37 | { 38 | //merge config: 39 | $this->mergeConfigFrom( 40 | __DIR__.'/../config/laravel-chained-translator.php', 'laravel-chained-translator' 41 | ); 42 | 43 | parent::register(); 44 | } 45 | 46 | /** 47 | * Register the translation line loader. 48 | * 49 | * @return void 50 | */ 51 | protected function registerLoader(): void 52 | { 53 | $this->app->singleton('translation.loader.default', function ($app) { 54 | return new FileLoader($app['files'], $app['path.lang']); 55 | }); 56 | 57 | $this->app->singleton('translation.loader.custom', function ($app) { 58 | return new NonPackageFileLoader($app['files'], $app['chained-translator.path.lang.custom']); 59 | }); 60 | 61 | //override the Laravel translation loader singleton: 62 | $this->app->singleton('translation.loader', function ($app) { 63 | $loader = new ChainLoader(); 64 | $loader->addLoader($app['translation.loader.custom']); 65 | $loader->addLoader($app['translation.loader.default']); 66 | 67 | return $loader; 68 | }); 69 | 70 | //added here to make sure when we inject the class name in a constructor this singleton is used: 71 | $this->app->alias('translation.loader', ChainLoader::class); 72 | } 73 | 74 | /** 75 | * Get the services provided by the provider. 76 | * 77 | * @return array 78 | */ 79 | public function provides(): array 80 | { 81 | return array_merge(parent::provides(), [ 82 | 'translation.loader.custom', 83 | 'translation.loader.default', 84 | 'translation.manager', 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ChainLoader.php: -------------------------------------------------------------------------------- 1 | loaders, $loader); 28 | } else { 29 | $this->loaders[] = $loader; 30 | } 31 | } 32 | 33 | /** 34 | * Removes the provided translation loader from the chain 35 | * 36 | * @param Loader $loader 37 | * @return bool True when removed, false otherwise 38 | */ 39 | public function removeLoader(Loader $loader): bool 40 | { 41 | foreach ($this->loaders as $i => $l) { 42 | if ($l === $loader) { 43 | unset($this->loaders[$i]); 44 | 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Gets all the chained loaders 54 | * 55 | * @return array 56 | */ 57 | public function loaders(): array 58 | { 59 | return $this->loaders; 60 | } 61 | 62 | /** 63 | * Load the messages for the given locale. 64 | * 65 | * @param string $locale 66 | * @param string $group 67 | * @param string|null $namespace 68 | * @return array 69 | */ 70 | public function load($locale, $group, $namespace = null): array 71 | { 72 | $messages = []; 73 | 74 | foreach ($this->loaders as $loader) { 75 | $messages = array_replace_recursive($loader->load($locale, $group, $namespace), $messages); 76 | } 77 | 78 | return $messages; 79 | } 80 | 81 | /** 82 | * Add a new namespace to the loader. 83 | * 84 | * @param string $namespace 85 | * @param string $hint 86 | * @return void 87 | */ 88 | public function addNamespace($namespace, $hint): void 89 | { 90 | foreach ($this->loaders as $loader) { 91 | $loader->addNamespace($namespace, $hint); 92 | } 93 | } 94 | 95 | /** 96 | * Add a new JSON path to the loader. 97 | * 98 | * @param string $path 99 | * @return void 100 | */ 101 | public function addJsonPath($path): void 102 | { 103 | foreach ($this->loaders as $loader) { 104 | $loader->addJsonPath($path); 105 | } 106 | } 107 | 108 | /** 109 | * Get an array of all the registered namespaces. 110 | * 111 | * @return array 112 | */ 113 | public function namespaces(): array 114 | { 115 | $namespaces = []; 116 | 117 | foreach ($this->loaders as $loader) { 118 | $namespaces += $loader->namespaces(); 119 | } 120 | 121 | return $namespaces; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/ChainedTranslationManager.php: -------------------------------------------------------------------------------- 1 | path = $path; 36 | $this->files = $files; 37 | $this->translationLoader = $translationLoader; 38 | } 39 | 40 | /** 41 | * Saves a translation 42 | * 43 | * @param string $locale 44 | * @param string $group 45 | * @param string $key 46 | * @param string $translation 47 | * @return void 48 | * @throws SaveTranslationFileException 49 | */ 50 | public function save(string $locale, string $group, string $key, string $translation): void 51 | { 52 | $translations = $this->getCustomTranslations($locale, $group); 53 | 54 | $translations->put($key, $translation); 55 | 56 | $this->saveGroupTranslations($locale, $group, $translations); 57 | } 58 | 59 | /** 60 | * Returns a list of translation groups. A translation group is the file name of the PHP files in the lang 61 | * directory. 62 | * @return array 63 | */ 64 | public function getTranslationGroups(): array 65 | { 66 | $groups = []; 67 | $langDirPath = function_exists('lang_path') ? lang_path() : resource_path('lang'); 68 | $filesAndDirs = $this->files->allFiles($langDirPath); 69 | foreach ($filesAndDirs as $file) { 70 | /* @var SplFileInfo $file */ 71 | if (!$file->isDir()) { 72 | $relativePath = $file->getRelativePath(); 73 | $group = null; 74 | $prefix = null; 75 | $subFolders = null; 76 | $vendorPath = strstr($relativePath, 'vendor'); 77 | 78 | if ($vendorPath) { 79 | $namespace = null; 80 | $vendorPath = Str::replaceFirst('vendor'.DIRECTORY_SEPARATOR, null, $vendorPath); 81 | 82 | //remove locale from vendor path for php files, json files have the locale in the file name, eg. en.json 83 | if (strtolower($file->getExtension()) === 'php') { 84 | $options = explode(DIRECTORY_SEPARATOR, $vendorPath); 85 | $namespace = $options[0]; 86 | unset($options[0]); 87 | unset($options[1]); 88 | $subFolders = implode(DIRECTORY_SEPARATOR, array_filter($options)); 89 | } 90 | 91 | $prefix = $namespace.'::'.$prefix; 92 | } else { 93 | if (strtolower($file->getExtension()) === 'php') { 94 | $options = explode(DIRECTORY_SEPARATOR, $relativePath); 95 | unset($options[0]); 96 | $subFolders = implode(DIRECTORY_SEPARATOR, array_filter($options)); 97 | } 98 | } 99 | if (strtolower($file->getExtension()) === 'php') { 100 | $group = $prefix.implode(DIRECTORY_SEPARATOR, array_filter([$subFolders, $file->getFilenameWithoutExtension()])); 101 | } else { 102 | if (strtolower($file->getExtension()) === 'json') { 103 | $group = $this->getJsonGroupName(); 104 | } 105 | } 106 | 107 | if ($group) { 108 | $groups[$group] = $group; 109 | } 110 | } 111 | } 112 | 113 | return array_values($groups); 114 | } 115 | 116 | public function getTranslationsForGroup(string $locale, string $group): array 117 | { 118 | $namespace = $this->pullNamespaceFromGroup($group); 119 | 120 | if ($group === $this->getJsonGroupName()) { 121 | return $this->compressHierarchicalTranslationsToDotNotation($this->translationLoader->load($locale, '*', '*')); 122 | } 123 | 124 | return $this->compressHierarchicalTranslationsToDotNotation($this->translationLoader->load($locale, $group, $namespace ?? null)); 125 | } 126 | 127 | /** 128 | * @throws SaveTranslationFileException 129 | */ 130 | public function mergeChainedTranslationsIntoDefaultTranslations(string $locale): void { 131 | $defaultLangPath = function_exists('lang_path') ? lang_path() : resource_path('lang'); 132 | if (! $this->localeFolderExists($locale)) { 133 | $this->createLocaleFolder($locale); 134 | } 135 | $groups = $this->getTranslationGroups(); 136 | 137 | foreach($groups as $group) { 138 | $groupWithNamespace = $group; 139 | $namespace = $this->pullNamespaceFromGroup($group); 140 | 141 | if ($group === $this->getJsonGroupName()) { 142 | $translations = collect($this->translationLoader->load($locale, '*', '*')); 143 | } else { 144 | $translations = collect($this->translationLoader->load($locale, $group, $namespace ?? null)); 145 | } 146 | 147 | if ($translations->isNotEmpty()) { 148 | $this->saveGroupTranslations($locale, $groupWithNamespace, $translations, $defaultLangPath); 149 | } 150 | } 151 | } 152 | 153 | private function compressHierarchicalTranslationsToDotNotation(array $translations): array 154 | { 155 | $iteratorIterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($translations)); 156 | $result = []; 157 | foreach ($iteratorIterator as $leafValue) { 158 | $keys = []; 159 | foreach (range(0, $iteratorIterator->getDepth()) as $depth) { 160 | $keys[] = $iteratorIterator->getSubIterator($depth)->key(); 161 | } 162 | $result[ join('.', $keys) ] = $leafValue; 163 | } 164 | return $result; 165 | } 166 | 167 | private function localeFolderExists(string $locale): bool 168 | { 169 | return $this->files->exists($this->path.DIRECTORY_SEPARATOR.$locale); 170 | } 171 | 172 | private function createLocaleFolder(string $locale): bool 173 | { 174 | return $this->files->makeDirectory($this->path.DIRECTORY_SEPARATOR.$locale, 0755, true); 175 | } 176 | 177 | public function getCustomTranslations(string $locale, string $group): Collection 178 | { 179 | $groupPath = $this->getGroupPath($locale, $group); 180 | 181 | if($this->files->exists($groupPath)) { 182 | if ($group === $this->getJsonGroupName()){ 183 | return collect(json_decode(file_get_contents($groupPath))); 184 | } 185 | return collect($this->files->getRequire($groupPath)); 186 | } 187 | 188 | return collect([]); 189 | } 190 | 191 | /** 192 | * @throws SaveTranslationFileException 193 | */ 194 | private function saveGroupTranslations(string $locale, string $group, Collection $translations, string $languagePath=null): void 195 | { 196 | $groupPath = $this->getGroupPath($locale, $group, $languagePath); 197 | $translations = $translations->toArray(); 198 | 199 | if ($group === $this->getJsonGroupName()){ 200 | ksort($translations); 201 | 202 | $contents = json_encode($translations, JSON_PRETTY_PRINT); 203 | } else { 204 | //Decide if dotted keys should stay or should be grouped into arrays 205 | if (config('laravel-chained-translator.group_keys_in_array', true)){ 206 | $translations = array_undot($translations); 207 | } 208 | ksort($translations); 209 | 210 | try { 211 | $contents = "files->put($groupPath, $contents); 219 | 220 | if(!$success){ 221 | throw new SaveTranslationFileException("The translation file $groupPath could not be saved."); 222 | } 223 | 224 | // clear the opcache of the group file, because otherwise in the next request, an old cached file can be read in 225 | // and the saved translation can be overwritten... 226 | if(function_exists('opcache_invalidate')) { 227 | opcache_invalidate($groupPath, true); 228 | } 229 | } 230 | 231 | private function getGroupPath(string $locale, string $group, string $languagePath=null): string 232 | { 233 | if ($group === $this->getJsonGroupName()) { 234 | return ($languagePath ?? $this->path).DIRECTORY_SEPARATOR.$locale.'.json'; 235 | } 236 | 237 | $basePath = $this->getGroupBasePath($locale, $group, $languagePath); 238 | 239 | $this->pullNamespaceFromGroup($group); 240 | $this->pullSubfoldersFromGroup($group); 241 | 242 | return $basePath.DIRECTORY_SEPARATOR.$group.'.php'; 243 | } 244 | 245 | private function getGroupBasePath(string $locale, string $group, string $languagePath=null): string 246 | { 247 | $languagePath = ($languagePath ?? $this->path); 248 | 249 | $namespace = $this->pullNamespaceFromGroup($group); 250 | if ($namespace){ 251 | $namespace = 'vendor'.DIRECTORY_SEPARATOR.$namespace; 252 | } 253 | $subFolders = $this->pullSubfoldersFromGroup($group); 254 | 255 | $groupBasePath = implode(DIRECTORY_SEPARATOR, array_filter([$languagePath, $namespace, $locale, $subFolders])); 256 | 257 | //create directory if not exists: 258 | $this->createDirectory($groupBasePath); 259 | 260 | return $groupBasePath; 261 | } 262 | 263 | private function createDirectory(string $path): void 264 | { 265 | if(!$this->files->exists($path)){ 266 | $this->files->makeDirectory($path, 0755, true); 267 | } 268 | } 269 | 270 | private function pullNamespaceFromGroup(string &$group): ?string 271 | { 272 | $namespace = null; 273 | 274 | if (Str::contains($group, '::')) { 275 | $namespace = Str::before($group, '::'); 276 | $group = Str::after($group, '::'); 277 | } 278 | 279 | return $namespace; 280 | } 281 | 282 | private function pullSubfoldersFromGroup(string &$group): ?string 283 | { 284 | $subFolders = null; 285 | if (Str::contains($group, DIRECTORY_SEPARATOR)){ 286 | $subFolders = Str::beforeLast($group, DIRECTORY_SEPARATOR); 287 | $group = Str::afterLast($group, DIRECTORY_SEPARATOR); 288 | } 289 | 290 | return $subFolders; 291 | } 292 | 293 | /** 294 | * @throws SaveTranslationFileException 295 | */ 296 | private function saveJson(string $locale, string $key, string $translation): void 297 | { 298 | $jsonGroup = $this->getJsonGroupName(); 299 | $translations = $this->getCustomTranslations($locale, $jsonGroup); 300 | 301 | $translations->put($key, $translation); 302 | 303 | $this->saveGroupTranslations($locale, $jsonGroup, $translations); 304 | } 305 | 306 | private function getJsonGroupName(): string 307 | { 308 | return config('laravel-chained-translator.json_group', 'single'); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Console/Commands/MergeTranslationsCommand.php: -------------------------------------------------------------------------------- 1 | argument('locale'); 33 | 34 | try { 35 | $chainedTranslationManager->mergeChainedTranslationsIntoDefaultTranslations($locale); 36 | } 37 | catch(SaveTranslationFileException $ex){ 38 | Log::error($ex); 39 | $this->error($ex->getMessage()); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Exceptions/SaveTranslationFileException.php: -------------------------------------------------------------------------------- 1 | $value) { 16 | // if there is a space after the dot, this could legitimately be 17 | // a single key and not nested. 18 | if (count(explode('. ', $key)) > 1) { 19 | $array[$key] = $value; 20 | } else { 21 | Arr::set($array, $key, $value); 22 | } 23 | } 24 | return $array; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NonPackageFileLoader.php: -------------------------------------------------------------------------------- 1 | hints[$namespace])) { 13 | //We removed the line from FileLoader that loads the translations from the packages in the /vendor folder. 14 | //This is to avoid overwriting published vendor translations done in the default /lang/vendor folder, 15 | //with the translations provided by the packages themselves in /vendor. 16 | return $this->loadNamespaceOverrides([], $locale, $group, $namespace); 17 | } 18 | 19 | return []; 20 | } 21 | } -------------------------------------------------------------------------------- /src/TranslationServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->instance('chained-translator.path.lang.custom', $this->getCustomLangPath()); 18 | 19 | //create custom language directory and add .gitignore file to avoid commits of customer translations: 20 | if (!file_exists($this->app->get('chained-translator.path.lang.custom'))) { 21 | $this->buildCustomLangDir(); 22 | } 23 | 24 | // add the parent dependencies 25 | parent::register(); 26 | 27 | // add the chained translation manager who needs parent dependencies 28 | $this->app->singleton(ChainedTranslationManager::class, function ($app) { 29 | return new ChainedTranslationManager($app['files'], $app['translation.loader'], $app['chained-translator.path.lang.custom']); 30 | }); 31 | } 32 | 33 | /** 34 | * Get the services provided by the provider. 35 | * 36 | * @return array 37 | */ 38 | public function provides(): array 39 | { 40 | return array_merge(parent::provides(), [ 41 | 'chained-translator.path.lang.custom', 42 | ChainedTranslationManager::class, 43 | ]); 44 | } 45 | 46 | private function getCustomLangPath(): string 47 | { 48 | $customLangDirName = config('laravel-chained-translator.custom_lang_directory_name', 'lang-custom'); 49 | 50 | if (!file_exists($this->app->basePath($customLangDirName))) { 51 | if (file_exists($this->app->resourcePath($customLangDirName)) || file_exists($this->app->resourcePath('lang'))) { 52 | return $this->app->resourcePath($customLangDirName); 53 | } 54 | } 55 | 56 | return $this->app->basePath($customLangDirName); 57 | } 58 | 59 | private function buildCustomLangDir(): void 60 | { 61 | /* @var Filesystem $fileSystem */ 62 | $fileSystem = $this->app->get('files'); 63 | $fileSystem->makeDirectory($this->app->get('chained-translator.path.lang.custom'), 0755, true); 64 | if (config('laravel-chained-translator.add_gitignore_to_custom_lang_directory', true)) { 65 | $fileSystem->put($this->app->get('chained-translator.path.lang.custom').'/.gitignore', "*\n!.gitignore\n"); 66 | } 67 | } 68 | } 69 | --------------------------------------------------------------------------------