<?php
declare(strict_types=1);
class Dictionary
{
/**
* @var string[] Array of languages, numeric index, two letter CC as values
*/
private static $languages = [];
/**
* @var array{'name': string, 'cc': string}|null Long name of language, and CC
*/
private static $languagesLong = null;
private static $stringCache = [];
public static function init(): void
{
foreach (glob('lang/??', GLOB_ONLYDIR) as $lang) {
if (!file_exists($lang . '/name.txt') && !file_exists($lang . '/flag.png'))
continue;
$lang = basename($lang);
if ($lang === '..')
continue;
self::$languages[] = $lang;
}
//Changes the language in case there is a request to
$lang = Request::get('lang');
if ($lang !== false && in_array($lang, self::$languages)) {
Util::clearCookie('lang');
setcookie('lang', $lang, time() + 86400 * 30 * 12);
$url = Request::get('url');
if ($url === false && isset($_SERVER['HTTP_REFERER'])) {
$url = $_SERVER['HTTP_REFERER'];
}
$parts = parse_url($url);
if ($url === false || $parts === false || empty($parts['query'])) {
$url = '?do=main';
} else {
$url = '?' . $parts['query'];
}
Util::redirect($url);
}
//Default language
$language = 'en';
if (isset($_COOKIE['lang']) && in_array($_COOKIE['lang'], self::$languages)) {
// Did user override language?
$language = $_COOKIE['lang'];
} else if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$langs = preg_split('/[,\s]+/', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
foreach ($langs as $lang) {
$lang = substr($lang, 0, 2);
if (in_array($lang, self::$languages)) {
$language = $lang;
break;
}
}
}
define('LANG', $language);
}
/**
* Format given number using country-specific decimal point and thousands
* separator.
* @param float $num Number to format
* @param int $decimals How many decimals to display
*/
public static function number(float $num, int $decimals = 0): string
{
static $dec = null, $tho = null;
if ($dec === null) {
if (LANG === 'de') {
$dec = ',';
$tho = '.';
} elseif (LANG !== 'en' && file_exists("lang/" . LANG . "/format.txt")) {
$tmp = file_get_contents("lang/" . LANG . "/format.txt");
$dec = $tmp[0];
$tho = $tmp[1];
} else {
$dec = '.';
$tho = ',';
}
}
return number_format($num, $decimals, $dec, $tho);
}
/**
* Get complete key=>value list for given module, file, language
*
* @param string $module Module name
* @param string $file Dictionary name
* @param ?string $lang Language CC, false === current language
* @return array assoc array mapping language tags to the translated strings
*/
public static function getArray(string $module, string $file, ?string $lang = null): array
{
if ($lang === null)
$lang = LANG;
$path = Util::safePath("modules/{$module}/lang/{$lang}/{$file}.json");
if ($path === null)
ErrorHandler::traceError("Invalid path");
if (isset(self::$stringCache[$path]))
return self::$stringCache[$path];
if (!file_exists($path))
return [];
$content = file_get_contents($path);
if ($content === false) { // File does not exist for language
$content = '[]';
}
$json = json_decode($content, true);
if (!is_array($json)) {
$json = [];
}
return self::$stringCache[$path] = $json;
}
/**
* Translate a tag from a dictionary of a module. The current
* language will be used.
*
* @param string $moduleId The module in question
* @param string $file Dictionary name
* @param string $tag Tag name
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
public static function translateFileModule(string $moduleId, string $file, string $tag, bool $returnTagOnMissing = true)
{
$strings = self::getArray($moduleId, $file);
if (!isset($strings[$tag])) {
if ($returnTagOnMissing) {
return '{{' . $tag . '}}';
}
return false;
}
return $strings[$tag];
}
/**
* Translate a tag from a dictionary of the current module, using the current language.
*
* @param string $file Dictionary name
* @param string $tag Tag name
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
public static function translateFile(string $file, string $tag, bool $returnTagOnMissing = true)
{
if (!class_exists('Page') || Page::getModule() === false)
return false; // We have no page - return false for now, as we're most likely running in api or install mode
return self::translateFileModule(Page::getModule()->getIdentifier(), $file, $tag, $returnTagOnMissing);
}
/**
* Translate a tag from the current module's default dictionary, using the current language.
*
* @param string $tag Tag name
* @param bool $returnTagOnMissing If true, the tag name enclosed in {{}} will be returned if the tag does not exist
* @return string|false The requested tag's translation, or false if not found and $returnTagOnMissing === false
*/
public static function translate(string $tag, bool $returnTagOnMissing = true)
{
$string = self::translateFile('module', $tag, false);
if ($string !== false)
return $string;
$string = self::translateFileModule('main', 'global-tags', $tag);
if ($string !== false || !$returnTagOnMissing)
return $string;
return '{{' . $tag . '}}';
}
/**
* Translate the given message id, reading the given module's messages dictionary.
*
* @param string $module Module the message belongs to
* @param string $id Message id
*/
public static function getMessage(string $module, string $id): string
{
$string = self::translateFileModule($module, 'messages', $id);
if ($string === false) {
return "($id) ({{0}}, {{1}}, {{2}}, {{3}})";
}
return $string;
}
/**
* Get translation of the given category.
*
* @param string $category Menu category to get localized name for
* @return string Category name, or some generic fallback to the given category id
*/
public static function getCategoryName(string $category): string
{
if (!empty($category)) {
if (!preg_match('/^(\w+)\.(.*)$/', $category, $out)) {
return 'Invalid Category ID format: ' . $category;
}
$string = self::translateFileModule($out[1], 'categories', $out[2]);
}
if (empty($category) || $string === false) {
return "!!{$category}!!";
}
return $string;
}
/**
* Get all supported languages as array.
*
* @param boolean $withName true = return assoc array containing cc and name of all languages;
* false = regular array containing only the ccs
* @return array List of languages
*/
public static function getLanguages(bool $withName = false): ?array
{
if (!$withName)
return self::$languages;
if (self::$languagesLong === null) {
self::$languagesLong = [];
foreach (self::$languages as $lang) {
if (file_exists("lang/$lang/name.txt")) {
$name = file_get_contents("lang/$lang/name.txt");
} else {
$name = false;
}
if (!isset($name) || $name === false) {
$name = $lang;
}
self::$languagesLong[] = [
'cc' => $lang,
'name' => $name,
];
}
}
return self::$languagesLong;
}
/**
* Get name of language matching given language CC.
* Default to the CC if the language isn't known.
*/
public static function getLanguageName(string $langCC): string
{
if (file_exists("lang/$langCC/name.txt")) {
$name = file_get_contents("lang/$langCC/name.txt");
}
if (!isset($name) || $name === false) {
$name = $langCC;
}
return $name;
}
/**
* Get an <img> tag for the given language. If there is no flag image,
* fall back to generating a .badge with the CC.
* If long mode is requested, returns the name of the language right next
* to the image, otherwise, it is just added as the title attribute.
*
* @param $caption bool with caption next to <img>
* @param $langCC ?string Language cc to get flag code for - defaults to current language
* @return string html code of img tag for language
*/
public static function getFlagHtml(bool $caption = false, string $langCC = null): string
{
if ($langCC === null) {
$langCC = LANG;
}
$flag = "lang/$langCC/flag.png";
$name = htmlspecialchars(self::getLanguageName($langCC));
if (file_exists($flag)) {
$img = '<img alt="' . $name . '" title="' . $name . '" src="' . $flag . '"> ';
if ($caption) {
$img .= $name;
}
} else {
$img = '<div class="badge" title="' . $name . '">' . $langCC . '</div>';
}
return $img;
}
}
Dictionary::init();