diff options
Diffstat (limited to 'lib/private/L10N')
-rw-r--r-- | lib/private/L10N/Factory.php | 684 | ||||
-rw-r--r-- | lib/private/L10N/L10N.php | 225 | ||||
-rw-r--r-- | lib/private/L10N/L10NString.php | 74 | ||||
-rw-r--r-- | lib/private/L10N/LanguageIterator.php | 126 | ||||
-rw-r--r-- | lib/private/L10N/LanguageNotFoundException.php | 10 | ||||
-rw-r--r-- | lib/private/L10N/LazyL10N.php | 52 |
6 files changed, 1171 insertions, 0 deletions
diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php new file mode 100644 index 00000000000..6a747744829 --- /dev/null +++ b/lib/private/L10N/Factory.php @@ -0,0 +1,684 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\L10N; + +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\L10N\ILanguageIterator; +use function is_null; + +/** + * A factory that generates language instances + */ +class Factory implements IFactory { + /** @var string */ + protected $requestLanguage = ''; + + /** + * cached instances + * @var array Structure: Lang => App => \OCP\IL10N + */ + protected $instances = []; + + /** + * @var array Structure: App => string[] + */ + protected $availableLanguages = []; + + /** + * @var array + */ + protected $localeCache = []; + + /** + * @var array + */ + protected $availableLocales = []; + + /** + * @var array Structure: string => callable + */ + protected $pluralFunctions = []; + + public const COMMON_LANGUAGE_CODES = [ + 'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it', + 'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko' + ]; + + /** + * Keep in sync with `build/translation-checker.php` + */ + public const RTL_LANGUAGES = [ + 'ar', // Arabic + 'fa', // Persian + 'he', // Hebrew + 'ps', // Pashto, + 'ug', // 'Uyghurche / Uyghur + 'ur_PK', // Urdu + ]; + + private ICache $cache; + + public function __construct( + protected IConfig $config, + protected IRequest $request, + protected IUserSession $userSession, + ICacheFactory $cacheFactory, + protected string $serverRoot, + protected IAppManager $appManager, + ) { + $this->cache = $cacheFactory->createLocal('L10NFactory'); + } + + /** + * Get a language instance + * + * @param string $app + * @param string|null $lang + * @param string|null $locale + * @return \OCP\IL10N + */ + public function get($app, $lang = null, $locale = null) { + return new LazyL10N(function () use ($app, $lang, $locale) { + $app = $this->appManager->cleanAppId($app); + if ($lang !== null) { + $lang = str_replace(['\0', '/', '\\', '..'], '', $lang); + } + + $forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false); + if (is_string($forceLang)) { + $lang = $forceLang; + } + + $forceLocale = $this->config->getSystemValue('force_locale', false); + if (is_string($forceLocale)) { + $locale = $forceLocale; + } + + $lang = $this->validateLanguage($app, $lang); + + if ($locale === null || !$this->localeExists($locale)) { + $locale = $this->findLocale($lang); + } + + if (!isset($this->instances[$lang][$app])) { + $this->instances[$lang][$app] = new L10N( + $this, + $app, + $lang, + $locale, + $this->getL10nFilesForApp($app, $lang) + ); + } + + return $this->instances[$lang][$app]; + }); + } + + /** + * Check that $lang is an existing language and not null, otherwise return the language to use instead + * + * @psalm-taint-escape callable + * @psalm-taint-escape cookie + * @psalm-taint-escape file + * @psalm-taint-escape has_quotes + * @psalm-taint-escape header + * @psalm-taint-escape html + * @psalm-taint-escape include + * @psalm-taint-escape ldap + * @psalm-taint-escape shell + * @psalm-taint-escape sql + * @psalm-taint-escape unserialize + */ + private function validateLanguage(string $app, ?string $lang): string { + if ($lang === null || !$this->languageExists($app, $lang)) { + return $this->findLanguage($app); + } else { + return $lang; + } + } + + /** + * Find the best language + * + * @param string|null $appId App id or null for core + * + * @return string language If nothing works it returns 'en' + */ + public function findLanguage(?string $appId = null): string { + // Step 1: Forced language always has precedence over anything else + $forceLang = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false); + if (is_string($forceLang)) { + $this->requestLanguage = $forceLang; + } + + // Step 2: Return cached language + if ($this->requestLanguage !== '' && $this->languageExists($appId, $this->requestLanguage)) { + return $this->requestLanguage; + } + + /** + * Step 3: At this point Nextcloud might not yet be installed and thus the lookup + * in the preferences table might fail. For this reason we need to check + * whether the instance has already been installed + * + * @link https://github.com/owncloud/core/issues/21955 + */ + if ($this->config->getSystemValueBool('installed', false)) { + $userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() : null; + if (!is_null($userId)) { + $userLang = $this->config->getUserValue($userId, 'core', 'lang', null); + } else { + $userLang = null; + } + } else { + $userId = null; + $userLang = null; + } + if ($userLang) { + $this->requestLanguage = $userLang; + if ($this->languageExists($appId, $userLang)) { + return $userLang; + } + } + + // Step 4: Check the request headers + try { + // Try to get the language from the Request + $lang = $this->getLanguageFromRequest($appId); + if ($userId !== null && $appId === null && !$userLang) { + $this->config->setUserValue($userId, 'core', 'lang', $lang); + } + return $lang; + } catch (LanguageNotFoundException $e) { + // Finding language from request failed fall back to default language + $defaultLanguage = $this->config->getSystemValue('default_language', false); + if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) { + return $defaultLanguage; + } + } + + // Step 5: fall back to English + return 'en'; + } + + public function findGenericLanguage(?string $appId = null): string { + // Step 1: Forced language always has precedence over anything else + $forcedLanguage = $this->request->getParam('forceLanguage') ?? $this->config->getSystemValue('force_language', false); + if ($forcedLanguage !== false) { + return $forcedLanguage; + } + + // Step 2: Check if we have a default language + $defaultLanguage = $this->config->getSystemValue('default_language', false); + if ($defaultLanguage !== false && $this->languageExists($appId, $defaultLanguage)) { + return $defaultLanguage; + } + + // Step 3.1: Check if Nextcloud is already installed before we try to access user info + if (!$this->config->getSystemValueBool('installed', false)) { + return 'en'; + } + // Step 3.2: Check the current user (if any) for their preferred language + $user = $this->userSession->getUser(); + if ($user !== null) { + $userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null); + if ($userLang !== null) { + return $userLang; + } + } + + // Step 4: Check the request headers + try { + return $this->getLanguageFromRequest($appId); + } catch (LanguageNotFoundException $e) { + // Ignore and continue + } + + // Step 5: fall back to English + return 'en'; + } + + /** + * find the best locale + * + * @param string $lang + * @return null|string + */ + public function findLocale($lang = null) { + $forceLocale = $this->config->getSystemValue('force_locale', false); + if (is_string($forceLocale) && $this->localeExists($forceLocale)) { + return $forceLocale; + } + + if ($this->config->getSystemValueBool('installed', false)) { + $userId = $this->userSession->getUser() !== null ? $this->userSession->getUser()->getUID() : null; + $userLocale = null; + if ($userId !== null) { + $userLocale = $this->config->getUserValue($userId, 'core', 'locale', null); + } + } else { + $userId = null; + $userLocale = null; + } + + if ($userLocale && $this->localeExists($userLocale)) { + return $userLocale; + } + + // Default : use system default locale + $defaultLocale = $this->config->getSystemValue('default_locale', false); + if ($defaultLocale !== false && $this->localeExists($defaultLocale)) { + return $defaultLocale; + } + + // If no user locale set, use lang as locale + if ($lang !== null && $this->localeExists($lang)) { + return $lang; + } + + // At last, return USA + return 'en_US'; + } + + /** + * find the matching lang from the locale + * + * @param string $app + * @param string $locale + * @return null|string + */ + public function findLanguageFromLocale(string $app = 'core', ?string $locale = null) { + if ($this->languageExists($app, $locale)) { + return $locale; + } + + // Try to split e.g: fr_FR => fr + $locale = explode('_', $locale)[0]; + if ($this->languageExists($app, $locale)) { + return $locale; + } + } + + /** + * Find all available languages for an app + * + * @param string|null $app App id or null for core + * @return string[] an array of available languages + */ + public function findAvailableLanguages($app = null): array { + $key = $app; + if ($key === null) { + $key = 'null'; + } + + if ($availableLanguages = $this->cache->get($key)) { + $this->availableLanguages[$key] = $availableLanguages; + } + + // also works with null as key + if (!empty($this->availableLanguages[$key])) { + return $this->availableLanguages[$key]; + } + + $available = ['en']; //english is always available + $dir = $this->findL10nDir($app); + if (is_dir($dir)) { + $files = scandir($dir); + if ($files !== false) { + foreach ($files as $file) { + if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) { + $available[] = substr($file, 0, -5); + } + } + } + } + + // merge with translations from theme + $theme = $this->config->getSystemValueString('theme'); + if (!empty($theme)) { + $themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot)); + + if (is_dir($themeDir)) { + $files = scandir($themeDir); + if ($files !== false) { + foreach ($files as $file) { + if (str_ends_with($file, '.json') && !str_starts_with($file, 'l10n')) { + $available[] = substr($file, 0, -5); + } + } + } + } + } + + $this->availableLanguages[$key] = $available; + $this->cache->set($key, $available, 60); + return $available; + } + + /** + * @return array|mixed + */ + public function findAvailableLocales() { + if (!empty($this->availableLocales)) { + return $this->availableLocales; + } + + $localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json'); + $this->availableLocales = \json_decode($localeData, true); + + return $this->availableLocales; + } + + /** + * @param string|null $app App id or null for core + * @param string $lang + * @return bool + */ + public function languageExists($app, $lang) { + if ($lang === 'en') { //english is always available + return true; + } + + $languages = $this->findAvailableLanguages($app); + return in_array($lang, $languages); + } + + public function getLanguageDirection(string $language): string { + if (in_array($language, self::RTL_LANGUAGES, true)) { + return 'rtl'; + } + + return 'ltr'; + } + + public function getLanguageIterator(?IUser $user = null): ILanguageIterator { + $user = $user ?? $this->userSession->getUser(); + if ($user === null) { + throw new \RuntimeException('Failed to get an IUser instance'); + } + return new LanguageIterator($user, $this->config); + } + + /** + * Return the language to use when sending something to a user + * + * @param IUser|null $user + * @return string + * @since 20.0.0 + */ + public function getUserLanguage(?IUser $user = null): string { + $language = $this->config->getSystemValue('force_language', false); + if ($language !== false) { + return $language; + } + + if ($user instanceof IUser) { + $language = $this->config->getUserValue($user->getUID(), 'core', 'lang', null); + if ($language !== null) { + return $language; + } + + if (($forcedLanguage = $this->request->getParam('forceLanguage')) !== null) { + return $forcedLanguage; + } + + // Use language from request + if ($this->userSession->getUser() instanceof IUser + && $user->getUID() === $this->userSession->getUser()->getUID()) { + try { + return $this->getLanguageFromRequest(); + } catch (LanguageNotFoundException $e) { + } + } + } + + return $this->request->getParam('forceLanguage') ?? $this->config->getSystemValueString('default_language', 'en'); + } + + /** + * @param string $locale + * @return bool + */ + public function localeExists($locale) { + if ($locale === 'en') { //english is always available + return true; + } + + if ($this->localeCache === []) { + $locales = $this->findAvailableLocales(); + foreach ($locales as $l) { + $this->localeCache[$l['code']] = true; + } + } + + return isset($this->localeCache[$locale]); + } + + /** + * @throws LanguageNotFoundException + */ + private function getLanguageFromRequest(?string $app = null): string { + $header = $this->request->getHeader('ACCEPT_LANGUAGE'); + if ($header !== '') { + $available = $this->findAvailableLanguages($app); + + // E.g. make sure that 'de' is before 'de_DE'. + sort($available); + + $preferences = preg_split('/,\s*/', strtolower($header)); + foreach ($preferences as $preference) { + [$preferred_language] = explode(';', $preference); + $preferred_language = str_replace('-', '_', $preferred_language); + + $preferred_language_parts = explode('_', $preferred_language); + foreach ($available as $available_language) { + if ($preferred_language === strtolower($available_language)) { + return $this->respectDefaultLanguage($app, $available_language); + } + if (strtolower($available_language) === $preferred_language_parts[0] . '_' . end($preferred_language_parts)) { + return $available_language; + } + } + + // Fallback from de_De to de + foreach ($available as $available_language) { + if (substr($preferred_language, 0, 2) === $available_language) { + return $available_language; + } + } + } + } + + throw new LanguageNotFoundException(); + } + + /** + * if default language is set to de_DE (formal German) this should be + * preferred to 'de' (non-formal German) if possible + */ + protected function respectDefaultLanguage(?string $app, string $lang): string { + $result = $lang; + $defaultLanguage = $this->config->getSystemValue('default_language', false); + + // use formal version of german ("Sie" instead of "Du") if the default + // language is set to 'de_DE' if possible + if ( + is_string($defaultLanguage) + && strtolower($lang) === 'de' + && strtolower($defaultLanguage) === 'de_de' + && $this->languageExists($app, 'de_DE') + ) { + $result = 'de_DE'; + } + + return $result; + } + + /** + * Checks if $sub is a subdirectory of $parent + * + * @param string $sub + * @param string $parent + * @return bool + */ + private function isSubDirectory($sub, $parent) { + // Check whether $sub contains no ".." + if (str_contains($sub, '..')) { + return false; + } + + // Check whether $sub is a subdirectory of $parent + if (str_starts_with($sub, $parent)) { + return true; + } + + return false; + } + + /** + * Get a list of language files that should be loaded + * + * @return string[] + */ + private function getL10nFilesForApp(string $app, string $lang): array { + $languageFiles = []; + + $i18nDir = $this->findL10nDir($app); + $transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json'; + + if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/') + || $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/') + || $this->isSubDirectory($transFile, $this->appManager->getAppPath($app) . '/l10n/')) + && file_exists($transFile) + ) { + // load the translations file + $languageFiles[] = $transFile; + } + + // merge with translations from theme + $theme = $this->config->getSystemValueString('theme'); + if (!empty($theme)) { + $transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot)); + if (file_exists($transFile)) { + $languageFiles[] = $transFile; + } + } + + return $languageFiles; + } + + /** + * find the l10n directory + * + * @param string $app App id or empty string for core + * @return string directory + */ + protected function findL10nDir($app = null) { + if (in_array($app, ['core', 'lib'])) { + if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) { + return $this->serverRoot . '/' . $app . '/l10n/'; + } + } elseif ($app) { + try { + return $this->appManager->getAppPath($app) . '/l10n/'; + } catch (AppPathNotFoundException) { + /* App not found, continue */ + } + } + return $this->serverRoot . '/core/l10n/'; + } + + /** + * @inheritDoc + */ + public function getLanguages(): array { + $forceLanguage = $this->config->getSystemValue('force_language', false); + if ($forceLanguage !== false) { + $l = $this->get('lib', $forceLanguage); + $potentialName = $l->t('__language_name__'); + + return [ + 'commonLanguages' => [[ + 'code' => $forceLanguage, + 'name' => $potentialName, + ]], + 'otherLanguages' => [], + ]; + } + + $languageCodes = $this->findAvailableLanguages(); + $reduceToLanguages = $this->config->getSystemValue('reduce_to_languages', []); + if (!empty($reduceToLanguages)) { + $languageCodes = array_intersect($languageCodes, $reduceToLanguages); + } + + $commonLanguages = []; + $otherLanguages = []; + + foreach ($languageCodes as $lang) { + $l = $this->get('lib', $lang); + // TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version + $potentialName = $l->t('__language_name__'); + if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') { //first check if the language name is in the translation file + $ln = [ + 'code' => $lang, + 'name' => $potentialName + ]; + } elseif ($lang === 'en') { + $ln = [ + 'code' => $lang, + 'name' => 'English (US)' + ]; + } else { //fallback to language code + $ln = [ + 'code' => $lang, + 'name' => $lang + ]; + } + + // put appropriate languages into appropriate arrays, to print them sorted + // common languages -> divider -> other languages + if (in_array($lang, self::COMMON_LANGUAGE_CODES)) { + $commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln; + } else { + $otherLanguages[] = $ln; + } + } + + ksort($commonLanguages); + + // sort now by displayed language not the iso-code + usort($otherLanguages, function ($a, $b) { + if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) { + // If a doesn't have a name, but b does, list b before a + return 1; + } + if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) { + // If a does have a name, but b doesn't, list a before b + return -1; + } + // Otherwise compare the names + return strcmp($a['name'], $b['name']); + }); + + return [ + // reset indexes + 'commonLanguages' => array_values($commonLanguages), + 'otherLanguages' => $otherLanguages + ]; + } +} diff --git a/lib/private/L10N/L10N.php b/lib/private/L10N/L10N.php new file mode 100644 index 00000000000..50db373a65d --- /dev/null +++ b/lib/private/L10N/L10N.php @@ -0,0 +1,225 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\L10N; + +use OCP\IL10N; +use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; +use Punic\Calendar; +use Symfony\Component\Translation\IdentityTranslator; + +class L10N implements IL10N { + /** @var IFactory */ + protected $factory; + + /** @var string App of this object */ + protected $app; + + /** @var string Language of this object */ + protected $lang; + + /** @var string Locale of this object */ + protected $locale; + + /** @var IdentityTranslator */ + private $identityTranslator; + + /** @var string[] */ + private $translations = []; + + /** + * @param IFactory $factory + * @param string $app + * @param string $lang + * @param string $locale + * @param array $files + */ + public function __construct(IFactory $factory, $app, $lang, $locale, array $files) { + $this->factory = $factory; + $this->app = $app; + $this->lang = $lang; + $this->locale = $locale; + + foreach ($files as $languageFile) { + $this->load($languageFile); + } + } + + /** + * The code (en, de, ...) of the language that is used for this instance + * + * @return string language + */ + public function getLanguageCode(): string { + return $this->lang; + } + + /** + * The code (en_US, fr_CA, ...) of the locale that is used for this instance + * + * @return string locale + */ + public function getLocaleCode(): string { + return $this->locale; + } + + /** + * Translating + * @param string $text The text we need a translation for + * @param array|string $parameters default:array() Parameters for sprintf + * @return string Translation or the same text + * + * Returns the translation. If no translation is found, $text will be + * returned. + */ + public function t(string $text, $parameters = []): string { + if (!\is_array($parameters)) { + $parameters = [$parameters]; + } + + return (string)new L10NString($this, $text, $parameters); + } + + /** + * Translating + * @param string $text_singular the string to translate for exactly one object + * @param string $text_plural the string to translate for n objects + * @param integer $count Number of objects + * @param array $parameters default:array() Parameters for sprintf + * @return string Translation or the same text + * + * Returns the translation. If no translation is found, $text will be + * returned. %n will be replaced with the number of objects. + * + * The correct plural is determined by the plural_forms-function + * provided by the po file. + * + */ + public function n(string $text_singular, string $text_plural, int $count, array $parameters = []): string { + $identifier = "_{$text_singular}_::_{$text_plural}_"; + if (isset($this->translations[$identifier])) { + return (string)new L10NString($this, $identifier, $parameters, $count); + } + + if ($count === 1) { + return (string)new L10NString($this, $text_singular, $parameters, $count); + } + + return (string)new L10NString($this, $text_plural, $parameters, $count); + } + + /** + * Localization + * @param string $type Type of localization + * @param \DateTime|int|string $data parameters for this localization + * @param array $options + * @return string|int|false + * + * Returns the localized data. + * + * Implemented types: + * - date + * - Creates a date + * - params: timestamp (int/string) + * - datetime + * - Creates date and time + * - params: timestamp (int/string) + * - time + * - Creates a time + * - params: timestamp (int/string) + * - firstday: Returns the first day of the week (0 sunday - 6 saturday) + * - jsdate: Returns the short JS date format + */ + public function l(string $type, $data = null, array $options = []) { + if ($this->locale === null) { + // Use the language of the instance + $this->locale = $this->getLanguageCode(); + } + if ($this->locale === 'sr@latin') { + $this->locale = 'sr_latn'; + } + + if ($type === 'firstday') { + return (int)Calendar::getFirstWeekday($this->locale); + } + if ($type === 'jsdate') { + return (string)Calendar::getDateFormat('short', $this->locale); + } + + $value = new \DateTime(); + if ($data instanceof \DateTime) { + $value = $data; + } elseif (\is_string($data) && !is_numeric($data)) { + $data = strtotime($data); + $value->setTimestamp($data); + } elseif ($data !== null) { + $data = (int)$data; + $value->setTimestamp($data); + } + + $options = array_merge(['width' => 'long'], $options); + $width = $options['width']; + switch ($type) { + case 'date': + return (string)Calendar::formatDate($value, $width, $this->locale); + case 'datetime': + return (string)Calendar::formatDatetime($value, $width, $this->locale); + case 'time': + return (string)Calendar::formatTime($value, $width, $this->locale); + case 'weekdayName': + return (string)Calendar::getWeekdayName($value, $width, $this->locale); + default: + return false; + } + } + + /** + * Returns an associative array with all translations + * + * Called by \OC_L10N_String + * @return array + */ + public function getTranslations(): array { + return $this->translations; + } + + /** + * @internal + * @return IdentityTranslator + */ + public function getIdentityTranslator(): IdentityTranslator { + if (\is_null($this->identityTranslator)) { + $this->identityTranslator = new IdentityTranslator(); + // We need to use the language code here instead of the locale, + // because Symfony does not distinguish between the two and would + // otherwise e.g. with locale "Czech" and language "German" try to + // pick a non-existing plural rule, because Czech has 4 plural forms + // and German only 2. + $this->identityTranslator->setLocale($this->getLanguageCode()); + } + + return $this->identityTranslator; + } + + /** + * @param string $translationFile + * @return bool + */ + protected function load(string $translationFile): bool { + $json = json_decode(file_get_contents($translationFile), true); + if (!\is_array($json)) { + $jsonError = json_last_error(); + \OCP\Server::get(LoggerInterface::class)->warning("Failed to load $translationFile - json error code: $jsonError", ['app' => 'l10n']); + return false; + } + + $this->translations = array_merge($this->translations, $json['translations']); + return true; + } +} diff --git a/lib/private/L10N/L10NString.php b/lib/private/L10N/L10NString.php new file mode 100644 index 00000000000..a6515e78f24 --- /dev/null +++ b/lib/private/L10N/L10NString.php @@ -0,0 +1,74 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\L10N; + +class L10NString implements \JsonSerializable { + /** @var L10N */ + protected $l10n; + + /** @var string */ + protected $text; + + /** @var array */ + protected $parameters; + + /** @var integer */ + protected $count; + + /** + * @param L10N $l10n + * @param string|string[] $text + * @param array $parameters + * @param int $count + */ + public function __construct(L10N $l10n, $text, array $parameters, int $count = 1) { + $this->l10n = $l10n; + $this->text = $text; + $this->parameters = $parameters; + $this->count = $count; + } + + public function __toString(): string { + $translations = $this->l10n->getTranslations(); + $identityTranslator = $this->l10n->getIdentityTranslator(); + + // Use the indexed version as per \Symfony\Contracts\Translation\TranslatorInterface + $identity = $this->text; + if (array_key_exists($this->text, $translations)) { + $identity = $translations[$this->text]; + } + + if (is_array($identity)) { + $pipeCheck = implode('', $identity); + if (str_contains($pipeCheck, '|')) { + return 'Can not use pipe character in translations'; + } + + $identity = implode('|', $identity); + } elseif (str_contains($identity, '|')) { + return 'Can not use pipe character in translations'; + } + + $beforeIdentity = $identity; + $identity = str_replace('%n', '%count%', $identity); + + $parameters = []; + if ($beforeIdentity !== $identity) { + $parameters = ['%count%' => $this->count]; + } + + // $count as %count% as per \Symfony\Contracts\Translation\TranslatorInterface + $text = $identityTranslator->trans($identity, $parameters); + + return vsprintf($text, $this->parameters); + } + + public function jsonSerialize(): string { + return $this->__toString(); + } +} diff --git a/lib/private/L10N/LanguageIterator.php b/lib/private/L10N/LanguageIterator.php new file mode 100644 index 00000000000..531eea5866b --- /dev/null +++ b/lib/private/L10N/LanguageIterator.php @@ -0,0 +1,126 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\L10N; + +use OCP\IConfig; +use OCP\IUser; +use OCP\L10N\ILanguageIterator; + +class LanguageIterator implements ILanguageIterator { + private $i = 0; + /** @var IConfig */ + private $config; + /** @var IUser */ + private $user; + + public function __construct(IUser $user, IConfig $config) { + $this->config = $config; + $this->user = $user; + } + + /** + * Rewind the Iterator to the first element + */ + public function rewind(): void { + $this->i = 0; + } + + /** + * Return the current element + * + * @since 14.0.0 + */ + public function current(): string { + switch ($this->i) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 0: + $forcedLang = $this->config->getSystemValue('force_language', false); + if (is_string($forcedLang)) { + return $forcedLang; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + // no break + case 1: + $forcedLang = $this->config->getSystemValue('force_language', false); + if (is_string($forcedLang) + && ($truncated = $this->getTruncatedLanguage($forcedLang)) !== $forcedLang + ) { + return $truncated; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + // no break + case 2: + $userLang = $this->config->getUserValue($this->user->getUID(), 'core', 'lang', null); + if (is_string($userLang)) { + return $userLang; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + // no break + case 3: + $userLang = $this->config->getUserValue($this->user->getUID(), 'core', 'lang', null); + if (is_string($userLang) + && ($truncated = $this->getTruncatedLanguage($userLang)) !== $userLang + ) { + return $truncated; + } + $this->next(); + // no break + case 4: + return $this->config->getSystemValueString('default_language', 'en'); + /** @noinspection PhpMissingBreakStatementInspection */ + case 5: + $defaultLang = $this->config->getSystemValueString('default_language', 'en'); + if (($truncated = $this->getTruncatedLanguage($defaultLang)) !== $defaultLang) { + return $truncated; + } + $this->next(); + // no break + default: + return 'en'; + } + } + + /** + * Move forward to next element + * + * @since 14.0.0 + */ + public function next(): void { + ++$this->i; + } + + /** + * Return the key of the current element + * + * @since 14.0.0 + */ + public function key(): int { + return $this->i; + } + + /** + * Checks if current position is valid + * + * @since 14.0.0 + */ + public function valid(): bool { + return $this->i <= 6; + } + + protected function getTruncatedLanguage(string $lang):string { + $pos = strpos($lang, '_'); + if ($pos !== false) { + $lang = substr($lang, 0, $pos); + } + return $lang; + } +} diff --git a/lib/private/L10N/LanguageNotFoundException.php b/lib/private/L10N/LanguageNotFoundException.php new file mode 100644 index 00000000000..087a384e00e --- /dev/null +++ b/lib/private/L10N/LanguageNotFoundException.php @@ -0,0 +1,10 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\L10N; + +class LanguageNotFoundException extends \Exception { +} diff --git a/lib/private/L10N/LazyL10N.php b/lib/private/L10N/LazyL10N.php new file mode 100644 index 00000000000..0d72f638632 --- /dev/null +++ b/lib/private/L10N/LazyL10N.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\L10N; + +use OCP\IL10N; + +class LazyL10N implements IL10N { + /** @var IL10N */ + private $l; + + /** @var \Closure */ + private $factory; + + + public function __construct(\Closure $factory) { + $this->factory = $factory; + } + + private function getL(): IL10N { + if ($this->l === null) { + $this->l = ($this->factory)(); + } + + return $this->l; + } + + public function t(string $text, $parameters = []): string { + return $this->getL()->t($text, $parameters); + } + + public function n(string $text_singular, string $text_plural, int $count, array $parameters = []): string { + return $this->getL()->n($text_singular, $text_plural, $count, $parameters); + } + + public function l(string $type, $data, array $options = []) { + return $this->getL()->l($type, $data, $options); + } + + public function getLanguageCode(): string { + return $this->getL()->getLanguageCode(); + } + + public function getLocaleCode(): string { + return $this->getL()->getLocaleCode(); + } +} |