2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Definition of classes used by language customization admin tool
21 * @subpackage customlang
22 * @copyright 2010 David Mudrak <david@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') || die();
29 * Provides various utilities to be used by the plugin
31 * All the public methods here are static ones, this class can not be instantiated
33 class tool_customlang_utils {
36 * Rough number of strings that are being processed during a full checkout.
37 * This is used to estimate the progress of the checkout.
39 const ROUGH_NUMBER_OF_STRINGS = 16500;
41 /** @var array cache of {@link self::list_components()} results */
42 protected static $components = null;
45 * This class can not be instantiated
47 private function __construct() {
51 * Returns a list of all components installed on the server
53 * @return array (string)legacyname => (string)frankenstylename
55 public static function list_components() {
57 $list['moodle'] = 'core';
59 $coresubsystems = get_core_subsystems();
60 ksort($coresubsystems); // should be but just in case
61 foreach ($coresubsystems as $name => $location) {
62 if ($name != 'moodle.org') {
63 $list[$name] = 'core_'.$name;
67 $plugintypes = get_plugin_types();
68 foreach ($plugintypes as $type => $location) {
69 $pluginlist = get_plugin_list($type);
70 foreach ($pluginlist as $name => $ununsed) {
72 if (array_key_exists($name, $list)) {
73 throw new Exception('Activity module and core subsystem name collision');
75 $list[$name] = $type.'_'.$name;
77 $list[$type.'_'.$name] = $type.'_'.$name;
86 * Updates the translator database with the strings from files
88 * This should be executed each time before going to the translation page
90 * @param string $lang language code to checkout
91 * @param progress_bar $progressbar optionally, the given progress bar can be updated
93 public static function checkout($lang, progress_bar $progressbar = null) {
96 // make sure that all components are registered
97 $current = $DB->get_records('tool_customlang_components', null, 'name', 'name,version,id');
98 foreach (self::list_components() as $component) {
99 if (empty($current[$component])) {
100 $record = new stdclass();
101 $record->name = $component;
102 if (!$version = get_component_version($component)) {
103 $record->version = null;
105 $record->version = $version;
107 $DB->insert_record('tool_customlang_components', $record);
108 } elseif ($version = get_component_version($component)) {
109 if (is_null($current[$component]->version) or ($version > $current[$component]->version)) {
110 $DB->set_field('tool_customlang_components', 'version', $version, array('id' => $current[$component]->id));
116 // initialize the progress counter - stores the number of processed strings
118 $strinprogress = get_string('checkoutinprogress', 'tool_customlang');
120 // reload components and fetch their strings
121 $stringman = get_string_manager();
122 $components = $DB->get_records('tool_customlang_components');
123 foreach ($components as $component) {
124 $sql = "SELECT stringid, id, lang, componentid, original, master, local, timemodified, timecustomized, outdated, modified
125 FROM {tool_customlang} s
126 WHERE lang = ? AND componentid = ?
128 $current = $DB->get_records_sql($sql, array($lang, $component->id));
129 $english = $stringman->load_component_strings($component->name, 'en', true, true);
133 $master = $stringman->load_component_strings($component->name, $lang, true, true);
135 $local = $stringman->load_component_strings($component->name, $lang, true, false);
137 foreach ($english as $stringid => $stringoriginal) {
138 $stringmaster = isset($master[$stringid]) ? $master[$stringid] : null;
139 $stringlocal = isset($local[$stringid]) ? $local[$stringid] : null;
142 if (!is_null($progressbar)) {
144 $donepercent = floor(min($done, self::ROUGH_NUMBER_OF_STRINGS) / self::ROUGH_NUMBER_OF_STRINGS * 100);
145 $progressbar->update_full($donepercent, $strinprogress);
148 if (isset($current[$stringid])) {
149 $needsupdate = false;
150 $currentoriginal = $current[$stringid]->original;
151 $currentmaster = $current[$stringid]->master;
152 $currentlocal = $current[$stringid]->local;
154 if ($currentoriginal !== $stringoriginal or $currentmaster !== $stringmaster) {
156 $current[$stringid]->original = $stringoriginal;
157 $current[$stringid]->master = $stringmaster;
158 $current[$stringid]->timemodified = $now;
159 $current[$stringid]->outdated = 1;
162 if ($stringmaster !== $stringlocal) {
164 $current[$stringid]->local = $stringlocal;
165 $current[$stringid]->timecustomized = $now;
169 $DB->update_record('tool_customlang', $current[$stringid]);
174 $record = new stdclass();
175 $record->lang = $lang;
176 $record->componentid = $component->id;
177 $record->stringid = $stringid;
178 $record->original = $stringoriginal;
179 $record->master = $stringmaster;
180 $record->timemodified = $now;
181 $record->outdated = 0;
182 if ($stringmaster !== $stringlocal) {
183 $record->local = $stringlocal;
184 $record->timecustomized = $now;
186 $record->local = null;
187 $record->timecustomized = null;
190 $DB->insert_record('tool_customlang', $record);
195 if (!is_null($progressbar)) {
196 $progressbar->update_full(100, get_string('checkoutdone', 'tool_customlang'));
201 * Exports the translator database into disk files
203 * @param mixed $lang language code
205 public static function checkin($lang) {
206 global $DB, $USER, $CFG;
207 require_once($CFG->libdir.'/filelib.php');
209 if ($lang !== clean_param($lang, PARAM_LANG)) {
213 // get all customized strings from updated components
214 $sql = "SELECT s.*, c.name AS component
215 FROM {tool_customlang} s
216 JOIN {tool_customlang_components} c ON s.componentid = c.id
218 AND (s.local IS NOT NULL OR s.modified = 1)
219 ORDER BY componentid, stringid";
220 $strings = $DB->get_records_sql($sql, array($lang));
223 foreach ($strings as $string) {
224 if (!is_null($string->local)) {
225 $files[$string->component][$string->stringid] = $string->local;
229 fulldelete(self::get_localpack_location($lang));
230 foreach ($files as $component => $strings) {
231 self::dump_strings($lang, $component, $strings);
234 $DB->set_field_select('tool_customlang', 'modified', 0, 'lang = ?', array($lang));
235 $sm = get_string_manager();
240 * Returns full path to the directory where local packs are dumped into
242 * @param string $lang language code
243 * @return string full path
245 protected static function get_localpack_location($lang) {
248 return $CFG->langlocalroot.'/'.$lang.'_local';
252 * Writes strings into a local language pack file
254 * @param string $component the name of the component
255 * @param array $strings
257 protected static function dump_strings($lang, $component, $strings) {
260 if ($lang !== clean_param($lang, PARAM_LANG)) {
261 debugging('Unable to dump local strings for non-installed language pack .'.s($lang));
264 if ($component !== clean_param($component, PARAM_COMPONENT)) {
265 throw new coding_exception('Incorrect component name');
267 if (!$filename = self::get_component_filename($component)) {
268 debugging('Unable to find the filename for the component '.s($component));
271 if ($filename !== clean_param($filename, PARAM_FILE)) {
272 throw new coding_exception('Incorrect file name '.s($filename));
274 list($package, $subpackage) = normalize_component($component);
275 $packageinfo = " * @package $package";
276 if (!is_null($subpackage)) {
277 $packageinfo .= "\n * @subpackage $subpackage";
279 $filepath = self::get_localpack_location($lang);
280 $filepath = $filepath.'/'.$filename;
281 if (!is_dir(dirname($filepath))) {
282 check_dir_exists(dirname($filepath));
285 if (!$f = fopen($filepath, 'w')) {
286 debugging('Unable to write '.s($filepath));
292 // This file is part of Moodle - http://moodle.org/
294 // Moodle is free software: you can redistribute it and/or modify
295 // it under the terms of the GNU General Public License as published by
296 // the Free Software Foundation, either version 3 of the License, or
297 // (at your option) any later version.
299 // Moodle is distributed in the hope that it will be useful,
300 // but WITHOUT ANY WARRANTY; without even the implied warranty of
301 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
302 // GNU General Public License for more details.
304 // You should have received a copy of the GNU General Public License
305 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
308 * Local language pack from $CFG->wwwroot
311 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
314 defined('MOODLE_INTERNAL') || die();
320 foreach ($strings as $stringid => $text) {
321 if ($stringid !== clean_param($stringid, PARAM_STRINGID)) {
322 debugging('Invalid string identifier '.s($stringid));
325 fwrite($f, '$string[\'' . $stringid . '\'] = ');
326 fwrite($f, var_export($text, true));
333 * Returns the name of the file where the component's local strings should be exported into
335 * @param string $component normalized name of the component, eg 'core' or 'mod_workshop'
336 * @return string|boolean filename eg 'moodle.php' or 'workshop.php', false if not found
338 protected static function get_component_filename($component) {
339 if (is_null(self::$components)) {
340 self::$components = self::list_components();
343 foreach (self::$components as $legacy => $normalized) {
344 if ($component === $normalized) {
345 $return = $legacy.'.php';
353 * Returns the number of modified strings checked out in the translator
355 * @param string $lang language code
358 public static function get_count_of_modified($lang) {
361 return $DB->count_records('tool_customlang', array('lang'=>$lang, 'modified'=>1));
365 * Saves filter data into a persistant storage such as user session
367 * @see self::load_filter()
368 * @param stdclass $data filter values
369 * @param stdclass $persistant storage object
371 public static function save_filter(stdclass $data, stdclass $persistant) {
372 if (!isset($persistant->tool_customlang_filter)) {
373 $persistant->tool_customlang_filter = array();
375 foreach ($data as $key => $value) {
376 if ($key !== 'submit') {
377 $persistant->tool_customlang_filter[$key] = serialize($value);
383 * Loads the previously saved filter settings from a persistent storage
385 * @see self::save_filter()
386 * @param stdclass $persistant storage object
387 * @return stdclass filter data
389 public static function load_filter(stdclass $persistant) {
390 $data = new stdclass();
391 if (isset($persistant->tool_customlang_filter)) {
392 foreach ($persistant->tool_customlang_filter as $key => $value) {
393 $data->{$key} = unserialize($value);
401 * Represents the action menu of the tool
403 class tool_customlang_menu implements renderable {
405 /** @var menu items */
406 protected $items = array();
408 public function __construct(array $items = array()) {
411 foreach ($items as $itemkey => $item) {
412 $this->add_item($itemkey, $item['title'], $item['url'], empty($item['method']) ? 'post' : $item['method']);
417 * Returns the menu items
419 * @return array (string)key => (object)[->(string)title ->(moodle_url)url ->(string)method]
421 public function get_items() {
426 * Adds item into the menu
428 * @param string $key item identifier
429 * @param string $title localized action title
430 * @param moodle_url $url action handler
431 * @param string $method form method
433 public function add_item($key, $title, moodle_url $url, $method) {
434 if (isset($this->items[$key])) {
435 throw new coding_exception('Menu item already exists');
437 if (empty($title) or empty($key)) {
438 throw new coding_exception('Empty title or item key not allowed');
440 $item = new stdclass();
441 $item->title = $title;
443 $item->method = $method;
444 $this->items[$key] = $item;
449 * Represents the translation tool
451 class tool_customlang_translator implements renderable {
453 /** @const int number of rows per page */
456 /** @var int total number of the rows int the table */
457 public $numofrows = 0;
459 /** @var moodle_url */
462 /** @var string language code */
465 /** @var int page to display, starting with page 0 */
466 public $currentpage = 0;
468 /** @var array of stdclass strings to display */
469 public $strings = array();
474 public function __construct(moodle_url $handler, $lang, $filter, $currentpage = 0) {
477 $this->handler = $handler;
479 $this->filter = $filter;
480 $this->currentpage = $currentpage;
482 if (empty($filter) or empty($filter->component)) {
484 $this->currentpage = 1;
488 list($insql, $inparams) = $DB->get_in_or_equal($filter->component, SQL_PARAMS_NAMED);
490 $csql = "SELECT COUNT(*)";
491 $fsql = "SELECT s.id, s.*, c.name AS component";
492 $sql = " FROM {tool_customlang_components} c
493 JOIN {tool_customlang} s ON s.componentid = c.id
497 $params = array_merge(array('lang' => $lang), $inparams);
499 if (!empty($filter->customized)) {
500 $sql .= " AND s.local IS NOT NULL";
503 if (!empty($filter->modified)) {
504 $sql .= " AND s.modified = 1";
507 if (!empty($filter->stringid)) {
508 $sql .= " AND s.stringid = :stringid";
509 $params['stringid'] = $filter->stringid;
512 if (!empty($filter->substring)) {
513 $sql .= " AND (".$DB->sql_like('s.original', ':substringoriginal', false)." OR
514 ".$DB->sql_like('s.master', ':substringmaster', false)." OR
515 ".$DB->sql_like('s.local', ':substringlocal', false).")";
516 $params['substringoriginal'] = '%'.$filter->substring.'%';
517 $params['substringmaster'] = '%'.$filter->substring.'%';
518 $params['substringlocal'] = '%'.$filter->substring.'%';
521 if (!empty($filter->helps)) {
522 $sql .= " AND ".$DB->sql_like('s.stringid', ':help', false); //ILIKE
523 $params['help'] = '%\_help';
525 $sql .= " AND ".$DB->sql_like('s.stringid', ':link', false, true, true); //NOT ILIKE
526 $params['link'] = '%\_link';
529 $osql = " ORDER BY c.name, s.stringid";
531 $this->numofrows = $DB->count_records_sql($csql.$sql, $params);
532 $this->strings = $DB->get_records_sql($fsql.$sql.$osql, $params, ($this->currentpage) * self::PERPAGE, self::PERPAGE);