<?php /*

 Composr
 Copyright (c) ocProducts, 2004-2016

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  ocProducts Ltd
 * @package    core
 */

/*EXTRA FUNCTIONS: levenshtein*/

/*
Terminology:

Language codename --> A particular pack's codename, e.g. EN
Language file --> A .ini file
Language string ID --> A particular string within a .ini file, e.g. MISSING_RESOURCE
Language string --> A more general word and can *either* refer to a string within a .ini file ("code") *or* a translation within the database
*/

/**
 * Standard code module initialisation function.
 *
 * @ignore
 */
function init__lang()
{
    global $COMCODE_LANG_STRING_CACHE;
    $COMCODE_LANG_STRING_CACHE = array();

    global $LANG_LOADED, $LANG_LOADED_LANG, $LANG_REQUESTED_LANG, $LANGS_REQUESTED;
    // Tracks what has already been require'd, although loading may have been deferred.
    /** What language packs ^ files have been requested.
     *
     * @global boolean $LANG_REQUESTED_LANG
     */
    $LANG_REQUESTED_LANG = array();
    /** What language files have been requested.
     *
     * @global boolean $LANGS_REQUESTED
     */
    $LANGS_REQUESTED = array();
    // Tracks what has already been require'd and not deferred. This will not track all require_lang calls, as Composr will try and use the page's own lang cache first.
    $LANG_LOADED_LANG = array(); // By lang pack and lang file
    $LANG_LOADED = array(); // Just by lang file

    global $LANGUAGE_STRINGS_CACHE;
    $LANGUAGE_STRINGS_CACHE = array();

    global $LANGS_MAP_CACHE;
    $LANGS_MAP_CACHE = null;

    global $USER_LANG_CACHED, $USER_LANG_EARLY_CACHED, $USER_LANG_LOOP, $REQUIRE_LANG_LOOP;
    global $RECORD_LANG_STRINGS, $RECORDED_LANG_STRINGS, $RECORD_LANG_STRINGS_CONTENT, $RECORDED_LANG_STRINGS_CONTENT;
    $RECORD_LANG_STRINGS = false;
    $RECORDED_LANG_STRINGS = array();
    $RECORD_LANG_STRINGS_CONTENT = false;
    $RECORDED_LANG_STRINGS_CONTENT = array();
    $USER_LANG_LOOP = false;
    $USER_LANG_CACHED = null;
    $USER_LANG_EARLY_CACHED = null;
    $REQUIRE_LANG_LOOP = 0;
    global $REQUIRED_ALL_LANG;
    $REQUIRED_ALL_LANG = array();

    // Lazy loading code: learning algorithm to cache strings against different pages without loading all, unless we get a cache miss in the page's pool
    global $PAGE_CACHE_LANG_LOADED, $PAGE_CACHE_LAZY_LOAD, $PAGE_CACHE_LANGS_REQUESTED, $SMART_CACHE;
    $PAGE_CACHE_LANG_LOADED = array();
    $PAGE_CACHE_LAZY_LOAD = false;
    $PAGE_CACHE_LANGS_REQUESTED = array();
    if (((function_exists('get_option')) && (get_option('is_on_lang_cache') == '1') && ((!array_key_exists('page', $_GET)) || ((is_string($_GET['page'])) && (strpos($_GET['page'], '..') === false))))) {
        if ($SMART_CACHE !== null) {
            $contents = $SMART_CACHE->get('lang_strings_' . user_lang());
            if ($contents !== null) {
                $PAGE_CACHE_LANG_LOADED = $contents;
                $LANGUAGE_STRINGS_CACHE = array(user_lang() => $contents);
                $PAGE_CACHE_LAZY_LOAD = true;
            }
        }
    }

    /** Used for filtering various things based on the language pack.
     *
     * @global boolean $LANG_FILTER_OB
     */
    global $LANG_FILTER_OB, $LANG_RUNTIME_PROCESSING;
    $lang_stripped = preg_replace('#[\-\_].*$#', '', user_lang());
    require_code('lang_filter_' . fallback_lang());
    if (((is_file(get_file_base() . '/sources/lang_filter_' . $lang_stripped . '.php')) || (is_file(get_file_base() . '/sources_custom/lang_filter_' . $lang_stripped . '.php'))) && (!in_safe_mode())) {
        require_code('lang_filter_' . $lang_stripped);
        $LANG_FILTER_OB = object_factory('LangFilter_' . $lang_stripped);
    } else {
        /*$LANG_FILTER_OB = new LangFilter(); Actually it's better to just fall back to the English one, rather than an empty one*/

        $LANG_FILTER_OB = object_factory('LangFilter_' . fallback_lang());
    }
    lang_load_runtime_processing();

    require_lang('critical_error');
    require_lang('global');

    /** Keywords parsed from an active search.
     *
     * @global boolean $SEARCH__CONTENT_BITS
     */
    global $SEARCH__CONTENT_BITS;
    $SEARCH__CONTENT_BITS = null;
}

// ====
// CODE
// ====

/**
 * Load language processing data.
 */
function lang_load_runtime_processing()
{
    global $LANG_RUNTIME_PROCESSING;
    if (function_exists('persistent_cache_get')) {
        $LANG_RUNTIME_PROCESSING = persistent_cache_get('LANG_RUNTIME_PROCESSING');
    }
    if ($LANG_RUNTIME_PROCESSING === null) {
        $needs_compiling = true;

        $path = get_custom_file_base() . '/caches/lang/_runtime_processing.lcd';
        if (is_file($path)) {
            $LANG_RUNTIME_PROCESSING = @unserialize(cms_file_get_contents_safe($path));
            if ($LANG_RUNTIME_PROCESSING !== false) {
                $needs_compiling = false;
            }
        }

        if ($needs_compiling) {
            require_code('lang_compile');
            require_code('files');
            $LANG_RUNTIME_PROCESSING = get_lang_file_section(user_lang(), null, 'runtime_processing');
            cms_file_put_contents_safe($path, serialize($LANG_RUNTIME_PROCESSING), FILE_WRITE_FAILURE_SOFT | FILE_WRITE_FIX_PERMISSIONS);
        }
    }
}

/**
 * Get the human-readable form of a language string ID.
 * Further documentation: https://www.youtube.com/watch?v=rinz9Avvq6A
 *
 * @param  ID_TEXT $codename The language string ID
 * @param  ?mixed $parameter1 The first parameter [string or Tempcode] (replaces {1}) (null: none)
 * @param  ?mixed $parameter2 The second parameter [string or Tempcode] (replaces {2}) (null: none)
 * @param  ?mixed $parameter3 The third parameter (replaces {3}). May be an array of [of string or Tempcode], to allow any number of additional args (null: none)
 * @param  ?LANGUAGE_NAME $lang The language to use (null: user's language)
 * @param  boolean $require_result Whether to cause Composr to exit if the lookup does not succeed
 * @return ?mixed The human-readable content (null: not found). String normally. Tempcode if Tempcode parameters.
 */
function do_lang($codename, $parameter1 = null, $parameter2 = null, $parameter3 = null, $lang = null, $require_result = true)
{
    return _do_lang($codename, $parameter1, $parameter2, $parameter3, $lang, $require_result);
}

/**
 * This function is called when no other language works, and it will return the original default language - 'EN'. You may change this to another language, but this is not advised, as Composr is being shipped with the EN language complete and unabridged as standard - hence you cannot go wrong if you leave it as EN.
 * In theory, this is the only hook to English that there is.
 *
 * @return LANGUAGE_NAME The fallback language
 */
function fallback_lang()
{
    return 'EN';
}

/**
 * Get the user's currently selected language.
 *
 * @return LANGUAGE_NAME The user's current language
 */
function user_lang()
{
    // Quick exit: Cache
    global $USER_LANG_CACHED;
    if ($USER_LANG_CACHED !== null) {
        return $USER_LANG_CACHED;
    }

    global $MEMBER_CACHED, $USER_LANG_LOOP, $IN_MINIKERNEL_VERSION;

    // Quick exit: Mini-kernel is very simple
    if ($IN_MINIKERNEL_VERSION) {
        return get_site_default_lang();
    }

    // Quick exit: No Internationalisation enabled
    if ((function_exists('get_option')) && (get_option('allow_international') === '0')) {
        $USER_LANG_CACHED = get_site_default_lang();
        return $USER_LANG_CACHED;
    }

    // ---

    // In URL somehow?
    $lang = '';
    $special_page_type = get_param_string('special_page_type', '');
    if ($special_page_type != '' && substr($special_page_type, 0, 5) == 'lang_') {
        if (substr($special_page_type, 0, 13) == 'lang_content_') {
            $lang = substr($special_page_type, 13);
        } else {
            $lang = substr($special_page_type, 5);
        }
    }
    if ($lang == '') {
        $lang = either_param_string('lang', get_param_string('keep_lang', ''));
        if ($lang != '') {
            $lang = filter_naughty($lang);
        }
    }
    if (($lang != '') && (!does_lang_exist($lang))) {
        $lang = '';
    }

    // Still booting up somehow, so we need to do a cruder non-cache-setting code branch
    if ((!function_exists('get_member')) || ($USER_LANG_LOOP) || ($MEMBER_CACHED === null)) {
        // Quick exit: Cache
        global $USER_LANG_EARLY_CACHED;
        if ($USER_LANG_EARLY_CACHED !== null) {
            return $USER_LANG_EARLY_CACHED;
        }

        // In browser?
        if ($lang == '') {
            if ((array_key_exists('GET_OPTION_LOOP', $GLOBALS)) && (!$GLOBALS['GET_OPTION_LOOP']) && (function_exists('get_option')) && (get_option('detect_lang_browser') == '1')) {
                $lang = get_lang_browser();
                if ($lang === null) {
                    $lang = '';
                }
            }
        }

        // Ok, just the default
        if ($lang == '') {
            $lang = get_site_default_lang();
        }

        // Return
        $USER_LANG_EARLY_CACHED = $lang;
        return $USER_LANG_EARLY_CACHED;
    }

    // Mark that we're processing, to avoid loops (see above handler for loop avoidance)
    $USER_LANG_LOOP = true;

    // In member or browser?
    if ($lang == '') {
        if (
            (
                (get_forum_type() == 'cns') ||
                (get_option('detect_lang_forum') == '1') ||
                (get_option('detect_lang_browser') == '1')
            ) &&
            (
                (!$GLOBALS['DEV_MODE']) ||
                (get_site_default_lang() != 'Gibb')
            )
        ) {
            // In member?
            if (($lang == '') && (get_option('detect_lang_forum') == '1')) {
                $lang = get_lang_member(get_member());
                if ($lang === null) {
                    $lang = '';
                }
            }

            // In browser?
            if (($lang == '') && (get_option('detect_lang_browser') == '1')) {
                $lang = get_lang_browser();
                if ($lang === null) {
                    $lang = '';
                }
            }
        }
    }

    // Ok, just the default
    if ($lang == '') {
        $lang = get_site_default_lang();
    }

    // Return
    $USER_LANG_CACHED = $lang;
    $USER_LANG_LOOP = false;
    return $USER_LANG_CACHED;
}

/**
 * Get the closest fit language codename to what the browser is requesting.
 *
 * @return ?LANGUAGE_NAME The closest-fit language to what the browser wants (null: browser doesn't ask)
 */
function get_lang_browser()
{
    // In browser?
    $http_lang = cms_srv('HTTP_ACCEPT_LANGUAGE');
    if (strlen($http_lang) > 0) {
        $http_langs = explode(',', $http_lang);
        foreach ($http_langs as $lang) {
            // Clean up
            $lang = strtoupper(trim($lang));
            $pos = strpos($lang, ';');
            if ($pos !== false) {
                $lang = substr($lang, 0, $pos);
            }
            $pos = strpos($lang, '-');
            if ($pos !== false) {
                $lang = substr($lang, 0, $pos);
            }

            if (does_lang_exist($lang)) {
                return $lang;
            }
        }
    }

    return null;
}

/**
 * Find whether the specified language exists.
 *
 * @param  LANGUAGE_NAME $lang The language
 * @return boolean Whether the language exists
 */
function does_lang_exist($lang)
{
    if ($lang == '') {
        return false;
    }
    if ($lang == 'Gibb') {
        return true; // Test language
    }
    if ($lang == 'xxx') {
        return true; // Test language
    }
    if ($lang == fallback_lang()) {
        return true;
    }

    $file_a = get_file_base() . '/lang/' . $lang;
    $file_b = get_custom_file_base() . '/lang_custom/' . $lang;
    $file_c = get_file_base() . '/lang_custom/' . $lang;

    return is_dir($file_c) || is_dir($file_b) || is_dir($file_a);
}

/**
 * Get the site's default language, with support for URL overrides.
 *
 * @return LANGUAGE_NAME The site's default language
 */
function get_site_default_lang()
{
    // Site default then
    global $SITE_INFO;
    if (empty($SITE_INFO['default_lang'])) { // We must be installing
        global $IN_MINIKERNEL_VERSION;
        if ($IN_MINIKERNEL_VERSION) {
            if (array_key_exists('lang', $_POST)) {
                return $_POST['lang'];
            }
            if (array_key_exists('lang', $_GET)) {
                return $_GET['lang'];
            }

            // Auto-detect if not got to selection step yet
            if (get_param_integer('step', 1) == 1) {
                $lang = get_lang_browser();
                if ($lang !== null) {
                    return $lang;
                }
            }
        }
        return fallback_lang();
    }
    return $SITE_INFO['default_lang'];
}

/**
 * Get what language the given member uses. The language is sent through a mapping to ensure it is in the right format, or dumped if it will not map.
 *
 * @param  MEMBER $member The member ID
 * @return ?LANGUAGE_NAME The language used by the member (null: the language will not map)
 */
function get_lang_member($member)
{
    if (is_guest($member)) {
        return get_site_default_lang();
    }

    // In forum?
    $lang = $GLOBALS['FORUM_DRIVER']->forum_get_lang($member);
    if ((!is_null($lang)) && ($lang != '')) {
        $_lang = strtoupper($lang);
        if (!does_lang_exist($_lang)) {
            require_code('files');
            $map_file_a = get_file_base() . '/lang/map.ini';
            $map_file_b = get_custom_file_base() . '/lang_custom/map.ini';
            if (!is_file($map_file_b)) {
                $map_file_b = $map_file_a;
            }
            $map = better_parse_ini_file($map_file_b);
            if (!array_key_exists($lang, $map)) {
                //fatal_exit('The specified language (' . $lang . ') is missing. The language needs installing/creating in Composr, or the language map file needs updating (to map this language to a known Composr one), or both.');
                $_lang = null; // Instead of the above, let's just fallback to default! So people's weird forum integration doesn't make Composr die
            } else {
                $_lang = $map[$lang];
                if ((!is_dir(get_file_base() . '/lang/' . $_lang)) && (!is_dir(get_custom_file_base() . '/lang_custom/' . $_lang)) && (!is_dir(get_file_base() . '/lang_custom/' . $_lang))) {
                    $_lang = null;
                }
            }
        }
        if (!is_null($_lang)) {
            return $_lang;
        }
    }

    return null;
}

/**
 * Get the current language.
 * First it tries to get the GET or POST language values, then it tries the user's language, then site default, then it resorts to EN.
 *
 * @param  ?MEMBER $member The member ID (null: site default language, although better just to call get_site_default_lang directly)
 * @return LANGUAGE_NAME The current language
 */
function get_lang($member)
{
    if ($member !== null) {
        if ($member == get_member()) {
            return user_lang();
        }

        $lang = get_lang_member($member);
        if ($lang !== null) {
            return $lang;
        }
    }

    return get_site_default_lang();
}

/**
 * Includes a language file for use in the script.
 * If $type is not null, then this specifies whether to use 'lang_custom' or 'custom' (otherwise, normal priorities occur).
 *
 * @param  ID_TEXT $codename The language file name
 * @param  ?LANGUAGE_NAME $lang The language (null: uses the current language)
 * @param  ?string $type The language type (lang_custom, or custom) (null: normal priorities are used)
 * @set    lang_custom custom
 * @param  boolean $ignore_errors Whether to just return if there was a loading error
 */
function require_lang($codename, $lang = null, $type = null, $ignore_errors = false) // $type is for efficiency only - to avoid needing to doubly-search when requiring all
{
    // So we can keep track of what code loads what langs
    global $LANGS_REQUESTED, $LANG_REQUESTED_LANG, $REQUIRE_LANG_LOOP, $PAGE_CACHE_LAZY_LOAD, $PAGE_CACHE_LANGS_REQUESTED, $LANG_LOADED_LANG, $LANGUAGE_STRINGS_CACHE;
    $LANGS_REQUESTED[$codename] = true;

    if ($lang === null) {
        $lang = user_lang();
    }

    if (isset($LANG_REQUESTED_LANG[$lang][$codename])) {
        return;
    }
    $LANG_REQUESTED_LANG[$lang][$codename] = true;

    $cfb = get_custom_file_base();
    $fb = get_file_base();
    if (strpos($codename, '..') !== false) {
        $codename = filter_naughty($codename);
    }

    if ($PAGE_CACHE_LAZY_LOAD) {
        $support_smart_decaching = support_smart_decaching(true);
        if ($support_smart_decaching) {
            $cache_path = $cfb . '/caches/lang/' . $lang . '/' . $codename . '.lcd';
            $lang_file_default = $fb . '/lang/' . $lang . '/' . $codename . '.ini';
            if (!is_file($lang_file_default)) {
                $lang_file_default = $fb . '/lang/' . fallback_lang() . '/' . $codename . '.ini';
            }
            $lang_file = $fb . '/lang_custom/' . $lang . '/' . $codename . '.ini';
            if (!is_file($lang_file)) {
                $lang_file = $lang_file_default;
            } else {
                if (!is_file($lang_file_default)) {
                    $lang_file_default = $lang_file;
                }
            }
        }
        if ((!$support_smart_decaching) || ((is_file($cache_path)) && (is_file($lang_file)) && (@/*race conditions*/filemtime($cache_path) > filemtime($lang_file)) && (@/*race conditions*/filemtime($cache_path) > filemtime($lang_file_default)))) {
            if ($lang === null) {
                $lang = user_lang();
            }
            $PAGE_CACHE_LANGS_REQUESTED[] = array($codename, $lang);
            return;
        } else { // Invalidate it, as our smart cache was dirty compared to latest .ini version
            global $SMART_CACHE;
            if ($SMART_CACHE !== null) {
                $SMART_CACHE->invalidate();
            }
            $LANGUAGE_STRINGS_CACHE = array();
            $PAGE_CACHE_LAZY_LOAD = false;
            $LANG_LOADED_LANG = array();
            $PAGE_CACHE_LANGS_REQUESTED[] = array($codename, $lang);

            foreach ($PAGE_CACHE_LANGS_REQUESTED as $request) {
                list($that_codename, $that_lang) = $request;
                unset($LANG_REQUESTED_LANG[$that_lang][$that_codename]);
                require_lang($that_codename, $that_lang, null, $ignore_errors);
            }
        }
    }

    if ((isset($LANG_LOADED_LANG[$lang])) && (isset($LANG_LOADED_LANG[$lang][$codename]))) {
        return;
    }

    $REQUIRE_LANG_LOOP++;

    if ((isset($_GET['keep_show_loading'])) && ($_GET['keep_show_loading'] == '1')) {
        $before = memory_get_usage();
    }

    $bad = false;
    $done = false;

    if (!isset($LANGUAGE_STRINGS_CACHE[$lang])) {
        $LANGUAGE_STRINGS_CACHE[$lang] = array();
    }

    $cache_path = $cfb . '/caches/lang/' . $lang . '/' . $codename . '.lcd';

    // Try language cache
    $desire_cache = (function_exists('has_caching_for') && has_caching_for('lang'));
    if ($desire_cache) {
        $cache_path = $cfb . '/caches/lang/' . $lang . '/' . $codename . '.lcd';
        $lang_file_default = $fb . '/lang/' . $lang . '/' . $codename . '.ini';
        if (!is_file($lang_file_default)) {
            $lang_file_default = $fb . '/lang/' . fallback_lang() . '/' . $codename . '.ini';
        }
        $lang_file = $fb . '/lang_custom/' . $lang . '/' . $codename . '.ini';
        if (!is_file($lang_file)) {
            $lang_file = $lang_file_default;
        }
        if (!is_file($lang_file_default)) {
            $lang_file_default = $lang_file;
        }

        if ((is_file($cache_path)) && ((!is_file($lang_file)) || ((@/*race conditions*/filemtime($cache_path) > filemtime($lang_file)) && (@/*race conditions*/filemtime($cache_path) > filemtime($lang_file_default))))) {
            $tmp = @cms_file_get_contents_safe($cache_path);
            if ($tmp != '') {
                $unserialized = @unserialize($tmp);
                if ($unserialized !== false) {
                    $LANGUAGE_STRINGS_CACHE[$lang] += $unserialized;
                    $done = true;
                }
            }
        }
    }
    if (!$done) {
        require_code('lang_compile');
        $bad = $bad || require_lang_compile($codename, $lang, $type, $cache_path, $ignore_errors);

        // Must have been dirty cache, so we need to kill compiled templates too (as lang is compiled into them)
        if (($desire_cache) && (is_file($cache_path)) && (filemtime($cache_path) == time()/*Was successfully rebuilt, no perm error*/)) {
            require_code('caches3');
            global $ERASED_TEMPLATES_ONCE;
            if (!$ERASED_TEMPLATES_ONCE) {
                erase_cached_templates(true, null, TEMPLATE_DECACHE_WITH_LANG);
            }
        }
    }

    global $LANG_LOADED;
    $LANG_LOADED[$codename] = $type;

    if (!isset($LANG_LOADED_LANG[$lang])) {
        $LANG_LOADED_LANG[$lang] = array();
    }
    $LANG_LOADED_LANG[$lang][$codename] = true;

    $REQUIRE_LANG_LOOP--;

    if ((isset($_GET['keep_show_loading'])) && ($_GET['keep_show_loading'] == '1')) {
        $msg = function_exists('clean_file_size') ? clean_file_size(memory_get_usage() - $before) : integer_format(memory_get_usage() - $before);
        if (function_exists('attach_message')) {
            attach_message('require_lang: ' . $codename . ' (' . $msg . ' used)', 'inform');
        } else {
            print('<!-- require_lang: ' . htmlentities($codename) . ' (' . htmlentities($msg) . ' used) -->' . "\n");
            flush();
        }
    }
}

/**
 * Include all the language files for use in the script.
 * NOTE: This may reduce performance, so you should only use it if you really have to.
 *
 * @param ?LANGUAGE_NAME   $lang The language to include files from (null: use current user's language).
 * @param boolean $only_if_for_lang Only load it up if it is specifically defined for our language.
 */
function require_all_lang($lang = null, $only_if_for_lang = false)
{
    $support_smart_decaching = support_smart_decaching(true);

    if (is_null($lang)) {
        $lang = user_lang();
    }

    global $REQUIRED_ALL_LANG;
    if (array_key_exists($lang, $REQUIRED_ALL_LANG)) {
        if ($support_smart_decaching && has_caching_for('block')) {
            disable_smart_decaching_temporarily(); // Too many file checks doing this
        }
        return;
    }
    $REQUIRED_ALL_LANG[$lang] = true;

    require_code('lang2');

    $lang_files = get_lang_files(fallback_lang());

    foreach (array_keys($lang_files) as $file) {
        if ((!$only_if_for_lang) || (is_file(get_custom_file_base() . '/lang_custom/' . $lang . '/' . $file . '.ini')) || (is_file(get_custom_file_base() . '/lang/' . $lang . '/' . $file . '.ini'))) {
            require_lang($file, $lang, null, true);
        }
    }

    if ($support_smart_decaching && has_caching_for('block')) {
        disable_smart_decaching_temporarily(); // Too many file checks doing this
    }
}

/**
 * Require all the open language files. This doesn't hurt performance a lot.
 *
 * @param  ?LANGUAGE_NAME $lang The language to require open files from (null: uses the current language)
 */
function require_all_open_lang_files($lang = null)
{
    global $PAGE_CACHE_LAZY_LOAD, $LANG_REQUESTED_LANG, $LANGS_REQUESTED;
    $PAGE_CACHE_LAZY_LOAD = false;
    $LANG_REQUESTED_LANG = array(); // So require_lang will do a re-load
    $langs_requested_copy = $LANGS_REQUESTED;
    foreach (array_keys($langs_requested_copy) as $toload) {
        require_lang($toload, $lang, null, true);
    }
    // Block caching might have hidden that we loaded these
    require_lang('global', $lang, null, true);
    require_lang('critical_error', $lang, null, true);
}

/**
 * Stop some text being escapable by the Tempcode layer.
 *
 * @param  mixed $in Text
 * @return Tempcode Text that can't be escaped
 */
function protect_from_escaping($in)
{
    return do_lang_tempcode('dont_escape_trick', $in);
}

/**
 * Get the human-readable form of a language string ID.
 *
 * @param  ID_TEXT $codename The language string ID
 * @param  ?mixed $parameter1 The first parameter [string or Tempcode] (replaces {1}) (null: none)
 * @param  ?mixed $parameter2 The second parameter [string or Tempcode] (replaces {2}) (null: none)
 * @param  ?mixed $parameter3 The third parameter (replaces {3}). May be an array of [of string or Tempcode], to allow any number of additional args (null: none)
 * @param  ?LANGUAGE_NAME $lang The language to use (null: user's language)
 * @param  boolean $require_result Whether to cause Composr to exit if the lookup does not succeed
 * @return ?mixed The human-readable content (null: not found). String normally. Tempcode if Tempcode parameters.
 *
 * @ignore
 */
function _do_lang($codename, $parameter1 = null, $parameter2 = null, $parameter3 = null, $lang = null, $require_result = true)
{
    global $LANGUAGE_STRINGS_CACHE, $USER_LANG_CACHED, $RECORD_LANG_STRINGS, $XSS_DETECT, $PAGE_CACHE_LANG_LOADED, $PAGE_CACHE_LAZY_LOAD, $SMART_CACHE, $PAGE_CACHE_LANGS_REQUESTED, $LANG_REQUESTED_LANG, $LANG_FILTER_OB, $LANG_RUNTIME_PROCESSING;

    if ($lang === null) {
        $lang = ($USER_LANG_CACHED === null) ? user_lang() : $USER_LANG_CACHED;
    }

    if ($GLOBALS['SEMI_DEV_MODE']) { // Special syntax for easily inlining language strings while coding
        $pos = strpos($codename, '=');
        if ($pos !== false) {
            require_code('lang2');
            inline_language_editing($codename, $lang);
        }
    }

    $there = isset($LANGUAGE_STRINGS_CACHE[$lang][$codename]);

    if (!$there) {
        $pos = strpos($codename, ':');
        if ($pos !== false) {
            $lang_file = substr($codename, 0, $pos);
            $codename = substr($codename, $pos + 1);

            $there = isset($LANGUAGE_STRINGS_CACHE[$lang][$codename]);
            if (!$there) {
                require_lang($lang_file, null, null, !$require_result);
            }
        }

        $there = isset($LANGUAGE_STRINGS_CACHE[$lang][$codename]);
    }

    if ($RECORD_LANG_STRINGS) {
        global $RECORDED_LANG_STRINGS;
        $RECORDED_LANG_STRINGS[$codename] = true;
    }

    if ((!$there) && ((!isset($LANGUAGE_STRINGS_CACHE[$lang])) || (!array_key_exists($codename, $LANGUAGE_STRINGS_CACHE[$lang])))) {
        if ($PAGE_CACHE_LAZY_LOAD) {
            // We're still doing lazy load, so we'll turn off lazy load and do it properly. This code path will only ever run once
            $PAGE_CACHE_LAZY_LOAD = false; // We can't be lazy any more, but we will keep growing our pool so hopefully CAN be lazy the next time
            foreach ($PAGE_CACHE_LANGS_REQUESTED as $request) {
                list($that_codename, $that_lang) = $request;
                unset($LANG_REQUESTED_LANG[$that_lang][$that_codename]);
                require_lang($that_codename, $that_lang, null, true);
            }
            $ret = _do_lang($codename, $parameter1, $parameter2, $parameter3, $lang, $require_result);
            if ($ret === null) {
                $PAGE_CACHE_LANG_LOADED[$lang][$codename] = null;
                if ($SMART_CACHE !== null) {
                    $SMART_CACHE->append('lang_strings_' . $lang, $codename, null);
                }
            }
            return $ret;
        }

        require_all_open_lang_files($lang);
    }

    if ($lang === 'xxx') {
        return 'xxx'; // Helpful for testing language compliancy. We don't expect to see non x's if we're running this language
    }

    if ((!isset($LANGUAGE_STRINGS_CACHE[$lang][$codename])) && (($require_result) || (!isset($LANGUAGE_STRINGS_CACHE[$lang])) || (!array_key_exists($codename, $LANGUAGE_STRINGS_CACHE[$lang])))) {
        if ($lang !== fallback_lang()) {
            $ret = do_lang($codename, $parameter1, $parameter2, $parameter3, fallback_lang(), $require_result);

            if ((!isset($PAGE_CACHE_LANG_LOADED[$lang][$codename])) && (isset($PAGE_CACHE_LANG_LOADED[fallback_lang()][$codename]))) {
                $PAGE_CACHE_LANG_LOADED[$lang][$codename] = $PAGE_CACHE_LANG_LOADED[fallback_lang()][$codename]; // Will have been cached into fallback_lang() from the nested do_lang call, we need to copy it into our cache bucket for this language
                if ($SMART_CACHE !== null) {
                    $SMART_CACHE->append('lang_strings_' . $lang, $codename, $PAGE_CACHE_LANG_LOADED[$lang][$codename]);
                }
            }

            return $ret;
        } else {
            if ($require_result) {
                global $USER_LANG_LOOP, $REQUIRE_LANG_LOOP;
                //print_r(debug_backtrace());
                if ($USER_LANG_LOOP) {
                    critical_error('RELAY', 'Missing language string ID: ' . escape_html($codename) . '. This language string ID is required to produce error messages, and thus a critical error was prompted by the non-ability to show less-critical error messages. It is likely the source language files (lang/' . fallback_lang() . '/*.ini) for Composr on this website have been corrupted.');
                }
                if ($REQUIRE_LANG_LOOP >= 2) {
                    return ''; // Probably failing to load global.ini, so just output with some text missing
                }

                require_code('site');
                attach_message(do_lang_tempcode('MISSING_LANG_STRING', escape_html($codename)), 'warn');
                return '';
            } else {
                if ($SMART_CACHE !== null) {
                    $SMART_CACHE->append('lang_strings_' . $lang, $codename, null);
                }

                return null;
            }
        }
    }

    if ((!isset($PAGE_CACHE_LANG_LOADED[$lang][$codename])) && ((!isset($PAGE_CACHE_LANG_LOADED[$lang])) || (!array_key_exists($codename, $PAGE_CACHE_LANG_LOADED[$lang])))) {
        $PAGE_CACHE_LANG_LOADED[$lang][$codename] = $LANGUAGE_STRINGS_CACHE[$lang][$codename];
        if ($SMART_CACHE !== null) {
            $SMART_CACHE->append('lang_strings_' . $lang, $codename, $LANGUAGE_STRINGS_CACHE[$lang][$codename]);
        }
    }

    // Put in parameters
    static $non_plural_non_vowel = array('1', 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', '{'/*for no-op param usage*/);
    $out = $LANGUAGE_STRINGS_CACHE[$lang][$codename];
    if ($out === null) {
        return null; // Learning cache pool has told us this string definitely does not exist
    }
    $plural_or_vowel_check = strpos($out, '|') !== false;
    if ($XSS_DETECT) {
        ocp_mark_as_escaped($out);
    }
    if ($parameter1 !== null) {
        if (((isset($parameter1->codename)/*faster than is_object*/) && ($parameter2 === null)) || (($parameter2 !== null) && (isset($parameter2->codename)/*faster than is_object*/))) { // Tempcode only supported in first two
            $bits = preg_split('#\{\d[^\}]*\}#', $out, 2, PREG_SPLIT_OFFSET_CAPTURE);

            $ret = new Tempcode();
            foreach ($bits as $bit) {
                if ($XSS_DETECT) {
                    ocp_mark_as_escaped($bit[0]);
                }

                $at = $bit[1];

                if ($at != 0) {
                    if ($out[$at - 2] === '1') {
                        $ret->attach($parameter1);
                    } elseif ($out[$at - 2] === '2') {
                        $ret->attach($parameter2);
                    } elseif (($plural_or_vowel_check) && (substr($out[$at - 2], 0, 2) === '1|')) {
                        $exploded = explode('|', $out[$at - 2]);
                        $_parameter = $parameter1->evaluate();
                        $_parameter_denum = str_replace(',', '', $_parameter);
                        $ret->attach((in_array(is_numeric($_parameter_denum) ? $_parameter_denum : cms_mb_strtolower(cms_mb_substr($_parameter, 0, 1)), $non_plural_non_vowel)) ? $exploded[1] : $exploded[2]);
                    } elseif (($plural_or_vowel_check) && (substr($out[$at - 2], 0, 2) === '2|')) {
                        $exploded = explode('|', $out[$at - 2]);
                        $_parameter = $parameter2->evaluate();
                        $_parameter_denum = str_replace(',', '', $_parameter);
                        $ret->attach((in_array(is_numeric($_parameter_denum) ? $_parameter_denum : cms_mb_strtolower(cms_mb_substr($_parameter, 0, 1)), $non_plural_non_vowel)) ? $exploded[1] : $exploded[2]);
                    }
                }
                $ret->attach($bit[0]);
            }

            if (isset($LANG_RUNTIME_PROCESSING[$codename])) {
                $flag = $LANG_RUNTIME_PROCESSING[$codename];
                $parameters = array($parameter1, $parameter2);
                if (is_array($parameter3)) {
                    $parameters = array_merge($parameters, $parameter3);
                } else {
                    $parameters[] = $parameter3;
                }
                $ret = protect_from_escaping($LANG_FILTER_OB->run_time($codename, $ret->evaluate(), $flag, $parameters));
            }

            return $ret;
        } elseif ($parameter1 !== null) {
            $kg = function_exists('has_solemnly_declared') && !has_solemnly_declared(I_UNDERSTAND_XSS);
            if ($kg) {
                kid_gloves_html_escaping_singular($parameter1);
            }

            $out = str_replace('{1}', $parameter1, $out);
            if ($plural_or_vowel_check) {
                $_parameter_denum = str_replace(',', '', $parameter1);
                $out = preg_replace('#\{1\|(.*)\|(.*)\}#U', (in_array(is_numeric($_parameter_denum) ? $_parameter_denum : cms_mb_strtolower(cms_mb_substr($parameter1, 0, 1)), $non_plural_non_vowel)) ? '\\1' : '\\2', $out);
            }
            if (($XSS_DETECT) && (ocp_is_escaped($parameter1))) {
                ocp_mark_as_escaped($out);
            }
        }

        if ($parameter2 !== null) {
            if ($kg) {
                kid_gloves_html_escaping_singular($parameter2);
            }

            if ($XSS_DETECT) {
                $escaped = ocp_is_escaped($out);
            }
            $out = str_replace('{2}', $parameter2, $out);
            if ($plural_or_vowel_check) {
                $_parameter_denum = str_replace(',', '', $parameter2);
                $out = preg_replace('#\{2\|(.*)\|(.*)\}#U', (in_array(is_numeric($_parameter_denum) ? $_parameter_denum : cms_mb_strtolower(cms_mb_substr($parameter2, 0, 1)), $non_plural_non_vowel)) ? '\\1' : '\\2', $out);
            }
            if (($XSS_DETECT) && (ocp_is_escaped($parameter2)) && ($escaped)) {
                ocp_mark_as_escaped($out);
            }

            if ($parameter3 !== null) {
                $i = 3;
                if (!is_array($parameter3)) {
                    $parameter3 = array($parameter3);
                }
                foreach ($parameter3 as $parameter) {
                    if ($kg) {
                        kid_gloves_html_escaping_singular($parameter);
                    }

                    if ($XSS_DETECT) {
                        $escaped = ocp_is_escaped($out);
                    }
                    $out = str_replace('{' . strval($i) . '}', $parameter, $out);
                    if ($plural_or_vowel_check) {
                        $_parameter_denum = str_replace(',', '', $parameter);
                        $out = preg_replace('#\{' . strval($i) . '\|(.*)\|(.*)\}#U', (in_array(is_numeric($_parameter_denum) ? $_parameter_denum : cms_mb_strtolower(cms_mb_substr($parameter, 0, 1)), $non_plural_non_vowel)) ? '\\1' : '\\2', $out);
                    }
                    if (($XSS_DETECT) && (ocp_is_escaped($parameter)) && ($escaped)) {
                        ocp_mark_as_escaped($out);
                    }
                    $i++;
                }
            }
        }
    }

    if (isset($LANG_RUNTIME_PROCESSING[$codename])) {
        $flag = $LANG_RUNTIME_PROCESSING[$codename];
        $parameters = array($parameter1, $parameter2);
        if (is_array($parameter3)) {
            $parameters = array_merge($parameters, $parameter3);
        } else {
            $parameters[] = $parameter3;
        }
        $out = $LANG_FILTER_OB->run_time($codename, $out, $flag, $parameters);
    }

    return $out;
}

/**
 * Get an array of all the installed languages that can be found in root/lang/ and root/lang_custom/
 *
 * @param  boolean $even_empty_langs Whether to even find empty languages
 * @return array The installed languages (map, lang=>type)
 */
function find_all_langs($even_empty_langs = false)
{
    require_code('lang3');
    return _find_all_langs($even_empty_langs);
}

/**
 * Get a nice formatted XHTML listed language selector.
 *
 * @param  ?LANGUAGE_NAME $select_lang The language to have selected by default (null: uses the current language)
 * @param  boolean $show_unset Whether to show languages that have no language details currently defined for them
 * @return Tempcode The language selector
 */
function create_selection_list_langs($select_lang = null, $show_unset = false)
{
    require_code('lang3');
    return _create_selection_list_langs($select_lang, $show_unset);
}

// =======
// CONTENT
// =======

/**
 * Insert a Comcode language string into the translation table, and returns the ID.
 *
 * @param  ID_TEXT $field_name The field name
 * @param  string $text The text
 * @param  integer $level The level of importance this language string holds
 * @set    1 2 3 4
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  boolean $insert_as_admin Whether to insert it as an admin (any Comcode parsing will be carried out with admin privileges)
 * @param  ?string $pass_id The special identifier for this language string on the page it will be displayed on; this is used to provide an explicit binding between languaged elements and greater templated areas (null: none)
 * @param  ?integer $wrap_pos Comcode parser wrap position (null: no wrapping)
 * @param  boolean $preparse_mode Whether to generate a fatal error if there is invalid Comcode
 * @param  boolean $save_as_volatile Whether we are saving as a 'volatile' file extension (used in the XML DB driver, to mark things as being non-syndicated to subversion)
 * @return array The language string ID save fields
 */
function insert_lang_comcode($field_name, $text, $level, $connection = null, $insert_as_admin = false, $pass_id = null, $wrap_pos = null, $preparse_mode = true, $save_as_volatile = false)
{
    if (is_null($connection)) {
        $connection = $GLOBALS['SITE_DB'];
    }

    if ((strpos($text, '[attachment') !== false) && ($preparse_mode) && ($pass_id === null)) {
        require_code('attachments2');
        return insert_lang_comcode_attachments($field_name, $level, $text, 'null', '', $connection, $insert_as_admin);
    }

    return insert_lang($field_name, $text, $level, $connection, true, null, null, $insert_as_admin, $pass_id, null, $wrap_pos, $preparse_mode, $save_as_volatile);
}

/**
 * Insert a language string into the translation table, and returns the ID.
 *
 * @param  ID_TEXT $field_name The field name
 * @param  string $text The text
 * @param  integer $level The level of importance this language string holds
 * @set    1 2 3 4
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  boolean $comcode Whether it is to be parsed as Comcode
 * @param  ?integer $id The ID to use for the language string (null: work out next available)
 * @param  ?LANGUAGE_NAME $lang The language (null: uses the current language)
 * @param  boolean $insert_as_admin Whether to insert it as an admin (any Comcode parsing will be carried out with admin privileges)
 * @param  ?string $pass_id The special identifier for this language string on the page it will be displayed on; this is used to provide an explicit binding between languaged elements and greater templated areas (null: none)
 * @param  ?string $text_parsed Assembled Tempcode portion (null: work it out)
 * @param  ?integer $wrap_pos Comcode parser wrap position (null: no wrapping)
 * @param  boolean $preparse_mode Whether to generate a fatal error if there is invalid Comcode
 * @param  boolean $save_as_volatile Whether we are saving as a 'volatile' file extension (used in the XML DB driver, to mark things as being non-syndicated to subversion)
 * @return array The language string ID save fields
 */
function insert_lang($field_name, $text, $level, $connection = null, $comcode = false, $id = null, $lang = null, $insert_as_admin = false, $pass_id = null, $text_parsed = null, $wrap_pos = null, $preparse_mode = true, $save_as_volatile = false)
{
    require_code('lang3');
    return _insert_lang($field_name, $text, $level, $connection, $comcode, $id, $lang, $insert_as_admin, $pass_id, $text_parsed, $wrap_pos, $preparse_mode, $save_as_volatile);
}

/**
 * Remap the specified Comcode language string ID, and return the ID again - the ID isn't changed.
 *
 * @param  ID_TEXT $field_name The field name
 * @param  mixed $id The ID (if multi-lang-content on), or the string itself
 * @param  string $text The text to remap to
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  ?string $pass_id The special identifier for this language string on the page it will be displayed on; this is used to provide an explicit binding between languaged elements and greater templated areas (null: none)
 * @param  ?MEMBER $source_user The member that owns the content this is for (null: current member)
 * @param  boolean $as_admin Whether to generate Comcode as arbitrary admin
 * @return array The language string ID save fields
 */
function lang_remap_comcode($field_name, $id, $text, $connection = null, $pass_id = null, $source_user = null, $as_admin = false)
{
    if ((strpos($text, '[attachment') !== false) && ($pass_id === null)) {
        require_code('attachments3');
        return update_lang_comcode_attachments($field_name, $id, $text, 'null', '', $connection);
    }

    return lang_remap($field_name, $id, $text, $connection, true, $pass_id, $source_user, $as_admin);
}

/**
 * Remap the specified language string ID, and return the ID again - the ID isn't changed.
 *
 * @param  ID_TEXT $field_name The field name
 * @param  mixed $id The ID (if multi-lang-content on), or the string itself
 * @param  string $text The text to remap to
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  boolean $comcode Whether it is to be parsed as Comcode
 * @param  ?string $pass_id The special identifier for this language string on the page it will be displayed on; this is used to provide an explicit binding between languaged elements and greater templated areas (null: none)
 * @param  ?MEMBER $source_user The member that owns the content this is for (null: current member)
 * @param  boolean $as_admin Whether to generate Comcode as arbitrary admin
 * @return array The language string ID save fields
 */
function lang_remap($field_name, $id, $text, $connection = null, $comcode = false, $pass_id = null, $source_user = null, $as_admin = false)
{
    require_code('lang3');
    return _lang_remap($field_name, $id, $text, $connection, $comcode, $pass_id, $source_user, $as_admin);
}

/**
 * Delete the specified language string from the translation table.
 *
 * @param  integer $id The ID
 * @param  ?object $connection The database connection to use (null: standard site connection)
 */
function delete_lang($id, $connection = null)
{
    if (!multi_lang_content()) {
        return;
    }

    if (is_null($connection)) {
        $connection = $GLOBALS['SITE_DB'];
    }
    $connection->query_delete('translate', array('id' => $id));
}

/**
 * Wrapper for get_translated_tempcode, which then converts complex Tempcode back to very simple flat Tempcode, as an optimisation.
 * We won't normally call this as it breaks our architecture, but webmaster may request it if they are okay with it.
 *
 * @param  ID_TEXT $table The table name
 * @param  array $row The table row. Must not have aspects of other tables in it (i.e. joins). Pre-filter using 'db_map_restrict' if required
 * @param  ID_TEXT $field_name The field name
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  ?LANGUAGE_NAME $lang The language (null: uses the current language)
 * @param  boolean $force Whether to force it to the specified language
 * @param  boolean $as_admin Whether to force as_admin, even if the language string isn't stored against an admin (designed for Comcode page caching)
 * @param  boolean $clear_away_from_cache Whether to remove from the Tempcode cache when we're done, for performance reasons (normally don't bother with this, but some code knows it won't be needed again -- esp Comcode cache layer -- and saves RAM by removing it)
 * @return ?Tempcode The parsed Comcode (null: the text couldn't be looked up)
 */
function get_translated_tempcode__and_simplify($table, $row, $field_name, $connection = null, $lang = null, $force = false, $as_admin = false, $clear_away_from_cache = false)
{
    if ($connection === null) {
        $connection = $GLOBALS['SITE_DB'];
    }
    $ret = get_translated_tempcode($table, $row, $field_name, $connection, $lang, $force, $as_admin, $clear_away_from_cache);
    if (is_null($ret)) {
        return $ret;
    }
    $ret = make_string_tempcode($ret->evaluate());
    if (multi_lang_content()) {
        $connection->query_update('translate', array('text_parsed' => $ret->to_assembly()), array('id' => $row[$field_name], 'language' => $lang), '', 1);
    } else {
        $connection->query_update($table, array($field_name . '__text_parsed' => $ret->to_assembly()), $row, '', 1);
    }
    return $ret;
}

/**
 * This function is an offshoot of get_translated_text, it instead returns parsed Comcode that is linked to the specified language string ID.
 *
 * @param  ID_TEXT $table The table name
 * @param  array $row The table row. Must not have aspects of other tables in it (i.e. joins). Pre-filter using 'db_map_restrict' if required
 * @param  ID_TEXT $field_name The field name
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  ?LANGUAGE_NAME $lang The language (null: uses the current language)
 * @param  boolean $force Whether to force it to the specified language
 * @param  boolean $as_admin Whether to force as_admin, even if the language string isn't stored against an admin (designed for Comcode page caching)
 * @param  boolean $clear_away_from_cache Whether to remove from the Tempcode cache when we're done, for performance reasons (normally don't bother with this, but some code knows it won't be needed again -- esp Comcode cache layer -- and saves RAM by removing it)
 * @param  boolean $ignore_browser_decaching If we have just re-populated so will not decache
 * @return ?Tempcode The parsed Comcode (null: the text couldn't be looked up)
 */
function get_translated_tempcode($table, $row, $field_name, $connection = null, $lang = null, $force = false, $as_admin = false, $clear_away_from_cache = false, $ignore_browser_decaching = false)
{
    if ($connection === null) {
        $connection = $GLOBALS['SITE_DB'];
    }

    if ($lang === null) {
        $lang = user_lang();
    }

    if (multi_lang_content()) {
        $entry = $row[$field_name];

        if ($entry == 0) {
            require_code('site');
            attach_message(do_lang_tempcode('FAILED_ENTRY'), 'warn');
            return new Tempcode();
        }

        global $RECORD_LANG_STRINGS_CONTENT;
        if ($RECORD_LANG_STRINGS_CONTENT) {
            global $RECORDED_LANG_STRINGS_CONTENT;
            $RECORDED_LANG_STRINGS_CONTENT[$entry] = is_forum_db($connection);
        }

        if ($lang === 'xxx') {
            return make_string_tempcode('!!!'); // Helpful for testing language compliancy. We don't expect to see non x's/!'s if we're running this language
        }

        if ((isset($connection->text_lookup_cache[$entry])) && ($lang === user_lang())) {
            $ret = $connection->text_lookup_cache[$entry];
            if ($ret !== '') {
                if (is_string($ret)) {
                    $connection->text_lookup_cache[$entry] = new Tempcode();
                    $connection->text_lookup_cache[$entry]->from_assembly($ret);
                    $ret = $connection->text_lookup_cache[$entry];
                }
                if ($clear_away_from_cache) {
                    unset($connection->text_lookup_cache[$entry]);
                    unset($connection->text_lookup_original_cache[$entry]);
                }
                return $ret;
            }
        }

        global $SEARCH__CONTENT_BITS;
        if ($SEARCH__CONTENT_BITS !== null) { // Doing a search so we need to reparse, with highlighting on
            $_result = $connection->query_select('translate', array('text_original', 'source_user'), array('id' => $entry, 'language' => $lang), '', 1);
            if (array_key_exists(0, $_result)) {
                global $LAX_COMCODE;
                $temp = $LAX_COMCODE;
                $LAX_COMCODE = true;
                $result = $_result[0];

                if (get_value('really_want_highlighting') === '1') {
                    require_code('comcode_from_html');
                    $result['text_original'] = force_clean_comcode($result['text_original']); // Highlighting only works with pure Comcode
                }

                $ret = comcode_to_tempcode($result['text_original'], $result['source_user'], $as_admin, null, null, $connection, false, false, false, false, false, $SEARCH__CONTENT_BITS);
                $LAX_COMCODE = $temp;
                return $ret;
            }
        }

        $_result = $connection->query_select('translate', array('text_parsed', 'text_original'), array('id' => $entry, 'language' => $lang), '', 1);
        $result = isset($_result[0]) ? $_result[0]['text_parsed'] : null;
        if (isset($_result[0])) {
            if ($lang === user_lang()) {
                $connection->text_lookup_original_cache[$entry] = $_result[0]['text_original'];
            }
        }
    } else {
        global $SEARCH__CONTENT_BITS;
        if ($SEARCH__CONTENT_BITS !== null) { // Doing a search so we need to reparse, with highlighting on
            global $LAX_COMCODE;
            $temp = $LAX_COMCODE;
            $LAX_COMCODE = true;

            if (get_value('really_want_highlighting') === '1') {
                require_code('comcode_from_html');
                $row[$field_name] = force_clean_comcode($row[$field_name]); // Highlighting only works with pure Comcode
            }

            $ret = comcode_to_tempcode($row[$field_name], $row[$field_name . '__source_user'], $as_admin, null, null, $connection, false, false, false, false, false, $SEARCH__CONTENT_BITS);
            $LAX_COMCODE = $temp;
            return $ret;
        }

        $result = $row[$field_name . '__text_parsed'];
    }

    if (($result === null) || ($result == '') || (!$ignore_browser_decaching && is_browser_decaching())) { // Not cached
        require_code('lang3');
        return parse_translated_text($table, $row, $field_name, $connection, $lang, $force, $as_admin);
    }

    $parsed = new Tempcode();
    if (!$parsed->from_assembly($result, true)) { // Corrupted
        require_code('lang3');
        return parse_translated_text($table, $row, $field_name, $connection, $lang, $force, $as_admin);
    }

    if (multi_lang_content()) {
        if ($lang === user_lang()) {
            $connection->text_lookup_cache[$entry] = $parsed;
        }
    }

    return $parsed;
}

/**
 * Try to return the human-readable version of the language string ID, passed in as $entry.
 *
 * @param  mixed $entry The ID (if multi-lang-content on), or the string itself
 * @param  ?object $connection The database connection to use (null: standard site connection)
 * @param  ?LANGUAGE_NAME $lang The language (null: uses the current language)
 * @param  boolean $force Whether to force it to the specified language
 * @return ?string The human-readable version (null: could not look up when $force was on)
 */
function get_translated_text($entry, $connection = null, $lang = null, $force = false)
{
    if (!multi_lang_content()) {
        return $entry;
    }

    if ($entry == 0) {
        require_code('site');
        attach_message(do_lang_tempcode('FAILED_ENTRY'), 'warn');
        return '';
    }

    if ($entry === null) {
        fatal_exit(do_lang_tempcode('NULL_LANG_STRING'));
    }

    if ($connection === null) {
        $connection = $GLOBALS['SITE_DB'];
    }

    global $RECORD_LANG_STRINGS_CONTENT;
    if ($RECORD_LANG_STRINGS_CONTENT) {
        global $RECORDED_LANG_STRINGS_CONTENT;
        $RECORDED_LANG_STRINGS_CONTENT[$entry] = is_forum_db($connection);
    }

    if ($lang === null) {
        $lang = user_lang();
    }

    if ((isset($connection->text_lookup_original_cache[$entry])) && ($lang === user_lang())) {
        return $connection->text_lookup_original_cache[$entry];
    }

    if ($lang === 'xxx') {
        return '!!!'; // Helpful for testing language compliancy. We don't expect to see non x's/!'s if we're running this language
    }
    $result = $connection->query_select('translate', array('text_original', 'text_parsed'), array('id' => $entry, 'language' => $lang), '', 1);
    if (!isset($result[0])) {
        if ($force) {
            return null;
        }

        $result = $connection->query_select('translate', array('*'), array('id' => $entry, 'language' => get_site_default_lang()), '', 1);
        if (!isset($result[0])) {
            $result = $connection->query_select('translate', array('*'), array('id' => $entry), '', 1);
        }
        if (isset($result[0])) {
            $connection->query_insert('translate', array('broken' => 1, 'language' => $lang) + $result[0]);
        }
    }
    if (!isset($result[0])) {
        $member_id = function_exists('get_member') ? get_member() : $GLOBALS['FORUM_DRIVER']->get_guest_id();
        $connection->query_insert('translate', array('id' => $entry, 'source_user' => $member_id, 'broken' => 0, 'importance_level' => 3, 'text_original' => '', 'text_parsed' => '', 'language' => $lang));
        $msg = do_lang('LANGUAGE_CORRUPTION', strval($entry));
        if ($GLOBALS['DEV_MODE']) {
            fatal_exit($msg);
        }
        require_code('site');
        attach_message(make_string_tempcode($msg), 'warn');
        return '';
    }
    if ($lang === user_lang()) {
        $connection->text_lookup_original_cache[$entry] = $result[0]['text_original'];
        $connection->text_lookup_cache[$entry] = $result[0]['text_parsed'];
    }

    return $result[0]['text_original'];
}

/**
 * Convert a language string that is Comcode to Tempcode, with potential caching in the db.
 *
 * @param  ID_TEXT $lang_code The language string ID
 * @return Tempcode The parsed Comcode
 */
function comcode_lang_string($lang_code)
{
    require_code('lang3');
    return _comcode_lang_string($lang_code);
}

/**
 * UI to choose a language.
 *
 * @param  Tempcode $title Title for the form
 * @param  boolean $tip Whether to give a tip about edit order
 * @param  boolean $allow_all_selection Whether to add an 'all' entry to the list
 * @return mixed The UI (Tempcode) or the language to use (string/LANGUAGE_NAME)
 */
function choose_language($title, $tip = false, $allow_all_selection = false)
{
    require_code('lang3');
    return _choose_language($title, $tip, $allow_all_selection);
}

/**
 * Get the ordinal suffix (e.g. nd, rd, st) for a number.
 *
 * @param  integer $index Number to do this for
 * @return string The suffix
 */
function get_ordinal_suffix($index)
{
    // Based on http://stackoverflow.com/questions/3109978/php-display-number-with-ordinal-suffix
    $ends = array('th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th');
    if (($index % 100) >= 11 && ($index % 100) <= 13) {
        $abbreviation = 'th';
    } else {
        $abbreviation = $ends[$index % 10];
    }
    return $abbreviation;
}

/**
 * Start locking and get faux auto-increment ID for inserting into a table.
 *
 * @param  object $connection Database connection to use
 * @param  ?integer $id ID number (returned by reference) (null: just do normal auto-increment)
 * @param  boolean $lock Whether locking has happened (returned by reference)
 * @param  string $table Translate table
 * @param  string $id_field ID field
 */
function table_id_locking_start($connection, &$id, &$lock, $table = 'translate', $id_field = 'id')
{
    if (($id === null) && (multi_lang()) && (strpos(get_db_type(), 'mysql') !== false)) { // Needed as MySQL auto-increment works separately for each combo of other key values (i.e. language in this case). We can't let a language string ID get assigned to something entirely different in another language. This MySQL behaviour is not well documented, it may work differently on different versions.
        $connection->query('LOCK TABLES ' . $connection->get_table_prefix() . $table, null, null, true);
        $lock = true;
        $id = $connection->query_select_value($table, 'MAX(' . $id_field . ')');
        $id = ($id === null) ? null : ($id + 1);
    } elseif (($id === null) && (multi_lang()) && (strpos(get_db_type(), 'sqlserver') !== false)) { // Needed as on SQL Server we need to choose our own key, as we cannot use an identity as a part of a shared key.
        db_start_transaction($connection->connection_write);
        $lock = true;
        $id = $connection->query_select_value($table, 'MAX(' . $id_field . ')');
        $id = ($id === null) ? db_get_first_id() : ($id + 1);
    } else {
        $lock = false;
    }
}

/**
 * End locking for inserting into a table.
 *
 * @param  object $connection Database connection to use
 * @param  ?integer $id ID number (null: just do normal auto-increment)
 * @param  boolean $lock Whether locking has happened
 * @param  string $table Translate table
 * @param  string $id_field ID field
 */
function table_id_locking_end($connection, $id, $lock, $table = 'translate', $id_field = 'id')
{
    if ($lock) {
        if (strpos(get_db_type(), 'mysql') !== false) {
            $connection->query('UNLOCK TABLES', null, null, true);
        } elseif (strpos(get_db_type(), 'sqlserver') !== false) {
            db_end_transaction($connection->connection_write);
        }
    }
}

/**
 * Do filtering for a language pack. This is the base class that doesn't actually do anything.
 *
 * @package        core
 */
class LangFilter
{
    /**
     * Do a compile-time filter.
     *
     * @param  ?string $key Language string ID (null: not a language string)
     * @param  string $value String value
     * @param  ?LANGUAGE_NAME $lang Language (null: current language)
     * @return string The suffix
     */
    public function compile_time($key, $value, $lang = null)
    {
        return $value;
    }

    /**
     * Do a run-time filter. Only happens for strings marked for processing with a flag.
     *
     * @param  string $key Language string ID
     * @param  string $value Language string value
     * @param  string $flag Flag value assigned to the string
     * @param  array $parameters The parameters
     * @return string The suffix
     */
    public function run_time($key, $value, $flag, $parameters)
    {
        return $value;
    }
}
