<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\models;
use seekquarry\yioop\configs as C;
use seekquarry\yioop\library as L;
use seekquarry\yioop\library\UrlParser;
/**
* Function for comparing two locale arrays by locale tag so can sort
*
* @param array $a an associative array of locale info
* @param array $b an associative array of locale info
*
* @return int -1, 0, or 1 depending on which is alphabetically smaller or if
* they are the same size
*/
function lessThanLocale($a, $b)
{
if ($a["LOCALE_TAG"] == $b["LOCALE_TAG"]) {
return 0;
}
return ($a["LOCALE_TAG"] < $b["LOCALE_TAG"]) ? -1 : 1;
}
/**
* Used to encapsulate information about a locale (data about a language in
* a given region).
*
* @author Chris Pollett
*/
class LocaleModel extends Model
{
/**
* Used to store ini file data of the current locale
* @var array
*/
public $configure = [];
/**
* Used to store ini file data of the default locale (will use if no
* translation current locale)
* @var array
*/
public $default_configure = [];
/**
* IANA tag name of current locale
* @var string
*/
public $locale_tag;
/**
* Locale name as a string it locale name's language
* @var string
*/
public $locale_name;
/**
* Combination of text direction and block progression as a string. Has one
* of four values: lr-tb, rl-tb, tb-lr, tb-rl. Other possible values for
* things like Arabic block quoted in Mongolian not supported
* @var string
*/
public $writing_mode;
/**
* Directories to try to extract translatable identifier strings from
* @var array
*/
public $extract_dirs = ["controllers", "views", "library/indexing_plugins"];
/**
* File extensions of files to try to extract translatable strings from
* @var array
*/
public $extensions = ["php"];
/**
* Associations of the form
* name of field for web forms => database column names/abbreviations
* In this case, things will in general map to the LOCALES tables in the
* Yioop database
* @var array
*/
public $search_table_column_map = ["name"=>"LOCALE_NAME",
"tag"=>"LOCALE_TAG", "mode" => "WRITING_MODE", "active" => "ACTIVE"];
/**
* These fields if present in $search_array (used by @see getRows() ),
* but with value "-1", will be skipped as part of the where clause
* but will be used for order by clause
* @var array
*/
public $any_fields = ["mode", "active"];
/**
* Holds global locale object currently in use for the session
* @var object
*/
public static $current_locale = null;
/**
* {@inheritDoc}
*
* @param mixed $args any additional arguments which should be used to
* determine the columns
* @return string a comma separated list of columns suitable for a SQL
* query
*/
public function selectCallback($args = null)
{
return "LOCALE_ID, LOCALE_TAG, LOCALE_NAME, WRITING_MODE, ACTIVE";
}
/**
* This is called after each row is retrieved by getRows. This method
* Then reads in the corresponding statistics.txt file (or rebuilds
* it from the Configure.ini file if it is out of date) to add to the
* row percent translated info.
*
* @param string $locale one getRows row corresponding to a given locale
* @param mixed $args additional arguments that might be used for this
* method (none used for this sub-class)
* @return array $locale row with PERCENT_WITH_STRINGS field added
*/
public function rowCallback($locale, $args)
{
/*
the statistics text file contains info used to calculate
what fraction of strings have been translated
*/
$tag_prefix = C\LOCALE_DIR . "/" . str_replace("-", "_",
$locale['LOCALE_TAG']);
if (!file_exists($tag_prefix)) {
mkdir($tag_prefix); //create locale_dirs that are missing
$this->db->setWorldPermissionsRecursive($tag_prefix);
}
if (!file_exists("$tag_prefix/statistics.txt") ||
filemtime("$tag_prefix/statistics.txt") <
filemtime("$tag_prefix/configure.ini")) {
$tmp = L\parse_ini_with_fallback(
"$tag_prefix/configure.ini");
$num_ids = 0;
$num_strings = 0;
$strings = (isset($tmp['strings']) &&
is_array($tmp['strings'])) ? $tmp['strings'] : [];
foreach ($strings as $msg_id => $msg_string) {
$num_ids++;
if (strlen($msg_string) > 0) {
$num_strings++;
}
}
if ($num_ids > 0) {
$locale['PERCENT_WITH_STRINGS'] =
floor(100 * $num_strings / $num_ids);
} else {
/* A locale folder whose configure.ini has no usable
strings section (an empty or stray folder, say) has
nothing to translate; report zero rather than divide
by zero, and name the folder so it can be cleaned up. */
$locale['PERCENT_WITH_STRINGS'] = 0;
L\crawlLog("Locale " . $locale['LOCALE_TAG'] .
" has no strings in $tag_prefix/configure.ini");
}
file_put_contents("$tag_prefix/statistics.txt",
serialize($locale['PERCENT_WITH_STRINGS']));
} else {
$locale['PERCENT_WITH_STRINGS'] =
unserialize(
file_get_contents("$tag_prefix/statistics.txt"));
}
return $locale;
}
/**
* Loads the provided locale's configure file (containing translation) and
* calls setlocale to set up locale specific string formatting
* (to format numbers, etc.)
*
* @param string $locale_tag the tag of the locale to use as the current
* locale
*/
public function initialize($locale_tag)
{
$replaced_tag = str_replace("-", "_", $locale_tag);
$old_style_dir = C\LOCALE_DIR . "/" . $locale_tag;
$new_style_dir = C\LOCALE_DIR . "/" . $replaced_tag;
if ($replaced_tag != $locale_tag) {
// renames locale using old style locale dir naming
if (file_exists($old_style_dir) && !file_exists($new_style_dir)) {
rename($old_style_dir, $new_style_dir);
if (file_exists($new_style_dir . "/resources/tokenizer.php")) {
rename($new_style_dir . "/resources/tokenizer.php",
$new_style_dir . "/resources/Tokenizer.php");
}
}
}
// Code to try to fallback from en to en-US if exists
if (!file_exists($new_style_dir) &&
substr_count($locale_tag, "-") == 0) {
$sub_locale_dirs = glob("$new_style_dir*", GLOB_ONLYDIR);
if (!empty($sub_locale_dirs[0])) {
$new_style_dir = $sub_locale_dirs[0];
$locale_tag = str_replace("_", "-", substr($new_style_dir,
strlen(C\LOCALE_DIR . "/")));
}
}
$this->configure = L\parse_ini_with_fallback($new_style_dir .
"/configure.ini");
if ($locale_tag != C\p('DEFAULT_LOCALE')) {
$this->default_configure = L\parse_ini_with_fallback(
C\LOCALE_DIR . "/" . str_replace("-", "_",
C\p('DEFAULT_LOCALE'))
."/configure.ini");
}
$this->locale_tag = $locale_tag;
$db = $this->db;
$sql = "SELECT LOCALE_NAME, WRITING_MODE ".
" FROM LOCALE WHERE LOCALE_TAG ='$locale_tag'";
$result = $db->execute($sql);
$row = false;
if ($result) {
$row = $db->fetchArray($result);
if (empty($row['LOCALE_NAME']) || empty($row['WRITING_MODE'])) {
$row = false;
$this->locale_name = "";
$this->writing_mode = "lr-tb";
} else {
$this->locale_name = $row['LOCALE_NAME'];
$this->writing_mode = $row['WRITING_MODE'];
}
} else {
$this->locale_name = "";
$this->writing_mode = "lr-tb";
}
$locale_tag_parts = preg_split("/\_|\-/", $locale_tag);
setlocale(LC_ALL, $locale_tag, $locale_tag . '.UTF-8',
$locale_tag.'.UTF8', $locale_tag . ".TCVN", $locale_tag.".VISCII",
$locale_tag_parts[0], $locale_tag_parts[0].'.UTF-8',
$locale_tag_parts[0].'.UTF8', $locale_tag_parts[0].".TCVN");
//hacks for things that didn't work from the above
if ($locale_tag == 'vi-VN') {
setlocale(LC_NUMERIC, 'fr_FR.UTF-8');
}
}
/**
* Returns information about available locales
*
* @param bool $only_active says whether to return info only about active
* locales (true) or about all locales (false);
* @param bool $only_db_info only get database information about the locale,
* skip calculating percent translated
* @return array rows of locale information
*/
public function getLocaleList($only_active = true, $only_db_info = true)
{
$db = $this->db;
$sql = "SELECT LOCALE_ID, LOCALE_TAG, LOCALE_NAME, WRITING_MODE ".
" FROM LOCALE";
if ($only_active) {
$sql .= " WHERE ACTIVE > 0";
}
$result = $db->execute($sql);
$i = 0;
$locales = [];
while ($locales[$i] = $db->fetchArray($result)) {
if (!$only_db_info) {
/*
the statistics text file contains info used to calculate
what fraction of strings have been translated
*/
$tag_prefix = C\LOCALE_DIR . "/" . str_replace(
"-", "_", $locales[$i]['LOCALE_TAG']);
if (!file_exists($tag_prefix)) {
mkdir($tag_prefix); //create locale_dirs that are missing
$this->db->setWorldPermissionsRecursive($tag_prefix);
}
if (!file_exists("$tag_prefix/statistics.txt") ||
!file_exists("$tag_prefix/configure.ini") ||
filemtime("$tag_prefix/statistics.txt") <
filemtime("$tag_prefix/configure.ini")) {
$tmp = null;
if (file_exists("$tag_prefix/configure.ini")) {
$tmp = L\parse_ini_with_fallback(
"$tag_prefix/configure.ini");
}
if ($tmp) {
$num_ids = 0;
$num_strings = 0;
foreach ($tmp['strings'] as $msg_id => $msg_string) {
$num_ids++;
if (strlen($msg_string) > 0) {
$num_strings++;
}
}
$locales[$i]['PERCENT_WITH_STRINGS'] =
floor(100 * $num_strings/$num_ids);
} else {
$locales[$i]['PERCENT_WITH_STRINGS'] = 0;
}
file_put_contents("$tag_prefix/statistics.txt",
serialize($locales[$i]['PERCENT_WITH_STRINGS']));
} else {
$locales[$i]['PERCENT_WITH_STRINGS'] =
unserialize(
file_get_contents("$tag_prefix/statistics.txt"));
}
}
$i++;
}
unset($locales[$i]); //last one will be null
usort($locales, C\NS . "Models\\lessThanLocale");
return $locales;
}
/**
* Check if there is a locale with tag equal to $locale_tag
*
* @param string $locale_tag to check for
* @return bool whether or not has exists
*/
public function checkLocaleExists($locale_tag)
{
$db = $this->db;
$params = [$locale_tag];
$sql = "SELECT COUNT(*) AS NUM FROM LOCALE WHERE
LOCALE_TAG=? ";
$result = $db->execute($sql, $params);
if (!$row = $db->fetchArray($result)) {
return false;
}
if ($row['NUM'] <= 0) {
return false;
}
return true;
}
/**
* Returns the locale name, tag, and writing mode for tag $locale_tag
*
* @param string $locale_tag to get name for
* @return string name of locale
*/
public function getLocaleInfo($locale_tag)
{
$db = $this->db;
$params = [$locale_tag];
$sql = "SELECT * FROM LOCALE WHERE
LOCALE_TAG=? ";
$result = $db->execute($sql, $params);
if (!$row = $db->fetchArray($result)) {
return "";
}
return $row;
}
/**
* Returns the name of the locale for tag $locale_tag
*
* @param string $locale_tag to get name for
* @return string name of locale
*/
public function getLocaleName($locale_tag)
{
$db = $this->db;
$params = [$locale_tag];
$sql = "SELECT LOCALE_NAME FROM LOCALE WHERE
LOCALE_TAG=? ";
$result = $db->execute($sql, $params);
if (!$row = $db->fetchArray($result)) {
return "";
}
return $row["LOCALE_NAME"];
}
/**
* Adds information concerning a new locale to the database
*
* @param string $locale_name the name of the locale in the locale's
* language
* @param string $locale_tag the IANA language tag for the locale
* @param string $writing_mode a combination of the horizontal and
* vertical text direction used for writing in the locale
* @param int $active whether the locale is current active (>0) or not
* used (== 0)
*/
public function addLocale($locale_name, $locale_tag, $writing_mode,
$active = 1)
{
$sql = "INSERT INTO LOCALE (LOCALE_NAME, LOCALE_TAG,
WRITING_MODE, ACTIVE) VALUES (?, ?, ?, ?)";
$this->db->execute($sql, [$locale_name, $locale_tag,
$writing_mode, $active]);
$locale_tag = str_replace("-", "_", $locale_tag);
if (!file_exists(C\LOCALE_DIR."/$locale_tag")) {
mkdir(C\LOCALE_DIR."/$locale_tag");
$this->db->setWorldPermissionsRecursive(
C\LOCALE_DIR."/$locale_tag");
}
}
/**
* Remove a locale from the database
*
* @param string $locale_tag the IANA language tag for the locale to remove
*/
public function deleteLocale($locale_tag)
{
$sql = "DELETE FROM LOCALE WHERE LOCALE_TAG = ?";
$this->db->execute($sql, [$locale_tag]);
if (file_exists(C\LOCALE_DIR . "/$locale_tag")) {
$this->db->unlinkRecursive(C\LOCALE_DIR . "/$locale_tag", true);
}
}
/**
* Used to update the fields stored in a LOCALE row according to
* an array holding new values
*
* @param array $locale_info updated values for a LOCALE row
*/
public function updateLocaleInfo($locale_info)
{
$locale_id = $locale_info['LOCALE_ID'];
unset($locale_info['LOCALE_ID']);
unset($locale_info['LOCALE_NAME']);
$sql = "UPDATE LOCALE SET ";
$comma ="";
$params = [];
foreach ($locale_info as $field => $value) {
$sql .= "$comma $field=? ";
$comma = ",";
$params[] = $value;
}
$sql .= " WHERE LOCALE_ID=?";
$params[] = $locale_id;
$this->db->execute($sql, $params);
}
/**
* For each translatable identifier string (either static from a
* configure ini file, or dynamic from the db)
* return its name together with its translation into the given locale
* if such a translation exists.
*
* @param string $locale_tag the IANA language tag to translate string into
* @return array rows of identifier string - translation pairs
*/
public function getStringData($locale_tag)
{
$db = $this->db;
$dir_locale_tag = str_replace("-", "_", $locale_tag);
$data = L\parse_ini_with_fallback(
C\LOCALE_DIR."/$dir_locale_tag/configure.ini");
$data = $data['strings'];
//hacky. Join syntax isn't quite the same between sqlite and mysql
if (in_array(C\p('DBMS'), ['sqlite3'])) {
$sql = "SELECT T.IDENTIFIER_STRING AS MSG_ID, ".
"TLL.TRANSLATION AS MSG_STRING " .
"FROM TRANSLATION T LEFT JOIN ".
//sqlite supports left but not right outer join
"(TRANSLATION_LOCALE TL JOIN LOCALE L ON ".
"L.LOCALE_TAG = ? AND ".
"L.LOCALE_ID = TL.LOCALE_ID) TLL " .
"ON T.TRANSLATION_ID = TLL.TRANSLATION_ID";
} else {
$sql = "SELECT T.IDENTIFIER_STRING AS MSG_ID, ".
"TL.TRANSLATION AS MSG_STRING " .
"FROM TRANSLATION T LEFT JOIN ".
"(TRANSLATION_LOCALE TL JOIN LOCALE L ON ".
"L.LOCALE_TAG = ? AND L.LOCALE_ID = TL.LOCALE_ID) ".
"ON T.TRANSLATION_ID = TL.TRANSLATION_ID";
}
$result = $this->db->execute($sql, [$locale_tag]);
while ($row = $this->db->fetchArray($result)) {
$data[$row['MSG_ID']] = $row['MSG_STRING'];
}
return $data;
}
/**
* Updates the identifier_string-translation pairs
* (both static and dynamic) for a given locale
*
* @param string $locale_tag the IANA language tag to update the strings of
* @param array $new_strings rows of identifier string - translation pairs
*/
public function updateStringData($locale_tag, $new_strings)
{
$db = $this->db;
$sql = "SELECT LOCALE_ID FROM LOCALE WHERE LOCALE_TAG = ? " .
$db->limitOffset(1);
$result = $db->execute($sql, [$locale_tag]);
$row = $db->fetchArray($result);
$locale_id = $row['LOCALE_ID'];
list($general_ini, $strings) = $this->extractMergeLocales();
$select_sql = "SELECT TRANSLATION_ID FROM TRANSLATION ".
"WHERE IDENTIFIER_STRING = ? " . $db->limitOffset(1);
$delete_sql = "DELETE FROM TRANSLATION_LOCALE ".
"WHERE TRANSLATION_ID =? AND LOCALE_ID = ?";
$insert_sql = "INSERT INTO TRANSLATION_LOCALE VALUES (?, ?, ?)";
foreach ($new_strings as $msg_id => $msg_string) {
if (strcmp($msg_id, strstr($msg_id, "db_")) == 0) {
$result = $db->execute($select_sql, [$msg_id]);
$row = $db->fetchArray($result);
$translate_id = $row['TRANSLATION_ID'];
$result = $db->execute($delete_sql, [$translate_id,
$locale_id]);
$result = $db->execute($insert_sql, [$translate_id,
$locale_id, $msg_string]);
$new_strings[$msg_id] = false;
}
}
array_filter($new_strings);
$data['strings'] = $new_strings;
$data['strings']["view_locale_version"] = C\RESOURCES_WIKI_VERSION;
$dir_locale_tag = str_replace("-", "_", $locale_tag);
$this->updateLocale(
$general_ini, $strings, C\LOCALE_DIR, $dir_locale_tag, $data);
}
/**
* Translate an array consisting of an identifier string together with
* additional variable parameters into the current locale.
*
* Suppose the identifier string was some_view_fraction_received and two
* additional arguments 5 and 10 were given. Suppose further that its
* translation into the current locale (say en_US) was "%s out of %s".
* Then the string returned by translate would be "5 out of 10".
*
* @param array $arr an array consisting of an identifier string followed
* optionally by parameter values.
* @return mixed the translation of the identifier string into the
* current locale where all %s have been replaced by the corresponding
* parameter values. Returns false if no translation
*/
public function translate($arr)
{
if (!is_array($arr)) {
return;
}
$num_args = count($arr);
if ($num_args < 1) {
return;
}
$msg_id = $arr[0];
$args = array_slice($arr, 1);
$msg_string = false;
if (isset($this->configure['strings'][$msg_id])) {
$msg_string = $this->configure['strings'][$msg_id];
}
if ($msg_string == "" &&
isset($this->default_configure['strings'][$msg_id])) {
$msg_string = $this->default_configure['strings'][$msg_id];
}
if ($msg_string !== false) {
$num_string_args = substr_count($msg_string, "%s");
if ($num_string_args >= $num_args) {
$tmp_args = array_fill(0, $num_string_args + 1
- $num_args, "%s");
$args = array_merge($args, $tmp_args);
}
$msg_string = vsprintf($msg_string, $args);
}
return $msg_string;
}
/**
* Get the current IANA language tag being used by the search engine
*
* @return string an IANA language tag
*/
public function getLocaleTag()
{
return $this->locale_tag;
}
/**
* The text direction of the current locale being used by the text engine
*
* @return string either ltr (left-to-right) or rtl (right-to-left)
*/
public function getLocaleDirection()
{
return match ($this->writing_mode) {
"rl-tb", "tb-rl" => "rtl",
default => "ltr",
};
}
/**
* Get the writing mode of the current locale (text and block directions)
*
* @return string the current writing mode
*/
public function getWritingMode()
{
return $this->writing_mode;
}
/**
* Used to extract identifier strings from files with correct extensions,
* then these strings are merged with existing extracted strings for each
* locale as well as their translations (if an extract string has a
* translation the translation is untouched by this process).
*
* @param array $force_folders which locale subfolders should be forced
* updated to the fallback dir's version
*
* @return array a pair consisting of the data from the General.ini file
* together with an array of msg_ids msg_strings.
*/
public function extractMergeLocales($force_folders = [])
{
$list = $this->getLocaleList(true, false);
// getLocaleList will also create any missing locale dirs
$strings =
$this->getTranslateStrings($this->extract_dirs, $this->extensions);
$general_ini = L\parse_ini_with_fallback(C\LOCALE_DIR . "/general.ini");
$this->updateLocales($general_ini, $strings, $force_folders);
return [$general_ini, $strings];
}
/**
* Cycles through locale subdirectories in LOCALE_DIR, for each
* locale it merges out the current general_ini and strings data.
* It deletes identifiers that are not in strings, it adds new identifiers
* and it leaves existing identifier translation pairs untouched.
*
* @param array $general_ini data that would typically come from the
* General.ini file
* @param array $strings lines from what is equivalent to an ini file
* of msg_id msg_string pairs these lines also have comments on the
* file that strings were extracted from
* @param array $force_folders which locale subfolders should be forced
* updated to the fallback dir's version
*
*/
public function updateLocales($general_ini, $strings, $force_folders = [])
{
$path = C\LOCALE_DIR;
if (!$dh = @opendir($path)) {
die("Couldn't read locale directory!\n");
}
while (($obj = readdir($dh)) !== false) {
if ($obj[0] == '.') {
continue;
}
$cur_path = $path . '/' . $obj;
if (is_dir($cur_path)) {
$this->updateLocale($general_ini, $strings, $path, $obj,
null, $force_folders);
}
}
}
/**
* Updates the Configure.ini file and static pages for a particular locale.
*
* The Configure.ini has general information (at this point not really
* being used) about all locales together with specific msg_id (identifiers
* to be translated) and msg_string (translation) data. updateLocale takes
* line data coming from the General.ini file, strings extracted from
* documents that might need to be translation, the old Configure.ini file
* (this might have existing translations), as well as new translation
* data that might come from a localizer via a web form and
* combines these to produce a new Configure.ini file
*
* @param array $general_ini data from the General.ini file
* @param array $strings line array data extracted from files in
* directories that have strings in need of translation
* @param string $dir the directory of all the locales
* @param string $locale the particular locale in $dir to update
* @param array $new_configure translations of identifier strings from
* another source such as a localizer using a web form
* @param array $force_folders which locale subfolders should be forced
* updated to the fallback dir's version
*/
public function updateLocale($general_ini, $strings,
$dir, $locale, $new_configure = null, $force_folders = [])
{
$old_configure = [];
$cur_path = $dir . '/' . $locale;
if (file_exists($cur_path.'/configure.ini')) {
$old_configure = L\parse_ini_with_fallback(
$cur_path.'/configure.ini');
}
$fallback_path = C\FALLBACK_LOCALE_DIR. '/' . $locale;
$fallback_configure = [];
if (file_exists($fallback_path . '/configure.ini')) {
$fallback_configure = L\parse_ini_with_fallback(
$fallback_path . '/configure.ini');
}
// Note: we only update if $cur_path != $fallback_path
if (file_exists($fallback_path . '/resources') &&
$cur_path != $fallback_path) {
if (in_array("resources", $force_folders) &&
file_exists($cur_path . '/resources')) {
rename($cur_path . '/resources', $cur_path.
'/resources'.time().'old');
}
$this->updateLocaleSubFolder($cur_path . '/resources',
$fallback_path . '/resources', ["js", "php", "ftr",
"txt.gz"]);
}
$n = [];
$n[] = <<<EOT
; ***** BEGIN LICENSE BLOCK *****
; SeekQuarry/Yioop Open Source Pure PHP Search Engine, Crawler, and Indexer
; Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
;
; This program is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation, either version 3 of the License, or
; (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program. If not, see <https://www.gnu.org/licenses/>.
; ***** END LICENSE BLOCK *****
;
; configure.ini
;
; $locale configuration file
;
EOT;
foreach ($general_ini as $general_name => $general_value) {
if (is_array($general_value)) {
$n[] = "[$general_name]";
$new_configure[$general_name] =
$new_configure[$general_name] ?? null;
$old_configure[$general_name] =
$old_configure[$general_name] ?? [];
$fallback_configure[$general_name] =
$fallback_configure[$general_name] ?? [];
foreach ($general_value as $name => $value) {
$n[] = $this->updateTranslation(
$new_configure[$general_name],
$old_configure[$general_name],
$fallback_configure[$general_name],
$name, $value);
}
} else {
$n[] = $this->updateTranslation($new_configure,
$old_configure, $fallback_configure, $general_name);
}
}
$n[] = ";\n; Strings to translate on various pages\n;";
$n[] = "[strings]";
foreach ($strings as $string) {
if (isset($string[0]) && $string[0] == ";") {
$n[] = $string;
} else {
$new_configure['strings'] =
$new_configure['strings'] ?? null;
$old_configure['strings'] = $old_configure['strings'] ?? [];
$fallback_configure['strings'] =
$fallback_configure['strings'] ?? [];
$n[] = $this->updateTranslation($new_configure['strings'],
$old_configure['strings'], $fallback_configure['strings'],
$string);
}
}
$out = implode("\n", $n);
$out .= "\n";
file_put_contents($cur_path . '/configure.ini', $out);
}
/**
* Computes a string of the form string_id = 'translation' for a string_id
* from among translation array data in $new_configure (most preferred,
* probably come from recent web form data), $old_configure
* (probably from work dir), and $fallback_configure (probably from base
* dir of Yioop instance, least preferred).
*
* @param array $new_configure string_id => translation pairs
* @param array $old_configure string_id => translation pairs
* @param array $fallback_configure string_id => translation pairs
* @param string $string_id an id to translate
* @param string $default_value value to use if no configuration
* has a translation for a string_id
* @return string translation in format describe above
*/
public function updateTranslation($new_configure, $old_configure,
$fallback_configure, $string_id, $default_value = "")
{
$translation = $string_id . ' = "' .
self::iniEscape($this->lookupTranslation($new_configure,
$old_configure, $fallback_configure, $string_id,
$default_value)).'"';
return $translation;
}
/**
* Escapes a string for safe inclusion inside a double-quoted INI
* value, in a way that round-trips losslessly through PHP's
* parse_ini_file(). Only "\" and '"' need to be escaped (in that
* order). Notably we do NOT use addslashes here: addslashes also
* escapes single quotes and NUL bytes, neither of which need
* escaping inside a double-quoted INI value, and the parser
* does not undo the apostrophe escape on read — so addslashes
* would silently grow the on-disk apostrophe-escape sequence
* on every save (the historical cause of the multi-hundred-
* backslash strings that accumulated in some locale files).
*
* @param string $s in-memory translation string
* @return string the string with backslashes and double quotes
* escaped for inclusion between double quotes in INI
*/
public static function iniEscape($s)
{
return str_replace(['\\', '"'], ['\\\\', '\\"'], $s);
}
/**
* Translates a string_id from among translation array data in
* $new_configure (most preferred, probably come from recent web form
* data), $old_configure (probably from work dir), and $fallback_configure
* (probably from base dir of Yioop instance, least preferred).
*
* @param array $new_configure string_id => translation pairs
* @param array $old_configure string_id => translation pairs
* @param array $fallback_configure string_id => translation pairs
* @param string $string_id an id to translate
* @param string $default_value value to use if no configuration
* has a translation for a string_id
* @return string translation of string id
*/
public function lookupTranslation($new_configure, $old_configure,
$fallback_configure, $string_id, $default_value = "")
{
$new_translation = $this->isTranslated($string_id, $new_configure);
$old_translation = $this->isTranslated($string_id, $old_configure);
if ($new_translation || (isset($new_configure[$string_id]) &&
$new_configure[$string_id] === "" && $old_translation)) {
if ($string_id == "view_locale_version") {
$translation = C\RESOURCES_WIKI_VERSION;
} else {
$translation = $new_configure[$string_id];
}
} elseif ($this->isTranslated($string_id, $old_configure)) {
$translation = $old_configure[$string_id];
} elseif ($this->isTranslated($string_id, $fallback_configure)) {
$translation = $fallback_configure[$string_id];
} else {
$translation = $default_value;
}
return $translation;
}
/**
* Checks if the given string_id has a translation in translations
*
* @param string $string_id what to check if translated
* @param array $translations of form string_id => translation
* defaults to current configuration
* @return bool whether a translation of nonzero length exists
*/
public function isTranslated($string_id, $translations = false)
{
if ($string_id == "view_locale_version") {
return true;
}
if ($translations === false) {
$translations = $this->configure['strings'];
}
return isset($translations[$string_id]) &&
strlen($translations[$string_id]) > 0;
}
/**
* Copies over subfolder items of the correct file extensions
* which exists in a fallback directory, but not in the actual directory
* of a locale.
*
* @param string $locale_pages_path static page directory to which will
* copy
* @param string $fallback_pages_path static page directory from which will
* copy
* @param array $file_extensions an array of strings names of file
* extensions for example: .txt.gz .thtml .php ,etc
*/
public function updateLocaleSubFolder($locale_pages_path,
$fallback_pages_path, $file_extensions)
{
$change = false;
if (!file_exists($locale_pages_path)) {
mkdir($locale_pages_path);
$change = true;
}
foreach ($file_extensions as $file_extension) {
foreach (glob($fallback_pages_path."/*.$file_extension")
as $fallback_page_name) {
$basename = basename($fallback_page_name);
$locale_page_name = "$locale_pages_path/$basename";
if (!file_exists($locale_page_name)) {
copy($fallback_page_name, $locale_page_name);
$change = true;
}
}
}
$this->db->setWorldPermissionsRecursive($locale_pages_path);
}
/**
* Searches the directories provided looking for files matching the
* extensions provided. When such a file is found it is loaded and scanned
* for tl() function calls. The identifier string in this function call is
* then extracted and added to a line array of strings to be translated.
* This line array is formatted so that each line looks like a line that
* might occur in an PHP ini file. To understand this format one can look at
* the parse_ini_string function in the PHP manual or look at the
* Configure.ini files in the locale directory
*
* @param array $extract_dirs directories to start looking for files with
* strings to be translated
* @param array $extensions file extensions of files which might contain
* such strings
* @return array of lines for any ini file of msg_id msg_string pairs
*/
public function getTranslateStrings($extract_dirs, $extensions)
{
$strings = [];
$base_dirs = [C\BASE_DIR, C\APP_DIR];
foreach ($extract_dirs as $dir) {
foreach ($base_dirs as $base_dir) {
$path = $base_dir."/".$dir;
$dir_strings =
$this->traverseExtractRecursive($path, $extensions);
if (count($dir_strings) > 0) {
$out_path = substr($path, strlen(C\PARENT_DIR));
$strings[] = ";";
$strings[] = "; $out_path";
$strings = array_merge($strings, $dir_strings);
}
}
}
return $strings;
}
/**
* Traverses a directory and its subdirectories looking for files
* whose extensions come from the extensions array. As the traversal
* is done a strings array is created. Each time a file is found of
* any identifiers of strings that need to be translated are added to
* the strings array. In addition, ini style comments are added givne the
* line file and line number of the item to be translated
*
* @param string $dir current directory to start looking for files with
* strings to be translated
* @param array $extensions file extensions of files which might contain
* such strings
* @return array of lines for any ini file of msg_id msg_string pairs
*/
public function traverseExtractRecursive($dir, $extensions)
{
$strings = [];
if (!is_dir($dir) || !$dh = @opendir($dir)) {
return [];
}
while (($obj = readdir($dh)) !== false) {
if ($obj == '.' || $obj == '..') {
continue;
}
$cur_path = $dir . '/' . $obj;
if (is_dir($cur_path)) {
$dir_strings =
$this->traverseExtractRecursive($cur_path, $extensions);
if (count($dir_strings) > 0) {
$out_path = substr($cur_path, strlen(C\PARENT_DIR));
$strings[] = ";";
$strings[] = "; $out_path";
$strings = array_merge($strings, $dir_strings);
}
}
if (is_file($cur_path)) {
$path_parts = pathinfo($cur_path);
$extension = (isset($path_parts['extension'])) ?
$path_parts['extension'] : "";
if (in_array($extension, $extensions)) {
$contents = file_get_contents($cur_path);
$out_strings = [];
$num_matches = preg_match_all('/tl\(\s*' .
'[\'|\"]?([[:word:]]+?)[\'|\"]?[(\))|(\s+\,)]/',
$contents, $to_translates);
if ($num_matches > 0) {
$out_strings = $to_translates[1];
}
$out_strings = array_unique($out_strings);
if (!empty($out_strings)) {
$strings[] = ";";
$strings[] = "; $obj";
$strings = array_merge($strings, $out_strings);
}
}
}
}
closedir($dh);
return $strings;
}
}