diff --git a/docs/developer/src/SUMMARY.md b/docs/developer/src/SUMMARY.md index a18b0fe02e..6231f1d5fa 100644 --- a/docs/developer/src/SUMMARY.md +++ b/docs/developer/src/SUMMARY.md @@ -28,7 +28,7 @@ - [ORM and Caching](./core/orm_and_caching.md) - [Interfaces](./core/interfaces.md) - [UI](./core/ui.md) - - [Internationalization](./core/i18n.md) + - [Internationalisation](./core/i18n.md) - [Utils](./core/util.md) - [Queues](./core/queues.md) - [Files](./core/files.md) diff --git a/docs/developer/src/core.md b/docs/developer/src/core.md index c557e71c98..88af5e6826 100644 --- a/docs/developer/src/core.md +++ b/docs/developer/src/core.md @@ -15,4 +15,5 @@ The `core` tries to be minimal. The essence of it being various wrappers around - [Queues](./core/queues.md); - [Files](./core/files.md); - [Sessions and Security](./core/security.md); -- [HTTP Client](./core/http.md). \ No newline at end of file +- [HTTP Client](./core/http.md). +- [Exceptions](./core/exception_handler.md). \ No newline at end of file diff --git a/docs/developer/src/i18n.md b/docs/developer/src/i18n.md index 015d768f4b..3ba613ceed 100644 --- a/docs/developer/src/i18n.md +++ b/docs/developer/src/i18n.md @@ -1,14 +1,61 @@ -### Internationalization and localization +Internationalisation +==================== -For info on helping with translations, see the platform currently in use -for translations: https://www.transifex.com/projects/p/gnu-social/ +Usage +----- -Translations use the gettext system . -If you for some reason do not wish to sign up to the Transifex service, -you can review the files in the "locale/" sub-directory of GNU social. -Each plugin also has its own translation files. +Basic usage is made by calling `App\Core\I18n\I18n::_m`, it works like this: +```php +// Both will return the string 'test string' +_m('test string'); +_m('test {thing}', ['thing' => 'string']); +``` -To get your own site to use all the translated languages, and you are -tracking the git repo, you will need to install at least 'gettext' on -your system and then run: - $ make translations +This function also supports ICU format, you can refer to [ICU User Guide](https://unicode-org.github.io/icu/userguide/), +for more details on how it works. Below you find some examples: +```php +$apples = [1 => '1 apple', '# apples']; + +_m($apples, ['count' => -42]); // -42 apples +_m($apples, ['count' => 0]); // 0 apples +_m($apples, ['count' => 1]); // 1 apple +_m($apples, ['count' => 2]); // 2 apples +_m($apples, ['count' => 42]); // 42 apples + +$apples = [0 => 'no apples', 1 => '1 apple', '# apples']; +_m($apples, ['count' => 0]); // no apples +_m($apples, ['count' => 1]); // 1 apple +_m($apples, ['count' => 2]); // 2 apples +_m($apples, ['count' => 42]); // 42 apples + +$pronouns = ['she' => 'her apple', 'he' => 'his apple', 'they' => 'their apple', 'someone\'s apple']; +_m($pronouns, ['pronoun' => 'she']); // her apple +_m($pronouns, ['pronoun' => 'he']); // his apple +_m($pronouns, ['pronoun' => 'they']); // their apple +_m($pronouns, ['pronoun' => 'unknown']); // someone's apple + +$complex = [ + 'she' => [1 => 'her apple', 'her # apples'], + 'he' => [1 => 'his apple', 'his # apples'], + 'their' => [1 => 'their apple', 'their # apples'], +]; + +_m($complex, ['pronoun' => 'she', 'count' => 1]); // her apple +_m($complex, ['pronoun' => 'he', 'count' => 1]); // his apple +_m($complex, ['pronoun' => 'she', 'count' => 2]); // her 2 apples +_m($complex, ['pronoun' => 'he', 'count' => 2]); // his 2 apples +_m($complex, ['pronoun' => 'she', 'count' => 42]); // her 42 apples +_m($complex, ['pronoun' => 'they', 'count' => 1]); // their apple +_m($complex, ['pronoun' => 'they', 'count' => 3]); // their 3 apples +``` + +Utilities +--------- + +Some common needs regarding user internationalisation are to know +his language and whether it should be handled Right to left: + +```php +$user_lang = $user->getLanguage(); +App\Core\I18n\I18n::isRtl($user_lang); +``` diff --git a/src/Core/I18n/I18n.php b/src/Core/I18n/I18n.php index 4b8af051b9..bee6ebce0d 100644 --- a/src/Core/I18n/I18n.php +++ b/src/Core/I18n/I18n.php @@ -36,7 +36,10 @@ namespace App\Core\I18n; +use App\Util\Common; +use App\Util\Exception\ServerException; use App\Util\Formatting; +use Exception; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\TranslatorInterface; @@ -70,6 +73,10 @@ abstract class I18n * Looks for which plugin we've been called from to get the gettext domain; * if not in a plugin subdirectory, we'll use the default 'core+intl-icu'. * + * @param string $path + * + * @throws ServerException + * * @return string */ public static function _mdomain(string $path): string @@ -105,7 +112,7 @@ abstract class I18n $all_languages = Common::config('site', 'languages'); preg_match_all('"(((\S\S)-?(\S\S)?)(;q=([0-9.]+))?)\s*(,\s*|$)"', - strtolower($http_accept_lang_header), $http_langs); + strtolower($http_accept_lang_header), $http_langs); for ($i = 0; $i < count($http_langs); ++$i) { if (!empty($http_langs[2][$i])) { @@ -256,7 +263,7 @@ abstract class I18n $pref = ''; $op = 'select'; } else { - throw new Exception('Invalid variable type. (int|string) only'); + throw new ServerException('Invalid variable type. (int|string) only'); } $res = "{$var}, {$op}, "; @@ -298,7 +305,9 @@ abstract class I18n * _m(string|string[] $msg, array $params) -- message * _m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two * - * @throws InvalidArgumentException + * @param mixed ...$args + * + * @throws ServerException * * @return string * @@ -307,33 +316,33 @@ abstract class I18n function _m(...$args): string { // Get the file where this function was called from, reducing the - // memory and performance inpact by not returning the arguments, + // memory and performance impact by not returning the arguments, // and only 2 frames (this and previous) $domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2); switch (count($args)) { - case 1: - // Empty parameters, simple message - return I18n::$translator->trans($args[0], [], $domain); - case 3: - if (is_int($args[2])) { - throw new Exception('Calling `_m()` with an explicit number is deprecated, ' . - 'use an explicit parameter'); - } + case 1: + // Empty parameters, simple message + return I18n::$translator->trans($args[0], [], $domain); + case 3: + if (is_int($args[2])) { + throw new Exception('Calling `_m()` with an explicit number is deprecated, ' . + 'use an explicit parameter'); + } // Falthrough // no break - case 2: - if (is_array($args[0])) { - $args[0] = I18n::formatICU($args[0], $args[1]); - } + case 2: + if (is_array($args[0])) { + $args[0] = I18n::formatICU($args[0], $args[1]); + } - if (is_string($args[0])) { - $msg = $args[0]; - $params = $args[1] ?? []; - return I18n::$translator->trans($msg, $params, $domain); - } + if (is_string($args[0])) { + $msg = $args[0]; + $params = $args[1] ?? []; + return I18n::$translator->trans($msg, $params, $domain); + } // Fallthrough // no break - default: - throw new InvalidArgumentException('Bad parameters to `_m()`'); + default: + throw new InvalidArgumentException('Bad parameters to `_m()`'); } } diff --git a/src/Core/I18n/TransExtractor.php b/src/Core/I18n/TransExtractor.php index 8928df6a73..ae74e249c3 100644 --- a/src/Core/I18n/TransExtractor.php +++ b/src/Core/I18n/TransExtractor.php @@ -35,10 +35,13 @@ namespace App\Core\I18n; use App\Util\Formatting; +use ArrayIterator; +use function count; +use InvalidArgumentException; +use Iterator; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\Extractor\ExtractorInterface; -use Symfony\Component\Translation\Extractor\PhpExtractor; use Symfony\Component\Translation\Extractor\PhpStringTokenParser; use Symfony\Component\Translation\MessageCatalogue; @@ -135,7 +138,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface /** * Seeks to a non-whitespace token. */ - private function seekToNextRelevantToken(\Iterator $tokenIterator) + private function seekToNextRelevantToken(Iterator $tokenIterator) { for (; $tokenIterator->valid(); $tokenIterator->next()) { $t = $tokenIterator->current(); @@ -148,7 +151,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface /** * {@inheritdoc} */ - private function skipMethodArgument(\Iterator $tokenIterator) + private function skipMethodArgument(Iterator $tokenIterator) { $openBraces = 0; @@ -170,10 +173,11 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface } /** - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * * @return bool * + * */ protected function canBeExtracted(string $file) { @@ -195,7 +199,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface * Extracts the message from the iterator while the tokens * match allowed message tokens. */ - private function getValue(\Iterator $tokenIterator) + private function getValue(Iterator $tokenIterator) { $message = ''; $docToken = ''; @@ -245,7 +249,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface */ protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) { - $tokenIterator = new \ArrayIterator($tokens); + $tokenIterator = new ArrayIterator($tokens); for ($key = 0; $key < $tokenIterator->count(); ++$key) { foreach ($this->sequences as $sequence) { @@ -262,7 +266,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface } elseif (self::MESSAGE_TOKEN === $item) { $message = $this->getValue($tokenIterator); - if (\count($sequence) === ($sequenceKey + 1)) { + if (count($sequence) === ($sequenceKey + 1)) { break; } } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {