an $jsSafe Make the result javascript safe. Mutually exclusive with * $interpretBackSlashes. * @param boolean $interpretBackSlashes Interpret \t and \n. Mutually exclusive with $jsSafe. * * @return string Human-readable string * @since 1.2.0 */ public function text(string $key, bool $jsSafe = false, bool $interpretBackSlashes = true): string { $key = strtoupper($key); $string = $this->strings[$key] ?? $key; if ($jsSafe) { return addslashes($string); } if ($interpretBackSlashes && (strpos($string, '\\') !== false)) { return str_replace(['\\\\', '\t', '\n'], ["\\", "\t", "\n"], $string); } return $string; } /** * Passes a string through sprintf. * * Note that this method can take a mixed number of arguments as for the sprintf function. * * @param string|null $string The key of the format string * @param mixed ...$arguments The values to use with sprintf * * @return string The translated strings * @since 1.2.0 */ public function sprintf(?string $string, ...$arguments): string { try { return sprintf($this->text($string), ...$arguments); } catch (\Throwable $e) { return 'BAD TRANSLATION. LANGUAGE KEY “' . $string . '” HAS THE WRONG NUMBER OR KIND OF VALUE ARGUMENTS.'; } } /** * Special case of sprintf for a single integer argument handling plural strings. * * Say that the $string is 'FOOBAR' and the $count is 5. This method will first try to find 'FOOBAR_5' and use it * with the sprintf() method. If the 'FOOBAR_5' key does not exist, it will use the sprintf method with the key * 'FOOBAR'. * * This is typically used in language files like so: * * ```ini * EXAMPLE_APPLES_N="%d apples have been eaten." * EXAMPLE_APPLES_N_1="One apple has been eaten." * ``` * * In some languages the declension of nouns changes depending on their number, e.g. you may have a different * declension for 0 items, 1-4 items, 5-9 items, and 10 or more items. This can also be dealt with by creating * the _1, and _5, _6, ..., _9 language strings. * * @param string $string The (base) translation key to use * @param int $count The count of items * * @return string The human-readable, translated string * @since 1.2.0 */ public function plural(string $string, int $count = 0) { $altKey = $string . '_' . $count; return $this->sprintf($this->hasKey($altKey) ? $altKey : $string, $count); } /** * Find the best language for a user. * * The returned language is, by order of preference: * * - User profile (the `language` user configuration parameter, if set) * - Browser settings * - Default site language (the `language` application configuration parameter, if set) * - English (United Kingdom) * * @param string|null $languagePath The language path to use * @param UserInterface|null $user User to look up languages for. NULL for the current user. * * @return string|null A language code, NULL if there is no good match. * @since 1.2.0 */ public function detectLanguage(?string $languagePath, ?UserInterface $user = null): ?string { // If there is a session started and a logged-in user, their language setting is our primary preference. if ($this->getContainer()->session->isStarted()) { $user = $user ?? $this->getContainer()->userManager->getUser(); } if ($user instanceof UserInterface) { $language = $user->getId() ? $user->getParameters()->get('language') : null; } else { $language = null; } // The secondary fallback is the language set in the user's browser. $language = $language ?? $this->detectLanguageFromBrowser($languagePath); // The tertiary fallback is the application-wide language. $language = $language ?? $this->getContainer()->appConfig->get('language'); // Finally, we fall back to English (United Kingdom) return $language ?? 'en-GB'; } /** * The last loaded language code * * @return null * @since 1.2.0 */ public function getLangCode() { return $this->langCode; } /** * Detect the best matching language from the browser settings * * @param string|null $languagePath The path we're going to be looking for language files in. * * @return string|null The detected language. NULL if there are no matches, or we hit an error. * @since 1.2.0 */ private function detectLanguageFromBrowser(?string $languagePath): ?string { if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { return null; } /** * Get the language preference from the Accept-Language HTTP header. * * We get something like: * fr-ch;q=0.3, da, en-us;q=0.8, en;q=0.5, fr;q=0.3 */ $languages = strtolower($_SERVER["HTTP_ACCEPT_LANGUAGE"]); // Remove spaces from strings to avoid errors $languages = str_replace(' ', '', $languages); $languages = explode(",", $languages); // First we need to sort languages by their weight $temp = []; foreach ($languages as $lang) { $parts = explode(';', $lang); $q = 1; if ((count($parts) > 1) && (substr($parts[1], 0, 2) == 'q=')) { $q = floatval(substr($parts[1], 2)); } $temp[$parts[0]] = $q; } arsort($temp); $languages = $temp; foreach ($languages as $language => $weight) { // Pull out the language, place languages into array of full and primary string structure. $temp_array = []; // Slice out the part before the dash, place into array $temp_array[0] = $language; //full language $parts = explode('-', $language); $temp_array[1] = $parts[0]; // cut out primary language if ((strlen($temp_array[0]) == 5) && ((substr($temp_array[0], 2, 1) == '-') || (substr( $temp_array[0], 2, 1 ) == '_'))) { $langLocation = strtoupper(substr($temp_array[0], 3, 2)); $temp_array[0] = $temp_array[1] . '-' . $langLocation; } // Place this array into main $user_languages language array $user_languages[] = $temp_array; } if (!isset($user_languages)) { return null; } $appName = $this->getContainer()->application_name; $languagePath = $languagePath ?: $this->getContainer()->languagePath; $baseName = $languagePath . '/' . strtolower($appName) . '/'; if (!@is_dir($baseName)) { $baseName = $languagePath . '/'; } if (!@is_dir($baseName)) { return null; } // Look for classic file layout foreach ($user_languages as $languageStruct) { // Search for exact language $langFilename = $baseName . $languageStruct[0] . '.ini'; if (!file_exists($langFilename)) { $langFilename = ''; if (function_exists('glob')) { $allFiles = glob($baseName . $languageStruct[1] . '-*.ini'); // Cover both failure cases: false (filesystem error) and empty array (no file found) if (!is_array($allFiles) || empty($allFiles)) { continue; } $langFilename = array_shift($allFiles); } } if (!empty($langFilename) && file_exists($langFilename)) { return basename($langFilename, '.ini'); } } // Look for subdirectory layout $allFolders = []; try { $di = new \DirectoryIterator($baseName); } catch (\Exception $e) { return null; } /** @var \DirectoryIterator $file */ foreach ($di as $file) { if ($di->isDot()) { continue; } if (!$di->isDir()) { continue; } $allFolders[] = $file->getFilename(); } foreach ($user_languages as $languageStruct) { if (array_key_exists($languageStruct[0], $allFolders)) { return $languageStruct[0]; } foreach ($allFolders as $folder) { if (strpos($folder, $languageStruct[1]) === 0) { return $folder; } } } return null; } }