forked from GNUsocial/gnu-social
[DOCS][Dev] Add Internationalisation
This commit is contained in:
parent
47171069c2
commit
477518abf7
@ -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)
|
||||||
|
@ -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).
|
@ -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);
|
||||||
|
```
|
||||||
|
@ -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()`');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user