├── .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 10 až 20 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 |