i18n_string.inc 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282
  1. <?php
  2. /**
  3. * @file
  4. * API for internationalization strings
  5. */
  6. /**
  7. * String object that contains source and translations.
  8. *
  9. * Note all database operations must go through textgroup object so we can switch storage at some point.
  10. */
  11. class i18n_string_object {
  12. // Updated source string
  13. public $string;
  14. // Properties from locale source
  15. public $lid;
  16. public $source;
  17. public $textgroup;
  18. public $location;
  19. public $context;
  20. public $version;
  21. // Properties from i18n_tring
  22. public $type;
  23. public $objectid;
  24. public $property;
  25. public $objectkey;
  26. public $format;
  27. // Properties from metadata
  28. public $title;
  29. // Array of translations to multiple languages
  30. public $translations;
  31. // Textgroup object
  32. protected $_textgroup;
  33. /**
  34. * Class constructor
  35. */
  36. public function __construct($data = NULL) {
  37. if ($data) {
  38. $this->set_properties($data);
  39. }
  40. }
  41. /**
  42. * Get message parameters from context and string.
  43. */
  44. public function get_args() {
  45. return array(
  46. '%location' => $this->location,
  47. '%textgroup' => $this->textgroup,
  48. '%string' => ($string = $this->get_string()) ? $string : t('[empty string]'),
  49. );
  50. }
  51. /**
  52. * Set context properties
  53. */
  54. public function set_context($context) {
  55. $parts = is_array($context) ? $context : explode(':', $context);
  56. $this->context = is_array($context) ? implode(':', $context) : $context;
  57. // Location will be the full string name
  58. $this->location = $this->textgroup . ':' . $this->context;
  59. $this->type = array_shift($parts);
  60. $this->objectid = $parts ? array_shift($parts) : '';
  61. $this->objectkey = (int)$this->objectid;
  62. // Remaining elements glued again with ':'
  63. $this->property = $parts ? implode(':', $parts) : '';
  64. return $this;
  65. }
  66. /**
  67. * Get string name including textgroup and context
  68. */
  69. public function get_name() {
  70. return $this->textgroup . ':' . $this->type . ':' . $this->objectid . ':' . $this->property;
  71. }
  72. /**
  73. * Get source string
  74. */
  75. public function get_string() {
  76. if (isset($this->string)) {
  77. return $this->string;
  78. }
  79. elseif (isset($this->source)) {
  80. return $this->source;
  81. }
  82. elseif ($this->textgroup()->debug) {
  83. return empty($this->lid) ? t('[Source not found]') : t('[String not found]');
  84. }
  85. else {
  86. return '';
  87. }
  88. }
  89. /**
  90. * Set source string
  91. *
  92. * @param $string
  93. * Plain string or array with 'string', 'format', etc...
  94. */
  95. public function set_string($string) {
  96. if (is_array($string)) {
  97. $this->string = isset($string['string']) ? $string['string'] : NULL;
  98. if (isset($string['format'])) {
  99. $this->format = $string['format'];
  100. }
  101. }
  102. else {
  103. $this->string = $string;
  104. }
  105. if (isset($string['title'])) {
  106. $this->title = $string['title'];
  107. }
  108. return $this;
  109. }
  110. /**
  111. * Get string title.
  112. */
  113. public function get_title() {
  114. return isset($this->title) ? $this->title : t('String');
  115. }
  116. /**
  117. * Get translation to language from string object
  118. */
  119. public function get_translation($langcode) {
  120. if (!isset($this->translations[$langcode])) {
  121. $translation = $this->textgroup()->load_translation($this, $langcode);
  122. if ($translation && isset($translation->translation)) {
  123. $this->set_translation($translation, $langcode);
  124. }
  125. else {
  126. // No source, no translation
  127. $this->translations[$langcode] = FALSE;
  128. }
  129. }
  130. // Which doesn't mean we've got a translation, only that we've got the result cached
  131. return $this->translations[$langcode];
  132. }
  133. /**
  134. * Set translation for language
  135. *
  136. * @param $translation
  137. * Translation object (from database) or string
  138. */
  139. public function set_translation($translation, $langcode = NULL) {
  140. if (is_object($translation)) {
  141. $langcode = $langcode ? $langcode : $translation->language;
  142. $string = isset($translation->translation) ? $translation->translation : FALSE;
  143. $this->set_properties($translation);
  144. }
  145. else {
  146. $string = $translation;
  147. }
  148. $this->translations[$langcode] = $string;
  149. return $this;
  150. }
  151. /**
  152. * Format the resulting translation or the default string applying callbacks
  153. *
  154. * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info
  155. */
  156. public function format_translation($langcode, $options = array()) {
  157. $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug);
  158. if ($translation = $this->get_translation($langcode)) {
  159. $string = $translation;
  160. if (isset($options['filter'])) {
  161. $string = call_user_func($options['filter'], $string);
  162. }
  163. }
  164. else {
  165. // Get default source string if no translation.
  166. $string = $this->get_string();
  167. $options['sanitize'] = !empty($options['sanitize default']);
  168. }
  169. if (!empty($this->format)) {
  170. $options += array('format' => $this->format);
  171. }
  172. // Add debug information if enabled
  173. if ($options['debug']) {
  174. $info = array($langcode, $this->textgroup, $this->context);
  175. if (!empty($this->format)) {
  176. $info[] = $this->format;
  177. }
  178. $options += array('suffix' => '');
  179. $options['suffix'] .= ' [' . implode(':', $info) . ']';
  180. }
  181. // Finally, apply options, filters, callback, etc...
  182. return i18n_string_format($string, $options);
  183. }
  184. /**
  185. * Get source string provided a string object.
  186. *
  187. * @return
  188. * String object if source exists.
  189. */
  190. public function get_source() {
  191. // If already searched and not found we don't have a source,
  192. if (isset($this->lid) && !$this->lid) {
  193. return NULL;
  194. }
  195. elseif (!isset($this->lid) || !isset($this->source)) {
  196. // We may have lid from loading a translation but not loaded the source yet.
  197. if ($source = $this->textgroup()->load_source($this)) {
  198. // Set properties but don't override existing ones
  199. $this->set_properties($source, FALSE, FALSE);
  200. if (!isset($this->string)) {
  201. $this->string = $source->source;
  202. }
  203. return $this;
  204. }
  205. else {
  206. $this->lid = FALSE;
  207. return NULL;
  208. }
  209. }
  210. else {
  211. return $this;
  212. }
  213. }
  214. /**
  215. * Set properties from object or array
  216. *
  217. * @param $properties
  218. * Obejct or array of properties
  219. * @param $set_null
  220. * Whether to set null properties too
  221. * @param $override
  222. * Whether to set properties that are already set in this object
  223. */
  224. public function set_properties($properties, $set_null = TRUE, $override = TRUE) {
  225. foreach ((array)$properties as $field => $value) {
  226. if (property_exists($this, $field) && ($set_null || isset($value)) && ($override || !isset($this->$field))) {
  227. $this->$field = $value;
  228. }
  229. }
  230. return $this;
  231. }
  232. /**
  233. * Access textgroup object
  234. */
  235. protected function textgroup() {
  236. if (!isset($this->_textgroup)) {
  237. $this->_textgroup = i18n_string_textgroup($this->textgroup);
  238. }
  239. return $this->_textgroup;
  240. }
  241. /**
  242. * Update this string.
  243. */
  244. public function update($options = array()) {
  245. return $this->textgroup()->string_update($this, $options);
  246. }
  247. /**
  248. * Delete this string.
  249. */
  250. public function remove($options = array()) {
  251. return $this->textgroup()->string_remove($this, $options);
  252. }
  253. /**
  254. * Check whether there is any problem for the user to translate a this string.
  255. *
  256. * @param $account
  257. * Optional user account, defaults to current user.
  258. *
  259. * @return
  260. * None if the user has access to translate the string.
  261. * Error message if the user cannot translate that string.
  262. */
  263. public function check_translate_access($account = NULL) {
  264. return i18n_string_translate_check_string($this, $account);
  265. }
  266. }
  267. /**
  268. * Textgroup handler for i18n_string API
  269. */
  270. class i18n_string_textgroup_default {
  271. // Text group name
  272. public $textgroup;
  273. // Debug flag, set to true to print out more information.
  274. public $debug;
  275. // Cached or preloaded string objects
  276. public $strings;
  277. // Multiple translations search map
  278. protected $cache_multiple;
  279. /**
  280. * Class constructor.
  281. *
  282. * There are to hidden variables to produce debugging information:
  283. * - 'i18n_string_debug', generic for all text groups.
  284. * - 'i18n_string_debug_TEXTGROUP', enable debug only for TEXTGROUP.
  285. */
  286. public function __construct($textgroup) {
  287. $this->textgroup = $textgroup;
  288. $this->debug = variable_get('i18n_string_debug', FALSE) || variable_get('i18n_string_debug_' . $textgroup, FALSE);
  289. }
  290. /**
  291. * Build string object
  292. *
  293. * @param $context
  294. * Context array or string
  295. * @param $string string
  296. * Current value for string source
  297. */
  298. public function build_string($context, $string = NULL) {
  299. // First try to locate string on cache
  300. $context = is_array($context) ? implode(':', $context) : $context;
  301. if ($cached = $this->cache_get($context)) {
  302. $i18nstring = $cached;
  303. }
  304. else {
  305. $i18nstring = new i18n_string_object();
  306. $i18nstring->textgroup = $this->textgroup;
  307. $i18nstring->set_context($context);
  308. $this->cache_set($context, $i18nstring);
  309. }
  310. if (isset($string)) {
  311. $i18nstring->set_string($string);
  312. }
  313. return $i18nstring;
  314. }
  315. /**
  316. * Add source string to the locale tables for translation.
  317. *
  318. * It will also add data into i18n_string table for faster retrieval and indexing of groups of strings.
  319. * Some string context doesn't have a numeric oid (I.e. content types), it will be set to zero.
  320. *
  321. * This function checks for already existing string without context for this textgroup and updates it accordingly.
  322. * It is intended for backwards compatibility, using already created strings.
  323. *
  324. * @param $i18nstring
  325. * String object
  326. * @param $format
  327. * Text format, for strings that will go through some filter
  328. * @return
  329. * Update status.
  330. */
  331. protected function string_add($i18nstring, $options = array()) {
  332. $options += array('watchdog' => TRUE);
  333. // Default return status if nothing happens
  334. $status = -1;
  335. $source = NULL;
  336. $location = $i18nstring->location;
  337. // The string may not be allowed for translation depending on its format.
  338. if (!$this->string_check($i18nstring, $options)) {
  339. // The format may have changed and it's not allowed now, delete the source string
  340. return $this->string_remove($i18nstring, $options);
  341. }
  342. elseif ($source = $i18nstring->get_source()) {
  343. if ($source->source != $i18nstring->string || $source->location != $location) {
  344. $i18nstring->location = $location;
  345. // String has changed, mark translations for update
  346. $status = $this->save_source($i18nstring);
  347. db_update('locales_target')
  348. ->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))
  349. ->condition('lid', $source->lid)
  350. ->execute();
  351. }
  352. elseif (empty($source->version)) {
  353. // When refreshing strings, we've done version = 0, update it
  354. $this->save_source($i18nstring);
  355. }
  356. }
  357. else {
  358. // We don't have the source object, create it
  359. $status = $this->save_source($i18nstring);
  360. }
  361. // Make sure we have i18n_string part, create or update
  362. // This will also create the source object if doesn't exist
  363. $this->save_string($i18nstring);
  364. if ($options['watchdog']) {
  365. switch ($status) {
  366. case SAVED_UPDATED:
  367. watchdog('i18n_string', 'Updated string %location for textgroup %textgroup: %string', $i18nstring->get_args());
  368. break;
  369. case SAVED_NEW:
  370. watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
  371. break;
  372. }
  373. }
  374. return $status;
  375. }
  376. /**
  377. * Check if string is ok for translation
  378. */
  379. protected static function string_check($i18nstring, $options = array()) {
  380. $options += array('messages' => FALSE, 'watchdog' => TRUE);
  381. if (!empty($i18nstring->format) && !i18n_string_allowed_format($i18nstring->format)) {
  382. // This format is not allowed, so we remove the string, in this case we produce a warning
  383. drupal_set_message(t('The string %location for textgroup %textgroup is not allowed for translation because of its text format.', $i18nstring->get_args()), 'warning');
  384. return FALSE;
  385. }
  386. else {
  387. return TRUE;
  388. }
  389. }
  390. /**
  391. * Filter array of strings
  392. *
  393. * @param $filter
  394. * Array of name value conditions.
  395. */
  396. protected static function string_filter($string_list, $filter) {
  397. // Remove 'language' and '*' conditions.
  398. if (isset($filter['language'])) {
  399. unset($filter['language']);
  400. }
  401. while ($field = array_search('*', $filter)) {
  402. unset($filter[$field]);
  403. }
  404. foreach ($string_list as $key => $string) {
  405. foreach ($filter as $field => $value) {
  406. if ($string->$field != $value) {
  407. unset($string_list[$key]);
  408. break;
  409. }
  410. }
  411. }
  412. return $string_list;
  413. }
  414. /**
  415. * Build query for i18n_string table
  416. */
  417. protected static function string_query($context, $multiple = FALSE) {
  418. // Search the database using lid if we've got it or textgroup, context otherwise
  419. $query = db_select('i18n_string', 's')->fields('s');
  420. if (!empty($context->lid)) {
  421. $query->condition('s.lid', $context->lid);
  422. }
  423. else {
  424. $query->condition('s.textgroup', $context->textgroup);
  425. if (!$multiple) {
  426. $query->condition('s.context', $context->context);
  427. }
  428. else {
  429. // Query multiple strings
  430. foreach (array('type', 'objectid', 'property') as $field) {
  431. if (!empty($context->$field)) {
  432. $query->condition('s.' . $field, $context->$field);
  433. }
  434. }
  435. }
  436. }
  437. return $query;
  438. }
  439. /**
  440. * Remove string object.
  441. *
  442. * @return
  443. * SAVED_DELETED | FALSE (If the operation failed because no source)
  444. */
  445. public function string_remove($i18nstring, $options = array()) {
  446. $options += array('watchdog' => TRUE, 'messages' => $this->debug);
  447. if ($source = $i18nstring->get_source()) {
  448. db_delete('locales_target')->condition('lid', $source->lid)->execute();
  449. db_delete('i18n_string')->condition('lid', $source->lid)->execute();
  450. db_delete('locales_source')->condition('lid', $source->lid)->execute();
  451. $this->cache_set($source->context, NULL);
  452. if ($options['watchdog']) {
  453. watchdog('i18n_string', 'Deleted string %location for text group %textgroup: %string', $i18nstring->get_args());
  454. }
  455. if ($options['messages']) {
  456. drupal_set_message(t('Deleted string %location for text group %textgroup: %string', $i18nstring->get_args()));
  457. }
  458. return SAVED_DELETED;
  459. }
  460. else {
  461. if ($options['messages']) {
  462. drupal_set_message(t('Cannot delete string, not found %location for text group %textgroup: %string', $i18nstring->get_args()));
  463. }
  464. return FALSE;
  465. }
  466. }
  467. /**
  468. * Translate string object
  469. *
  470. * @param $i18nstring
  471. * String object
  472. * @param $options
  473. * Array with aditional options
  474. */
  475. protected function string_translate($i18nstring, $options = array()) {
  476. $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  477. // Search for existing translation (result will be cached in this function call)
  478. $i18nstring->get_translation($langcode);
  479. return $i18nstring;
  480. }
  481. /**
  482. * Update / create / remove string.
  483. *
  484. * @param $name
  485. * String context.
  486. * @pram $string
  487. * New value of string for update/create. May be empty for removing.
  488. * @param $format
  489. * Text format, that must have been checked against allowed formats for translation
  490. * @param $options
  491. * Processing options, the ones used here are:
  492. * - 'watchdog', whether to produce watchdog messages.
  493. * - 'messages', whether to produce user messages.
  494. * - 'check', whether to check string format and then update/delete if not allowed.
  495. * @return status
  496. * SAVED_UPDATED | SAVED_NEW | SAVED_DELETED | FALSE (If the string is to be removed but has no source)
  497. */
  498. public function string_update($i18nstring, $options = array()) {
  499. $options += array('watchdog' => TRUE, 'messages' => $this->debug, 'check' => TRUE);
  500. if ((!$options['check'] || $this->string_check($i18nstring, $options)) && $i18nstring->get_string()) {
  501. // String is ok, has a value so we store it into the database.
  502. $status = $this->string_add($i18nstring, $options);
  503. }
  504. elseif ($i18nstring->get_source()) {
  505. // Just remove it if we already had a source created before.
  506. $status = $this->string_remove($i18nstring, $options);
  507. }
  508. else {
  509. // String didn't pass validation or we have an empty string but was not stored anyway.
  510. $status = FALSE;
  511. }
  512. if ($options['messages']) {
  513. switch ($status) {
  514. case SAVED_UPDATED:
  515. drupal_set_message(t('Updated string %location for text group %textgroup: %string', $i18nstring->get_args()));
  516. break;
  517. case SAVED_NEW:
  518. drupal_set_message(t('Created string %location for text group %textgroup: %string', $i18nstring->get_args()));
  519. break;
  520. }
  521. }
  522. if ($options['watchdog']) {
  523. switch ($status) {
  524. case SAVED_UPDATED:
  525. watchdog('i18n_string', 'Updated string %location for text group %textgroup: %string', $i18nstring->get_args());
  526. break;
  527. case SAVED_NEW:
  528. watchdog('i18n_string', 'Created string %location for text group %textgroup: %string', $i18nstring->get_args());
  529. break;
  530. }
  531. }
  532. return $status;
  533. }
  534. /**
  535. * Set string object into cache
  536. */
  537. protected function cache_set($context, $string) {
  538. $this->strings[$context] = $string;
  539. }
  540. /**
  541. * Get translation from cache
  542. */
  543. protected function cache_get($context) {
  544. return isset($this->strings[$context]) ? $this->strings[$context] : NULL;
  545. }
  546. /**
  547. * Reset cache, needed for tests
  548. */
  549. public function cache_reset() {
  550. $this->strings = array();
  551. $this->string_format = array();
  552. $this->translations = array();
  553. }
  554. /**
  555. * Load multiple strings.
  556. *
  557. * @return array
  558. * List of strings indexed by full string name.
  559. */
  560. public function load_strings($conditions = array()) {
  561. // Add textgroup condition and load all
  562. $conditions['textgroup'] = $this->textgroup;
  563. $list = array();
  564. foreach (i18n_string_load_multiple($conditions) as $string) {
  565. $list[$string->get_name()] = $string;
  566. $this->cache_set($string->context, $string);
  567. }
  568. return $list;
  569. }
  570. /**
  571. * Load string source from db
  572. */
  573. public static function load_source($i18nstring) {
  574. // Search the database using lid if we've got it or textgroup, context otherwise
  575. $query = db_select('locales_source', 's')->fields('s');
  576. $query->leftJoin('i18n_string', 'i', 's.lid = i.lid');
  577. $query->fields('i', array('format', 'objectid', 'type', 'property', 'objectindex'));
  578. if (!empty($i18nstring->lid)) {
  579. $query->condition('s.lid', $i18nstring->lid);
  580. }
  581. else {
  582. $query->condition('s.textgroup', $i18nstring->textgroup);
  583. $query->condition('s.context', $i18nstring->context);
  584. }
  585. // Speed up the query, we just need one row
  586. return $query->range(0, 1)->execute()->fetchObject();
  587. }
  588. /**
  589. * Load translation from db
  590. *
  591. * @todo Optimize when we've already got the source string
  592. */
  593. public static function load_translation($i18nstring, $langcode) {
  594. // Search the database using lid if we've got it or textgroup, context otherwise
  595. if (!empty($i18nstring->lid)) {
  596. // We've alreay got lid, we just need translation data
  597. $query = db_select('locales_target', 't');
  598. $query->condition('t.lid', $i18nstring->lid);
  599. }
  600. else {
  601. // Still don't have lid, load string properties too
  602. $query = db_select('i18n_string', 's')->fields('s');
  603. $query->leftJoin('locales_target', 't', 's.lid = t.lid');
  604. $query->condition('s.textgroup', $i18nstring->textgroup);
  605. $query->condition('s.context', $i18nstring->context);
  606. }
  607. // Add translation fields
  608. $query->fields('t', array('translation', 'i18n_status'));
  609. $query->condition('t.language', $langcode);
  610. // Speed up the query, we just need one row
  611. $query->range(0, 1);
  612. return $query->execute()->fetchObject();
  613. }
  614. /**
  615. * Save / update string object
  616. *
  617. * There seems to be a race condition sometimes so skip errors, #277711
  618. *
  619. * @param $string
  620. * Full string object to be saved
  621. * @param $source
  622. * Source string object
  623. */
  624. protected function save_string($string, $update = FALSE) {
  625. if (!$string->get_source()) {
  626. // Create source string so we get an lid
  627. $this->save_source($string);
  628. }
  629. if (!isset($string->objectkey)) {
  630. $string->objectkey = (int)$string->objectid;
  631. }
  632. if (!isset($string->format)) {
  633. $string->format = '';
  634. }
  635. $status = db_merge('i18n_string')
  636. ->key(array('lid' => $string->lid))
  637. ->fields(array(
  638. 'textgroup' => $string->textgroup,
  639. 'context' => $string->context,
  640. 'objectid' => $string->objectid,
  641. 'type' => $string->type,
  642. 'property' => $string->property,
  643. 'objectindex' => $string->objectkey,
  644. 'format' => $string->format,
  645. ))
  646. ->execute();
  647. return $status;
  648. }
  649. /**
  650. * Save translation to the db
  651. *
  652. * @param $string
  653. * Full string object with translation data (language, translation)
  654. */
  655. protected function save_translation($string, $langcode) {
  656. db_merge('locales_target')
  657. ->key(array('lid' => $string->lid, 'language' => $langcode))
  658. ->fields(array('translation' => $string->get_translation($langcode)))
  659. ->execute();
  660. }
  661. /**
  662. * Save source string (create / update)
  663. */
  664. protected static function save_source($source) {
  665. if (isset($source->string)) {
  666. $source->source = $source->string;
  667. }
  668. if (empty($source->version)) {
  669. $source->version = 1;
  670. }
  671. return drupal_write_record('locales_source', $source, !empty($source->lid) ? 'lid' : array());
  672. }
  673. /**
  674. * Remove source and translations for user defined string.
  675. *
  676. * Though for most strings the 'name' or 'string id' uniquely identifies that string,
  677. * there are some exceptions (like profile categories) for which we need to use the
  678. * source string itself as a search key.
  679. *
  680. * @param $context
  681. * Textgroup and location glued with ':'.
  682. * @param $string
  683. * Optional source string (string in default language).
  684. */
  685. public function context_remove($context, $string = NULL, $options = array()) {
  686. $options += array('messages' => $this->debug);
  687. $i18nstring = $this->build_string($context, $string);
  688. $status = $this->string_remove($i18nstring, $options);
  689. return $this;
  690. }
  691. /**
  692. * Translate source string
  693. */
  694. public function context_translate($context, $string, $options = array()) {
  695. $i18nstring = $this->build_string($context, $string);
  696. return $this->string_translate($i18nstring, $options);
  697. }
  698. /**
  699. * Update / create translation source for user defined strings.
  700. *
  701. * @param $name
  702. * Textgroup and location glued with ':'.
  703. * @param $string
  704. * Source string in default language. Default language may or may not be English.
  705. * @param $options
  706. * Array with additional options:
  707. * - 'format', String format if the string has text format.
  708. * - 'messages', Whether to print out status messages.
  709. * - 'check', whether to check string format and then update/delete if not allowed.
  710. */
  711. public function context_update($context, $string, $options = array()) {
  712. $options += array('format' => FALSE, 'messages' => $this->debug, 'watchdog' => TRUE, 'check' => TRUE);
  713. $i18nstring = $this->build_string($context, $string);
  714. $i18nstring->format = $options['format'];
  715. $this->string_update($i18nstring, $options);
  716. return $this;
  717. }
  718. /**
  719. * Build combinations of an array of arrays respecting keys.
  720. *
  721. * Example:
  722. * array(array(a,b), array(1,2)) will translate into
  723. * array(a,1), array(a,2), array(b,1), array(b,2)
  724. */
  725. protected static function multiple_combine($properties) {
  726. $combinations = array();
  727. // Get first key, value. We need to make sure the array pointer is reset.
  728. $value = reset($properties);
  729. $key = key($properties);
  730. array_shift($properties);
  731. $values = is_array($value) ? $value : array($value);
  732. foreach ($values as $value) {
  733. if ($properties) {
  734. foreach (self::multiple_combine($properties) as $merge) {
  735. $combinations[] = array_merge(array($key => $value), $merge);
  736. }
  737. }
  738. else {
  739. $combinations[] = array($key => $value);
  740. }
  741. }
  742. return $combinations;
  743. }
  744. /**
  745. * Get multiple translations with search conditions.
  746. *
  747. * @param $translations
  748. * Array of translation objects as loaded from the db.
  749. * @param $langcode
  750. * Language code, array of language codes or * to search all translations.
  751. *
  752. * @return array
  753. * Array of i18n string objects.
  754. */
  755. protected function multiple_translation_build($translations, $langcode) {
  756. $strings = array();
  757. foreach ($translations as $translation) {
  758. // The string object may be already in list
  759. if (isset($strings[$translation->context])) {
  760. $string = $strings[$translation->context];
  761. }
  762. else {
  763. $string = $this->build_string($translation->context);
  764. $string->set_properties($translation);
  765. $strings[$string->context] = $string;
  766. }
  767. // If this is a translation we set it there too
  768. if ($translation->language && $translation->translation) {
  769. $string->set_translation($translation);
  770. }
  771. elseif ($langcode) {
  772. // This may only happen when we have a source string but not translation.
  773. $string->set_translation(FALSE, $langcode);
  774. }
  775. }
  776. return $strings;
  777. }
  778. /**
  779. * Load multiple translations from db
  780. *
  781. * @todo Optimize when we've already got the source object
  782. *
  783. * @param $conditions
  784. * Array of field values to use as query conditions.
  785. * @param $langcode
  786. * Language code to search.
  787. * @param $index
  788. * Field to use as index for the result.
  789. * @return array
  790. * Array of string objects with translation set.
  791. */
  792. protected function multiple_translation_load($conditions, $langcode) {
  793. $conditions += array(
  794. 'language' => $langcode,
  795. 'textgroup' => $this->textgroup
  796. );
  797. // We may be querying all translations at the same time or just one language.
  798. // The language field needs some special treatment though.
  799. $query = db_select('i18n_string', 's')->fields('s');
  800. $query->leftJoin('locales_target', 't', 's.lid = t.lid');
  801. $query->fields('t', array('translation', 'language', 'i18n_status'));
  802. foreach ($conditions as $field => $value) {
  803. // Single array value, reduce array
  804. if (is_array($value) && count($value) == 1) {
  805. $value = reset($value);
  806. }
  807. if ($value === '*') {
  808. continue;
  809. }
  810. elseif ($field == 'language') {
  811. $query->condition('t.language', $value);
  812. }
  813. else {
  814. $query->condition('s.' . $field, $value);
  815. }
  816. }
  817. return $this->multiple_translation_build($query->execute()->fetchAll(), $langcode);
  818. }
  819. /**
  820. * Search multiple translations with key combinations.
  821. *
  822. * Each $context field may be a single value, an array of values or '*'.
  823. * Example:
  824. * array('term', array(1,2), '*')
  825. * This will be mapped into the following conditions (provided language code is 'es')
  826. * array('type' => 'term', 'objectid' => array(1,2), 'property' => '*', 'language' => 'es')
  827. * And will result in these combinations to search for
  828. * array('type' => 'term', 'objectid' => 1, 'property' => '*', 'language' => 'es')
  829. * array('type' => 'term', 'objectid' => 2, 'property' => '*', 'language' => 'es')
  830. *
  831. * @param $context array
  832. * Array with String context conditions.
  833. *
  834. * @return
  835. * Array of translation objects indexed by context.
  836. */
  837. public function multiple_translation_search($context, $langcode) {
  838. // First, build conditions and identify the variable field.
  839. $keys = array('type', 'objectid', 'property');
  840. $conditions = array_combine($keys, $context) + array('language' => $langcode);
  841. // Find existing searches in cache, compile remaining ones.
  842. $translations = $search = array();
  843. foreach ($this->multiple_combine($conditions) as $combination) {
  844. $cached = $this->multiple_cache_get($combination);
  845. if (isset($cached)) {
  846. // Cache hit. Merge and remove value from search.
  847. $translations += $cached;
  848. }
  849. else {
  850. // Not in cache, add to search conditions skipping duplicated values.
  851. // As array_merge_recursive() has some bug in PHP 5.2, http://drupal.org/node/1244598
  852. // we use our simplified version here, instead of $search = array_merge_recursive($search, $combination);
  853. foreach ($combination as $key => $value) {
  854. if (!isset($search[$key]) || !in_array($value, $search[$key], TRUE)) {
  855. $search[$key][] = $value;
  856. }
  857. }
  858. }
  859. }
  860. // If we've got any search values left, find translations.
  861. if ($search) {
  862. // Load translations for conditions and set them to the cache
  863. $loaded = $this->multiple_translation_load($search, $langcode);
  864. if ($loaded) {
  865. $translations += $loaded;
  866. }
  867. // Set cache for each of the multiple search keys.
  868. foreach ($this->multiple_combine($search) as $combination) {
  869. $list = $loaded ? $this->string_filter($loaded, $combination) : array();
  870. $this->multiple_cache_set($combination, $list);
  871. }
  872. }
  873. return $translations;
  874. }
  875. /**
  876. * Set multiple cache.
  877. *
  878. * @param $context
  879. * String context with language property at the end.
  880. * @param $strings
  881. * Array of strings (may be empty) to cache.
  882. */
  883. protected function multiple_cache_set($context, $strings) {
  884. $cache_key = implode(':', $context);
  885. $this->cache_multiple[$cache_key] = $strings;
  886. }
  887. /**
  888. * Get strings from multiple cache.
  889. *
  890. * @param $context array
  891. * String context as array with language property at the end.
  892. *
  893. * @return mixed
  894. * Array of strings (may be empty) if we've got a cache hit.
  895. * Null otherwise.
  896. */
  897. protected function multiple_cache_get($context) {
  898. $cache_key = implode(':', $context);
  899. if (isset($this->cache_multiple[$cache_key])) {
  900. return $this->cache_multiple[$cache_key];
  901. }
  902. else {
  903. // Now we try more generic keys. For instance, if we are searching 'term:1:*'
  904. // we may try too 'term:*:*' and filter out the results.
  905. foreach ($context as $key => $value) {
  906. if ($value != '*') {
  907. $try = array_merge($context, array($key => '*'));
  908. $cache_key = implode(':', $try);
  909. if (isset($this->cache_multiple[$cache_key])) {
  910. // As we've found some more generic key, we need to filter using original conditions.
  911. $strings = $this->string_filter($this->cache_multiple[$cache_key], $context);
  912. return $strings;
  913. }
  914. }
  915. }
  916. // If we've reached here, we didn't find any cache match.
  917. return NULL;
  918. }
  919. }
  920. /**
  921. * Translate array of source strings
  922. *
  923. * @param $context
  924. * Context array with placeholders (*)
  925. * @param $strings
  926. * Optional array of source strings indexed by the placeholder property
  927. *
  928. * @return array
  929. * Array of string objects (with translation) indexed by the placeholder field
  930. */
  931. public function multiple_translate($context, $strings = array(), $options = array()) {
  932. // First, build conditions and identify the variable field
  933. $search = $context = array_combine(array('type', 'objectid', 'property'), $context);
  934. $langcode = isset($options['langcode']) ? $options['langcode'] : i18n_langcode();
  935. // If we've got keyed source strings set the array of keys on the placeholder field
  936. // or if not, remove that condition so we search all strings with that keys.
  937. foreach ($search as $field => $value) {
  938. if ($value === '*') {
  939. $property = $field;
  940. if ($strings) {
  941. $search[$field] = array_keys($strings);
  942. }
  943. }
  944. }
  945. // Now we'll add the language code to conditions and get the translations indexed by the property field
  946. $result = $this->multiple_translation_search($search, $langcode);
  947. // Remap translations using property field. If we've got strings it is important that they are in the same order.
  948. $translations = $strings;
  949. foreach ($result as $key => $i18nstring) {
  950. $translations[$i18nstring->$property] = $i18nstring;
  951. }
  952. // Set strings as source or create
  953. foreach ($strings as $key => $source) {
  954. if (isset($translations[$key]) && is_object($translations[$key])) {
  955. $translations[$key]->set_string($source);
  956. }
  957. else {
  958. // Not found any string for this property, create it to map in the response
  959. // But make sure we set this language's translation to FALSE so we don't search again
  960. $newcontext = $context;
  961. $newcontext[$property] = $key;
  962. $translations[$key] = $this->build_string($newcontext)
  963. ->set_string($source)
  964. ->set_translation(FALSE, $langcode);
  965. }
  966. }
  967. return $translations;
  968. }
  969. /**
  970. * Update string translation, only if source exists.
  971. *
  972. * @param $context
  973. * String context as array
  974. * @param $langcode
  975. * Language code to create the translation for
  976. * @param $translation
  977. * String translation for this language
  978. */
  979. function update_translation($context, $langcode, $translation) {
  980. $i18nstring = $this->build_string($context);
  981. if ($source = $i18nstring->get_source()) {
  982. $source->set_translation($translation, $langcode);
  983. $this->save_translation($source, $langcode);
  984. return $source;
  985. }
  986. }
  987. /**
  988. * Recheck strings after update
  989. */
  990. public function update_check() {
  991. // Find strings in locales_source that have no data in i18n_string
  992. $query = db_select('locales_source', 'l')
  993. ->fields('l')
  994. ->condition('l.textgroup', $this->textgroup);
  995. $alias = $query->leftJoin('i18n_string', 's', 'l.lid = s.lid');
  996. $query->isNull('s.lid');
  997. foreach ($query->execute()->fetchAll() as $string) {
  998. $i18nstring = $this->build_string($string->context, $string->source);
  999. $this->save_string($i18nstring);
  1000. }
  1001. }
  1002. }
  1003. /**
  1004. * String object wrapper
  1005. */
  1006. class i18n_string_object_wrapper extends i18n_object_wrapper {
  1007. // Text group object
  1008. protected $textgroup;
  1009. // Properties for translation
  1010. protected $properties;
  1011. /**
  1012. * Get object strings for translation
  1013. *
  1014. * This will return a simple array of string objects, indexed by full string name.
  1015. *
  1016. * @param $options
  1017. * Array with processing options.
  1018. * - 'empty', whether to return empty strings, defaults to FALSE.
  1019. */
  1020. public function get_strings($options = array()) {
  1021. $options += array('empty' => FALSE);
  1022. $strings = array();
  1023. foreach ($this->get_properties() as $textgroup => $textgroup_list) {
  1024. foreach ($textgroup_list as $type => $type_list) {
  1025. foreach ($type_list as $object_id => $object_list) {
  1026. foreach ($object_list as $key => $string) {
  1027. if ($options['empty'] || !empty($string['string'])) {
  1028. // Build string object, that will trigger static caches everywhere.
  1029. $i18nstring = i18n_string_textgroup($textgroup)
  1030. ->build_string(array($type, $object_id, $key))
  1031. ->set_string($string);
  1032. $strings[$i18nstring->get_name()] = $i18nstring;
  1033. }
  1034. }
  1035. }
  1036. }
  1037. }
  1038. return $strings;
  1039. }
  1040. /**
  1041. * Get object translatable properties
  1042. *
  1043. * This will return a big array indexed by textgroup, object type, object id and string key.
  1044. * Each element is an array with string information, and may have these properties:
  1045. * - 'string', the string itself, will be NULL if the object doesn't have that string
  1046. * - 'format', string format when needed
  1047. * - 'title', string readable name
  1048. */
  1049. public function get_properties() {
  1050. if (!isset($this->properties)) {
  1051. $this->properties = $this->build_properties();
  1052. // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules
  1053. drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object);
  1054. }
  1055. return $this->properties;
  1056. }
  1057. /**
  1058. * Build properties from object.
  1059. */
  1060. protected function build_properties() {
  1061. list($string_type, $object_id) = $this->get_string_context();
  1062. $object_keys = array(
  1063. $this->get_textgroup(),
  1064. $string_type,
  1065. $object_id,
  1066. );
  1067. $strings = array();
  1068. foreach ($this->get_string_info('properties', array()) as $field => $info) {
  1069. $info = is_array($info) ? $info : array('title' => $info);
  1070. $field_name = isset($info['field']) ? $info['field'] : $field;
  1071. $value = $this->get_field($field_name);
  1072. $strings[$this->get_textgroup()][$string_type][$object_id][$field] = array(
  1073. 'string' => is_array($value) || isset($info['empty']) && $value === $info['empty'] ? NULL : $value,
  1074. 'title' => $info['title'],
  1075. 'format' => isset($info['format']) ? $this->get_field($info['format']) : NULL,
  1076. 'name' => array_merge($object_keys, array($field)),
  1077. );
  1078. }
  1079. return $strings;
  1080. }
  1081. /**
  1082. * Get string context
  1083. */
  1084. public function get_string_context() {
  1085. return array($this->get_string_info('type'), $this->get_key());
  1086. }
  1087. /**
  1088. * Get translate path for object
  1089. *
  1090. * @param $langcode
  1091. * Language code if we want ti for a specific language
  1092. */
  1093. public function get_translate_path($langcode = NULL) {
  1094. $replacements = array('%i18n_language' => $langcode ? $langcode : '');
  1095. if ($path = $this->get_string_info('translate path')) {
  1096. return $this->path_replace($path, $replacements);
  1097. }
  1098. elseif ($path = $this->get_info('translate tab')) {
  1099. // If we've got a translate tab path, we just add language to it
  1100. return $this->path_replace($path . '/%i18n_language', $replacements);
  1101. }
  1102. }
  1103. /**
  1104. * Translation mode for object
  1105. */
  1106. public function get_translate_mode() {
  1107. return !$this->get_langcode() ? I18N_MODE_LOCALIZE : I18N_MODE_NONE;
  1108. }
  1109. /**
  1110. * Get textgroup name
  1111. */
  1112. public function get_textgroup() {
  1113. return $this->get_string_info('textgroup');
  1114. }
  1115. /**
  1116. * Get textgroup object
  1117. */
  1118. protected function textgroup() {
  1119. if (!isset($this->textgroup)) {
  1120. $this->textgroup = i18n_string_textgroup($this->get_textgroup());
  1121. }
  1122. return $this->textgroup;
  1123. }
  1124. /**
  1125. * Translate object.
  1126. *
  1127. * Translations are cached so it runs only once per language.
  1128. *
  1129. * @return object/array
  1130. * A clone of the object with its properties translated.
  1131. */
  1132. public function translate($langcode, $options = array()) {
  1133. // We may have it already translated. As objects are statically cached, translations are too.
  1134. if (!isset($this->translations[$langcode])) {
  1135. $this->translations[$langcode] = $this->translate_object($langcode, $options);
  1136. }
  1137. return $this->translations[$langcode];
  1138. }
  1139. /**
  1140. * Translate access (localize strings)
  1141. */
  1142. protected function localize_access() {
  1143. return user_access('translate interface') && user_access('translate user-defined strings');
  1144. }
  1145. /**
  1146. * Translate all properties for object.
  1147. *
  1148. * On top of object strings we search for all textgroup:type:objectid:* properties
  1149. *
  1150. * @param $langcode
  1151. * A clone of the object or array
  1152. */
  1153. protected function translate_object($langcode, $options) {
  1154. // Clone object or array so we don't affect the original one.
  1155. $object = is_object($this->object) ? clone $this->object : $this->object;
  1156. // Get object strings for translatable properties.
  1157. if ($strings = $this->get_strings()) {
  1158. // We preload some of the property translations with a single query.
  1159. if ($context = $this->get_translate_context($langcode, $options)) {
  1160. $found = $this->textgroup()->multiple_translation_search($context, $langcode);
  1161. }
  1162. // Replace all strings in object.
  1163. foreach ($strings as $i18nstring) {
  1164. $this->translate_field($object, $i18nstring, $langcode, $options);
  1165. }
  1166. }
  1167. return $object;
  1168. }
  1169. /**
  1170. * Context to be pre-loaded before translation.
  1171. */
  1172. protected function get_translate_context($langcode, $options) {
  1173. // One-query translation of all textgroup:type:objectid:* properties
  1174. $context = $this->get_string_context();
  1175. $context[] = '*';
  1176. return $context;
  1177. }
  1178. /**
  1179. * Translate object property.
  1180. *
  1181. * Mot often, this is a direct field set, but sometimes fields may have different formats.
  1182. */
  1183. protected function translate_field(&$object, $i18nstring, $langcode, $options) {
  1184. $field_name = $i18nstring->property;
  1185. $translation = $i18nstring->format_translation($langcode, $options);
  1186. if (is_object($object)) {
  1187. $object->$field_name = $translation;
  1188. }
  1189. elseif (is_array($object)) {
  1190. $object[$field_name] = $translation;
  1191. }
  1192. }
  1193. /**
  1194. * Remove all strings for this object.
  1195. */
  1196. public function strings_remove($options = array()) {
  1197. $result = array();
  1198. foreach ($this->load_strings() as $key => $string) {
  1199. $result[$key] = $string->remove($options);
  1200. }
  1201. return _i18n_string_result_count($result);
  1202. }
  1203. /**
  1204. * Update all strings for this object.
  1205. */
  1206. public function strings_update($options = array()) {
  1207. $options += array('empty' => TRUE, 'update' => TRUE);
  1208. $result = array();
  1209. $existing = $this->load_strings();
  1210. // Update object strings
  1211. foreach ($this->get_strings($options) as $key => $string) {
  1212. $result[$key] = $string->update($options);
  1213. unset($existing[$key]);
  1214. }
  1215. // Delete old existing strings.
  1216. foreach ($existing as $key => $string) {
  1217. $result[$key] = $string->remove($options);
  1218. }
  1219. return _i18n_string_result_count($result);
  1220. }
  1221. /**
  1222. * Load all existing strings for this object.
  1223. */
  1224. public function load_strings() {
  1225. list($type, $id) = $this->get_string_context();
  1226. return $this->textgroup()->load_strings(array('type' => $type, 'objectid' => $id));
  1227. }
  1228. }