Force language to subset

Force language to subset

by Aaron Johnson -
Number of replies: 13

Hi,

On my site (Moodle 3.6.5), I have several courses with content in multiple languages. I share content between courses, so as I am developing new courses, some of the content is already translated. I find it unprofessional to have half of a course translated, so I force the language on new courses to English. Once the translated material is in place, then I can set "Force language" to "Do not force" and all languages are visible.

My problem is what to do when the material has only been translated into a few of those languages, but not all. What I would like to be able to do is select a subset of the languages available to the site to be available to my course. The current setup for "Force language" allows for all (with "Do not force") or one. How could I get somewhere in between? Could I hack the language selection menu for a course to be different than for the site?

Thanks,
Aaron

Average of ratings: -
In reply to Aaron Johnson

Re: Force language to subset

by Michael Milette -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers Picture of Translators
Hi Aaron,

The following is all theoretical. I have never tried it and it will require some programming.

First you would need to upgrade to Moodle 3.7. It introduced the concept of course custom fields. Then create a course field for available-languages.

Next, modify your theme to check that list of available languages and force the language to the site default language if the current language is not in the list of available languages.

If you would rather do it with a separate plugin, developing a conditional availability type plugin might be worth considering. The Restriction by Language plugin might be a good place to start:
https://moodle.org/plugins/availability_language

Another option might be to create a slightly modified custom course format however, unless you have other reasons to do this, I would try to avoid this as you would then need to maintain it and would be limited to use only the course formats you customized. Then again, you could always contribute such a modification back to the moodle core code in hopes that it would be integrated into future releases of Moodle.

Hope you find something in all of this useful. Let us know what you ended up doing and share your final code if possible. I am sure there are others with multilingual sites in a similar situation who would be grateful.

Best regards,

Michael
Average of ratings: Useful (3)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -

Thanks, Michael.

I'll probably try your first suggestion. I think the course custom fields could be valuable for me for other reasons too. We tried upgrading our site to 3.7 when it came out, but we ran into some trouble. Looks like I have another reason to motivate my IT guys to try it again.

I'll post back here once I've made some progress, but it will likely take a bit.

Aaron

In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Hi Michael,

I'm finally coming back to this issue. I've got a test installation of 3.8.2 running on my local machine and I've created a child theme to Boost. I have a course custom field and am ready to try to override the default language list, but am having the hardest time finding where that is rendered. Could you perhaps point me to the renderer or other component that controls the language list which I need to overwrite in my theme? I spent some time searching, but I'm stumped.

Thanks,
Aaron
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Hi Michael,

I've made good progress but am getting stumped again. I created a theme based on Boost (https://github.com/amjohnson46/moodle-theme_elemacad). In /classes, I have my core_renderer.php file to override core_renderer.php. There are three functions in core_renderer that seem relevant here: lang_menu, custom_menu_flat, and render_custom_menu. They all call out to get_list_of_translations, which seems to be in lib/classes/string_manager_standard.php. Playing around, it seems the deciding factor in what goes into the language menu is how the $langs array is defined in render_custom_menu. If I do this hack:

        // $langs = get_string_manager()->get_list_of_translations();
        $langs = [
            "en" => "English render_custom_menu",
            "es" => "Spanish",
            "fr" => "French",
            "it" => "Italian",
            "xx" => "Nothing",
        ];

Then regardless of what is in the setting "Site administration > Language > Language settings > Languages on language menu", the language menu is the list hard-coded above. Switching languages and all other current functionality remains, for example:
  • If I force the language on a course, the language menu disappears.
  • If I select a language that is not installed/active, then it doesn't change the setting.
What I don't have working yet:
  1. Populating the $langs array from the custom course field.
  2. Reverting to the default language if I change the language in the address bar to an installed/active language not in the menu list (e.g. ...lang=de should revert to English, but it doesn't).
Point 2 is not as important initially (if a student wants to hack their way into a language, then they should not be surprised when it is not all translated).

To point 1, can you help me figure out how to access the custom course field? I followed the example on the Custom fields API page (https://docs.moodle.org/dev/Custom_fields_API#Example_code_for_course_custom_fields), but couldn't get the call to get_course_metadata to work.

A really clunky workaround would be to create child themes of my custom theme for each language combination and then set the theme on the course level, but using the custom course field would be more elegant and sustainable.

Thanks,
Aaron
In reply to Aaron Johnson

Re: Force language to subset

by Michael Milette -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers Picture of Translators

If I understand you correctly, Moodle can do most of the work for you without requiring any custom coding at all. Most of what you want to do is build in.

Configure the Moodle Language Menu

The first part here describes the process for enabling your Moodle site for multi-language support.

  1. As a Moodle Administrator, install the desired language packs that you want your Moodle site to support. You can do this by going to Site Administration > Language > Language Packs and installing the desired languages. Take note of the ISO codes for each language. You will need this in step 6.
  2. Go to Site Administration > Language > Language settings.
  3. Enable Language Autodetect. That way, if the user is using one of your supported languages, it will default to that language.
  4. Set the Default Language. This is the language that will be used is if the user's browser is in an unsupported language.
  5. Enable Display Language Menu. This will make the language menu appear in Boost.
  6. Set Languages on language menu to "en,es,fr,it". If you have a dialect, such as fr_ca for French Canadian, include that one instead of "fr", even though the French language pack is installed. e.g. "en,es,fr_ca,it"
  7. Save your changes.

That's all there is to it. Your language menu should now appear and work correctly.


IMPORTANT: There is no such thing "No language" or "Nothing" in Moodle. Moodle must have a language and it can only be one of the languages installed. So doing a xx = "Nothing" as you indicated will never work. However, switching to a language that does not exist in your Moodle site will cause Moodle to switch to the default language. You can try this from your dashboard by adding ?lang=xx to the URL.

In your message, I also noticed that all of your language names are in English. This is not a good idea. If the person only speaks Spanish and not English, they will not be able to understand your language menu in order to make the desired selection. If you really want to change the names of the languages, you can do this by going to Site Administration > Language > Language Customization. For more information on how to customize language strings in Moodle, see https://docs.moodle.org/en/Language_customisation

In my case, I have English and French Canadian installed so Moodle/Boost renders the language selection menu as:

Customizing the Language Selection Menu

Now, let's say that you don't particularly like the way Moodle is displaying the languages, e.g. English (en). This is the only time you will need to change some PHP in order to customize the Language selection menu.

The following describes how you can customize the names of the languages and remove the ISO language code.

A word of caution: It is never a good idea to customize the Boost theme. Why? Because the next time you update Moodle, any customizations you made to core Moodle code, including the Boost theme, will be overwritten and lost. I highly recommend that you clone the Boost theme first and then make your changes to that theme - if you have not already done so of course.

As you already figured out, the file to customize is indeed the classes/output/core_renderer.php. In this file, near the top, you will find a line that says use moodle_url; . Insert the following line right below it:

use custom_menu;

Next, right below the class core_renderer extends \core_renderer { line, add the following code:

    /* Language menu customization by Michael Milette - May 3, 2020
     * Trim unwanted parts of the language name.
     */
    private function formatlangname($langname) {
        // Remove the ISO language code between parentheses. e.g. English (en).
        if (strpos($langname, '(')) {
            $langname = substr($langname, 0, strpos($langname, '(') - 4);
        }
        // Remove the dialect part of the language (e.g. Français - Canada ‎(fr_ca)‎).
        if (strpos($langname, ' - ')) {
            $langname = substr($langname, 0, strpos($langname, ' - '));
        }
        return trim($langname);
    }
    /**
     * We want to show the custom menus as a list of links in the footer on small screens.
     * Just return the menu object exported so we can render it differently.
     */
    public function custom_menu_flat() {
        global $CFG;
        $custommenuitems = '';
        if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
            $custommenuitems = $CFG->custommenuitems;
        }
        $custommenu = new custom_menu($custommenuitems, current_language());
        $langs = get_string_manager()->get_list_of_translations();
        $haslangmenu = $this->lang_menu() != '';
        if ($haslangmenu) {
            $strlang = get_string('language');
            $currentlang = current_language();
            if (isset($langs[$currentlang])) {
                $currentlang = $langs[$currentlang];
            } else {
                $currentlang = $strlang;
            }
            $currentlang = $this->formatlangname($currentlang);
            $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
            foreach ($langs as $langtype => $langname) {
                $langname = $this->formatlangname($langname);
                $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
            }
        }
        return $custommenu->export_for_template($this);
    }
    /**
     * Renders a custom menu object (located in outputcomponents.php)
     *
     * The custom menu this method produces makes use of the YUI3 menunav widget
     * and requires very specific html elements and classes.
     *
     * @staticvar int $menucount
     * @param custom_menu $menu
     * @return string
     */
    protected function render_custom_menu(custom_menu $menu) {
        global $CFG;
        $langs = get_string_manager()->get_list_of_translations();
        $haslangmenu = $this->lang_menu() != '';
        if (!$menu->has_children() && !$haslangmenu) {
            return '';
        }
        if ($haslangmenu) {
            $strlang = get_string('language');
            $currentlang = current_language();
            if (isset($langs[$currentlang])) {
                $currentlang = $langs[$currentlang];
            } else {
                $currentlang = $strlang;
            }
            $currentlang = $this->formatlangname($currentlang);
            $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000);
            foreach ($langs as $langtype => $langname) {
                $langname = $this->formatlangname($langname);
                $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
            }
        }
        $content = '';
        foreach ($menu->get_children() as $item) {
            $context = $item->export_for_template($this);
            $content .= $this->render_from_template('core/custom_menu_item', $context);
        }
        return $content;
    }

Explanation of related functions;

  • lang_menu() function - Is currently only be used to detect whether a language menu exists. It is possible that some themes may use what comes out of it  but, from what I could see, Moodle core uses this as if it was a function called "haslangmenu". This is why it is not customized here.
  • custom_menu_flat() function - This is the function that generates the language menu on narrow screens when the custom menu ends up being in the footer of the page.
  • render_custom_menu() function - This is the function that generates the language menu at the end of the custom menu when viewed on a wider desktop screen.

If you are going to customize the language menu, be sure to apply any changes to both custom_menu_flat and render_custom_menu.

The above code is almost identical to the code found in the /lib/outputrenderer.php with the following modifications:

  1. Added the formatlangname function. As coded above, this function makes some formatting changes to the names of the languages which will be displayed.
  2. Added the 4 lines containing the word formatlangname.

The result? A beautiful, clean dropdown language menu like the ones you would see on other websites.

Of course, once you have it working, you can always customize the formatlangname function to apply any custom formatting that you might like. For example, some people like to include images of a country flags along with or instead of the language names while others might prefer to simply have a menu made up of 2 character language codes. My recommendation would be to keep accessibility in mind when doing such changes.

Other options

There is an other simple option: Place a list of languages within the General section of your courses and only including the available languages for that course? Example:

English | Français | ...

Best part: No programming involved. These can just be links but, add some Bootstrap classes to the links, and you can easily turn the links into a list of buttons. I would omit the current language from the list. This can easily be accomplished using the Multi-Language Content (v2) {mlang} tags.

This is probably better than trying to customize the language selection menu for every course.

Default Language

I don't know if you have ever tried switching language to one that does not exist in Moodle. It is safe and will simply switch you to the default language. By the way, there is no way in Moodle to 

Course Custom Profile Fields\

With regards to accessing the course custom fields, as an example, take a look at the source code in FilterCodes' filter.php, specifically at the tag that retrieves custom course fields.

Hope you find this useful. I hope to see you at MoodleMoot Global 2020 Online next month!

Best regards,

Michael Milette

Average of ratings: Useful (2)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -

Hi Michael,

Thanks for the thorough, complete answer. Thanks for the polishing tips on my code. I was still at the proof-of-concept stage, so those points will help get it to a true final product. Your formatlangname function is simple and effective and I was able to implement it no problem. Thanks!

I'm intrigued by your link list idea under "Other options". With that, would you disable the language menu, and then just put in a row of links/buttons that did the same thing? So the link for Deutsch would be:
"https://my.site.com/course/view.php?id=###&lang=de"? Is there a way to automate getting the ID number so that I could copy the link between courses?

I don't think you finished your thought under "Default Language". The last sentence is incomplete.

Thanks for the custom course fields reference. I should be able to work with that.

And thanks for the head-up about MoodleMoot Global 2020 Online. I signed up and am looking forward to it.

Thanks again,
Aaron Johnson

In reply to Aaron Johnson

Re: Force language to subset

by Michael Milette -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers Picture of Translators
Hi Aaron, sorry for the delay in getting back to you. Been pretty busy with work recently.

Other options: That is correct. You would disable the language menu. However, you could gain some flexibility here by leaving the language menu enabled and forcing the language of the course. That way the UI language would always match the language of the course. If you are in the English course, Moodle would also be in English. If you were in the German version of the course, Moodle would be in German. When you are not in a course, you would still be able to switch Moodle's language using the language selection menu.

Automating the ID number: There really isn't any way to automate this since there is no way for one course to know what ID number it's corresponding course in the other language might be, You could create a custom course field and insert the alternate course number in that field. Then, in the course, you could use the {course_field_fieldshortname} tag from FilterCodes. Example: {course_field_de} where the custom course field "de" would contain the ID number of the German course name. If you want to make it even simpler, put all of the HTML for the list of language links in the custom course field. That way, all you need to tell course creators will be insert is the specific tag.

Notes of caution when using the link approach:

If someone starts a course in one language and then switches to another, their completion will not follow them. Moodle will see the two courses as completely independent having no relationship between them. They may also need to re-enrol in the alternate version of the course. If activity completion tracking is not important, this will not be an issue for you.

The links will always point the user to the outline page of the course. So they won't be able to switch between languages within a course activity.

Default Language: You are right. I think what I wanted to say was that, out of the box, you can't filter the course listings and course search results to only show courses in the current language. You could, however, accomplish this by making customizations to the Moodle theme.

Hope to bump into you at MoodleMoot Global online 2020!

Best regards,

Michael
Average of ratings: Useful (1)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Thanks, Michael.
I've been trying to avoid having separate courses for each language. Completion tracking is important for my site and I didn't want the extra maintenance that would require. Plus I want the additional languages to be available to participants as they come online. We write everything in English first, but I try to motivate them to purchase the trainings with the promise that the version in their language will be available to them once completed. Thanks for your help. I should be able to make some improvements based on that and the examples from FilterCodes.
Best regards,
Aaron
In reply to Aaron Johnson

Re: Force language to subset

by Michael Milette -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers Picture of Translators
Hi Aaron,

You are welcome. The only problem that you might encounter with starting with an English only course first will be updating some of the activities. For example, Moodle does not allow you to edit a Quiz once there has been at least once attempt made. That is why Quizzes have a "preview" option so that you, as a course creator, can try it out without triggering an attempt. This makes sense of course since it would not be fair to past students if a quiz changed after they completed it. Just a friendly heads-up.

Best regards,

Michael
Average of ratings: Useful (1)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Thanks again, Michael.
I get around that by using my text filters for all text in the site. When it comes time to add a new language, I just have to update the filter with a new lang file. So I actually don't change any quiz questions or other activities, just the filters and add new content PDFs. It makes for a bit of work when adding the first additional language, but after that it is a snap. It isn't really that much extra work, because if I were using one of the multi-lang filters, I'd have to go into all the content and add the extra text there anyway. This way is actually less work for the second language, and way less work after that. It does require me maintaining a couple text filters, but I think that actually makes managing the translation work easier.
Thanks again,
Aaron
Average of ratings: Useful (1)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Hi Michael,

I tried implementing the FilterCodes plugin to retrieve custom course fields, but it doesn't seem to be working. I tried several other tags, and they all worked, but the {course_fields} and {course_fieldshortname} tags did not. I attached the HTML input for a label (first pic) and then what is displayed (second pic). Any ideas?

Thanks,
Aaron
Attachment Input.JPG
Attachment Output.JPG
In reply to Aaron Johnson

Re: Force language to subset

by Michael Milette -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Plugin developers Picture of Testers Picture of Translators

Hi Aaron,

Thank you for your question. The course field tags are a new for the upcoming release of Filtercodes which will be released this week (in a few days) on Moodle.org. If you want to try it before, feel free to use the version of FilterCodes on Github.

Also, what is the shortname of the custom course field that you are trying to access?

Best regards,

Michael

Average of ratings: Useful (1)
In reply to Michael Milette

Re: Force language to subset

by Aaron Johnson -
Hi Michael,

That was it. After installing the update everything works great, and I was able to get my plugin working too! I decided the easiest way to do it was pull the normal list using get_list_of_translations and then remove anything that is not in the custom course field 'courselanglist', which is a comma separated list of ISO lang codes. Interestingly, after implementing this, your formatlangname function had to be adjusted to remove the "-4" when removing the ISO code in parentheses. Not sure why. I'm also guessing there is a way to capture my course field without needing to cycle through them all, but I couldn't figure it out. (I should probably sit down and really learn PHP instead of this hacky approach I'm taking. I keep hoping that I'm done programming, but then keep finding new things to do. ;) )

The last little annoyance I have is that when switching to a course from a language that is not allowed in the course. For example, if I'm on the dashboard in French and go to a course that only has English and German, the course content is displayed in English as it should, but all the structure around it is still in French. See the screenshot below. It stays that way until selecting a language from the language menu. So apparently "$this->page->course->lang = "en";" isn't doing all I had hoped it would. Any ideas on how to set the language for the whole page to a default?

Thanks,
Aaron

class theme_elemacad_core_renderer extends core_renderer {

    /* Language menu customization by Michael Milette - May 3, 2020
     * Trim unwanted parts of the language name.
     */
    private function formatlangname($langname) {
        // Remove the ISO language code between parentheses. e.g. English (en).
        if (strpos($langname, '(')) {
            // $langname = substr($langname, 0, strpos($langname, '(') - 4);
            $langname = substr($langname, 0, strpos($langname, '('));
        }
        // Remove the dialect part of the language (e.g. Français - Canada (fr_ca)).
        if (strpos($langname, ' - ')) {
            $langname = substr($langname, 0, strpos($langname, ' - '));
        }
        return trim($langname);
    }
   
   
    /**
     * We want to show the custom menus as a list of links in the footer on small screens.
     * Just return the menu object exported so we can render it differently.
     */
    public function custom_menu_flat() {
        global $CFG;
        $custommenuitems = '';
        if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
            $custommenuitems = $CFG->custommenuitems;
        }
        $custommenu = new custom_menu($custommenuitems, current_language());
        // Set the language list using custom function.
        $langs = $this->get_course_langlist();
        if (empty($langs)){
            // Use the default function
            $langs = get_string_manager()->get_list_of_translations();
        }
        $haslangmenu = $this->lang_menu() != '';
        if ($haslangmenu) {
            $strlang = get_string('language');
            $currentlang = current_language();
            if (isset($langs[$currentlang])) {
                $currentlang = $langs[$currentlang];
            } else {
                $currentlang = $strlang;
                $this->page->course->lang = "en";
            }
            $currentlang = $this->formatlangname($currentlang);
            $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
            foreach ($langs as $langtype => $langname) {
                $langname = $this->formatlangname($langname);
                $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
            }
        }
        return $custommenu->export_for_template($this);
    }

    /**
     * Renders a custom menu object (located in outputcomponents.php)
     *
     * The custom menu this method produces makes use of the YUI3 menunav widget
     * and requires very specific html elements and classes.
     *
     * @staticvar int $menucount
     * @param custom_menu $menu
     * @return string
     */
    protected function render_custom_menu(custom_menu $menu) {
        global $CFG;
        // Set the language list using custom function.
        $langs = $this->get_course_langlist();
        if (empty($langs)){
            // Use the default function
            $langs = get_string_manager()->get_list_of_translations();
        }
        $haslangmenu = $this->lang_menu() != '';
        if (!$menu->has_children() && !$haslangmenu) {
            return '';
        }
        if ($haslangmenu) {
            $strlang = get_string('language');
            $currentlang = current_language();
            if (isset($langs[$currentlang])) {
                $currentlang = $langs[$currentlang];
            } else {
                $currentlang = $strlang;
                $this->page->course->lang = "en";
            }
            $currentlang = $this->formatlangname($currentlang);
            $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000);
            foreach ($langs as $langtype => $langname) {
                $langname = $this->formatlangname($langname);
                $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
            }
        }
        $content = '';
        foreach ($menu->get_children() as $item) {
            $context = $item->export_for_template($this);
            $content .= $this->render_from_template('core/custom_menu_item', $context);
        }
        return $content;
    }

   

    private function get_course_langlist() {
        global $CFG, $PAGE;
       
        // Get site lang list.
        $site_langs = get_string_manager()->get_list_of_translations();

        // Get the custom course field with lang menu list (courselanglist). Modified from filter_filtercodes
        static $coursefields;
        $course_langs = [];
        if (!isset($coursefields)) {
            $handler = core_course\customfield\course_handler::create();
            $coursefields = $handler->export_instance_data_object($PAGE->course->id, true);
         }
        foreach ($coursefields as $field => $value) {
            $shortname = strtolower($field);
            // Just get courselanglist
            if ($shortname === 'courselanglist') {
                $courselangliststring = $value;
            }
        }
       
        // Remove all language options from site lang list that aren't in course lang list.
        if(empty($courselangliststring)){
            return $site_langs;
        } else {
            $course_langs = $site_langs;
            foreach ($course_langs as $field => $value) {
                if (strpos($courselangliststring, $field) === false) {
                    unset($course_langs[$field]);
                }
            }
        }
       
        return $course_langs;
    }
}

Attachment Capture.JPG