(int)date('y')) { $year = ((int)substr(date('Y'), 0, 2) - 1) . $year; } else { $year = substr(date('Y'), 0, 2) . $year; } } $month = substr($upload_date, 2, 2); $day = substr($upload_date, 4, 2); $hour = substr($upload_date, 6, 2); $minute = substr($upload_date, 8, 2); $second = substr($upload_date, 10, 2); $date = nicedate("$year-$month-$day $hour:$minute:$second", true); return escape($lang['upload'] . ' ' . $date); } # Check if it is a Research Request if (substr($name_untranslated, 0, 9) == "Research:") { return escape("{$lang["research"]}: " . i18n_get_translated(substr($name_untranslated, 9))); } # Ordinary collection - translate with i18n_get_translated return escape(i18n_get_translated($name_untranslated)); } /** * For field names / values using the i18n syntax, return all language versions, as necessary for indexing. * * @param string $text The text to process * @return string */ function i18n_get_indexable($text) { // Make sure keywords don't get squashed together, then trim $text = str_replace(array("
","
","\\r","\\n"," "), " ", $text); $text = trim($text); $text = preg_replace('/~(.*?):/', ',', $text);// remove i18n strings, which shouldn't be in the keywords # For multiple keywords, parse each keyword. if (substr($text, 0, 1) != "," && (strpos($text, ",") !== false) && (strpos($text, "~") !== false)) { $s = explode(",", $text); $out = ""; for ($n = 0; $n < count($s); $n++) { if ($n > 0) { $out .= ","; } $out .= i18n_get_indexable(trim($s[$n])); } return $out; } # Split $s = explode("~", $text); # Not a translatable field? if (count($s) < 2) { return $text; } $out = ""; for ($n = 1; $n < count($s); $n++) { if (substr($s[$n], 2, 1) != ":") { return $text; } if ($out != "") { $out .= ","; } $out .= substr($s[$n], 3); } return $out; } /** * For a string in the language format, return all translations as an associative array * E.g. "en"->"English translation"; * "fr"->"French translation" * * @param string $value A string in the language format * @return array */ function i18n_get_translations($value) { # global $defaultlanguage; if (strpos($value, "~") === false) { return array($defaultlanguage => $value); } $s = explode("~", $value); $return = array(); for ($n = 1; $n < count($s); $n++) { $e = explode(":", $s[$n], 2); if (count($e) == 2 && strlen($e[0]) > 1) { $return[$e[0]] = $e[1]; } } return $return; } /** * Returns a string with all occurrences of the $mixedplaceholder in $subject replaced with the $mixedreplace. * If $mixedplaceholder is a string but $mixedreplace is an array, the $mixedreplace is imploded to a string using $separator. * The replace values are formatted according to the formatting of the placeholders. * The placeholders may be written in UPPERCASE, lowercase or Uppercasefirst. * Each placeholder will be replaced by the replace value, * written with the same case as the placeholder. * It's possible to also include "?" as a placeholder for legacy reasons. * * Example #1: * str_replace_formatted_placeholder("%extension", $resource["file_extension"], $lang["originalfileoftype"], true) * will search for the three words "%EXTENSION", "%extension" and "%Extension" and also the char "?" * in the string $lang["originalfileoftype"]. If the found placeholder is %extension * it will be replaced by the value of $resource["file_extension"], * written in lowercase. If the found placeholder instead would have been "?" the value * would have been written in UPPERCASE. * * Example #2: * str_replace_formatted_placeholder("%resourcetypes%", $searched_resource_types_names_array, * $lang["resourcetypes-collections"], false, $lang["resourcetypes_separator"]) * will search for the three words "%RESOURCETYPES%", "%resourcetypes%" and "%Resourcetypes%" * in the string $lang["resourcetypes-collections"]. If the found placeholder is %resourcetypes% * all elements in $searched_resource_types_names_array will be written in lowercase and separated by * $lang["resourcetypes_separator"] before the resulting string will replace the placeholder. * * @param mixed $mixedplaceholder * @param mixed $mixedreplace * @param mixed $subject * @param mixed $question_mark * @param string $separator * @return string */ function str_replace_formatted_placeholder($mixedplaceholder, $mixedreplace, $subject, $question_mark = false, $separator = ", ") { # Creates a multi-dimensional array of the placeholders written in different case styles. $array_placeholder = array(); if (is_array($mixedplaceholder)) { $placeholder = $mixedplaceholder; } else { $placeholder = array($mixedplaceholder); } for ($n = 0; $n < count($placeholder); $n++) { $array_placeholder[$n] = array(strtoupper((string)$placeholder[$n]), strtolower((string)$placeholder[$n]), ucfirstletter((string)$placeholder[$n])); } # Creates a multi-dimensional array of the replace values written in different case styles. if (is_array($mixedreplace)) { $replace = $mixedreplace; } else { $replace = array($mixedreplace); } for ($n = 0; $n < count($replace); $n++) { $array_replace[$n] = array(strtoupper((string)$replace[$n]), strtolower((string)$replace[$n]), ucfirst(strtolower((string)$replace[$n]))); } # Adds "?" to the arrays if required. if ($question_mark) { $array_placeholder[] = "?"; $array_replace[] = strtoupper((string)$replace[0]); } # Replaces the placeholders with the replace values and returns the new string. $result = $subject; if (count($placeholder) == 1 && count($replace) > 1) { # The placeholder shall be replaced by an imploded array. $array_replace_strings = array(implode($separator, array_map(function ($column) { return $column[0]; }, $array_replace)), implode($separator, array_map(function ($column) { return $column[1]; }, $array_replace)), implode($separator, array_map(function ($column) { return $column[2]; }, $array_replace))); $result = str_replace($array_placeholder[0], $array_replace_strings, $result); } else { for ($n = 0; $n < count($placeholder); $n++) { if (!isset($array_replace[$n][0])) { break; } else { $result = str_replace($array_placeholder[$n], $array_replace[$n], $result); } } } return $result; } /** * Returns a string with the first LETTER of $string capitalized. * Compare with ucfirst($string) which returns a string with first CHAR of $string capitalized: * ucfirstletter("abc") / ucfirstletter("%abc") returns "Abc" / "%Abc" * ucfirst("abc") / ucfirst("%abc") returns "Abc" / "%abc" * * Search for the first letter ([a-zA-Z]), which may or may not be followed by other characters (.*). * Replaces the found substring ('$0') with the same substring but now with the first character capitalized, using ucfirst(). * Note the /e modifier: If this modifier is set, preg_replace() does normal substitution of backreferences in the replacement * string, evaluates it as PHP code, and uses the result for replacing the search string. * * @param mixed $string * @return string|array|null */ function ucfirstletter($string) { return preg_replace_callback("/[a-zA-Z].*/", "ucfirstletter_callback", $string); } /** * Callback used by ucfirstletter * * @param array $matches * @return void */ function ucfirstletter_callback($matches) { return ucfirst($matches[0]); } /** * Normalize the text if function available * * @param string $keyword Keyword to normalize * @param bool $user_language Flag to enable normalizing based on the current user language. * Some languages consider characters with accents to be different characters * and therefore order them after z and other consider them to be the same as the * character without the accent. * See https://www.php.net/manual/en/class.normalizer.php for more information * @return string Normalized keyword */ function normalize_keyword($keyword, bool $user_language = false) { global $keywords_remove_diacritics,$language_normalize_mapping; if (function_exists('normalizer_normalize')) { if ($user_language && key_exists($GLOBALS["language"], $language_normalize_mapping)) { $form = $language_normalize_mapping[$GLOBALS["language"]]; } else { $form = Normalizer::FORM_C; } $keyword = normalizer_normalize($keyword, $form); } if ($keywords_remove_diacritics) { $keyword = remove_accents($keyword); } return $keyword; } /** * This function and seems_utf8 are reused from WordPress. See documentation/licenses/wordpress.txt for license information * * Converts all accent characters to ASCII characters. * * If there are no accent characters, then the string given is just returned. * * @param string $string Text that might have accent characters * @return string Filtered string with replaced "nice" characters. */ function remove_accents($string) { if (!preg_match('/[\x80-\xff]/', $string)) { return $string; } if (seems_utf8($string)) { $chars = array( // Decompositions for Latin-1 Supplement chr(195) . chr(128) => 'A', chr(195) . chr(129) => 'A', chr(195) . chr(130) => 'A', chr(195) . chr(131) => 'A', chr(195) . chr(132) => 'A', chr(195) . chr(133) => 'A', chr(195) . chr(135) => 'C', chr(195) . chr(136) => 'E', chr(195) . chr(137) => 'E', chr(195) . chr(138) => 'E', chr(195) . chr(139) => 'E', chr(195) . chr(140) => 'I', chr(195) . chr(141) => 'I', chr(195) . chr(142) => 'I', chr(195) . chr(143) => 'I', chr(195) . chr(145) => 'N', chr(195) . chr(146) => 'O', chr(195) . chr(147) => 'O', chr(195) . chr(148) => 'O', chr(195) . chr(149) => 'O', chr(195) . chr(150) => 'O', chr(195) . chr(153) => 'U', chr(195) . chr(154) => 'U', chr(195) . chr(155) => 'U', chr(195) . chr(156) => 'U', chr(195) . chr(157) => 'Y', chr(195) . chr(159) => 's', chr(195) . chr(160) => 'a', chr(195) . chr(161) => 'a', chr(195) . chr(162) => 'a', chr(195) . chr(163) => 'a', chr(195) . chr(164) => 'a', chr(195) . chr(165) => 'a', chr(195) . chr(167) => 'c', chr(195) . chr(168) => 'e', chr(195) . chr(169) => 'e', chr(195) . chr(170) => 'e', chr(195) . chr(171) => 'e', chr(195) . chr(172) => 'i', chr(195) . chr(173) => 'i', chr(195) . chr(174) => 'i', chr(195) . chr(175) => 'i', chr(195) . chr(177) => 'n', chr(195) . chr(178) => 'o', chr(195) . chr(179) => 'o', chr(195) . chr(180) => 'o', chr(195) . chr(181) => 'o', chr(195) . chr(182) => 'o', chr(195) . chr(182) => 'o', chr(195) . chr(185) => 'u', chr(195) . chr(186) => 'u', chr(195) . chr(187) => 'u', chr(195) . chr(188) => 'u', chr(195) . chr(189) => 'y', chr(195) . chr(191) => 'y', // Decompositions for Latin Extended-A chr(196) . chr(128) => 'A', chr(196) . chr(129) => 'a', chr(196) . chr(130) => 'A', chr(196) . chr(131) => 'a', chr(196) . chr(132) => 'A', chr(196) . chr(133) => 'a', chr(196) . chr(134) => 'C', chr(196) . chr(135) => 'c', chr(196) . chr(136) => 'C', chr(196) . chr(137) => 'c', chr(196) . chr(138) => 'C', chr(196) . chr(139) => 'c', chr(196) . chr(140) => 'C', chr(196) . chr(141) => 'c', chr(196) . chr(142) => 'D', chr(196) . chr(143) => 'd', chr(196) . chr(144) => 'D', chr(196) . chr(145) => 'd', chr(196) . chr(146) => 'E', chr(196) . chr(147) => 'e', chr(196) . chr(148) => 'E', chr(196) . chr(149) => 'e', chr(196) . chr(150) => 'E', chr(196) . chr(151) => 'e', chr(196) . chr(152) => 'E', chr(196) . chr(153) => 'e', chr(196) . chr(154) => 'E', chr(196) . chr(155) => 'e', chr(196) . chr(156) => 'G', chr(196) . chr(157) => 'g', chr(196) . chr(158) => 'G', chr(196) . chr(159) => 'g', chr(196) . chr(160) => 'G', chr(196) . chr(161) => 'g', chr(196) . chr(162) => 'G', chr(196) . chr(163) => 'g', chr(196) . chr(164) => 'H', chr(196) . chr(165) => 'h', chr(196) . chr(166) => 'H', chr(196) . chr(167) => 'h', chr(196) . chr(168) => 'I', chr(196) . chr(169) => 'i', chr(196) . chr(170) => 'I', chr(196) . chr(171) => 'i', chr(196) . chr(172) => 'I', chr(196) . chr(173) => 'i', chr(196) . chr(174) => 'I', chr(196) . chr(175) => 'i', chr(196) . chr(176) => 'I', chr(196) . chr(177) => 'i', chr(196) . chr(178) => 'IJ',chr(196) . chr(179) => 'ij', chr(196) . chr(180) => 'J', chr(196) . chr(181) => 'j', chr(196) . chr(182) => 'K', chr(196) . chr(183) => 'k', chr(196) . chr(184) => 'k', chr(196) . chr(185) => 'L', chr(196) . chr(186) => 'l', chr(196) . chr(187) => 'L', chr(196) . chr(188) => 'l', chr(196) . chr(189) => 'L', chr(196) . chr(190) => 'l', chr(196) . chr(191) => 'L', chr(197) . chr(128) => 'l', chr(197) . chr(129) => 'L', chr(197) . chr(130) => 'l', chr(197) . chr(131) => 'N', chr(197) . chr(132) => 'n', chr(197) . chr(133) => 'N', chr(197) . chr(134) => 'n', chr(197) . chr(135) => 'N', chr(197) . chr(136) => 'n', chr(197) . chr(137) => 'N', chr(197) . chr(138) => 'n', chr(197) . chr(139) => 'N', chr(197) . chr(140) => 'O', chr(197) . chr(141) => 'o', chr(197) . chr(142) => 'O', chr(197) . chr(143) => 'o', chr(197) . chr(144) => 'O', chr(197) . chr(145) => 'o', chr(197) . chr(146) => 'OE',chr(197) . chr(147) => 'oe', chr(197) . chr(148) => 'R',chr(197) . chr(149) => 'r', chr(197) . chr(150) => 'R',chr(197) . chr(151) => 'r', chr(197) . chr(152) => 'R',chr(197) . chr(153) => 'r', chr(197) . chr(154) => 'S',chr(197) . chr(155) => 's', chr(197) . chr(156) => 'S',chr(197) . chr(157) => 's', chr(197) . chr(158) => 'S',chr(197) . chr(159) => 's', chr(197) . chr(160) => 'S', chr(197) . chr(161) => 's', chr(197) . chr(162) => 'T', chr(197) . chr(163) => 't', chr(197) . chr(164) => 'T', chr(197) . chr(165) => 't', chr(197) . chr(166) => 'T', chr(197) . chr(167) => 't', chr(197) . chr(168) => 'U', chr(197) . chr(169) => 'u', chr(197) . chr(170) => 'U', chr(197) . chr(171) => 'u', chr(197) . chr(172) => 'U', chr(197) . chr(173) => 'u', chr(197) . chr(174) => 'U', chr(197) . chr(175) => 'u', chr(197) . chr(176) => 'U', chr(197) . chr(177) => 'u', chr(197) . chr(178) => 'U', chr(197) . chr(179) => 'u', chr(197) . chr(180) => 'W', chr(197) . chr(181) => 'w', chr(197) . chr(182) => 'Y', chr(197) . chr(183) => 'y', chr(197) . chr(184) => 'Y', chr(197) . chr(185) => 'Z', chr(197) . chr(186) => 'z', chr(197) . chr(187) => 'Z', chr(197) . chr(188) => 'z', chr(197) . chr(189) => 'Z', chr(197) . chr(190) => 'z', chr(197) . chr(191) => 's', // Euro Sign chr(226) . chr(130) . chr(172) => 'E', // GBP (Pound) Sign chr(194) . chr(163) => ''); $string = strtr($string, $chars); } else { // Assume ISO-8859-1 if not UTF-8 $chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158) . chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194) . chr(195) . chr(196) . chr(197) . chr(199) . chr(200) . chr(201) . chr(202) . chr(203) . chr(204) . chr(205) . chr(206) . chr(207) . chr(209) . chr(210) . chr(211) . chr(212) . chr(213) . chr(214) . chr(216) . chr(217) . chr(218) . chr(219) . chr(220) . chr(221) . chr(224) . chr(225) . chr(226) . chr(227) . chr(228) . chr(229) . chr(231) . chr(232) . chr(233) . chr(234) . chr(235) . chr(236) . chr(237) . chr(238) . chr(239) . chr(241) . chr(242) . chr(243) . chr(244) . chr(245) . chr(246) . chr(248) . chr(249) . chr(250) . chr(251) . chr(252) . chr(253) . chr(255); $chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy"; $string = strtr($string, $chars['in'], $chars['out']); $double_chars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)); $double_chars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'); $string = str_replace($double_chars['in'], $double_chars['out'], $string); } return $string; } /** * Looks for particular patterns to attempt to determine if the provided string is in UTF8 format * * @param string $str * @return boolean True if it's possibly UTF8 */ function seems_utf8($str) { $length = strlen($str); for ($i = 0; $i < $length; $i++) { $c = ord($str[$i]); if ($c < 0x80) { $n = 0; # 0bbbbbbb } elseif (($c & 0xE0) == 0xC0) { $n = 1; # 110bbbbb } elseif (($c & 0xF0) == 0xE0) { $n = 2; # 1110bbbb } elseif (($c & 0xF8) == 0xF0) { $n = 3; # 11110bbb } elseif (($c & 0xFC) == 0xF8) { $n = 4; # 111110bb } elseif (($c & 0xFE) == 0xFC) { $n = 5; # 1111110b } else { return false; # Does not match any model } for ($j = 0; $j < $n; $j++) { # n bytes matching 10bbbbbb follow ? if ((++$i == $length) || ((ord($str[$i]) & 0xC0) != 0x80)) { return false; } } } return true; } /** * Use the browser settings to determine the default / preferred language * * @param boolean $strict_mode * @return string The language string the user may prefer */ function http_get_preferred_language($strict_mode = false) { global $languages; if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { return null; } $accepted_languages = preg_split('/,\s*/', $_SERVER['HTTP_ACCEPT_LANGUAGE']); $current_lang = false; $current_quality = 0; $language_map = array(); foreach ($languages as $key => $value) { $language_map[strtolower($key)] = $key; } foreach ($accepted_languages as $accepted_language) { $res = preg_match('/^([a-z]{1,8}(?:-[a-z]{1,8})*)(?:;\s*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?$/i', $accepted_language, $matches); if (!$res) { continue; } $lang_code = explode('-', $matches[1]); // Use specified quality, if any if (isset($matches[2])) { $lang_quality = (float)$matches[2]; } else { $lang_quality = 1.0; } while (count($lang_code)) { $short = strtolower(join('-', $lang_code)); if (array_key_exists($short, $language_map) && $lang_quality > $current_quality) { $current_lang = $language_map[$short]; $current_quality = $lang_quality; } if ($strict_mode) { break; } array_pop($lang_code); } } return $current_lang; } /** * Set the user's current language based on get/post/cookie values as appropriate. * * @return string The language string set */ function setLanguage() { global $browser_language,$disable_languages,$defaultlanguage,$languages,$baseurl_short; $language = ""; if (isset($_GET["language_set"])) { $language = $_GET["language_set"]; if (array_key_exists($language, $languages)) { # Cannot use the general.php: rs_setcookie() here since general may not have been included. # Set new cookie setcookie("language", $language, time() + (3600 * 24 * 1000)); setcookie("language", $language, time() + (3600 * 24 * 1000), $baseurl_short . "pages/", '', false, true); return $language; } else { $language = ""; } } if (isset($_GET["language"]) && array_key_exists((string)$_GET["language"], $languages)) { return $_GET["language"]; } if (isset($_POST["language"]) && array_key_exists((string)$_POST["language"], $languages)) { return $_POST["language"]; } if (isset($_COOKIE["language"]) && array_key_exists((string)$_COOKIE["language"], $languages)) { return $_COOKIE["language"]; } if (!$disable_languages && $browser_language && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $language = http_get_preferred_language(); if (!empty($language) && array_key_exists($language, $languages)) { return $language; } } if (($disable_languages || $language === "") && isset($defaultlanguage)) { return $defaultlanguage; } # Final case. return 'en'; } /** * Load all site text for the given page and language into the global $lang array * * @param array $lang Passed by reference * @param string $pagename Pagename * @param string $language Language * @return void */ function lang_load_site_text(&$lang, $pagename, $language = "") { global $defaultlanguage, $site_text; $site_text = array(); $results = ps_query("SELECT language,name,text FROM site_text WHERE (page=? OR page='all' OR page='') AND (specific_to_group IS NULL OR specific_to_group=0)", array("s",$pagename), "sitetext"); for ($n = 0; $n < count($results); $n++) { $site_text[$results[$n]["language"] . "-" . $results[$n]["name"]] = $results[$n]["text"]; } $query = " SELECT `name`, `text`, `page`, `language`, specific_to_group FROM site_text WHERE (`language` = ? OR `language` = ?) AND (specific_to_group IS NULL OR specific_to_group = 0) "; $parameters = array("s",$language,"s",$defaultlanguage); if ($pagename != "admin_content") { // Load all content on the admin_content page to allow management. $query .= "AND (page = ? OR page = 'all' OR page = '' " . (($pagename == "dash_tile") ? " OR page = 'home'" : "") . ")"; $parameters[] = "s"; $parameters[] = $pagename; } $results = ps_query($query, $parameters, "sitetext"); // Create a new array to hold customised text at any stage, may be overwritten in authenticate.php. Needed so plugin lang file can be overidden if plugin only enabled for specific groups $GLOBALS['customsitetext'] = array(); // Set the default language first, user language second so we can override the default with any language specific entries foreach ([$defaultlanguage, $language] as $check_lang) { foreach ($results as $result) { if ($result['language'] != $check_lang) { continue; } if ($result['page'] == '') { $lang[$result['name']] = $result['text']; $GLOBALS['customsitetext'][$result['name']] = $result['text']; } else { $lang["{$result['page']}__{$result['name']}"] = $result['text']; } } } } /** * Return an array of all available language strings for the given id, with the language code as the key * * @param string $langid The identifier of the lang string * * @return array * */ function i18n_get_all_translations(string $langid): array { global $lang; $savedlang = $lang; $alltranslations = []; foreach ($GLOBALS["languages"] as $langcode => $availlanguage) { if ($langcode != "en") { if (substr($langcode, 2, 1) != '-') { $langcode = substr($langcode, 0, 2); } $use_error_exception_cache = $GLOBALS["use_error_exception"] ?? false; $GLOBALS["use_error_exception"] = true; try { include __DIR__ . "/../languages/" . safe_file_name($langcode) . ".php"; } catch (Exception $e) { debug("Unable to include language file $langcode.php" . $e->getMessage()); } $GLOBALS["use_error_exception"] = $use_error_exception_cache; } $alltranslations[$langcode] = $lang[$langid]; } // Revert to original $lang = $savedlang; return $alltranslations; } /** * Merge values for across multiple translation strings. * Where a value is missing for a language used elsewhere i18n_get_translated will be used. * If no value is provided and no English (en) string is found then blank will be returned. * * Example input: ["~en:cheese~fr:fromage","~en:bread~fr:pain"] * * Example output: ["~en:cheese, bread","~fr:fromage, pain"] * * @param array $values Translation strings to merge. * * @return array Each language string as a comma separated list. */ function i18n_merge_translations(array $values): array { if (count($values) <= 1) { return $values; } $values_split = array_map('i18n_get_translations', $values); $languages = []; foreach ($values_split as $value) { $languages = array_unique(array_merge($languages, array_keys($value))); } $options = []; foreach ($values_split as $value) { foreach ($languages as $language) { if (key_exists($language, $value)) { $options[$language][] = $value[$language]; } else { $options[$language][] = i18n_get_translated($value['en'] ?? ""); } } } return array_map( fn(string $lang, array $values): string => "~$lang:" . implode(", ", $values), array_keys($options), array_values($options) ); } /** * Get the name of a language in your own language plus its native name. * Used for language selection dropdowns. * * @param string $language_key Key of $languages array, e.g. "fr" * @param string $language_value The value (full name) from the $languages array, e.g. "Français" */ function get_display_language($language_key, $language_value): string { global $lang; $display_lang = $lang["language-" . $language_key]; if ($lang["language-" . $language_key ] !== $language_value) { $display_lang .= " (" . $language_value . ")"; } return $display_lang; } /** * Simple check to determine if the text string supplied contains i18n language translations. * Returns true if match found e.g. '~en:test~en-US:test'. Returns false if no code is found. e.g. 'test'. * * @param string $text Text string to check e.g. a node name or field value. */ function is_i18n_language_string($text): bool { return preg_match('/~(.*?):/', $text) === 1; }