3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Definition of classes used by language customization admin report
22 * @subpackage customlang
23 * @copyright 2010 David Mudrak <david@moodle.com>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
30 * Provides various utilities to be used by the plugin
32 * All the public methods here are static ones, this class can not be instantiated
34 class report_customlang_utils {
37 * Rough number of strings that are being processed during a full checkout.
38 * This is used to estimate the progress of the checkout.
40 const ROUGH_NUMBER_OF_STRINGS = 16500;
42 /** @var array cache of {@link self::list_components()} results */
43 protected static $components = null;
46 * This class can not be instantiated
48 private function __construct() {
52 * Returns a list of all components installed on the server
54 * @return array (string)legacyname => (string)frankenstylename
56 public static function list_components() {
58 $list['moodle'] = 'core';
60 $coresubsystems = get_core_subsystems();
61 ksort($coresubsystems); // should be but just in case
62 foreach ($coresubsystems as $name => $location) {
63 if ($name != 'moodle.org') {
64 $list[$name] = 'core_'.$name;
68 $plugintypes = get_plugin_types();
69 foreach ($plugintypes as $type => $location) {
70 $pluginlist = get_plugin_list($type);
71 foreach ($pluginlist as $name => $ununsed) {
73 if (array_key_exists($name, $list)) {
74 throw new Exception('Activity module and core subsystem name collision');
76 $list[$name] = $type.'_'.$name;
78 $list[$type.'_'.$name] = $type.'_'.$name;
87 * Updates the translator database with the strings from files
89 * This should be executed each time before going to the translation page
91 * @param string $lang language code to checkout
92 * @param progress_bar $progressbar optionally, the given progress bar can be updated
94 public static function checkout($lang, progress_bar $progressbar = null) {
97 // make sure that all components are registered
98 $current = $DB->get_records('report_customlang_components', null, 'name', 'name,version,id');
99 foreach (self::list_components() as $component) {
100 if (empty($current[$component])) {
101 $record = new stdclass();
102 $record->name = $component;
103 if (!$version = get_component_version($component)) {
104 $record->version = null;
106 $record->version = $version;
108 $DB->insert_record('report_customlang_components', $record);
109 } elseif ($version = get_component_version($component)) {
110 if (is_null($current[$component]->version) or ($version > $current[$component]->version)) {
111 $DB->set_field('report_customlang_components', 'version', $version, array('id' => $current[$component]->id));
117 // initialize the progress counter - stores the number of processed strings
119 $strinprogress = get_string('checkoutinprogress', 'report_customlang');
121 // reload components and fetch their strings
122 $stringman = get_string_manager();
123 $components = $DB->get_records('report_customlang_components');
124 foreach ($components as $component) {
125 $sql = "SELECT stringid, id, lang, componentid, original, master, local, timemodified, timecustomized, outdated, modified
126 FROM {report_customlang} s
127 WHERE lang = ? AND componentid = ?
129 $current = $DB->get_records_sql($sql, array($lang, $component->id));
130 $english = $stringman->load_component_strings($component->name, 'en', true, true);
134 $master = $stringman->load_component_strings($component->name, $lang, true, true);
136 $local = $stringman->load_component_strings($component->name, $lang, true, false);
138 foreach ($english as $stringid => $stringoriginal) {
139 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null;
140 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null;
143 if (!is_null($progressbar)) {
145 $donepercent = floor(min($done, self::ROUGH_NUMBER_OF_STRINGS) / self::ROUGH_NUMBER_OF_STRINGS * 100);
146 $progressbar->update_full($donepercent, $strinprogress);
149 if (isset($current[$stringid])) {
150 $needsupdate = false;
151 $currentoriginal = $current[$stringid]->original;
152 $currentmaster = $current[$stringid]->master;
153 $currentlocal = $current[$stringid]->local;
155 if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) {
157 $current[$stringid]->original = $stringoriginal;
158 $current[$stringid]->master = $stringmaster;
159 $current[$stringid]->timemodified = $now;
160 $current[$stringid]->outdated = 1;
163 if ($stringmaster !== $stringlocal) {
165 $current[$stringid]->local = $stringlocal;
166 $current[$stringid]->timecustomized = $now;
170 $DB->update_record('report_customlang', $current[$stringid]);
175 $record = new stdclass();
176 $record->lang = $lang;
177 $record->componentid = $component->id;
178 $record->stringid = $stringid;
179 $record->original = $stringoriginal;
180 $record->master = $stringmaster;
181 $record->timemodified = $now;
182 $record->outdated = 0;
183 if ($stringmaster !== $stringlocal) {
184 $record->local = $stringlocal;
185 $record->timecustomized = $now;
187 $record->local = null;
188 $record->timecustomized = null;
191 $DB->insert_record('report_customlang', $record);
196 if (!is_null($progressbar)) {
197 $progressbar->update_full(100, get_string('checkoutdone', 'report_customlang'));
202 * Exports the translator database into disk files
204 * @param mixed $lang language code
206 public static function checkin($lang) {
207 global $DB, $USER, $CFG;
208 require_once($CFG->libdir.'/filelib.php');
210 if ($lang !== clean_param($lang, PARAM_LANG)) {
214 // get all customized strings from updated components
215 $sql = "SELECT s.*, c.name AS component
216 FROM {report_customlang} s
217 JOIN {report_customlang_components} c ON s.componentid = c.id
219 AND (s.local IS NOT NULL OR s.modified = 1)
220 ORDER BY componentid, stringid";
221 $strings = $DB->get_records_sql($sql, array($lang));
224 foreach ($strings as $string) {
225 if (!is_null($string->local)) {
226 $files[$string->component][$string->stringid] = $string->local;
230 fulldelete(self::get_localpack_location($lang));
231 foreach ($files as $component => $strings) {
232 self::dump_strings($lang, $component, $strings);
235 $DB->set_field_select('report_customlang', 'modified', 0, 'lang = ?', array($lang));
236 $sm = get_string_manager();
241 * Returns full path to the directory where local packs are dumped into
243 * @param string $lang language code
244 * @return string full path
246 protected static function get_localpack_location($lang) {
249 return $CFG->langlocalroot.'/'.$lang.'_local';
253 * Writes strings into a local language pack file
255 * @param string $component the name of the component
256 * @param array $strings
258 protected static function dump_strings($lang, $component, $strings) {
261 if ($lang !== clean_param($lang, PARAM_LANG)) {
262 debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
265 if ($component !== clean_param($component, PARAM_COMPONENT)) {
266 throw new coding_exception('Incorrect component name');
268 if (!$filename = self::get_component_filename($component)) {
269 debugging('Unable to find the filename for the component '.s($component));
272 if ($filename !== clean_param($filename, PARAM_FILE)) {
273 throw new coding_exception('Incorrect file name '.s($filename));
275 list($package, $subpackage) = normalize_component($component);
276 $packageinfo = " * @package $package";
277 if (!is_null($subpackage)) {
278 $packageinfo .= "\n * @subpackage $subpackage";
280 $filepath = self::get_localpack_location($lang);
281 $filepath = $filepath.'/'.$filename;
282 if (!is_dir(dirname($filepath))) {
283 check_dir_exists(dirname($filepath));
286 if (!$f = fopen($filepath, 'w')) {
287 debugging('Unable to write '.s($filepath));
293 // This file is part of Moodle - http://moodle.org/
295 // Moodle is free software: you can redistribute it and/or modify
296 // it under the terms of the GNU General Public License as published by
297 // the Free Software Foundation, either version 3 of the License, or
298 // (at your option) any later version.
300 // Moodle is distributed in the hope that it will be useful,
301 // but WITHOUT ANY WARRANTY; without even the implied warranty of
302 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
303 // GNU General Public License for more details.
305 // You should have received a copy of the GNU General Public License
306 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
309 * Local language pack from $CFG->wwwroot
312 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
315 defined('MOODLE_INTERNAL') || die();
321 foreach ($strings as $stringid => $text) {
322 if ($stringid !== clean_param($stringid, PARAM_STRINGID)) {
323 debugging('Invalid string identifier '.s($stringid));
326 fwrite($f, '$string[\'' . $stringid . '\'] = ');
327 fwrite($f, var_export($text, true));
334 * Returns the name of the file where the component's local strings should be exported into
336 * @param string $component normalized name of the component, eg 'core' or 'mod_workshop'
337 * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found
339 protected static function get_component_filename($component) {
340 if (is_null(self::$components)) {
341 self::$components = self::list_components();
344 foreach (self::$components as $legacy => $normalized) {
345 if ($component === $normalized) {
346 $return = $legacy.'.php';
354 * Returns the number of modified strings checked out in the translator
356 * @param string $lang language code
359 public static function get_count_of_modified($lang) {
362 return $DB->count_records('report_customlang', array('lang'=>$lang, 'modified'=>1));
366 * Saves filter data into a persistant storage such as user session
368 * @see self::load_filter()
369 * @param stdclass $data filter values
370 * @param stdclass $persistant storage object
372 public static function save_filter(stdclass $data, stdclass $persistant) {
373 if (!isset($persistant->report_customlang_filter)) {
374 $persistant->report_customlang_filter = array();
376 foreach ($data as $key => $value) {
377 if ($key !== 'submit') {
378 $persistant->report_customlang_filter[$key] = serialize($value);
384 * Loads the previously saved filter settings from a persistent storage
386 * @see self::save_filter()
387 * @param stdclass $persistant storage object
388 * @return stdclass filter data
390 public static function load_filter(stdclass $persistant) {
391 $data = new stdclass();
392 if (isset($persistant->report_customlang_filter)) {
393 foreach ($persistant->report_customlang_filter as $key => $value) {
394 $data->{$key} = unserialize($value);
402 * Represents the action menu of the report
404 class report_customlang_menu implements renderable {
406 /** @var menu items */
407 protected $items = array();
409 public function __construct(array $items = array()) {
412 foreach ($items as $itemkey => $item) {
413 $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']);
418 * Returns the menu items
420 * @return array (string)key => (object)[->(string)title ->(moodle_url)url ->(string)method]
422 public function get_items() {
427 * Adds item into the menu
429 * @param string $key item identifier
430 * @param string $title localized action title
431 * @param moodle_url $url action handler
432 * @param string $method form method
434 public function add_item($key, $title, moodle_url $url, $method) {
435 if (isset($this->items[$key])) {
436 throw new coding_exception('Menu item already exists');
438 if (empty($title) or empty($key)) {
439 throw new coding_exception('Empty title or item key not allowed');
441 $item = new stdclass();
442 $item->title = $title;
444 $item->method = $method;
445 $this->items[$key] = $item;
450 * Represents the translation tool
452 class report_customlang_translator implements renderable {
454 /** @const int number of rows per page */
457 /** @var int total number of the rows int the table */
458 public $numofrows = 0;
460 /** @var moodle_url */
463 /** @var string language code */
466 /** @var int page to display, starting with page 0 */
467 public $currentpage = 0;
469 /** @var array of stdclass strings to display */
470 public $strings = array();
475 public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) {
478 $this->handler = $handler;
480 $this->filter = $filter;
481 $this->currentpage = $currentpage;
483 if (empty($filter) or empty($filter->component)) {
485 $this->currentpage = 1;
489 list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
491 $csql = "SELECT COUNT(*)";
492 $fsql = "SELECT s.id, s.*, c.name AS component";
493 $sql = " FROM {report_customlang_components} c
494 JOIN {report_customlang} s ON s.componentid = c.id
498 $params = array_merge(array('lang' => $lang), $inparams);
500 if (!empty($filter->customized)) {
501 $sql .= " AND s.local IS NOT NULL";
504 if (!empty($filter->modified)) {
505 $sql .= " AND s.modified = 1";
508 if (!empty($filter->stringid)) {
509 $sql .= " AND s.stringid = :stringid";
510 $params['stringid'] = $filter->stringid;
513 if (!empty($filter->substring)) {
514 $sql .= " AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR
515 ".$DB->sql_like('s.master', ':substringmaster', false)." OR
516 ".$DB->sql_like('s.local', ':substringlocal', false).")";
517 $params['substringoriginal'] = '%'.$filter->substring.'%';
518 $params['substringmaster'] = '%'.$filter->substring.'%';
519 $params['substringlocal'] = '%'.$filter->substring.'%';
522 if (!empty($filter->helps)) {
523 $sql .= " AND ".$DB->sql_like('s.stringid', ':help', false); //ILIKE
524 $params['help'] = '%\_help';
526 $sql .= " AND ".$DB->sql_like('s.stringid', ':link', false, true, true); //NOT ILIKE
527 $params['link'] = '%\_link';
530 $osql = " ORDER BY c.name, s.stringid";
532 $this->numofrows = $DB->count_records_sql($csql.$sql, $params);
533 $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE);