l10n_update.locale.inc 18 KB

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