Files
hocusfokus-web/kirby/src/Toolkit/I18n.php
Bastian Allgeier 8381ccb96c Upgrade to 3.8.3
2022-12-06 15:37:13 +01:00

306 lines
7.0 KiB
PHP

<?php
namespace Kirby\Toolkit;
use Closure;
use NumberFormatter;
/**
* Localization class, roughly inspired by VueI18n
*
* @package Kirby Toolkit
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class I18n
{
/**
* Custom loader function
*
* @var Closure
*/
public static $load = null;
/**
* Current locale
*
* @var string|\Closure
*/
public static $locale = 'en';
/**
* All registered translations
*
* @var array
*/
public static $translations = [];
/**
* The fallback locale or a
* list of fallback locales
*
* @var string|array|\Closure
*/
public static $fallback = ['en'];
/**
* Cache of `NumberFormatter` objects by locale
*
* @var array
*/
protected static $decimalsFormatters = [];
/**
* Returns the list of fallback locales
*
* @return array
*/
public static function fallbacks(): array
{
if (
is_array(static::$fallback) === true ||
is_string(static::$fallback) === true
) {
return A::wrap(static::$fallback);
}
if (is_callable(static::$fallback) === true) {
return static::$fallback = A::wrap((static::$fallback)());
}
return static::$fallback = ['en'];
}
/**
* Returns singular or plural
* depending on the given number
*
* @param int $count
* @param bool $none If true, 'none' will be returned if the count is 0
* @return string
*/
public static function form(int $count, bool $none = false): string
{
if ($none === true && $count === 0) {
return 'none';
}
return $count === 1 ? 'singular' : 'plural';
}
/**
* Formats a number
*
* @param int|float $number
* @param string $locale
* @return string
*/
public static function formatNumber($number, string $locale = null): string
{
$locale ??= static::locale();
$formatter = static::decimalNumberFormatter($locale);
$number = $formatter?->format($number) ?? $number;
return (string)$number;
}
/**
* Returns the locale code
*
* @return string
*/
public static function locale(): string
{
if (is_string(static::$locale) === true) {
return static::$locale;
}
if (is_callable(static::$locale) === true) {
return static::$locale = (static::$locale)();
}
return static::$locale = 'en';
}
/**
* Translates a given message
* according to the currently set locale
*
* @param string|array $key
* @param string|array|null $fallback
* @param string|null $locale
* @return string|array|null
*/
public static function translate($key, $fallback = null, string $locale = null)
{
$locale ??= static::locale();
if (is_array($key) === true) {
// try to use actual locale
if (isset($key[$locale]) === true) {
return $key[$locale];
}
// try to use language code, e.g. `es` when locale is `es_ES`
$lang = Str::before($locale, '_');
if (isset($key[$lang]) === true) {
return $key[$lang];
}
// use global wildcard as i18n key
if (isset($key['*']) === true) {
return static::translate($key['*'], $key['*']);
}
// use fallback
if (is_array($fallback) === true) {
return $fallback[$locale] ??
$fallback['en'] ??
reset($fallback);
}
return $fallback;
}
if ($translation = static::translation($locale)[$key] ?? null) {
return $translation;
}
if ($fallback !== null) {
return $fallback;
}
foreach (static::fallbacks() as $fallback) {
// skip locales we have already tried
if ($locale === $fallback) {
continue;
}
if ($translation = static::translation($fallback)[$key] ?? null) {
return $translation;
}
}
return null;
}
/**
* Translate by key and then replace
* placeholders in the text
*
* @param string $key
* @param string|array|null $fallback
* @param array|null $replace
* @param string|null $locale
* @return string
*/
public static function template(string $key, $fallback = null, array|null $replace = null, string|null $locale = null): string
{
if (is_array($fallback) === true) {
$replace = $fallback;
$fallback = null;
$locale = null;
}
$template = static::translate($key, $fallback, $locale);
return Str::template($template, $replace, [
'fallback' => '-',
'start' => '{',
'end' => '}'
]);
}
/**
* Returns the current or any other translation
* by locale. If the translation does not exist
* yet, the loader will try to load it, if defined.
*/
public static function translation(string $locale = null): array
{
$locale ??= static::locale();
if (isset(static::$translations[$locale]) === true) {
return static::$translations[$locale];
}
if (static::$load instanceof Closure) {
return static::$translations[$locale] = (static::$load)($locale);
}
// try to use language code, e.g. `es` when locale is `es_ES`
$lang = Str::before($locale, '_');
if (isset(static::$translations[$lang]) === true) {
return static::$translations[$lang];
}
return static::$translations[$locale] = [];
}
/**
* Returns all loaded or defined translations
*/
public static function translations(): array
{
return static::$translations;
}
/**
* Returns (and creates) a decimal number formatter for a given locale
*/
protected static function decimalNumberFormatter(string $locale): NumberFormatter|null
{
if (isset(static::$decimalsFormatters[$locale]) === true) {
return static::$decimalsFormatters[$locale];
}
if (
extension_loaded('intl') !== true ||
class_exists('NumberFormatter') !== true
) {
return null; // @codeCoverageIgnore
}
return static::$decimalsFormatters[$locale] = new NumberFormatter($locale, NumberFormatter::DECIMAL);
}
/**
* Translates amounts
*
* Translation definition options:
* - Translation is a simple string: `{{ count }}` gets replaced in the template
* - Translation is an array with a value for each count: Chooses the correct template and
* replaces `{{ count }}` in the template; if no specific template for the input count is
* defined, the template that is defined last in the translation array is used
* - Translation is a callback with a `$count` argument: Returns the callback return value
*
* @param string $key
* @param int $count
* @param string|null $locale
* @param bool $formatNumber If set to `false`, the count is not formatted
* @return mixed
*/
public static function translateCount(string $key, int $count, string $locale = null, bool $formatNumber = true)
{
$locale ??= static::locale();
$translation = static::translate($key, null, $locale);
if ($translation === null) {
return null;
}
if ($translation instanceof Closure) {
return $translation($count);
}
$message = match (true) {
is_string($translation) => $translation,
isset($translation[$count]) => $translation[$count],
default => end($translation)
};
if ($formatNumber === true) {
$count = static::formatNumber($count, $locale);
}
return str_replace('{{ count }}', $count, $message);
}
}