├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── check ├── composer.json ├── phpstan.neon └── src ├── Gloss.php ├── GlossServiceProvider.php ├── GlossTranslator.php └── helpers.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ['syntax' => 'short'], 8 | 'binary_operator_spaces' => [ 9 | 'default' => 'single_space', 10 | 'operators' => [ 11 | '=>' => null, 12 | '|' => 'no_space', 13 | ], 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'no_superfluous_phpdoc_tags' => true, 18 | 'blank_line_before_statement' => [ 19 | 'statements' => ['return'], 20 | ], 21 | 'braces' => true, 22 | 'cast_spaces' => true, 23 | 'class_definition' => true, 24 | 'concat_space' => [ 25 | 'spacing' => 'one', 26 | ], 27 | 'declare_equal_normalize' => true, 28 | 'elseif' => true, 29 | 'encoding' => true, 30 | 'full_opening_tag' => true, 31 | 'declare_strict_types' => true, 32 | 'fully_qualified_strict_types' => true, // added by Shift 33 | 'function_declaration' => true, 34 | 'function_typehint_space' => true, 35 | 'heredoc_to_nowdoc' => true, 36 | 'include' => true, 37 | 'increment_style' => ['style' => 'post'], 38 | 'indentation_type' => true, 39 | 'linebreak_after_opening_tag' => true, 40 | 'line_ending' => true, 41 | 'lowercase_cast' => true, 42 | 'constant_case' => true, 43 | 'lowercase_keywords' => true, 44 | 'lowercase_static_reference' => true, // added from Symfony 45 | 'magic_method_casing' => true, // added from Symfony 46 | 'magic_constant_casing' => true, 47 | 'method_argument_space' => true, 48 | 'native_function_casing' => true, 49 | 'no_alias_functions' => true, 50 | 'no_extra_blank_lines' => [ 51 | 'tokens' => [ 52 | 'extra', 53 | 'throw', 54 | 'use', 55 | 'use_trait', 56 | ], 57 | ], 58 | 'no_blank_lines_after_class_opening' => true, 59 | 'no_blank_lines_after_phpdoc' => true, 60 | 'no_closing_tag' => true, 61 | 'no_empty_phpdoc' => true, 62 | 'no_empty_statement' => true, 63 | 'no_leading_import_slash' => true, 64 | 'no_leading_namespace_whitespace' => true, 65 | 'no_mixed_echo_print' => [ 66 | 'use' => 'echo', 67 | ], 68 | 'no_multiline_whitespace_around_double_arrow' => true, 69 | 'multiline_whitespace_before_semicolons' => [ 70 | 'strategy' => 'no_multi_line', 71 | ], 72 | 'no_short_bool_cast' => true, 73 | 'no_singleline_whitespace_before_semicolons' => true, 74 | 'no_spaces_after_function_name' => true, 75 | 'no_spaces_around_offset' => true, 76 | 'no_spaces_inside_parenthesis' => true, 77 | 'no_trailing_comma_in_list_call' => true, 78 | 'no_trailing_comma_in_singleline_array' => true, 79 | 'no_trailing_whitespace' => true, 80 | 'no_trailing_whitespace_in_comment' => true, 81 | 'no_unneeded_control_parentheses' => true, 82 | 'no_unreachable_default_argument_value' => true, 83 | 'no_useless_return' => true, 84 | 'no_whitespace_before_comma_in_array' => true, 85 | 'no_whitespace_in_blank_line' => true, 86 | 'normalize_index_brace' => true, 87 | 'not_operator_with_successor_space' => true, 88 | 'object_operator_without_whitespace' => true, 89 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 90 | 'phpdoc_indent' => true, 91 | 'general_phpdoc_tag_rename' => true, 92 | 'phpdoc_no_access' => true, 93 | 'phpdoc_no_package' => true, 94 | 'phpdoc_no_useless_inheritdoc' => true, 95 | 'phpdoc_scalar' => true, 96 | 'phpdoc_single_line_var_spacing' => true, 97 | 'phpdoc_summary' => true, 98 | 'phpdoc_to_comment' => false, 99 | 'phpdoc_trim' => true, 100 | 'phpdoc_types' => true, 101 | 'phpdoc_var_without_name' => true, 102 | 'psr_autoloading' => true, 103 | 'self_accessor' => true, 104 | 'short_scalar_cast' => true, 105 | 'simplified_null_return' => false, // disabled by Shift 106 | 'single_blank_line_at_eof' => true, 107 | 'single_blank_line_before_namespace' => true, 108 | 'single_class_element_per_statement' => true, 109 | 'single_import_per_statement' => false, 110 | 'single_line_after_imports' => true, 111 | 'no_unused_imports' => true, 112 | 'single_line_comment_style' => [ 113 | 'comment_types' => ['hash'], 114 | ], 115 | 'single_quote' => true, 116 | 'space_after_semicolon' => true, 117 | 'standardize_not_equals' => true, 118 | 'switch_case_semicolon_to_colon' => true, 119 | 'switch_case_space' => true, 120 | 'ternary_operator_spaces' => true, 121 | 'trailing_comma_in_multiline' => true, 122 | 'trim_array_spaces' => true, 123 | 'unary_operator_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]; 126 | 127 | $project_path = getcwd(); 128 | $finder = Finder::create() 129 | ->in([ 130 | $project_path . '/src', 131 | ]) 132 | ->name('*.php') 133 | ->notName('*.blade.php') 134 | ->ignoreDotFiles(true) 135 | ->ignoreVCS(true); 136 | 137 | return (new Config()) 138 | ->setFinder($finder) 139 | ->setRules($rules) 140 | ->setRiskyAllowed(true) 141 | ->setUsingCache(true); 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Samuel Štancl 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 | # 🔍 Gloss ✨ — Brilliant localization for Laravel 2 | 3 | Gloss is a Laravel package for advanced localization. 4 | 5 | Laravel's localization system is perfect for many languages, but it breaks down when you need a bit more control. 6 | 7 | For example, some languages change words depending on the context they're used in. The words may get prefixed, suffixed, or even changed completely. 8 | 9 | Aside from adding support for complex languages, Gloss also ships with quality of life improvements such as the ability to add formatting to a translation string. 10 | 11 | ## Installation 12 | 13 | Laravel 6 or 8 is required, PHP 7.4 is required. 14 | 15 | ``` 16 | composer require leanadmin/gloss 17 | ``` 18 | 19 | ## Configuration 20 | 21 | By default, Gloss comes with the `gloss()` helper and `___()` helper. 22 | 23 | If you wish, you may disable the `___()` helper by setting: 24 | ```php 25 | use Gloss; 26 | 27 | Gloss::$underscoreHelper = false; 28 | ``` 29 | 30 | And if you wish to make all existing `__()` calls Gloss-aware: 31 | ```php 32 | use Gloss; 33 | 34 | Gloss::$shouldReplaceTranslator = true; 35 | ``` 36 | 37 | A good place for these calls is the `boot()` method of your `LeanServiceProvider` or `AppServiceProvider`. 38 | 39 | ## Usage 40 | 41 | Gloss can be used just like the standard Laravel localization helpers: 42 | 43 | ```php 44 | ___('Create :Resource', ['resource' => 'product']); 45 | 46 | // 'resources.edit' => 'Show :Resource :title' 47 | gloss('resources.create', ['resource' => 'product', 'title' => 'MacBook Pro 2019']); // Show Product MacBook Pro 2019 48 | 49 | // 'notifications.updated' => ':Resource :title has been updated!' 50 | Gloss::get('resources.edit', ['resource' => 'product', 'title' => 'iPhone 12']); // Product iPhone 12 has been updated! 51 | 52 | // 'foo.apple' => 'There is one apple|There are many apples' 53 | Gloss::choice('foo.apple', ['count' => 2]); // There are many apples 54 | ``` 55 | 56 | However, unlike the standard localization, it lets you make changes to these strings on the fly: 57 | 58 | ### Value overrides 59 | 60 | Imagine that you're editing the `Order` resource in an admin panel. Your resource's singular label is `Objednávka`, which is Czech for `Order`. 61 | 62 | The language string for the create button is 63 | ```php 64 | // Original in English: 'create' => 'Create :Resource', 65 | 'create' => 'Vytvořit :Resource', 66 | ``` 67 | 68 | If we fill the value with the resource name, we get `Vytvořit Objednávka`. Unfortunately, that's wrong not once, but twice. 69 | 70 | Firstly, it should be Objednávk**u**, because the suffix changes with context. And secondly, it's grammatically incorrect to capitalize the word here. 71 | 72 | ```diff 73 | - Vytvořit Objednávka 74 | + Vytvořit objednávku 75 | ``` 76 | 77 | So we want to specify a **complete override** of that language string whenever we're in the `Order` resource context. 78 | 79 | To do this, simply call: 80 | ```php 81 | Gloss::value('resource.create', 'Vytvořit objednávku'); 82 | ``` 83 | 84 | (From an appropriate place, where the application is working with the Order resource.) 85 | 86 | If you're using Lean, this is taken care of for you. You can simply define entire language strings in the `$lang` property of your resource, and they'll be used in all templates which use the resource. 87 | 88 | Also note that the example above mentions resources, but that's just how Lean is implemented. Gloss will work with any setup. 89 | 90 | You can also set multiple value overrides in a single call: 91 | 92 | ```php 93 | Gloss::values([ 94 | 'resource.create' => 'Vytvořit objednávku', 95 | 'resource.edit' => 'Upravit objednávku', 96 | ]); 97 | ``` 98 | 99 | You may also use the `gloss()` helper for this. Simply pass the array as the first argument. 100 | 101 | ### Scoping overrides 102 | 103 | Sometimes you may want to scope your overrides. For example, rather than overriding all `resource.create` with `Vytvořit objednávku`, you may want to only do that if the `resource` parameter is `order`. 104 | 105 | To do this, pass a third argument: 106 | 107 | ```php 108 | Gloss::value('resource.create', 'Vytvořit objednávku', ['resource' => 'order'); 109 | ``` 110 | 111 | The condition can also be passed to `values()`: 112 | ```php 113 | Gloss::values([ 114 | 'resource.create' => 'Vytvořit objednávku', 115 | 'resource.edit' => 'Upravit objednávku', 116 | ], ['resource' => 'order']); 117 | ``` 118 | 119 | Or to the `gloss()` helper when setting overrides: 120 | 121 | ```php 122 | gloss([ 123 | 'resource.create' => 'Vytvořit objednávku', 124 | 'resource.edit' => 'Upravit objednávku', 125 | ], ['resource' => 'order']); 126 | ``` 127 | 128 | ### Key overrides 129 | 130 | To build up on the example above, let's say our admin panel uses multiple languages. So replacing the language string with a translation that's part of the code isn't feasible. 131 | 132 | For this reason, Gloss also lets you alias keys to another keys: 133 | 134 | ```php 135 | // 'orders.create' => 'Vytvořit objednávku', 136 | // 'resources.create' => 'Vytvořit :resource', 137 | 138 | Gloss::key('resource.create', 'orders.create'); 139 | ``` 140 | 141 | This is equivalent to fetching the value using a translation helper. 142 | ```php 143 | Gloss::value('resource.create', gloss('orders.create')); 144 | Gloss::key('resource.create', 'orders.create'); 145 | ``` 146 | 147 | As with `value()`, you can pass conditions: 148 | ```php 149 | Gloss::key('resource.create', 'orders.create', ['resource' => 'order'); 150 | ``` 151 | 152 | ### Extending values 153 | 154 | You may also build upon fully resolved language strings. 155 | 156 | For example, consider the following example: 157 | ```html 158 | Showing 10 to 20 of 50 results. 159 | ``` 160 | 161 | To localize this, we'd either have to localize each word separately (which is what Laravel does, and it breaks down similarly to the "Order" word example), or we'd have to add the markup to the translation strings (which sucks for security, translator life quality), or we'd have to ditch the formatting completely. 162 | 163 | All of those are unnecessary trade-offs. 164 | 165 | Gloss lets you add formatting after the string is fully built: 166 | 167 | ```php 168 | // 'pagination' => 'Showing :start to :end of :total results', 169 | 170 | Gloss::extend('foo.pagination', fn ($value, $replace) => $replace($value, [ 171 | ':start' => ':start', 172 | ':end' => ':end', 173 | ':total' => ':total', 174 | ])); 175 | 176 | Gloss::get('foo.pagination', ['start' => 10, 'end' => 20, 'total' => 50]) 177 | // Showing 10 to 20 of 50 results 178 | ``` 179 | 180 | Of course, `extend()` works perfectly with localized strings: 181 | ```php 182 | // 'pagination' => 'Zobrazeno :start až :end z :total výsledků', 183 | 184 | // Zobrazeno 1020 z 50 výsledků 185 | ``` 186 | 187 | It even works with pluralized/choice-based strings: 188 | 189 | ```php 190 | // 'apples' => '{0} There are no apples|[1,*]There are :count apples' 191 | 192 | Gloss::extend('foo.apples', fn ($apples, $replace) => $replace($apples, [ 193 | ':count' => ':count', 194 | ])); 195 | 196 | gloss()->choice('foo.apples', 0); // There are no apples 197 | gloss()->choice('foo.apples', 5); // There are 5 apples 198 | ``` 199 | 200 | The second argument is a callable that can return any string, but in most cases this will simply be the resolved string with a few segments replaced. 201 | 202 | For that reason, Gloss automatically passes `$replace` to the callable, which lets you replace parts of the string using beautiful, arrow function-compatible syntax. 203 | 204 | ```php 205 | // So elegant! 206 | 207 | fn ($string, $replace) => $replace($string, [ 208 | 'elegant' => 'eloquent', 209 | ]); 210 | 211 | // So eloquent! 212 | ``` 213 | 214 | ### Callable translation strings 215 | 216 | Gloss also adds support for callable translation strings. 217 | 218 | Those can be useful when you have some code for dealing with things like inflection. 219 | 220 | For example, consider these three language strings: 221 | ```php 222 | 'index' => ':resources', 223 | 'create' => 'Create :resource', 224 | 'edit' => 'Edit :resource :title', 225 | 'delete' => 'Delete :resource :title', 226 | ``` 227 | 228 | In many languages that have declension (inflection of nouns, read more about the complexities of localization on [in our documentation](https://lean-admin.dev/docs/localization)), the form of `:Resource` will be the same for `create`, `edit`, and `delete`. 229 | 230 | It would be painful to translate each string manually for no reason. A better solution is to use intelligent inflection logic **as the default, while still keeping the option to manually change specific strings if needed**. 231 | 232 | ```php 233 | 'index' => fn ($resource) => nominative($resource, 'plural'), 234 | 'create' => fn ($resource) => 'Vytvořit ' . oblique($resource, 'singular'), 235 | 'edit' => fn ($resource, $title) => 'Upravit ' . oblique($resource, 'singular') . $title, 236 | 'delete' => fn ($resource, $title) => 'Smazat ' . oblique($resource, 'singular') . $title, 237 | ``` 238 | 239 | You could have logic like this (with your own helpers) for the default values, and only use the overrides when some words are have irregular grammar rules and need custom values. 240 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | offer_run() { 5 | read -p "For more output, run $1. Run it now (Y/n)? " run 6 | 7 | case ${run:0:1} in 8 | n|N ) 9 | exit 1 10 | ;; 11 | * ) 12 | $1 13 | ;; 14 | esac 15 | 16 | exit 1 17 | } 18 | 19 | if (php-cs-fixer fix --dry-run --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 20 | echo '✅ php-cs-fixer OK' 21 | else 22 | read -p "⚠️ php-cs-fixer found issues. Fix (Y/n)? " fix 23 | case ${fix:0:1} in 24 | n|N ) 25 | echo '❌ php-cs-fixer FAIL' 26 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 27 | ;; 28 | * ) 29 | if (php-cs-fixer fix --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 30 | echo '✅ php-cs-fixer OK' 31 | else 32 | echo '❌ php-cs-fixer FAIL' 33 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 34 | fi 35 | ;; 36 | esac 37 | fi 38 | 39 | if (./vendor/bin/phpstan analyse > /dev/null 2>/dev/null); then 40 | echo '✅ PHPStan OK' 41 | else 42 | echo '❌ PHPStan FAIL' 43 | offer_run './vendor/bin/phpstan analyse' 44 | fi 45 | 46 | if (./vendor/bin/phpunit > /dev/null 2>/dev/null); then 47 | echo '✅ PHPUnit OK' 48 | else 49 | echo '❌ PHPUnit FAIL' 50 | offer_run './vendor/bin/phpunit' 51 | fi 52 | 53 | echo '==================' 54 | echo '✅ Everything OK' 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leanadmin/gloss", 3 | "description": "Brilliant localization for Laravel", 4 | "license": "MIT", 5 | "autoload": { 6 | "psr-4": { 7 | "Lean\\Gloss\\": "src" 8 | }, 9 | "files": [ 10 | "src/helpers.php" 11 | ] 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "Lean\\Gloss\\Tests\\": "tests" 16 | } 17 | }, 18 | "require": { 19 | "illuminate/translation": "^9.0|^10.0" 20 | }, 21 | "require-dev": { 22 | "orchestra/testbench": "^7.0|^8.0", 23 | "phpunit/phpunit": "^9.5", 24 | "nunomaduro/larastan": "^2.4" 25 | }, 26 | "extra": { 27 | "laravel": { 28 | "providers": [ 29 | "Lean\\Gloss\\GlossServiceProvider" 30 | ], 31 | "aliases": { 32 | "Gloss": "Lean\\Gloss\\Gloss" 33 | } 34 | } 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true 38 | } 39 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | 4 | parameters: 5 | paths: 6 | - src 7 | - tests 8 | 9 | level: 8 10 | 11 | universalObjectCratesClasses: 12 | - Illuminate\Routing\Route 13 | 14 | ignoreErrors: 15 | - 16 | message: '#has no return type specified#' 17 | paths: 18 | - tests/* 19 | - src/GlossTranslator.php 20 | - 21 | message: '#Cannot call method (.*?) on Lean\\Gloss\\GlossTranslator\|#' 22 | paths: 23 | - tests/* 24 | - 25 | message: '#with no type specified#' 26 | paths: 27 | - src/GlossTranslator.php 28 | 29 | checkMissingIterableValueType: false 30 | -------------------------------------------------------------------------------- /src/Gloss.php: -------------------------------------------------------------------------------- 1 | app->singleton(Gloss::$containerKey, function ($app) { 15 | /** @var Translator $translator */ 16 | $translator = $app['translator']; 17 | 18 | $trans = new GlossTranslator($translator->getLoader(), $translator->getLocale()); 19 | 20 | $trans->setFallback($app['config']['app.fallback_locale']); 21 | 22 | return $trans; 23 | }); 24 | 25 | if (Gloss::$shouldReplaceTranslator) { 26 | $this->app->extend('translator', fn () => $this->app->make(Gloss::$containerKey)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/GlossTranslator.php: -------------------------------------------------------------------------------- 1 | true; 33 | } elseif (! is_callable($condition)) { 34 | $condition = fn ($data) => array_intersect_assoc($data, $condition) !== []; 35 | } 36 | 37 | $this->keyOverrides[$shortKey][] = [ 38 | 'condition' => $condition, 39 | 'value' => $newKey, 40 | ]; 41 | } 42 | 43 | /** 44 | * Register an override that returns a value. 45 | * 46 | * @param array|null|callable $condition 47 | * @return void 48 | */ 49 | public function value(string $shortKey, string $value, $condition = null) 50 | { 51 | if ($condition === null) { 52 | $condition = fn () => true; 53 | } elseif (! is_callable($condition)) { 54 | $condition = fn ($data) => array_intersect_assoc($data, $condition) !== []; 55 | } 56 | 57 | $this->valueOverrides[$shortKey][] = [ 58 | 'condition' => $condition, 59 | 'value' => $value, 60 | ]; 61 | } 62 | 63 | /** 64 | * Register multiple value overrides. 65 | * 66 | * @param array|null|callable $condition 67 | * @return void 68 | */ 69 | public function values(array $values, $condition = null) 70 | { 71 | /** @var string $key */ 72 | foreach ($values as $key => $value) { 73 | $this->value($key, $value, $condition); 74 | } 75 | } 76 | 77 | /** 78 | * Customize a translation string's value using a callback. 79 | * 80 | * @return void 81 | */ 82 | public function extend(string $shortKey, callable $value) 83 | { 84 | $this->extensions[$shortKey][] = $value; 85 | } 86 | 87 | /** 88 | * Get a translation string. 89 | * 90 | * @param string $key 91 | * @param string|null $locale 92 | * @param bool $fallback 93 | * @return string 94 | */ 95 | public function get($key, array $replace = [], $locale = null, $fallback = true) 96 | { 97 | if (array_key_exists($key, $this->extensions)) { 98 | // We recursively call the same method, but we make sure to skip this branch. 99 | $stringWithoutReplacedVariables = $this->getWithoutExtensions($key, [], $locale, $fallback); 100 | 101 | $replacer = function (string $string, array $replacements) { 102 | foreach ($replacements as $from => $to) { 103 | $string = str_replace($from, $to, $string); 104 | } 105 | 106 | return $string; 107 | }; 108 | 109 | // We run all of the extend() callbacks 110 | $extendedString = $key; 111 | foreach ($this->extensions[$key] as $extension) { 112 | $extendedString = $extension($stringWithoutReplacedVariables, $replacer); 113 | } 114 | 115 | // Finally, we run the string through trans() once again 116 | // to do the replacements in Laravel and potentially 117 | // catch edge case overrides for values in Gloss. 118 | $key = $extendedString; 119 | } 120 | 121 | return $this->getWithoutExtensions($key, $replace, $locale, $fallback); 122 | } 123 | 124 | /** 125 | * Get a translation string and skip extensions. 126 | * 127 | * @param array $replace 128 | * @param string|null $locale 129 | * @param bool $fallback 130 | * @return string 131 | */ 132 | protected function getWithoutExtensions(string $key, $replace = [], $locale = null, $fallback = true) 133 | { 134 | return $this->getKeyOverride($key, $replace) 135 | ?? $this->getValueOverride($key, $replace) 136 | ?? parent::get($key, $replace, $locale, $fallback); 137 | } 138 | 139 | protected function getKeyOverride(string $key, array $data) 140 | { 141 | if (isset($this->keyOverrides[$key])) { 142 | foreach ($this->keyOverrides[$key] as $override) { 143 | if ($override['condition']($data)) { 144 | return $this->get($override['value'], $data); 145 | } 146 | } 147 | } 148 | 149 | return null; 150 | } 151 | 152 | protected function getValueOverride(string $key, array $data) 153 | { 154 | if (isset($this->valueOverrides[$key])) { 155 | foreach ($this->valueOverrides[$key] as $override) { 156 | if ($override['condition']($data)) { 157 | return $this->get($override['value'], $data); 158 | } 159 | } 160 | } 161 | 162 | return null; 163 | } 164 | 165 | public function choice($key, $number, array $replace = [], $locale = null) 166 | { 167 | if (array_key_exists($key, $this->extensions)) { 168 | // We recursively call the same method, but we make sure to skip this branch. 169 | $stringWithoutReplacedVariables = $this->getWithoutExtensions($key, [], $locale); 170 | 171 | $replacer = function (string $string, array $replacements) { 172 | foreach ($replacements as $from => $to) { 173 | $string = str_replace($from, $to, $string); 174 | } 175 | 176 | return $string; 177 | }; 178 | 179 | // We run all of the extend() callbacks 180 | $extendedString = $key; 181 | foreach ($this->extensions[$key] as $extension) { 182 | $extendedString = $extension($stringWithoutReplacedVariables, $replacer); 183 | } 184 | 185 | // Finally, we run the string through trans() once again 186 | // to do the replacements in Laravel and potentially 187 | // catch edge case overrides for values in Gloss. 188 | $key = $extendedString; 189 | } 190 | 191 | return $this->choiceWithoutExtensions($key, $number, $replace, $locale); 192 | } 193 | 194 | protected function choiceWithoutExtensions($key, $number, array $replace = [], $locale = null) 195 | { 196 | $line = $this->getWithoutExtensions( 197 | $key, 198 | $replace, 199 | $locale = $this->localeForChoice($locale) 200 | ); 201 | 202 | // If the given "number" is actually an array or countable we will simply count the 203 | // number of elements in an instance. This allows developers to pass an array of 204 | // items without having to count it on their end first which gives bad syntax. 205 | if (is_array($number) || $number instanceof Countable) { 206 | $number = count($number); 207 | } 208 | 209 | $replace['count'] = $number; 210 | 211 | return $this->makeReplacements( 212 | $this->getSelector()->choose($line, $number, $locale), 213 | $replace 214 | ); 215 | } 216 | 217 | protected function getLine($namespace, $group, $locale, $item, array $replace) 218 | { 219 | $this->load($namespace, $group, $locale); 220 | 221 | $line = Arr::get($this->loaded[$namespace][$group][$locale], $item); 222 | 223 | if (is_string($line) || is_callable($line)) { // Changed 224 | return $this->makeReplacements($line, $replace); 225 | } elseif (is_array($line) && count($line) > 0) { 226 | return $this->makeReplacementsInArray($line, $replace); 227 | } 228 | 229 | return null; 230 | } 231 | 232 | protected function makeReplacementsInArray(array $lines, array $replace): array 233 | { 234 | foreach ($lines as $key => $value) { 235 | if (is_array($value)) { 236 | $value = $this->makeReplacementsInArray($value, $replace); 237 | } else { 238 | $value = $this->makeReplacements($value, $replace); 239 | } 240 | 241 | $lines[$key] = $value; 242 | } 243 | 244 | return $lines; 245 | } 246 | 247 | /** 248 | * @param string|callable $line 249 | * @return string 250 | */ 251 | protected function makeReplacements($line, array $replace) 252 | { 253 | if (is_callable($line) && ! is_string($line)) { 254 | try { 255 | $line = app()->call($line, $replace); 256 | } catch (BindingResolutionException $exception) { 257 | // We keep it a Closure if we can't safely call it 258 | } 259 | } 260 | 261 | return parent::makeReplacements($line, $replace); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |