[DOCS][Dev] Add Internationalisation

This commit is contained in:
Diogo Peralta Cordeiro 2021-08-01 18:50:27 +01:00
parent 47171069c2
commit 477518abf7
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
5 changed files with 104 additions and 43 deletions

View File

@ -28,7 +28,7 @@
- [ORM and Caching](./core/orm_and_caching.md) - [ORM and Caching](./core/orm_and_caching.md)
- [Interfaces](./core/interfaces.md) - [Interfaces](./core/interfaces.md)
- [UI](./core/ui.md) - [UI](./core/ui.md)
- [Internationalization](./core/i18n.md) - [Internationalisation](./core/i18n.md)
- [Utils](./core/util.md) - [Utils](./core/util.md)
- [Queues](./core/queues.md) - [Queues](./core/queues.md)
- [Files](./core/files.md) - [Files](./core/files.md)

View File

@ -16,3 +16,4 @@ The `core` tries to be minimal. The essence of it being various wrappers around
- [Files](./core/files.md); - [Files](./core/files.md);
- [Sessions and Security](./core/security.md); - [Sessions and Security](./core/security.md);
- [HTTP Client](./core/http.md). - [HTTP Client](./core/http.md).
- [Exceptions](./core/exception_handler.md).

View File

@ -1,14 +1,61 @@
### Internationalization and localization Internationalisation
====================
For info on helping with translations, see the platform currently in use Usage
for translations: https://www.transifex.com/projects/p/gnu-social/ -----
Translations use the gettext system <http://www.gnu.org/software/gettext/>. Basic usage is made by calling `App\Core\I18n\I18n::_m`, it works like this:
If you for some reason do not wish to sign up to the Transifex service, ```php
you can review the files in the "locale/" sub-directory of GNU social. // Both will return the string 'test string'
Each plugin also has its own translation files. _m('test string');
_m('test {thing}', ['thing' => 'string']);
```
To get your own site to use all the translated languages, and you are This function also supports ICU format, you can refer to [ICU User Guide](https://unicode-org.github.io/icu/userguide/),
tracking the git repo, you will need to install at least 'gettext' on for more details on how it works. Below you find some examples:
your system and then run: ```php
$ make translations $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);
```

View File

@ -36,7 +36,10 @@
namespace App\Core\I18n; namespace App\Core\I18n;
use App\Util\Common;
use App\Util\Exception\ServerException;
use App\Util\Formatting; use App\Util\Formatting;
use Exception;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Translation\TranslatorInterface; 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; * 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'. * if not in a plugin subdirectory, we'll use the default 'core+intl-icu'.
* *
* @param string $path
*
* @throws ServerException
*
* @return string * @return string
*/ */
public static function _mdomain(string $path): string public static function _mdomain(string $path): string
@ -105,7 +112,7 @@ abstract class I18n
$all_languages = Common::config('site', 'languages'); $all_languages = Common::config('site', 'languages');
preg_match_all('"(((\S\S)-?(\S\S)?)(;q=([0-9.]+))?)\s*(,\s*|$)"', 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) { for ($i = 0; $i < count($http_langs); ++$i) {
if (!empty($http_langs[2][$i])) { if (!empty($http_langs[2][$i])) {
@ -256,7 +263,7 @@ abstract class I18n
$pref = ''; $pref = '';
$op = 'select'; $op = 'select';
} else { } else {
throw new Exception('Invalid variable type. (int|string) only'); throw new ServerException('Invalid variable type. (int|string) only');
} }
$res = "{$var}, {$op}, "; $res = "{$var}, {$op}, ";
@ -298,7 +305,9 @@ abstract class I18n
* _m(string|string[] $msg, array $params) -- message * _m(string|string[] $msg, array $params) -- message
* _m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two * _m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two
* *
* @throws InvalidArgumentException * @param mixed ...$args
*
* @throws ServerException
* *
* @return string * @return string
* *
@ -307,33 +316,33 @@ abstract class I18n
function _m(...$args): string function _m(...$args): string
{ {
// Get the file where this function was called from, reducing the // 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) // and only 2 frames (this and previous)
$domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2); $domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2);
switch (count($args)) { switch (count($args)) {
case 1: case 1:
// Empty parameters, simple message // Empty parameters, simple message
return I18n::$translator->trans($args[0], [], $domain); return I18n::$translator->trans($args[0], [], $domain);
case 3: case 3:
if (is_int($args[2])) { if (is_int($args[2])) {
throw new Exception('Calling `_m()` with an explicit number is deprecated, ' . throw new Exception('Calling `_m()` with an explicit number is deprecated, ' .
'use an explicit parameter'); 'use an explicit parameter');
} }
// Falthrough // Falthrough
// no break // no break
case 2: case 2:
if (is_array($args[0])) { if (is_array($args[0])) {
$args[0] = I18n::formatICU($args[0], $args[1]); $args[0] = I18n::formatICU($args[0], $args[1]);
} }
if (is_string($args[0])) { if (is_string($args[0])) {
$msg = $args[0]; $msg = $args[0];
$params = $args[1] ?? []; $params = $args[1] ?? [];
return I18n::$translator->trans($msg, $params, $domain); return I18n::$translator->trans($msg, $params, $domain);
} }
// Fallthrough // Fallthrough
// no break // no break
default: default:
throw new InvalidArgumentException('Bad parameters to `_m()`'); throw new InvalidArgumentException('Bad parameters to `_m()`');
} }
} }

View File

@ -35,10 +35,13 @@
namespace App\Core\I18n; namespace App\Core\I18n;
use App\Util\Formatting; use App\Util\Formatting;
use ArrayIterator;
use function count;
use InvalidArgumentException;
use Iterator;
use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\Extractor\PhpExtractor;
use Symfony\Component\Translation\Extractor\PhpStringTokenParser; use Symfony\Component\Translation\Extractor\PhpStringTokenParser;
use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogue;
@ -135,7 +138,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
/** /**
* Seeks to a non-whitespace token. * Seeks to a non-whitespace token.
*/ */
private function seekToNextRelevantToken(\Iterator $tokenIterator) private function seekToNextRelevantToken(Iterator $tokenIterator)
{ {
for (; $tokenIterator->valid(); $tokenIterator->next()) { for (; $tokenIterator->valid(); $tokenIterator->next()) {
$t = $tokenIterator->current(); $t = $tokenIterator->current();
@ -148,7 +151,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
private function skipMethodArgument(\Iterator $tokenIterator) private function skipMethodArgument(Iterator $tokenIterator)
{ {
$openBraces = 0; $openBraces = 0;
@ -170,10 +173,11 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
} }
/** /**
* @throws \InvalidArgumentException * @throws InvalidArgumentException
* *
* @return bool * @return bool
* *
*
*/ */
protected function canBeExtracted(string $file) protected function canBeExtracted(string $file)
{ {
@ -195,7 +199,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
* Extracts the message from the iterator while the tokens * Extracts the message from the iterator while the tokens
* match allowed message tokens. * match allowed message tokens.
*/ */
private function getValue(\Iterator $tokenIterator) private function getValue(Iterator $tokenIterator)
{ {
$message = ''; $message = '';
$docToken = ''; $docToken = '';
@ -245,7 +249,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
*/ */
protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
{ {
$tokenIterator = new \ArrayIterator($tokens); $tokenIterator = new ArrayIterator($tokens);
for ($key = 0; $key < $tokenIterator->count(); ++$key) { for ($key = 0; $key < $tokenIterator->count(); ++$key) {
foreach ($this->sequences as $sequence) { foreach ($this->sequences as $sequence) {
@ -262,7 +266,7 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
} elseif (self::MESSAGE_TOKEN === $item) { } elseif (self::MESSAGE_TOKEN === $item) {
$message = $this->getValue($tokenIterator); $message = $this->getValue($tokenIterator);
if (\count($sequence) === ($sequenceKey + 1)) { if (count($sequence) === ($sequenceKey + 1)) {
break; break;
} }
} elseif (self::METHOD_ARGUMENTS_TOKEN === $item) { } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {