add_lang_key.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. /**
  3. * Add a missing language key for en or ru.
  4. *
  5. * The string is written into the module language XML (skipped when that
  6. * key is already there) and into the database, then the language cache
  7. * is recompiled. An existing translation is left unchanged.
  8. *
  9. * Usage:
  10. * php scripts/add_lang_key.php <en|ru> <key> <translation>
  11. * php scripts/add_lang_key.php <en|ru> <key> - <<'EOF'
  12. * line one
  13. * line two
  14. * EOF
  15. *
  16. * A translation of "-" is read from stdin. A single trailing newline on
  17. * stdin is removed; newlines inside the text are kept.
  18. *
  19. * Run inside the php container:
  20. * docker exec -i una-php-1 php /opt/una/scripts/add_lang_key.php en _sys_example "Hello"
  21. */
  22. if (PHP_SAPI !== 'cli') {
  23. fwrite(STDERR, "CLI only\n");
  24. exit(1);
  25. }
  26. $sLang = isset($argv[1]) ? strtolower($argv[1]) : '';
  27. $sKey = isset($argv[2]) ? $argv[2] : '';
  28. $bFromStdin = !isset($argv[3]) || $argv[3] === '-';
  29. if (!in_array($sLang, array('en', 'ru'), true) || $sKey === '' || isset($argv[4])) {
  30. fwrite(STDERR, usage());
  31. exit(2);
  32. }
  33. if (strpbrk($sKey, "\r\n") !== false) {
  34. fwrite(STDERR, "Key must be a single line\n");
  35. exit(2);
  36. }
  37. if ($bFromStdin) {
  38. $sTranslation = stream_get_contents(STDIN);
  39. if ($sTranslation === false)
  40. $sTranslation = '';
  41. if (substr($sTranslation, -2) === "\r\n")
  42. $sTranslation = substr($sTranslation, 0, -2);
  43. else if (substr($sTranslation, -1) === "\n")
  44. $sTranslation = substr($sTranslation, 0, -1);
  45. }
  46. else {
  47. $sTranslation = $argv[3];
  48. }
  49. if ($sTranslation === '') {
  50. fwrite(STDERR, "Translation is empty\n");
  51. exit(2);
  52. }
  53. if (function_exists('mb_check_encoding') && !mb_check_encoding($sTranslation, 'UTF-8')) {
  54. fwrite(STDERR, "Translation is not valid UTF-8\n");
  55. exit(2);
  56. }
  57. $sRoot = dirname(__DIR__) . '/';
  58. $aModules = loadModules($sRoot);
  59. $aLangModules = loadLanguageModules($sRoot);
  60. $aModule = findModuleByExistingKey($sRoot, $sKey, $aModules);
  61. if (!$aModule)
  62. $aModule = inferModule($sKey, $aModules);
  63. $sFile = resolveLangFile($sRoot, $sLang, $aModule, $aLangModules);
  64. $sRel = ltrim(str_replace('\\', '/', substr($sFile, strlen($sRoot))), '/');
  65. $sExisting = is_file($sFile) ? readLangString($sFile, $sKey) : null;
  66. if ($sExisting !== null) {
  67. echo "xml: already present in $sRel\n";
  68. if ($sExisting !== $sTranslation)
  69. echo "kept the translation already in the language file\n";
  70. $sStore = $sExisting;
  71. }
  72. else {
  73. if (!writeLangString($sFile, $sLang, $sKey, $sTranslation)) {
  74. fwrite(STDERR, "Failed to write $sRel\n");
  75. exit(1);
  76. }
  77. echo "xml: added to $sRel\n";
  78. $sStore = $sTranslation;
  79. }
  80. if (dbAddMissing($sLang, $sKey, $sStore, $aModule['language_category']) === false)
  81. exit(1);
  82. exit(0);
  83. function usage()
  84. {
  85. return <<<TXT
  86. Usage: php add_lang_key.php <en|ru> <key> <translation>
  87. php add_lang_key.php <en|ru> <key> - (translation from stdin, may span lines)
  88. Adds the key only when that language does not already have it.
  89. TXT;
  90. }
  91. function loadModules($sRoot)
  92. {
  93. $a = array(
  94. 'system' => array(
  95. 'name' => 'system',
  96. 'home_dir' => 'system/',
  97. 'language_category' => 'System',
  98. ),
  99. );
  100. foreach (glob($sRoot . 'modules/*/*/install/config.php') as $sConfig) {
  101. $s = file_get_contents($sConfig);
  102. if ($s === false)
  103. continue;
  104. $sName = configValue($s, 'name');
  105. $sHome = configValue($s, 'home_dir');
  106. if ($sName === null || $sHome === null || $sName === '')
  107. continue;
  108. $sCategory = configValue($s, 'language_category');
  109. if ($sCategory === null || $sCategory === '')
  110. $sCategory = $sName;
  111. $a[$sName] = array(
  112. 'name' => $sName,
  113. 'home_dir' => rtrim($sHome, '/') . '/',
  114. 'language_category' => $sCategory,
  115. );
  116. }
  117. return $a;
  118. }
  119. function loadLanguageModules($sRoot)
  120. {
  121. $a = array(
  122. 'en' => array('home_dir' => 'boonex/english/'),
  123. 'ru' => array('home_dir' => 'boonex/russian/'),
  124. );
  125. foreach (glob($sRoot . 'modules/*/*/install/config.php') as $sConfig) {
  126. $s = file_get_contents($sConfig);
  127. if ($s === false)
  128. continue;
  129. $sUri = configValue($s, 'home_uri');
  130. $sHome = configValue($s, 'home_dir');
  131. if (($sUri !== 'en' && $sUri !== 'ru') || $sHome === null)
  132. continue;
  133. $a[$sUri] = array('home_dir' => rtrim($sHome, '/') . '/');
  134. }
  135. return $a;
  136. }
  137. function configValue($sSource, $sKey)
  138. {
  139. $sPattern = '/[\'"]' . preg_quote($sKey, '/') . '[\'"]\s*=>\s*[\'"]([^\'"]*)[\'"]/';
  140. if (!preg_match($sPattern, $sSource, $aMatch))
  141. return null;
  142. return $aMatch[1];
  143. }
  144. function findModuleByExistingKey($sRoot, $sKey, $aModules)
  145. {
  146. $aFound = array();
  147. $aFiles = array_merge(
  148. glob($sRoot . 'modules/*/*/install/langs/*.xml') ?: array(),
  149. glob($sRoot . 'modules/*/*/data/langs/*/*.xml') ?: array()
  150. );
  151. foreach ($aFiles as $sFile) {
  152. if (!fileHasKey($sFile, $sKey))
  153. continue;
  154. $sNorm = str_replace('\\', '/', $sFile);
  155. $aModule = null;
  156. if (preg_match('#/data/langs/([^/]+)/[^/]+\.xml$#', $sNorm, $aMatch) && isset($aModules[$aMatch[1]]))
  157. $aModule = $aModules[$aMatch[1]];
  158. else if (preg_match('#/modules/(.+)/install/langs/[^/]+\.xml$#', $sNorm, $aMatch)) {
  159. $sHome = $aMatch[1] . '/';
  160. foreach ($aModules as $aCandidate) {
  161. if ($aCandidate['home_dir'] === $sHome) {
  162. $aModule = $aCandidate;
  163. break;
  164. }
  165. }
  166. }
  167. if ($aModule)
  168. $aFound[$aModule['name']] = $aModule;
  169. }
  170. if (!$aFound)
  171. return null;
  172. if (isset($aFound['system']) && count($aFound) > 1)
  173. unset($aFound['system']);
  174. return reset($aFound);
  175. }
  176. function inferModule($sKey, $aModules)
  177. {
  178. $sBare = ltrim($sKey, '_');
  179. $aNames = array_keys($aModules);
  180. usort($aNames, function ($sA, $sB) {
  181. return strlen($sB) - strlen($sA);
  182. });
  183. foreach ($aNames as $sName) {
  184. if ($sName === 'system')
  185. continue;
  186. if ($sBare === $sName || strpos($sBare, $sName . '_') === 0)
  187. return $aModules[$sName];
  188. }
  189. return $aModules['system'];
  190. }
  191. function resolveLangFile($sRoot, $sLang, $aModule, $aLangModules)
  192. {
  193. $sPack = $sRoot . 'modules/' . $aLangModules[$sLang]['home_dir'] . 'data/langs/' . $aModule['name'] . '/' . $sLang . '.xml';
  194. if (is_file($sPack))
  195. return $sPack;
  196. if ($aModule['name'] !== 'system') {
  197. $sInstall = $sRoot . 'modules/' . $aModule['home_dir'] . 'install/langs/' . $sLang . '.xml';
  198. if (is_file($sInstall) || $sLang === 'en')
  199. return $sInstall;
  200. }
  201. return $sPack;
  202. }
  203. function fileHasKey($sPath, $sKey)
  204. {
  205. return readLangString($sPath, $sKey) !== null;
  206. }
  207. function readLangString($sPath, $sKey)
  208. {
  209. $s = @file_get_contents($sPath);
  210. if ($s === false)
  211. return null;
  212. $aVariants = array_unique(array($sKey, htmlspecialchars($sKey, ENT_QUOTES | ENT_XML1, 'UTF-8')));
  213. foreach ($aVariants as $sVariant) {
  214. $sPattern = '/<string\b[^>]*\bname=(["\'])' . preg_quote($sVariant, '/') . '\\1[^>]*>(.*?)<\/string>/s';
  215. if (!preg_match($sPattern, $s, $aMatch))
  216. continue;
  217. $sInner = $aMatch[2];
  218. if (preg_match('/^\s*<!\[CDATA\[(.*)\]\]>\s*$/s', $sInner, $aCdata))
  219. return str_replace(']]]]><![CDATA[>', ']]>', $aCdata[1]);
  220. return html_entity_decode($sInner, ENT_QUOTES | ENT_XML1, 'UTF-8');
  221. }
  222. return null;
  223. }
  224. function writeLangString($sFile, $sLang, $sKey, $sTranslation)
  225. {
  226. $aMeta = array(
  227. 'en' => array('flag' => 'gb', 'title' => 'English'),
  228. 'ru' => array('flag' => 'ru', 'title' => 'Russian'),
  229. );
  230. $sNl = "\n";
  231. if (is_file($sFile)) {
  232. $sXml = file_get_contents($sFile);
  233. if ($sXml === false)
  234. return false;
  235. if (strpos($sXml, "\r\n") !== false)
  236. $sNl = "\r\n";
  237. }
  238. else {
  239. $sDir = dirname($sFile);
  240. if (!is_dir($sDir) && !mkdir($sDir, 0775, true))
  241. return false;
  242. $sXml = '<?xml version="1.0" encoding="utf-8"?>' . $sNl
  243. . '<resources name="' . $sLang . '" flag="' . $aMeta[$sLang]['flag'] . '" title="' . $aMeta[$sLang]['title'] . '">' . $sNl
  244. . '</resources>' . $sNl;
  245. }
  246. $iPos = strrpos($sXml, '</resources>');
  247. if ($iPos === false)
  248. return false;
  249. $sLine = ' <string name="' . htmlspecialchars($sKey, ENT_QUOTES | ENT_XML1, 'UTF-8') . '"><![CDATA[' . cdata($sTranslation) . ']]></string>' . $sNl;
  250. if ($iPos > 0 && $sXml[$iPos - 1] !== "\n")
  251. $sLine = $sNl . $sLine;
  252. $sXml = substr($sXml, 0, $iPos) . $sLine . substr($sXml, $iPos);
  253. return file_put_contents($sFile, $sXml, LOCK_EX) !== false;
  254. }
  255. function cdata($s)
  256. {
  257. return str_replace(']]>', ']]]]><![CDATA[>', $s);
  258. }
  259. function dbAddMissing($sLang, $sKey, $sTranslation, $sCategory)
  260. {
  261. bootUna();
  262. $oDb = BxDolDb::getInstance();
  263. $aLang = $oDb->getRow($oDb->prepare("SELECT `ID` AS `id` FROM `sys_localization_languages` WHERE `Name` = ? LIMIT 1", $sLang));
  264. if (!$aLang) {
  265. fwrite(STDERR, "Language '$sLang' is not installed in the database\n");
  266. return false;
  267. }
  268. $iLangId = (int)$aLang['id'];
  269. $aKey = $oDb->getRow($oDb->prepare("SELECT `ID` AS `id` FROM `sys_localization_keys` WHERE `Key` = ? LIMIT 1", $sKey));
  270. if (!$aKey) {
  271. $iCategoryId = categoryId($oDb, $sCategory);
  272. if (!$iCategoryId) {
  273. fwrite(STDERR, "Could not resolve language category '$sCategory'\n");
  274. return false;
  275. }
  276. $iInserted = (int)$oDb->query($oDb->prepare(
  277. "INSERT INTO `sys_localization_keys` (`IDCategory`, `Key`) VALUES (?, ?)",
  278. $iCategoryId,
  279. $sKey
  280. ));
  281. if ($iInserted <= 0) {
  282. fwrite(STDERR, "Could not insert language key\n");
  283. return false;
  284. }
  285. $iKeyId = (int)$oDb->lastId();
  286. echo "db: created key $iKeyId in '$sCategory'\n";
  287. }
  288. else {
  289. $iKeyId = (int)$aKey['id'];
  290. }
  291. $aString = $oDb->getRow($oDb->prepare(
  292. "SELECT `IDKey` AS `id` FROM `sys_localization_strings` WHERE `IDKey` = ? AND `IDLanguage` = ? LIMIT 1",
  293. $iKeyId,
  294. $iLangId
  295. ));
  296. if ($aString) {
  297. echo "db: already present for $sLang\n";
  298. return true;
  299. }
  300. $iInserted = (int)$oDb->query($oDb->prepare(
  301. "INSERT INTO `sys_localization_strings` (`IDKey`, `IDLanguage`, `String`) VALUES (?, ?, ?)",
  302. $iKeyId,
  303. $iLangId,
  304. $sTranslation
  305. ));
  306. if ($iInserted <= 0) {
  307. fwrite(STDERR, "Could not insert translation\n");
  308. return false;
  309. }
  310. bx_import('BxDolStudioLanguagesUtils');
  311. BxDolLanguages::getInstance();
  312. if (!BxDolStudioLanguagesUtils::getInstance()->compileLanguage($iLangId, true)) {
  313. fwrite(STDERR, "Translation stored, but the language cache was not recompiled\n");
  314. return false;
  315. }
  316. echo "db: added translation for $sLang and recompiled\n";
  317. return true;
  318. }
  319. function categoryId($oDb, $sCategory)
  320. {
  321. $aCategory = $oDb->getRow($oDb->prepare(
  322. "SELECT `ID` AS `id` FROM `sys_localization_categories` WHERE `Name` = ? LIMIT 1",
  323. $sCategory
  324. ));
  325. if ($aCategory)
  326. return (int)$aCategory['id'];
  327. $iInserted = (int)$oDb->query($oDb->prepare(
  328. "INSERT INTO `sys_localization_categories` (`Name`) VALUES (?)",
  329. $sCategory
  330. ));
  331. if ($iInserted <= 0)
  332. return 0;
  333. return (int)$oDb->lastId();
  334. }
  335. function bootUna()
  336. {
  337. $GLOBALS['bx_profiler_disable'] = true;
  338. if (!defined('BX_DOL_CRON_EXECUTE'))
  339. define('BX_DOL_CRON_EXECUTE', '1');
  340. $_ENV['UNA_SKIP_REDIRECT'] = '1';
  341. $_ENV['UNA_SKIP_MINIMAL_REQUIREMENTS'] = '1';
  342. $_ENV['UNA_SKIP_INSTALL_FOLDER_CHECK'] = '1';
  343. if (empty($_SERVER['HTTP_HOST'])) {
  344. $_SERVER['HTTP_HOST'] = 'hihi.com';
  345. $_SERVER['REQUEST_URI'] = '/una/';
  346. $_SERVER['SERVER_NAME'] = 'hihi.com';
  347. $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  348. }
  349. require_once dirname(__DIR__) . '/inc/header.inc.php';
  350. require_once BX_DIRECTORY_PATH_INC . 'design.inc.php';
  351. }