3 * Display the admin/language menu and process strings translation.
5 * @param string $mode the mode of the script: null, "compare", "missing"
6 * @param string $currentfile the filename of the English file to edit (if mode==compare)
7 * @param bool $uselocal save translations into *_local pack?
10 $dbg = ''; // debug output;
12 require_once('../config.php');
13 require_once($CFG->libdir.'/adminlib.php');
15 admin_externalpage_setup('langedit');
17 $context = get_context_instance(CONTEXT_SYSTEM);
19 define('LANG_SUBMIT_REPEAT', 1); // repeat displaying submit button?
20 define('LANG_SUBMIT_REPEAT_EVERY', 20); // if so, after how many lines?
21 define('LANG_DISPLAY_MISSING_LINKS', 1); // display "go to first/next missing string" links?
22 define('LANG_DEFAULT_FILE', ''); // default file to translate. Empty allowed
23 define('LANG_DEFAULT_HELPFILE', ''); // default helpfile to translate. Empty allowed
24 define('LANG_LINK_MISSING_STRINGS', 1); // create links from "missing" page to "compare" page?
25 define('LANG_DEFAULT_USELOCAL', 0); // should *_utf8_local be used by default?
26 define('LANG_MISSING_TEXT_MAX_LEN', 60); // maximum length of the missing text to display
27 define('LANG_KEEP_ORPHANS', 1); // keep orphaned strings (i.e. strings w/o English reference)
28 define('LANG_SEARCH_EXTRA', 1); // search lang files in extra locations
29 define('LANG_ALWAYS_TEXTAREA', 1); // always use <textarea> even for short strings MDL-15738
31 $mode = optional_param('mode', '', PARAM_ALPHA);
32 if ($mode == 'helpfiles') {
33 // use different PARAM_ options according to mode
34 $currentfile = optional_param('currentfile', LANG_DEFAULT_HELPFILE, PARAM_PATH);
36 $currentfile = optional_param('currentfile', LANG_DEFAULT_FILE, PARAM_FILE);
38 $uselocal = optional_param('uselocal', -1, PARAM_INT);
40 if ($uselocal == -1) {
41 if (isset($SESSION->langtranslateintolocal)) {
42 $uselocal = $SESSION->langtranslateintolocal;
44 $uselocal = LANG_DEFAULT_USELOCAL;
47 $SESSION->langtranslateintolocal = $uselocal;
50 if (!has_capability('moodle/site:langeditmaster', $context, $USER->id, false)) {
55 if (!has_capability('moodle/site:langeditmaster', $context, $USER->id, false) && (!$uselocal)) {
56 print_error('cannoteditmasterlang', 'error');
59 if ((!has_capability('moodle/site:langeditlocal', $context, $USER->id, false)) && ($uselocal)) {
60 print_error('cannotcustomizelocallang', 'error');
63 $strlanguage = get_string("language");
64 $strcurrentlanguage = get_string("currentlanguage");
65 $strmissingstrings = get_string("missingstrings");
66 $streditstrings = get_string("editstrings", 'admin');
67 $stredithelpdocs = get_string("edithelpdocs", 'admin');
68 $strthislanguage = get_string("thislanguage");
69 $strgotofirst = get_string('gotofirst','admin');
70 $strfilestoredin = get_string('filestoredin', 'admin');
71 $strfilestoredinhelp = get_string('filestoredinhelp', 'admin');
72 $strswitchlang = get_string('switchlang', 'admin');
73 $strchoosefiletoedit = get_string('choosefiletoedit', 'admin');
74 $streditennotallowed = get_string('langnoeditenglish', 'admin');
75 $strfilecreated = get_string('filecreated', 'admin');
76 $strprev = get_string('previous');
77 $strnext = get_string('next');
78 $strlocalstringcustomization = get_string('localstringcustomization', 'admin');
79 $strlangpackmaintaining = get_string('langpackmaintaining', 'admin');
80 $strnomissingstrings = get_string('nomissingstrings', 'admin');
81 $streditingnoncorelangfile = get_string('editingnoncorelangfile', 'admin');
82 $strlanglocalpackage = get_string('langlocalpackage', 'admin');
83 $strlangmasterpackage = get_string('langmasterpackage', 'admin');
84 $strlangmasterenglish = get_string('langmasterenglish', 'admin');
86 $currentlang = current_language();
90 // Missing array keys are not bugs here but missing strings
91 error_reporting(E_ALL ^ E_NOTICE);
92 $title = $strmissingstrings;
95 $title = $streditstrings;
98 $title = $stredithelpdocs;
100 $title = $strlanguage;
104 echo $OUTPUT->header();
106 // Prepare and render menu tabs
108 $secondrow = array();
113 $inactive = array('uselocal');
114 $activated = array('uselocal');
116 $inactive = array('usemaster');
117 $activated = array('usemaster');
119 if (has_capability('moodle/site:langeditlocal', $context, $USER->id, false)) {
120 $firstrow[] = new tabobject('uselocal',
121 "$CFG->wwwroot/$CFG->admin/lang.php?mode=$mode&currentfile=$currentfile&uselocal=1",
122 $strlocalstringcustomization );
124 if (has_capability('moodle/site:langeditmaster', $context, $USER->id, false)) {
125 $firstrow[] = new tabobject('usemaster',
126 "$CFG->wwwroot/$CFG->admin/lang.php?mode=$mode&currentfile=$currentfile&uselocal=0",
127 $strlangpackmaintaining );
129 $secondrow[] = new tabobject('missing', "$CFG->wwwroot/$CFG->admin/lang.php?mode=missing", $strmissingstrings );
130 $secondrow[] = new tabobject('compare', "$CFG->wwwroot/$CFG->admin/lang.php?mode=compare", $streditstrings );
131 $secondrow[] = new tabobject('helpfiles', "$CFG->wwwroot/$CFG->admin/lang.php?mode=helpfiles", $stredithelpdocs );
132 $tabs = array($firstrow, $secondrow);
133 print_tabs($tabs, $currenttab, $inactive, $activated);
137 // TODO this is a very nice place to put some translation statistics
138 echo $OUTPUT->box_start();
139 $currlang = current_language();
140 $langs = get_list_of_languages(false, true);
141 $select = new single_select(new moodle_url('/admin/lang.php'), 'lang', $langs, $currlang, null);
142 $select->label = $strcurrentlanguage.':';
143 $select->formid = 'chooselang';
144 echo $OUTPUT->render($select);
145 echo $OUTPUT->box_end();
146 echo $OUTPUT->footer();
150 // Get a list of all the root files in the English directory
152 $langbase = $CFG->dataroot . '/lang';
153 $enlangdir = "$CFG->dirroot/lang/en_utf8";
154 if ($currentlang == 'en_utf8') {
155 $langdir = $enlangdir;
157 $langdir = "$langbase/$currentlang";
159 $locallangdir = "$langbase/{$currentlang}_local";
160 $saveto = $uselocal ? $locallangdir : $langdir;
162 if (($mode == 'missing') || ($mode == 'compare')) {
163 // get the list of all English stringfiles
164 $stringfiles = lang_standard_locations();
165 if (LANG_SEARCH_EXTRA) {
166 $stringfiles += lang_extra_locations();
168 if (count($stringfiles) == 0) {
169 print_error('cannotfindlang', 'error', '', 'English');
171 } elseif ($mode == 'helpfiles') {
172 $helpfiles = lang_help_standard_locations();
173 if (LANG_SEARCH_EXTRA) {
174 $helpfiles += lang_help_extra_locations();
176 if (count($helpfiles) == 0) {
177 print_error("cannotfindhelp", 'error', '', 'English');
183 if ($mode == 'missing') {
184 if (!file_exists($langdir)) {
185 print_error('invalidlangpack');
188 // Following variables store the HTML output to be echo-ed
194 // Total number of strings and missing strings
195 $totalcounter->strings = 0;
196 $totalcounter->missing = 0;
198 // For each file, check that a counterpart exists, then check all the strings
199 foreach ($stringfiles as $stringfile) {
200 $location = $stringfile['location'];
201 $plugin = $stringfile['plugin'];
202 $prefix = $stringfile['prefix'];
203 $filename = $stringfile['filename'];
206 // Get some information about file locations:
207 // $enfilepath = the path to the English file distributed either in the core space or in plugin space
208 // $trfilepath = the path to the translated file distributed either in the lang pack or in plugin space
209 // $lcfilepath = the path to the _local customization
210 // $trfilename = the filename of the translated version of the file (including prefix for non-core files)
211 if ($location || $plugin) {
212 // non-core file in an extra location
213 $enfilepath = "$CFG->dirroot/$location/$plugin/lang/en_utf8/$filename";
214 $trfilepath = "$CFG->dirroot/$location/$plugin/lang/$currentlang/$filename";
215 $lcfilepath = "$locallangdir/$filename";
216 $trfilename = $filename;
222 // core file in standard location
223 $enfilepath = "$CFG->dirroot/lang/en_utf8/$filename";
224 $trfilepath = "$langdir/$filename";
225 $lcfilepath = "$locallangdir/$filename";
226 $trfilename = $filename;
228 // $enstring = English strings distributed either in the core space or in plugin space
229 include($enfilepath);
230 $enstring = isset($string) ? $string : array();
234 //$lcstring = local customizations
236 if (file_exists($lcfilepath)) {
237 include($lcfilepath);
238 $localfileismissing = 0;
239 if (isset($string) && is_array($string)) {
245 $localfileismissing = 1;
248 // $string = translated strings distibuted either in core lang pack or in plugin space
250 if (file_exists($trfilepath)) {
251 include($trfilepath);
255 $o .= $OUTPUT->notification(get_string("filemissing", "", $trfilepath), "notifyproblem");
260 $first = true; // first missing string found in the file
261 // For all English strings in the current file check distributed translations and _local customizations
262 foreach ($enstring as $key => $value) {
263 $totalcounter->strings++;
264 $missingstring = false;
265 $missinglocalstring = false;
266 $translationsdiffer = false;
267 if (empty($string[$key]) and $string[$key] != "0") { // MDL-4735
268 // string is missing in distributed pack
269 $missingstring = true;
271 if (empty($lcstring[$key]) and $lcstring[$key] != "0") { // MDL-4735
272 // string is missing in _local customization
273 $missinglocalstring = true;
275 if (!$missingstring && !$missinglocalstring && ($lcstring[$key] != $string[$key])) {
276 $translationsdiffer = true;
278 if ($missingstring || $translationsdiffer) {
279 $value = htmlspecialchars($value);
280 $value = str_replace("$"."a", "\\$"."a", $value);
281 $value = str_replace("%%","%",$value);
283 $m .= "<a href=\"lang.php?mode=missing#$trfilename\">$trfilename";
284 $m .= $fileismissing ? '*' : '';
285 $m .= '</a> ';
286 $o .= "<p><a name=\"$trfilename\"></a><b>".
287 get_string("stringsnotset","", $trfilepath)."</b></p><pre>";
289 $somethingfound = true;
291 if ($missingstring) {
293 $totalcounter->missing++;
295 if ($translationsdiffer) {
298 if (LANG_LINK_MISSING_STRINGS && ($missingstring || $translationsdiffer)) {
299 $missinglinkstart = "<a href=\"lang.php?mode=compare&currentfile=$filename#$key\">";
300 $missinglinkend = '</a>';
302 $missinglinkstart = '';
303 $missinglinkend = '';
305 if (strlen($value) > LANG_MISSING_TEXT_MAX_LEN) {
306 $value = lang_xhtml_save_substr($value, 0, LANG_MISSING_TEXT_MAX_LEN) . ' ...'; // MDL-8852
308 if ($translationsdiffer) {
311 $o .= "$"."string['".$missinglinkstart.$key.$missinglinkend."'] = \"$value\";";
312 if ($translationsdiffer) {
313 $o .= ' // differs from the translation in _local';
314 } elseif (!$missinglocalstring) {
315 $o .= ' // translated only in _local';
321 $o .= '</pre><hr />';
325 if ($totalcounter->missing > 0) {
326 $totalcounter->missingpercent = sprintf('%02.1f', ($totalcounter->missing / $totalcounter->strings * 100));
327 echo $OUTPUT->heading(get_string('numberofstrings', 'admin', $totalcounter), 4);
329 echo $OUTPUT->heading($strnomissingstrings, 4, 'notifysuccess');
333 echo $OUTPUT->box($m, 'filenames');
338 if (! $files = get_directory_list("$CFG->dirroot/lang/en_utf8/help", "CVS")) {
339 print_error("cannotfindhelp", 'error', '', 'English');
342 foreach ($files as $filekey => $file) { // check all the help files.
343 if (!file_exists("$langdir/help/$file")) {
344 echo $OUTPUT->notification(get_string("filemissing", "", "$langdir/help/$file"), 'notifyproblem');
345 $somethingfound = true;
350 if (! $files = get_directory_list("$CFG->dirroot/lang/en_utf8/docs", "CVS")) {
351 print_error("cannotfinddoc", 'error', '', 'English');
353 foreach ($files as $filekey => $file) { // check all the docs files.
354 if (!file_exists("$langdir/docs/$file")) {
355 echo $OUTPUT->notification(get_string("filemissing", "", "$langdir/docs/$file"), 'notifyproblem');
356 $somethingfound = true;
361 if (!empty($somethingfound)) {
362 echo $OUTPUT->continue_button("lang.php");
364 notice(get_string("languagegood"), "lang.php" );
367 } else if ($mode == 'compare') {
369 if (!file_exists($langbase) ){
370 if (!lang_make_directory($langbase) ){
371 print_error('cannotcreatelangbase', 'error');
373 echo '<div class="notifysuccess">Created directory '.
374 $langbase .'</div>'."<br />\n";
377 if (!$uselocal && !file_exists($langdir)) {
378 if (!lang_make_directory($langdir)) {
379 print_error('cannotcreatelangdir', 'error');
381 echo '<div class="notifysuccess">Created directory '.
382 $langdir .'</div>'."<br />\n";
385 if ($uselocal && !file_exists($locallangdir)) {
386 if (!lang_make_directory($locallangdir)) {
387 echo '<div class="notifyproblem">ERROR: Could not create directory '.
388 $locallangdir .'</div>'."<br />\n";
391 echo '<div class="notifysuccess">Created directory '.
392 $locallangdir .'</div>'."<br />\n";
396 if ($currentfile <> '') {
397 if (!$fileinfo = lang_get_file_info($currentfile, $stringfiles)) {
398 print_error('cannotfindinfo', 'error', '', $currentfile);
400 // check the filename is set up correctly, prevents bugs similar to MDL-10920
401 $location = $fileinfo['location'];
402 $plugin = $fileinfo['plugin'];
403 $prefix = $fileinfo['prefix'];
404 $filename = $fileinfo['filename'];
405 if ($location || $plugin) {
406 // file in an extra location
407 if ($currentfile != "{$prefix}{$plugin}.php") {
408 print_error('filemismatch', 'error', '', (object)array('current'=>$currectfile, 'file'=>$prefix.$plugin.'.php'));
411 echo $OUTPUT->notification($streditingnoncorelangfile);
415 // file in standard location
416 if ($currentfile != $filename) {
417 print_error('filemismatch', 'error', '', (object)array('current'=>$currectfile, 'file'=>$filename.'.php'));
421 // Get some information about file locations:
422 // $enfilepath = the path to the English file distributed either in the core space or in plugin space
423 // $trfilepath = the path to the translated file distributed either in the lang pack or in plugin space
424 // $lcfilepath = the path to the _local customization
425 // $trfilename = the filename of the translated version of the file (including prefix for non-core files)
426 if ($location || $plugin) {
427 // non-core file in an extra location
428 $enfilepath = "$CFG->dirroot/$location/$plugin/lang/en_utf8/$filename";
429 $trfilepath = "$CFG->dirroot/$location/$plugin/lang/$currentlang/$filename";
430 $lcfilepath = "$locallangdir/$filename";
431 $trfilename = $filename;
433 // core file in standard location
434 $enfilepath = "$CFG->dirroot/lang/en_utf8/$filename";
435 $trfilepath = "$langdir/$filename";
436 $lcfilepath = "$locallangdir/$filename";
437 $trfilename = $filename;
441 if (isset($_POST['currentfile'])){ // Save a file
442 if (!confirm_sesskey()) {
443 print_error('confirmsesskeybad', 'error');
446 $newstrings = array();
448 foreach ($_POST as $postkey => $postval) {
449 $stringkey = lang_file_string_key($postkey);
450 $newstrings[$stringkey] = $postval;
453 unset($newstrings['currentfile']);
455 $packstring = array();
456 $saveinto = $langdir;
458 if(file_exists($trfilepath)) {
459 include($trfilepath);
460 if (isset($string)) {
461 $packstring = $string;
465 $saveinto = $locallangdir;
468 if (lang_save_file($saveinto, $currentfile, $newstrings, $uselocal, $packstring)) {
469 echo $OUTPUT->notification(get_string("changessaved")." ($saveinto/$currentfile)", "notifysuccess");
471 print_error('cannotsavefile', 'error', 'lang.php?mode=compare&currentfile=$currentfile', $saveinto.'/'.$currentfile);
476 echo $OUTPUT->box_start('generalbox editstrings');
477 $menufiles = array();
478 $menufiles_coregrp = 1;
479 foreach ($stringfiles as $stringfile) {
480 $item_key = $stringfile['filename'];
481 $item_label = $stringfile['filename'];
482 if ($stringfile['location'] != '' && $stringfile['plugin'] != '') {
483 $item_label .= ' ('.$stringfile['location'].'/'.$stringfile['plugin'].')';
484 if ($menufiles_coregrp == 1) {
485 $menufiles['extra'] = '------------';
486 $menufiles_coregrp = 0;
489 $menufiles[$item_key] = $item_label;
491 $selectionlabel = '<code class="path">';
492 //$selectionlabel .= $strfilestoredin;
493 $selectionlabel .= $uselocal ? "{$currentlang}_local" : $currentlang;
494 $selectionlabel .= '/</code>';
495 $select = new single_select(new moodle_url('/admin/lang.php?mode=compare'), 'currentfile', $menufiles, $currentfile, array(''=>$strchoosefiletoedit));
496 $select->label = $selectionlabel;
497 $select->formid = 'choosefile';
498 echo $OUTPUT->render($select);
499 echo $OUTPUT->help_icon('langswitchstorage', $strfilestoredinhelp);
500 echo $OUTPUT->box_end();
502 if ($currentfile <> '') {
504 if (!isset($editable) || $editable) {
505 if (!file_exists("$saveto/$currentfile")) {
506 if (!@touch("$saveto/$currentfile")) {
507 echo $OUTPUT->heading(get_string("filemissing", "", "$saveto/$currentfile"), 4, 'error');
509 echo $OUTPUT->heading($strfilecreated, 4, 'notifysuccess');
512 if ($currentlang == "en_utf8" && !$uselocal) {
514 echo $OUTPUT->heading($streditennotallowed, 4);
515 } elseif ($f = fopen("$saveto/$currentfile","r+")) {
520 echo $OUTPUT->notification(get_string("makeeditable", "", "$saveto/$currentfile"), 'notifyproblem');
523 error_reporting($CFG->debug);
525 $o = ''; // stores the HTML output to be echo-ed
528 include($enfilepath);
529 $enstring = isset($string) ? $string : array();
531 // Following strings have moved into langconfig.php, but keep the here for backward compatibility
533 if ($currentlang != 'en' and $currentfile == 'moodle.php') {
534 $enstring['thislanguage'] = "<< TRANSLATORS: Specify the name of your language here. If possible use Unicode Numeric Character References >>";
535 $enstring['thischarset'] = "<< TRANSLATORS: Charset encoding - always use utf-8 >>";
536 $enstring['thisdirection'] = "<< TRANSLATORS: This string specifies the direction of your text, either left-to-right or right-to-left. Insert either 'ltr' or 'rtl' here. >>";
537 $enstring['parentlanguage'] = "<< TRANSLATORS: If your language has a Parent Language that Moodle should use when strings are missing from your language pack, then specify the code for it here. If you leave this blank then English will be used. Example: nl >>";
542 @include($lcfilepath);
543 $localstring = isset($string) ? $string : array();
547 @include($trfilepath);
548 $string = isset($string) ? $string : array();
552 $o .= "<form id=\"$currentfile\" action=\"lang.php\" method=\"post\">";
555 $o .= "<table summary=\"\" width=\"100%\" class=\"translator\">";
558 foreach ($enstring as $key => $envalue) {
560 if (LANG_SUBMIT_REPEAT && $editable && $linescounter % LANG_SUBMIT_REPEAT_EVERY == 0) {
561 $o .= '<tr><td> </td><td><br />';
562 $o .= '<input type="submit" tabindex="'.$missingcounter.'" name="update" value="'.get_string('savechanges').': '.$currentfile.'" />';
563 $o .= '<br /> </td></tr>';
565 $envalue = nl2br(htmlspecialchars($envalue));
566 $envalue = preg_replace('/(\$a\-\>[a-zA-Z0-9]*|\$a)/', '<b>$0</b>', $envalue); // Make variables bold.
567 $envalue = str_replace("%%","%",$envalue);
568 $envalue = str_replace("\\","",$envalue); // Delete all slashes
570 $o .= "\n\n".'<tr class="';
571 if ($linescounter % 2 == 0) {
577 $o .= '<td dir="ltr" lang="en">';
578 $o .= '<span id="'.$key.'" class="stren">'.$envalue.'</span>';
580 $o .= '<span class="strkey">'.$key.'</span>';
583 // Missing array keys are not bugs here but missing strings
584 error_reporting(E_ALL ^ E_NOTICE);
586 $value = lang_fix_value_from_file($localstring[$key]);
587 $value2 = lang_fix_value_from_file($string[$key]);
592 $value = lang_fix_value_from_file($string[$key]);
593 $value2 = lang_fix_value_from_file($localstring[$key]);
595 error_reporting($CFG->debug);
600 $usetabindex = false;
602 // the string is not present in the pack being processed
604 $cellcolour = 'class="bothmissing"';
607 $cellcolour = 'class="mastermissing"';
611 if (LANG_DISPLAY_MISSING_LINKS) {
612 $missingtarget = '<a name="missing'.$missingcounter.'"></a>';
613 $missingnext = '<a href="#missing'.($missingcounter+1).'">'.
614 '<img src="' . $OUTPUT->pix_url('t/down') . '" class="iconsmall" alt="'.$strnext.'" /></a>';
615 $missingprev = '<a href="#missing'.($missingcounter-1).'">'.
616 '<img src="' . $OUTPUT->pix_url('t/up') . '" class="iconsmall" alt="'.$strprev.'" /></a>';
619 // the string is translated in the pack being processed
620 if ($value <> $value2 && ($value2 <> '')) {
621 $cellcolour = 'class="localdifferent"';
624 if (LANG_DISPLAY_MISSING_LINKS) {
625 $missingtarget = '<a name="missing'.$missingcounter.'"></a>';
626 $missingnext = '<a href="#missing'.($missingcounter+1).'">'.
627 '<img src="' . $OUTPUT->pix_url('t/down') . '" class="iconsmall" alt="'.$strnext.'" /></a>';
628 $missingprev = '<a href="#missing'.($missingcounter-1).'">'.
629 '<img src="' . $OUTPUT->pix_url('t/up') . '" class="iconsmall" alt="'.$strprev.'" /></a>';
635 $o .= '<td '.$cellcolour.' valign="top">';
636 if ($missingcounter > 1) {
639 $o .= $missingtarget."\n";
640 if (isset($string[$key])) {
641 $valuelen = strlen($value);
643 $valuelen = strlen($envalue);
647 $tabindex = 'tabindex="'.$missingcounter.'"';
651 if (strstr($value, "\r") or strstr($value, "\n") or $valuelen > $cols) {
652 $rows = ceil($valuelen / $cols);
653 $o .= '<textarea name="stringXXX'.lang_form_string_key($key).'" cols="'.$cols.'" rows="'.$rows.'" '.$tabindex.'>'.$value.'</textarea>'."\n";
656 $cols = $valuelen + 5;
658 if (LANG_ALWAYS_TEXTAREA) {
659 $o .= '<textarea name="stringXXX'.lang_form_string_key($key).'" cols="'.$cols.'" rows="1" '.$tabindex.'>'.$value.'</textarea>'."\n";
661 $o .= '<input type="text" name="stringXXX'.lang_form_string_key($key).'" value="'.$value.'" size="'.$cols.'" '.$tabindex.' />';
664 if ($value2 <> '' && $value <> $value2) {
665 $o .= '<br /><span style="font-size:small">'.$value2.'</span>';
667 $o .= $missingnext . '</td>';
670 $o .= '<td '.$cellcolour.' valign="top">'.$value.'<br />'.$value2.'</td>';
675 $o .= '<tr><td> </td><td><br />';
676 $o .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
677 $o .= '<input type="hidden" name="currentfile" value="'.$currentfile.'" />';
678 $o .= '<input type="hidden" name="mode" value="compare" />';
679 $o .= '<input type="submit" name="update" tabindex="'.$missingcounter.'" value="'.get_string('savechanges').': '.$currentfile.'" />';
688 if (LANG_DISPLAY_MISSING_LINKS) {
689 if ($missingcounter > 0) {
690 echo $OUTPUT->heading(get_string('numberofmissingstrings', 'admin', $missingcounter), 4);
692 echo $OUTPUT->heading('<a href="#missing1">'.$strgotofirst.'</a>', 4);
695 echo $OUTPUT->heading($strnomissingstrings, 4, 'notifysuccess');
701 // no $currentfile specified
702 // no useful information to display - maybe some help? instructions?
705 } elseif ($mode == 'helpfiles') {
707 $saveto = $saveto.'/help'; // the edited content will be saved to
708 $enlangdir = $enlangdir.'/help'; // English master help files localtion
709 $langdir = $langdir.'/help'; // current language master help files location
710 $locallangdir = $locallangdir.'/help'; // local modifications of help files location
711 $altdir = $uselocal ? $langdir : $locallangdir; // alternative to $saveto
713 $fileeditorrows = 10; // number of textareas' rows
714 $fileeditorcols = 100; // dtto cols
715 $filemissingmark = ' (***)'; // mark to add to non-existing or zero-length files
716 $fileoldmark = ' (old?)'; // mark to add to filenames in selection form if the English version is newer
717 $filetemplate = ''; // template for new files, e.g. CVS identification
719 if (isset($_POST['currentfile'])) { // Save a file
720 if (!confirm_sesskey()) {
721 print_error('confirmsesskeybad', 'error');
723 if (lang_help_save_file($saveto, $currentfile, $_POST['filedata'])) {
724 echo $OUTPUT->notification(get_string("changessaved")." ($saveto/$currentfile)", "notifysuccess");
726 print_error('cannotsavefile', 'error', 'lang.php?mode=compare&currentfile=$currentfile', array($saveinto, $currentfile));
730 echo $OUTPUT->box_start('generalbox editstrings');
731 $menufiles = array();
732 $menufiles_coregrp = 1;
733 $origlocation = ''; // the location of the currentfile's English source will be stored here
734 $origplugin = ''; // dtto plugin
735 foreach ($helpfiles as $helppath => $helpfile) {
736 $item_key = $helpfile['filename'];
737 $item_label = $helpfile['filename'];
738 if ((!file_exists($saveto.'/'.$helpfile['filename'])) || (filesize($saveto.'/'.$helpfile['filename']) == 0)) {
739 $item_label .= $filemissingmark;
741 if (filemtime($saveto.'/'.$helpfile['filename']) < filemtime($helppath)) {
742 $item_label .= $fileoldmark;
744 if ($helpfile['location'] != '' && $helpfile['plugin'] != '') {
745 $item_label .= ' ('.$helpfile['location'].'/'.$helpfile['plugin'].')';
746 if ($menufiles_coregrp == 1) {
747 $menufiles['extra'] = '------------';
748 $menufiles_coregrp = 0;
752 $menufiles[$item_key] = $item_label;
753 if ($currentfile == $helpfile['filename']) {
754 $origlocation = $helpfile['location'];
755 $origplugin = $helpfile['plugin'];
758 $selectionlabel = '<code class="path">';
759 //$selectionlabel .= $strfilestoredin;
760 $selectionlabel .= $uselocal ? "{$currentlang}_local" : $currentlang;
761 $selectionlabel .= '/help/</code>';
762 $select = new single_select(new moodle_url('/admin/lang.php?mode=helpfiles'), 'currentfile', $menufiles, $currentfile, array(''=>$strchoosefiletoedit));
763 $select->label = $selectionlabel;
764 $select->formid = 'choosefile';
765 echo $OUTPUT->render($select);
766 echo $OUTPUT->help_icon('langswitchstorage', $strfilestoredinhelp);
767 echo $OUTPUT->box_end();
769 if (!empty($currentfile)) {
771 if (!file_exists("$saveto/$currentfile")) {
772 $dbg .= "File does not exist: $saveto/$currentfile\n";
773 //check if directory exist
774 if (!file_exists(dirname("$saveto/$currentfile"))) {
775 if(!lang_make_directory(dirname("$saveto/$currentfile"))) {
776 echo ('Cannot create directory: '.dirname("$saveto/$currentfile"));
780 // file doesn't exist - let's check webserver's permission to create it
782 if (!@touch("$saveto/$currentfile")) {
784 // webserver is unable to create new file
786 echo $OUTPUT->notification(get_string('filemissing', '', "$saveto/$currentfile" ));
787 echo $OUTPUT->notification(get_string('makeeditable', '', "$saveto/$currentfile"));
791 // webserver can create new file - we can delete it now and let
792 // it create again if its filesize() > 0
795 unlink("$saveto/$currentfile");
797 } elseif (is_writable("$saveto/$currentfile")) {
801 // file exists but it is not writeable by web server process :-(
804 echo $OUTPUT->notification(get_string('makeeditable', '', "$saveto/$currentfile"));
807 // master en_utf8 in dataroot is not editable
808 if ((!$uselocal) && ($currentlang == 'en_utf8')) {
815 $strsavetotitle = $strlanglocalpackage . $OUTPUT->help_icon('langpackages', $strlanglocalpackage);
816 $straltdirtitle = $strlangmasterpackage . $OUTPUT->help_icon('langpackages', $strlangmasterpackage);
818 $straltdirtitle = $strlanglocalpackage . $OUTPUT->help_icon('langpackages', $strlanglocalpackage);
819 $strsavetotitle = $strlangmasterpackage . $OUTPUT->help_icon('langpackages', $strlangmasterpackage);
824 // generate an editor for the current help file in $saveto
825 echo '<fieldset><legend>'.$strsavetotitle.'</legend>';
826 echo "<form id=\"helpfileeditor\" action=\"lang.php\" method=\"post\">";
827 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
828 echo '<input type="hidden" name="currentfile" value="'.$currentfile.'" />';
829 echo '<input type="hidden" name="mode" value="helpfiles" />';
830 echo "<div class='mdl-align'>\n";
831 echo "<textarea rows=\"$fileeditorrows\" cols=\"$fileeditorcols\" name=\"filedata\">";
832 if (file_exists("$saveto/$currentfile")) {
833 echo htmlspecialchars(file_get_contents("$saveto/$currentfile"));
835 echo ($filetemplate);
837 echo "</textarea>\n</div>\n";
838 echo '<div class="mdl-align"><input type="submit" value="'.get_string('savechanges').'" /></div>';
840 $preview_url = lang_help_preview_url($currentfile, !$uselocal);
842 echo $OUTPUT->action_link($preview_url, get_string('preview'), new popup_action('click', $preview_url));
847 if (is_readable("$altdir/$currentfile")) {
848 // show the content of the same help file in alternative location
849 echo '<fieldset><legend>'.$straltdirtitle.'</legend>';
850 echo "<div class='mdl-align'>\n";
851 echo "<textarea rows=\"$fileeditorrows\" cols=\"$fileeditorcols\" name=\"\">";
852 if (file_exists("$altdir/$currentfile")) {
853 echo htmlspecialchars(file_get_contents("$altdir/$currentfile"));
855 echo ($filetemplate);
857 echo "</textarea>\n</div>\n";
858 $preview_url = lang_help_preview_url($currentfile, $uselocal);
860 echo $OUTPUT->action_link($preview_url, get_string('preview'), new popup_action('click', $preview_url));
865 // show the content of the original English file either in core space or plugin space
866 if ($origlocation != '' && $origplugin != '') {
867 // non-core help file
868 $ensrc = "$CFG->dirroot/$origlocation/$origplugin/lang/en_utf8/help/$currentfile";
871 $ensrc = "$enlangdir/$currentfile";
873 if (is_readable($ensrc)) {
874 echo '<fieldset><legend>'.$strlangmasterenglish;
875 echo $OUTPUT->help_icon('langpackages', $strlangmasterenglish);
877 echo "<div class='mdl-align'>\n<textarea rows=\"$fileeditorrows\" cols=\"$fileeditorcols\" name=\"\">";
878 echo htmlspecialchars(file_get_contents($ensrc));
879 echo "</textarea>\n</div>\n";
880 $preview_url = lang_help_preview_url($currentfile, true, 'en_utf8'); // do not display en_utf8_local
882 echo $OUTPUT->action_link($preview_url, get_string('preview'), new popup_action('click', $preview_url));
887 echo '</div>'; // translator box
888 error_reporting($CFG->debug);
891 if (false && $CFG->debugdisplay && debugging('', DEBUG_DEVELOPER) ) {
893 echo $OUTPUT->heading('Debugging info');
894 echo '<pre class="notifytiny">';
896 print_r("\n\$currentfile = $currentfile");
897 print_r("\n\$enlangdir = $enlangdir");
898 print_r("\n\$langdir = $langdir");
899 print_r("\n\$locallangdir = $locallangdir");
900 print_r("\n\$saveto = $saveto");
901 print_r("\n\$altdir = $altdir");
902 print_r("\n\$origlocation = $origlocation");
903 print_r("\n\$origplugin = $origplugin");
904 print_r("\n\$ensrc = $ensrc");
905 print_r("\n\$helpfiles = ");
910 } // fi $mode == 'helpfiles'
913 echo $OUTPUT->footer();
915 //////////////////////////////////////////////////////////////////////
918 * Save language translation file.
920 * Thanks to Petri Asikainen for the original version of code
921 * used to save language files.
925 * @param string $path Full pathname to the directory to use
926 * @param string $file File to overwrite
927 * @param array $strings Array of strings to write
928 * @param bool $local Should *_local version be saved?
929 * @param array $packstrings Array of default langpack strings (needed if $local)
930 * @return bool Created successfully?
932 function lang_save_file($path, $file, $strings, $local, $packstrings) {
934 if (LANG_KEEP_ORPHANS) {
935 // let us load the current content of the file
937 @include("$path/$file");
938 if (isset($string)) {
945 // let us rewrite the file
946 if (!$f = @fopen("$path/$file","w")) {
950 fwrite($f, "<?PHP // \$Id\$ \n");
951 fwrite($f, " // $file - created with Moodle $CFG->release ($CFG->version)\n");
953 fwrite($f, " // local modifications from $CFG->wwwroot\n");
957 foreach ($strings as $key => $value) {
958 @list($id, $stringname) = explode('XXX',$key);
959 $value = lang_fix_value_before_save($value);
960 if ($id == "string" and $value != ""){
961 if ((!$local) || (!isset($packstrings[$stringname])) || (lang_fix_value_from_file($packstrings[$stringname]) <> lang_fix_value_from_file($value))) {
962 // Either we are saving the master language pack
963 // or the string is not saved in packstring - fixes PHP notices about missing key
964 // or we are saving local language pack and the strings differ.
965 fwrite($f,"\$string['$stringname'] = '$value';\n");
967 if (LANG_KEEP_ORPHANS && isset($orphans[$stringname])) {
968 unset($orphans[$stringname]);
972 if (LANG_KEEP_ORPHANS) {
973 // let us add orphaned strings, i.e. already translated strings without the English referential source
974 foreach ($orphans as $key => $value) {
975 fwrite($f,"\$string['$key'] = '".lang_fix_value_before_save($value)."'; // ORPHANED\n");
984 * Fix value of the translated string after it is load from the file.
986 * These modifications are typically necessary to work with the same string coming from two sources.
987 * We need to compare the content of these sources and we want to have e.g. "This string\r\n"
988 * to be the same as " This string\n".
990 * @param string $value Original string from the file
991 * @return string Fixed value
993 function lang_fix_value_from_file($value='') {
994 $value = str_replace("\r","",$value); // Bad character caused by Windows
995 $value = preg_replace("/\n{3,}/", "\n\n", $value); // Collapse runs of blank lines
996 $value = trim($value); // Delete leading/trailing white space
997 $value = str_replace("\\","",$value); // Delete all slashes
998 $value = str_replace("%%","%",$value);
999 $value = str_replace("&","&",$value); // Fixes MDL-9248
1000 $value = str_replace("<","<",$value);
1001 $value = str_replace(">",">",$value);
1002 $value = str_replace('"',""",$value);
1007 * Fix value of the translated string before it is saved into the file
1010 * @param string $value Raw string to be saved into the lang pack
1011 * @return string Fixed value
1013 function lang_fix_value_before_save($value='') {
1015 if ($CFG->lang != "zh_hk" and $CFG->lang != "zh_tw") { // Some MB languages include backslash bytes
1016 $value = str_replace("\\","",$value); // Delete all slashes
1018 if (ini_get_bool('magic_quotes_sybase')) { // Unescape escaped sybase quotes
1019 $value = str_replace("''", "'", $value);
1021 // escape all embedded variables
1022 $value = str_replace('$', '\$', $value); // Add slashes for $
1023 // unescape placeholders: only $a and $a->something are allowed. All other $variables are left escaped
1024 $value = preg_replace('/\\\\\$a($|[^_a-zA-Z0-9\-]|\->[a-zA-Z0-9_]+)/', '$a\\1', $value);
1025 $value = str_replace("'", "\\'", $value); // Add slashes for '
1026 $value = str_replace('"', "\\\"", $value); // Add slashes for "
1027 $value = str_replace("%","%%",$value); // Escape % characters
1028 $value = str_replace("\r", "",$value); // Remove linefeed characters
1029 $value = trim($value); // Delete leading/trailing white space
1034 * Try and create a new language directory.
1036 * Uses PHP>=5.0 syntax of mkdir and tries to create directories recursively.
1039 * @param string $directory full path to the directory under $langbase
1040 * @return string|false Returns full path to directory if successful, false if not
1042 function lang_make_directory($dir, $shownotices=true) {
1045 if (! file_exists($dir)) {
1046 if (! @mkdir($dir, $CFG->directorypermissions, true)) { // recursive=true; PHP>=5.0 needed
1049 //@chmod($dir, $CFG->directorypermissions); // Just in case mkdir didn't do it
1055 * Return the string key name for use in HTML form.
1057 * Required because '.' in form input names get replaced by '_' by PHP.
1059 * @param string $keyfromfile The key name containing '.'
1060 * @return string The key name without '.'
1062 function lang_form_string_key($keyfromfile) {
1063 return str_replace('.', '##46#', $keyfromfile); /// Derived from ., the ascii value for a period.
1067 * Return the string key name for use in file.
1069 * Required because '.' in form input names get replaced by '_' by PHP.
1071 * @param string $keyfromfile The key name without '.'
1072 * @return string The key name containing '.'
1074 function lang_file_string_key($keyfromform) {
1075 return str_replace('##46#', '.', $keyfromform);
1079 * Return the substring of the string and take care of XHTML compliance.
1081 * There was a problem with pure substr() which could possibly produce XHTML parsing error:
1082 * substr('Marks & Spencer', 0, 9) -> 'Marks &am' ... is not XHTML compliance
1083 * This function takes care of these cases. Fixes MDL-8852.
1085 * Thanks to kovacsendre, the author of the function at http://php.net/substr
1087 * @param string $str The original string
1088 * @param int $start Start position in the $value string
1089 * @param int $length Optional length of the returned substring
1090 * @return string The substring as returned by substr() with XHTML compliance
1091 * @todo Seems the function does not work with negative $start together with $length being set
1093 function lang_xhtml_save_substr($str, $start, $length = NULL) {
1094 if ($length === 0) {
1095 //stop wasting our time ;)
1099 //check if we can simply use the built-in functions
1100 if (strpos($str, '&') === false) {
1101 // No entities. Use built-in functions
1102 if ($length === NULL) {
1103 return substr($str, $start);
1105 return substr($str, $start, $length);
1109 // create our array of characters and html entities
1110 $chars = preg_split('/(&[^;\s]+;)|/', $str, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE);
1111 $html_length = count($chars);
1113 // check if we can predict the return value and save some processing time, i.e.:
1114 // input string was empty OR
1115 // $start is longer than the input string OR
1116 // all characters would be omitted
1117 if (($html_length === 0) or ($start >= $html_length) or (isset($length) and ($length <= -$html_length))) {
1121 //calculate start position
1123 $real_start = $chars[$start][1];
1125 //start'th character from the end of string
1126 $start = max($start,-$html_length);
1127 $real_start = $chars[$html_length+$start][1];
1130 if (!isset($length)) {
1131 // no $length argument passed, return all remaining characters
1132 return substr($str, $real_start);
1133 } elseif ($length > 0) {
1134 // copy $length chars
1135 if ($start+$length >= $html_length) {
1136 // return all remaining characters
1137 return substr($str, $real_start);
1139 //return $length characters
1140 return substr($str, $real_start, $chars[max($start,0)+$length][1] - $real_start);
1143 //negative $length. Omit $length characters from end
1144 return substr($str, $real_start, $chars[$html_length+$length][1] - $real_start);
1149 * Finds all English string files in the standard lang/en_utf8 location.
1151 * Core lang files should always be stored here and not in the module space (MDL-10920).
1152 * The English version of the file may be found in
1153 * $CFG->dirroot/lang/en_utf8/filename
1154 * The localised version of the found file should be saved into
1155 * $CFG->dataroot/lang/currentlang[_local]/filename
1156 * where "filename" is returned as a part of the file record.
1158 * @return array Array of a file information. Compatible format with {@link lang_extra_locations()}
1160 function lang_standard_locations() {
1163 // Standard location of master English string files.
1164 $places = array($CFG->dirroot.'/lang/en_utf8');
1165 foreach ($places as $place) {
1166 foreach (get_directory_list($place, '', false) as $file) {
1167 if ((substr($file, -4) == ".php") && ($file != "langconfig.php")) {
1168 $fullpath = $place.'/'.$file;
1169 $files[$fullpath] = array(
1170 'filename' => $file,
1182 * Finds all English string files in non-standard location.
1184 * Searches for lang/en_utf8/*.php in various types of plugins (blocks, database presets, question types,
1185 * 3rd party modules etc.) and returns an array of found files details.
1187 * The English version of the file may be found in
1188 * $CFG->dirroot/location/plugin/lang/en_utf8/filename
1189 * The localised version of the found file should be saved into
1190 * $CFG->dataroot/lang/currentlang[_local]/prefix_plugin.php
1191 * where "location", "plugin", "prefix" and "filename" are returned as a part of the file record.
1193 * @return array Array of a file information. Compatible format with {@link lang_standard_locations()}
1195 function lang_extra_locations() {
1198 $places = get_string_manager()->get_registered_plugin_types();
1199 foreach ($places as $prefix => $directories) {
1200 foreach ($directories as $directory) {
1201 foreach (get_list_of_plugins($directory) as $plugin) {
1202 $enlangdirlocation = $CFG->dirroot.'/'.$directory.'/'.$plugin.'/lang/en_utf8';
1203 foreach (get_directory_list($enlangdirlocation, '', false) as $file) {
1204 if ((substr($file, -4) == ".php") && ($file != "langconfig.php")) {
1205 $fullpath = $enlangdirlocation.'/'.$file;
1206 $files[$fullpath] = array(
1207 'filename' => $file,
1208 'location' => $directory,
1209 'plugin' => $plugin,
1210 'prefix' => $prefix,
1221 * Lookup for a stringfile details.
1223 * English files can be stored in several places (core space or module/plugin space). Their translations
1224 * go into the one directory - the current language pack. Therefore, the name of the stringfile may be
1225 * considered as a key of the list of all stringfiles.
1227 * @param string $currentfile the filename
1228 * @param array $stringfiles the array of file info returned by {@link lang_extra_locations()}
1229 * @return array Array of a file information (filename, location, plugin, prefix) or null.
1231 function lang_get_file_info($currentfile, $stringfiles) {
1233 foreach ($stringfiles as $path=>$stringfile) {
1234 if ($stringfile['filename'] == $currentfile) {
1237 $ret['fullpath'] = $path;
1249 * Returns all English help files in the standard lang/en_utf8/help location.
1251 * Core help files should always be stored here and not in the module space (MDL-10920).
1252 * The English version of the file may be found in
1253 * $CFG->dirroot/lang/en_utf8/help/filename
1254 * The localised version of the found file should be saved into
1255 * $CFG->dataroot/lang/currentlang[_local]/help/filename
1256 * where "filename" is returned as a part of the file record.
1258 * @return array Array of a file information. Compatible format with {@link lang_extra_locations()}
1260 function lang_help_standard_locations() {
1263 // Standard location of master English help files.
1264 $places = array($CFG->dirroot.'/lang/en_utf8/help');
1265 foreach ($places as $place) {
1266 foreach (get_directory_list($place, 'CVS') as $file) {
1267 if ((substr($file, -5) == '.html') || (substr($file, -4) == '.txt' )) {
1268 $fullpath = $place.'/'.$file;
1269 $files[$fullpath] = array(
1270 'filename' => $file,
1282 * Returns all English help files in non-standard location.
1284 * Searches for lang/en_utf8/help/* files in various types of plugins (blocks, database presets, question types,
1285 * 3rd party modules etc.) and returns an array of found files details.
1287 * The English version of the file may be found in
1288 * $CFG->dirroot/location/plugin/lang/en_utf8/help/filename
1289 * The localised version of the found file should be saved into
1290 * $CFG->dataroot/lang/currentlang[_local]/help/prefix_plugin/filename (XXX is "prefix" here right?)
1291 * where "location", "plugin", "prefix" and "filename" are returned as a part of the file record.
1293 * @return array Array of a file information. Compatible format with {@link lang_standard_locations()}
1295 function lang_help_extra_locations() {
1298 $places = get_string_manager()->get_registered_plugin_types();
1299 foreach ($places as $prefix => $directories) {
1300 foreach ($directories as $directory) {
1301 foreach (get_list_of_plugins($directory) as $plugin) {
1302 $enlangdirlocation = $CFG->dirroot.'/'.$directory.'/'.$plugin.'/lang/en_utf8/help';
1303 foreach (get_directory_list($enlangdirlocation, 'CVS') as $file) {
1304 if ((substr($file, -5) == '.html') || (substr($file, -4) == '.txt' )) {
1305 $fullpath = $enlangdirlocation.'/'.$file;
1306 $files[$fullpath] = array(
1307 'filename' => $file,
1308 'location' => $directory,
1309 'plugin' => $plugin,
1310 'prefix' => $prefix,
1321 * Return a preview URL for help file, if available.
1323 * @param string $currentfile The relative path to the help file, e.g. "assignment/types.html" - MDL-12291
1324 * @param bool $skiplocal Force displaying the helpfile from a master lang pack
1325 * @param string $forcelang Force language of the help, e.g. "en_utf8"
1326 * @return string $url
1328 function lang_help_preview_url($currentfile, $skiplocal=false, $forcelang = '') {
1329 $currentpathexp = explode('/', $currentfile);
1330 if (count($currentpathexp) > 1) {
1331 $url = '/help.php?module='.implode('/',array_slice($currentpathexp,0,count($currentpathexp)-1)).'&file='.end($currentpathexp);
1333 $url = '/help.php?module=moodle&file='.$currentfile;
1336 $url .= '&skiplocal=1';
1339 $url .= '&forcelang='.$forcelang;
1346 * Saves (overwrites) translated help file.
1348 * @param string $helproot The path to the "help" folder
1349 * @param string $file The relative path to the html help file
1350 * @param string $content HTML data to be saved
1351 * @return bool False if save failed, true otherwise
1353 function lang_help_save_file($helproot, $file, $content) {
1354 global $CFG, $USER, $OUTPUT;
1356 $content = str_replace("\r", "",$content); // Remove linefeed characters
1357 $content = preg_replace("/\n{3,}/", "\n\n", $content); // Collapse runs of blank lines
1358 $content = trim($content); // Delete leading/trailing whitespace
1359 if (is_readable("$helproot/$file") && filesize("$helproot/$file") > 0 && $content == '') {
1360 echo $OUTPUT->notification(get_string('langrmyourself', 'admin'));
1365 if (!$f = fopen("$helproot/$file","w")) {
1366 error_reporting($CFG->debug);
1369 error_reporting($CFG->debug);
1371 fwrite($f, $content);
1374 // Remove file if its empty
1375 if (filesize("$helproot/$file") == 0) {
1376 unlink("$helproot/$file");