[DOCS][Dev] Add Internationalisation

This commit is contained in:
Diogo Peralta Cordeiro 2021-08-01 18:50:27 +01:00 committed by Hugo Sales
parent 3dffbdd0b7
commit 27fb2da1d0
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
5 changed files with 104 additions and 43 deletions

View File

@ -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)

View File

@ -16,3 +16,4 @@ The `core` tries to be minimal. The essence of it being various wrappers around
- [Files](./core/files.md);
- [Sessions and Security](./core/security.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
for translations: https://www.transifex.com/projects/p/gnu-social/
Usage
-----
Translations use the gettext system <http://www.gnu.org/software/gettext/>.
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);
```

View File

@ -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()`');
}
}

View File

@ -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) {