l10n_update.locale.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <?php
  2. /**
  3. * @file
  4. * Override part of locale.inc library so we can manage string status
  5. */
  6. /**
  7. * Parses Gettext Portable Object file information and inserts into database
  8. *
  9. * This is an improved version of _locale_import_po() to handle translation status
  10. *
  11. * @param $file
  12. * Drupal file object corresponding to the PO file to import
  13. * @param $langcode
  14. * Language code
  15. * @param $mode
  16. * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
  17. * @param $group
  18. * Text group to import PO file into (eg. 'default' for interface translations)
  19. *
  20. * @return boolean
  21. * Result array on success. FALSE on failure
  22. */
  23. function _l10n_update_locale_import_po($file, $langcode, $mode, $group = NULL) {
  24. // Try to allocate enough time to parse and import the data.
  25. drupal_set_time_limit(240);
  26. // Check if we have the language already in the database.
  27. if (!db_query("SELECT COUNT(language) FROM {languages} WHERE language = :language", array(':language' => $langcode))->fetchField()) {
  28. drupal_set_message(t('The language selected for import is not supported.'), 'error');
  29. return FALSE;
  30. }
  31. // Get strings from file (returns on failure after a partial import, or on success)
  32. $status = _l10n_update_locale_import_read_po('db-store', $file, $mode, $langcode, $group);
  33. if ($status === FALSE) {
  34. // Error messages are set in _locale_import_read_po().
  35. return FALSE;
  36. }
  37. // Get status information on import process.
  38. list($header_done, $additions, $updates, $deletes, $skips) = _l10n_update_locale_import_one_string('db-report');
  39. if (!$header_done) {
  40. drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => $file->filename)), 'error');
  41. }
  42. watchdog('locale', 'Imported %file into %locale: %number new strings added, %update updated and %delete removed.', array('%file' => $file->filename, '%locale' => $langcode, '%number' => $additions, '%update' => $updates, '%delete' => $deletes));
  43. if ($skips) {
  44. watchdog('locale', '@count disallowed HTML string(s) in %file', array('@count' => $skips, '%file' => $file->uri), WATCHDOG_WARNING);
  45. }
  46. // Return results of this import.
  47. return array(
  48. 'file' => $file,
  49. 'language' => $langcode,
  50. 'add' => $additions,
  51. 'update' => $updates,
  52. 'delete' => $deletes,
  53. 'skip' => $skips,
  54. );
  55. }
  56. /**
  57. * Parses Gettext Portable Object file into an array
  58. *
  59. * @param $op
  60. * Storage operation type: db-store or mem-store
  61. * @param $file
  62. * Drupal file object corresponding to the PO file to import
  63. * @param $mode
  64. * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
  65. * @param $lang
  66. * Language code
  67. * @param $group
  68. * Text group to import PO file into (eg. 'default' for interface translations)
  69. */
  70. function _l10n_update_locale_import_read_po($op, $file, $mode = NULL, $lang = NULL, $group = 'default') {
  71. $fd = fopen($file->uri, "rb"); // File will get closed by PHP on return
  72. if (!$fd) {
  73. _locale_import_message('The translation import failed, because the file %filename could not be read.', $file);
  74. return FALSE;
  75. }
  76. $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR
  77. $current = array(); // Current entry being read
  78. $plural = 0; // Current plural form
  79. $lineno = 0; // Current line
  80. while (!feof($fd)) {
  81. $line = fgets($fd, 10*1024); // A line should not be this long
  82. if ($lineno == 0) {
  83. // The first line might come with a UTF-8 BOM, which should be removed.
  84. $line = str_replace("\xEF\xBB\xBF", '', $line);
  85. }
  86. $lineno++;
  87. $line = trim(strtr($line, array("\\\n" => "")));
  88. if (!strncmp("#", $line, 1)) { // A comment
  89. if ($context == "COMMENT") { // Already in comment context: add
  90. $current["#"][] = substr($line, 1);
  91. }
  92. elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
  93. _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
  94. $current = array();
  95. $current["#"][] = substr($line, 1);
  96. $context = "COMMENT";
  97. }
  98. else { // Parse error
  99. _locale_import_message('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', $file, $lineno);
  100. return FALSE;
  101. }
  102. }
  103. elseif (!strncmp("msgid_plural", $line, 12)) {
  104. if ($context != "MSGID") { // Must be plural form for current entry
  105. _locale_import_message('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', $file, $lineno);
  106. return FALSE;
  107. }
  108. $line = trim(substr($line, 12));
  109. $quoted = _locale_import_parse_quoted($line);
  110. if ($quoted === FALSE) {
  111. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  112. return FALSE;
  113. }
  114. $current["msgid"] = $current["msgid"] . "\0" . $quoted;
  115. $context = "MSGID_PLURAL";
  116. }
  117. elseif (!strncmp("msgid", $line, 5)) {
  118. if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
  119. _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
  120. $current = array();
  121. }
  122. elseif ($context == "MSGID") { // Already in this context? Parse error
  123. _locale_import_message('The translation file %filename contains an error: "msgid" is unexpected on line %line.', $file, $lineno);
  124. return FALSE;
  125. }
  126. $line = trim(substr($line, 5));
  127. $quoted = _locale_import_parse_quoted($line);
  128. if ($quoted === FALSE) {
  129. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  130. return FALSE;
  131. }
  132. $current["msgid"] = $quoted;
  133. $context = "MSGID";
  134. }
  135. elseif (!strncmp("msgctxt", $line, 7)) {
  136. if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one
  137. _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
  138. $current = array();
  139. }
  140. elseif (!empty($current["msgctxt"])) { // Already in this context? Parse error
  141. _locale_import_message('The translation file %filename contains an error: "msgctxt" is unexpected on line %line.', $file, $lineno);
  142. return FALSE;
  143. }
  144. $line = trim(substr($line, 7));
  145. $quoted = _locale_import_parse_quoted($line);
  146. if ($quoted === FALSE) {
  147. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  148. return FALSE;
  149. }
  150. $current["msgctxt"] = $quoted;
  151. $context = "MSGCTXT";
  152. }
  153. elseif (!strncmp("msgstr[", $line, 7)) {
  154. if (($context != "MSGID") && ($context != "MSGCTXT") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgxtxt, msgid_plural, or msgstr[]
  155. _locale_import_message('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', $file, $lineno);
  156. return FALSE;
  157. }
  158. if (strpos($line, "]") === FALSE) {
  159. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  160. return FALSE;
  161. }
  162. $frombracket = strstr($line, "[");
  163. $plural = substr($frombracket, 1, strpos($frombracket, "]") - 1);
  164. $line = trim(strstr($line, " "));
  165. $quoted = _locale_import_parse_quoted($line);
  166. if ($quoted === FALSE) {
  167. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  168. return FALSE;
  169. }
  170. $current["msgstr"][$plural] = $quoted;
  171. $context = "MSGSTR_ARR";
  172. }
  173. elseif (!strncmp("msgstr", $line, 6)) {
  174. if (($context != "MSGID") && ($context != "MSGCTXT")) { // Should come just after a msgid or msgctxt block
  175. _locale_import_message('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', $file, $lineno);
  176. return FALSE;
  177. }
  178. $line = trim(substr($line, 6));
  179. $quoted = _locale_import_parse_quoted($line);
  180. if ($quoted === FALSE) {
  181. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  182. return FALSE;
  183. }
  184. $current["msgstr"] = $quoted;
  185. $context = "MSGSTR";
  186. }
  187. elseif ($line != "") {
  188. $quoted = _locale_import_parse_quoted($line);
  189. if ($quoted === FALSE) {
  190. _locale_import_message('The translation file %filename contains a syntax error on line %line.', $file, $lineno);
  191. return FALSE;
  192. }
  193. if (($context == "MSGID") || ($context == "MSGID_PLURAL")) {
  194. $current["msgid"] .= $quoted;
  195. }
  196. elseif ($context == "MSGCTXT") {
  197. $current["msgctxt"] .= $quoted;
  198. }
  199. elseif ($context == "MSGSTR") {
  200. $current["msgstr"] .= $quoted;
  201. }
  202. elseif ($context == "MSGSTR_ARR") {
  203. $current["msgstr"][$plural] .= $quoted;
  204. }
  205. else {
  206. _locale_import_message('The translation file %filename contains an error: there is an unexpected string on line %line.', $file, $lineno);
  207. return FALSE;
  208. }
  209. }
  210. }
  211. // End of PO file, flush last entry.
  212. if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) {
  213. _l10n_update_locale_import_one_string($op, $current, $mode, $lang, $file, $group);
  214. }
  215. elseif ($context != "COMMENT") {
  216. _locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
  217. return FALSE;
  218. }
  219. }
  220. /**
  221. * Imports a string into the database
  222. *
  223. * @param $op
  224. * Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'
  225. * @param $value
  226. * Details of the string stored
  227. * @param $mode
  228. * Should existing translations be replaced LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE
  229. * @param $lang
  230. * Language to store the string in
  231. * @param $file
  232. * Object representation of file being imported, only required when op is 'db-store'
  233. * @param $group
  234. * Text group to import PO file into (eg. 'default' for interface translations)
  235. */
  236. function _l10n_update_locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL, $group = 'default') {
  237. $report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
  238. $header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
  239. $strings = &drupal_static(__FUNCTION__ . ':strings', array());
  240. switch ($op) {
  241. // Return stored strings
  242. case 'mem-report':
  243. return $strings;
  244. // Store string in memory (only supports single strings)
  245. case 'mem-store':
  246. $strings[isset($value['msgctxt']) ? $value['msgctxt'] : ''][$value['msgid']] = $value['msgstr'];
  247. return;
  248. // Called at end of import to inform the user
  249. case 'db-report':
  250. return array($header_done, $report['additions'], $report['updates'], $report['deletes'], $report['skips']);
  251. // Store the string we got in the database.
  252. case 'db-store':
  253. // We got header information.
  254. if ($value['msgid'] == '') {
  255. $languages = language_list();
  256. if (($mode != LOCALE_IMPORT_KEEP) || empty($languages[$lang]->plurals)) {
  257. // Since we only need to parse the header if we ought to update the
  258. // plural formula, only run this if we don't need to keep existing
  259. // data untouched or if we don't have an existing plural formula.
  260. $header = _locale_import_parse_header($value['msgstr']);
  261. // Get the plural formula and update in database.
  262. if (isset($header["Plural-Forms"]) && $p = _locale_import_parse_plural_forms($header["Plural-Forms"], $file->uri)) {
  263. list($nplurals, $plural) = $p;
  264. db_update('languages')
  265. ->fields(array(
  266. 'plurals' => $nplurals,
  267. 'formula' => $plural,
  268. ))
  269. ->condition('language', $lang)
  270. ->execute();
  271. }
  272. else {
  273. db_update('languages')
  274. ->fields(array(
  275. 'plurals' => 0,
  276. 'formula' => '',
  277. ))
  278. ->condition('language', $lang)
  279. ->execute();
  280. }
  281. }
  282. $header_done = TRUE;
  283. }
  284. else {
  285. // Some real string to import.
  286. $comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
  287. if (strpos($value['msgid'], "\0")) {
  288. // This string has plural versions.
  289. $english = explode("\0", $value['msgid'], 2);
  290. $entries = array_keys($value['msgstr']);
  291. for ($i = 3; $i <= count($entries); $i++) {
  292. $english[] = $english[1];
  293. }
  294. $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
  295. $english = array_map('_locale_import_append_plural', $english, $entries);
  296. foreach ($translation as $key => $trans) {
  297. if ($key == 0) {
  298. $plid = 0;
  299. }
  300. $plid = _l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $group, $comments, $mode, L10N_UPDATE_STRING_DEFAULT, $plid, $key);
  301. }
  302. }
  303. else {
  304. // A simple string to import.
  305. $english = $value['msgid'];
  306. $translation = $value['msgstr'];
  307. _l10n_update_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $group, $comments, $mode);
  308. }
  309. }
  310. } // end of db-store operation
  311. }
  312. /**
  313. * Import one string into the database.
  314. *
  315. * @param $report
  316. * Report array summarizing the number of changes done in the form:
  317. * array(inserts, updates, deletes).
  318. * @param $langcode
  319. * Language code to import string into.
  320. * @param $context
  321. * The context of this string.
  322. * @param $source
  323. * Source string.
  324. * @param $translation
  325. * Translation to language specified in $langcode.
  326. * @param $textgroup
  327. * Name of textgroup to store translation in.
  328. * @param $location
  329. * Location value to save with source string.
  330. * @param $mode
  331. * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
  332. * @param $status
  333. * Status of translation if created: L10N_UPDATE_STRING_DEFAULT or L10N_UPDATE_STRING_CUSTOM
  334. * @param $plid
  335. * Optional plural ID to use.
  336. * @param $plural
  337. * Optional plural value to use.
  338. * @return
  339. * The string ID of the existing string modified or the new string added.
  340. */
  341. function _l10n_update_locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $status = L10N_UPDATE_STRING_DEFAULT, $plid = 0, $plural = 0) {
  342. $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(':source' => $source, ':context' => $context, ':textgroup' => $textgroup))->fetchField();
  343. if (!empty($translation)) {
  344. // Skip this string unless it passes a check for dangerous code.
  345. // Text groups other than default still can contain HTML tags
  346. // (i.e. translatable blocks).
  347. if ($textgroup == "default" && !locale_string_is_safe($translation)) {
  348. $report['skips']++;
  349. $lid = 0;
  350. watchdog('locale', 'Disallowed HTML detected. String not imported: %string', array('%string' => $translation), WATCHDOG_WARNING);
  351. }
  352. elseif ($lid) {
  353. // We have this source string saved already.
  354. db_update('locales_source')
  355. ->fields(array(
  356. 'location' => $location,
  357. ))
  358. ->condition('lid', $lid)
  359. ->execute();
  360. $exists = db_query("SELECT lid, l10n_status FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchObject();
  361. if (!$exists) {
  362. // No translation in this language.
  363. db_insert('locales_target')
  364. ->fields(array(
  365. 'lid' => $lid,
  366. 'language' => $langcode,
  367. 'translation' => $translation,
  368. 'plid' => $plid,
  369. 'plural' => $plural,
  370. ))
  371. ->execute();
  372. $report['additions']++;
  373. }
  374. elseif (($exists->l10n_status == L10N_UPDATE_STRING_DEFAULT && $mode == LOCALE_UPDATE_OVERRIDE_DEFAULT) || $mode == LOCALE_IMPORT_OVERWRITE) {
  375. // Translation exists, only overwrite if instructed.
  376. db_update('locales_target')
  377. ->fields(array(
  378. 'translation' => $translation,
  379. 'plid' => $plid,
  380. 'plural' => $plural,
  381. ))
  382. ->condition('language', $langcode)
  383. ->condition('lid', $lid)
  384. ->execute();
  385. $report['updates']++;
  386. }
  387. }
  388. else {
  389. // No such source string in the database yet.
  390. $lid = db_insert('locales_source')
  391. ->fields(array(
  392. 'location' => $location,
  393. 'source' => $source,
  394. 'context' => (string) $context,
  395. 'textgroup' => $textgroup,
  396. ))
  397. ->execute();
  398. db_insert('locales_target')
  399. ->fields(array(
  400. 'lid' => $lid,
  401. 'language' => $langcode,
  402. 'translation' => $translation,
  403. 'plid' => $plid,
  404. 'plural' => $plural,
  405. 'l10n_status' => $status,
  406. ))
  407. ->execute();
  408. $report['additions']++;
  409. }
  410. }
  411. elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
  412. // Empty translation, remove existing if instructed.
  413. db_delete('locales_target')
  414. ->condition('language', $langcode)
  415. ->condition('lid', $lid)
  416. ->condition('plid', $plid)
  417. ->condition('plural', $plural)
  418. ->execute();
  419. $report['deletes']++;
  420. }
  421. return $lid;
  422. }