[{"data":1,"prerenderedAt":2196},["ShallowReactive",2],{"post-en-moodle-opencc-traditional-chinese":3,"surround-en-blog\u002Fen\u002Fblog\u002Fmoodle-opencc-traditional-chinese.md":2189},{"id":4,"title":5,"body":6,"canonical":307,"category":2172,"date":2173,"description":2174,"extension":2175,"image":2176,"meta":2177,"navigation":771,"path":2178,"seo":2179,"slug":2180,"status":2181,"stem":2182,"tags":2183,"translationSlug":2180,"__hash__":2188},"blog\u002Fen\u002Fblog\u002Fmoodle-opencc-traditional-chinese.md","Zero-DB Traditional Chinese Conversion for Moodle with OpenCC (and How to Bypass RequireJS Pitfalls)",{"type":7,"value":8,"toc":2148},"minimark",[9,14,18,21,24,70,89,92,96,99,104,110,113,123,134,159,197,206,210,213,281,283,290,300,310,318,325,364,393,401,414,417,423,440,452,470,473,482,517,589,592,594,598,602,625,629,657,1915,1944,1948,1951,2080,2098,2100,2104,2113,2144],[10,11,13],"h2",{"id":12},"_1-motivation-and-bluf-bottom-line-up-front","1. Motivation and BLUF (Bottom Line Up Front)",[15,16,17],"p",{},"If you have built a Simplified Chinese AI course on Moodle, don't want to leave out Traditional Chinese learners, and don't want the burden of duplicating and maintaining a separate Traditional Chinese course, give this approach a try.",[15,19,20],{},"The constraints are clear: do not alter the existing Simplified Chinese database, do not duplicate courses and question banks into two separate branches to maintain, and avoid rebuilding Docker containers or maintaining custom PHP backend filter plugins—while still allowing learners who switch to Traditional Chinese to see a fully localized interface, course text, and quiz questions.",[15,22,23],{},"Today I verified the entire workflow in a live Moodle environment. The working solution consists of two layers:",[25,26,27,40],"ol",{},[28,29,30,34,35,39],"li",{},[31,32,33],"strong",{},"System UI Layer",": Install Moodle's official ",[36,37,38],"code",{},"zh_tw"," (Traditional Chinese) language pack in Site Administration to handle navigation bars, buttons, and system prompts.",[28,41,42,45,46,49,50,53,54,57,58,61,62,65,66,69],{},[31,43,44],{},"Course & Question Bank Content Layer",": Use Moodle's built-in ",[31,47,48],{},"Additional HTML"," setting to inject a frontend script powered by ",[36,51,52],{},"opencc-js"," (",[36,55,56],{},"cn -> twp",") right before the closing ",[36,59,60],{},"\u003C\u002Fbody>"," tag. This converts Simplified Chinese DOM text and regional vocabulary (e.g., ",[36,63,64],{},"文件 → 檔案",", ",[36,67,68],{},"视频 → 影片",") into Taiwan Traditional Chinese on the fly.",[15,71,72,73,76,77,80,81,88],{},"What looked like a one-line ",[36,74,75],{},"\u003Cscript>"," tag injection turned into two unexpected debugging hurdles on real Moodle pages: ",[31,78,79],{},"Moodle's built-in RequireJS hijacking UMD modules"," and ",[31,82,83,84,87],{},"misleading ",[36,85,86],{},"\u003Chtml lang>"," attributes",". Below is the underlying mechanism, the debugging journey, and the copy-pasteable production snippet.",[90,91],"hr",{},[10,93,95],{"id":94},"_2-core-mechanics-why-naive-approaches-fail","2. Core Mechanics: Why Naive Approaches Fail",[15,97,98],{},"Before writing the snippet, three architectural boundaries in Moodle need to be clarified:",[100,101,103],"h3",{"id":102},"_21-official-language-packs-do-not-touch-course-content-in-the-database","2.1 Official Language Packs Do Not Touch Course Content in the Database",[15,105,106,107,109],{},"Moodle's official ",[36,108,38],{}," language pack is comprehensive, but it only translates static UI strings defined in Moodle's source code (such as turning \"我的主页\" into \"我的主頁\" in the top bar). Course titles, section introductions, page resources, and quiz stems stored in the database are completely untouched by language packs.",[15,111,112],{},"Writing a backend PHP Filter plugin would require compiling C++ dependencies and PHP extensions inside the server container (since there is no official OpenCC plugin in the Moodle plugins directory) and spending server CPU cycles on dictionary matching for every request. Offloading conversion to the browser incurs zero server load and can be rolled back anytime by deleting the snippet.",[100,114,116,117,119,120],{"id":115},"_22-detecting-simplified-vs-traditional-chinese-ignore-html-lang-check-bodyclasslist","2.2 Detecting Simplified vs. Traditional Chinese: Ignore ",[36,118,86],{},", Check ",[36,121,122],{},"body.classList",[15,124,125,126,129,130,133],{},"Intuitively, a frontend script checking whether the current page is in Traditional Chinese would inspect ",[36,127,128],{},"document.documentElement.lang"," (the ",[36,131,132],{},"\u003Chtml lang=\"...\">"," attribute).",[15,135,136,137,140,141,65,144,146,147,150,151,154,155,158],{},"However, testing on a live Moodle instance revealed that whether you visit ",[36,138,139],{},"?lang=zh_cn"," or ",[36,142,143],{},"?lang=zh_tw",[36,145,128],{}," always returns ",[36,148,149],{},"\"zh\"",", making it impossible to distinguish Simplified from Traditional Chinese. The place where Moodle reliably exposes the active language pack is the ",[36,152,153],{},"\u003Cbody>"," element's ",[36,156,157],{},"class"," list:",[160,161,162,176,186],"ul",{},[28,163,164,165,168,169,172,173],{},"Simplified Chinese (",[36,166,167],{},"zh_cn","): ",[36,170,171],{},"document.body.className"," contains ",[36,174,175],{},"lang-zh_cn",[28,177,178,179,168,181,172,183],{},"Taiwan Traditional Chinese (",[36,180,38],{},[36,182,171],{},[36,184,185],{},"lang-zh_tw",[28,187,188,189,168,192,172,194],{},"Hong Kong Traditional Chinese (",[36,190,191],{},"zh_hk",[36,193,171],{},[36,195,196],{},"lang-zh_hk",[15,198,199,200,140,203,205],{},"Checking ",[36,201,202],{},"document.body.classList.contains('lang-zh_tw')",[36,204,196],{}," right at the start of the script ensures that Simplified Chinese learners exit immediately without downloading any conversion dictionaries.",[100,207,209],{"id":208},"_23-does-frontend-text-conversion-break-quiz-auto-grading","2.3 Does Frontend Text Conversion Break Quiz Auto-Grading?",[15,211,212],{},"It depends on how each Moodle question type submits answers:",[160,214,215,243,256],{},[28,216,217,232,233,65,236,65,239,242],{},[31,218,219,220,223,224,227,228,231],{},"Multiple choice (",[36,221,222],{},"multichoice","), True\u002FFalse (",[36,225,226],{},"truefalse","), and Matching (",[36,229,230],{},"matching",")",": Completely unaffected. The script only alters the displayed text nodes; when a learner submits the quiz, the browser sends option IDs (such as ",[36,234,235],{},"1",[36,237,238],{},"2",[36,240,241],{},"3",") to the backend.",[28,244,245,251,252,255],{},[31,246,247,248,231],{},"Select missing words (",[36,249,250],{},"gapselect",": Despite sounding like a fill-in-the-blank question, inspecting Moodle's question bank XML shows that it renders as ",[36,253,254],{},"\u003Cselect>"," dropdown menus on the page. Submission sends the selected option index, so grading is 100% unaffected.",[28,257,258,272,273,276,277,280],{},[31,259,260,261,264,265,268,269,231],{},"Short answer (",[36,262,263],{},"shortanswer",") and embedded ",[36,266,267],{},"SHORTANSWER"," Cloze (",[36,270,271],{},"cloze",": These require learners to type text into an ",[36,274,275],{},"\u003Cinput>"," box. If the backend answer key only stores Simplified Chinese, submitting Traditional Chinese characters directly would be marked wrong. To handle this, we attach a capture-phase ",[36,278,279],{},"submit"," listener that converts text input values back to Simplified Chinese before the form is sent to the server.",[90,282],{},[10,284,286,287,289],{"id":285},"_3-debugging-log-opencc-js-hijacked-by-requirejs","3. Debugging Log: ",[36,288,52],{}," Hijacked by RequireJS",[15,291,292,293,295,296,299],{},"With the design settled, my first iteration dynamically appended a ",[36,294,75],{}," tag loading ",[36,297,298],{},"opencc-js@1.0.5\u002Fdist\u002Fumd\u002Ffull.js"," from jsDelivr CDN. The browser console immediately threw:",[301,302,308],"pre",{"className":303,"code":305,"language":306,"meta":307},[304],"language-text","Uncaught ReferenceError: OpenCC is not defined\n","text","",[36,309,305],{"__ignoreMap":307},[100,311,313,314,317],{"id":312},"round-1-why-wasnt-windowopencc-defined","Round 1: Why Wasn't ",[36,315,316],{},"window.OpenCC"," Defined?",[15,319,320,321,324],{},"Inspecting the header of ",[36,322,323],{},"opencc-js\u002Fdist\u002Fumd\u002Ffull.js"," shows a standard UMD wrapper:",[301,326,330],{"className":327,"code":328,"language":329,"meta":307,"style":307},"language-javascript shiki shiki-themes github-dark","(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OpenCC = {}));\n})(this, (function (exports) { 'use strict'; ... }));\n","javascript",[36,331,332,340,346,352,358],{"__ignoreMap":307},[333,334,337],"span",{"class":335,"line":336},"line",1,[333,338,339],{},"(function (global, factory) {\n",[333,341,343],{"class":335,"line":342},2,[333,344,345],{},"  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n",[333,347,349],{"class":335,"line":348},3,[333,350,351],{},"  typeof define === 'function' && define.amd ? define(['exports'], factory) :\n",[333,353,355],{"class":335,"line":354},4,[333,356,357],{},"  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.OpenCC = {}));\n",[333,359,361],{"class":335,"line":360},5,[333,362,363],{},"})(this, (function (exports) { 'use strict'; ... }));\n",[15,365,199,366,369,370,373,374,377,378,381,382,385,386,389,390,392],{},[36,367,368],{},"typeof window.define"," in the Moodle console returned ",[36,371,372],{},"\"function\""," with ",[36,375,376],{},"window.define.amd"," truthy—",[31,379,380],{},"Moodle globally mounts RequireJS",". When the UMD wrapper sees ",[36,383,384],{},"define.amd",", it takes the second branch (",[36,387,388],{},"define(['exports'], factory)",") and registers itself as an anonymous AMD module instead of falling through to the third branch that assigns ",[36,391,316],{},".",[100,394,396,397,400],{"id":395},"round-2-why-temporarily-nulling-out-windowdefine-crashed-on-real-pages","Round 2: Why Temporarily Nulling Out ",[36,398,399],{},"window.define"," Crashed on Real Pages",[15,402,403,404,407,408,410,411,392],{},"A common workaround for RequireJS hijacking is setting ",[36,405,406],{},"window.define = undefined"," before inserting the ",[36,409,75],{}," tag and restoring it inside ",[36,412,413],{},"script.onload",[15,415,416],{},"While this worked in isolated console tests, saving it into Moodle's Additional HTML and refreshing an actual course page left the course titles in Simplified Chinese and triggered an even worse error in the console:",[301,418,421],{"className":419,"code":420,"language":306,"meta":307},[304],"Uncaught TypeError: Cannot read properties of undefined (reading 'Converter')\nUncaught Error: Mismatched anonymous define() module: function (exports) { 'use strict'; ...\n",[36,422,420],{"__ignoreMap":307},[15,424,425,426,429,430,432,433,435,436,439],{},"The reason is that ",[31,427,428],{},"during Moodle page initialization, RequireJS is actively loading dozens of core Moodle AMD modules asynchronously",". Nulling out ",[36,431,399],{}," during the several hundred milliseconds while ",[36,434,75],{}," downloads over the network—or restoring it while scripts interleave—either breaks Moodle's own module loader or causes RequireJS to catch an unmatched anonymous ",[36,437,438],{},"define()"," call.",[100,441,443,444,447,448,451],{"id":442},"final-solution-fetch-new-function-to-force-the-commonjs-branch","Final Solution: ",[36,445,446],{},"fetch"," + ",[36,449,450],{},"new Function"," to Force the CommonJS Branch",[15,453,454,455,457,458,460,461,463,464,467,468,392],{},"To export ",[36,456,52],{}," cleanly without touching ",[36,459,399],{}," at all, we can bypass the ",[36,462,75],{}," tag entirely: fetch the UMD source code as plain text via ",[36,465,466],{},"fetch()",", and execute it inside an isolated local scope using ",[36,469,450],{},[15,471,472],{},"Look closely at the first branch of the UMD header:",[301,474,476],{"className":327,"code":475,"language":329,"meta":307,"style":307},"typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : ...\n",[36,477,478],{"__ignoreMap":307},[333,479,480],{"class":335,"line":336},[333,481,475],{},[15,483,484,485,80,488,491,492,494,495,497,498,501,502,505,506,508,509,512,513,516],{},"It checks both ",[36,486,487],{},"exports",[36,489,490],{},"module"," (passing only ",[36,493,487],{}," without ",[36,496,490],{}," still falls through and causes ",[36,499,500],{},"exports.Converter is not a function","). By declaring ",[36,503,504],{},"'module', 'exports', 'define'"," as parameters to ",[36,507,450],{}," and passing ",[36,510,511],{},"undefined"," for ",[36,514,515],{},"define",", we deterministically force the UMD bundle into the first (CommonJS) branch:",[301,518,520],{"className":327,"code":519,"language":329,"meta":307,"style":307},"function loadOpenCC() {\n    if (window._OpenCC) return Promise.resolve(window._OpenCC);\n    return fetch('https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fopencc-js@1.0.5\u002Fdist\u002Fumd\u002Ffull.js')\n        .then(function(res) { return res.text(); })\n        .then(function(code) {\n            var module = { exports: {} };\n            var fn = new Function('module', 'exports', 'define', code);\n            fn(module, module.exports, undefined);\n            window._OpenCC = module.exports;\n            return window._OpenCC;\n        });\n}\n",[36,521,522,527,532,537,542,547,553,559,565,571,577,583],{"__ignoreMap":307},[333,523,524],{"class":335,"line":336},[333,525,526],{},"function loadOpenCC() {\n",[333,528,529],{"class":335,"line":342},[333,530,531],{},"    if (window._OpenCC) return Promise.resolve(window._OpenCC);\n",[333,533,534],{"class":335,"line":348},[333,535,536],{},"    return fetch('https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fopencc-js@1.0.5\u002Fdist\u002Fumd\u002Ffull.js')\n",[333,538,539],{"class":335,"line":354},[333,540,541],{},"        .then(function(res) { return res.text(); })\n",[333,543,544],{"class":335,"line":360},[333,545,546],{},"        .then(function(code) {\n",[333,548,550],{"class":335,"line":549},6,[333,551,552],{},"            var module = { exports: {} };\n",[333,554,556],{"class":335,"line":555},7,[333,557,558],{},"            var fn = new Function('module', 'exports', 'define', code);\n",[333,560,562],{"class":335,"line":561},8,[333,563,564],{},"            fn(module, module.exports, undefined);\n",[333,566,568],{"class":335,"line":567},9,[333,569,570],{},"            window._OpenCC = module.exports;\n",[333,572,574],{"class":335,"line":573},10,[333,575,576],{},"            return window._OpenCC;\n",[333,578,580],{"class":335,"line":579},11,[333,581,582],{},"        });\n",[333,584,586],{"class":335,"line":585},12,[333,587,588],{},"}\n",[15,590,591],{},"With this loader in place, refreshing Moodle produced zero RequireJS console errors and initialized the converter reliably every time.",[90,593],{},[10,595,597],{"id":596},"_4-step-by-step-implementation","4. Step-by-Step Implementation",[100,599,601],{"id":600},"step-1-install-the-traditional-chinese-language-pack-in-moodle","Step 1: Install the Traditional Chinese Language Pack in Moodle",[25,603,604,610,622],{},[28,605,606,607,392],{},"Log in to Moodle as an administrator and navigate to ",[31,608,609],{},"Site administration → Language → Language packs",[28,611,612,613,618,619,621],{},"Find ",[31,614,615,616,231],{},"繁體中文 (",[36,617,38],{}," in the list of available language packs on the right (install ",[36,620,191],{}," as well if you also serve learners in Hong Kong) and click Install.",[28,623,624],{},"Once installed, the language switcher menu will appear in the top navigation bar.",[100,626,628],{"id":627},"step-2-inject-the-conversion-script-via-additional-html","Step 2: Inject the Conversion Script via \"Additional HTML\"",[25,630,631,641,651],{},[28,632,633,634,637,638],{},"Go to ",[31,635,636],{},"Site administration → Appearance → Additional HTML",", or open the path directly:\n",[36,639,640],{},"\u002Fadmin\u002Fsettings.php?section=additionalhtml",[28,642,643,644,53,647,650],{},"Scroll down to the third textarea: ",[31,645,646],{},"Before BODY is closed",[36,648,649],{},"#id_s__additionalhtmlfooter",").",[28,652,653,656],{},[31,654,655],{},"Keep any existing snippets in that field"," (many sites place Google Analytics or Tag Manager scripts here—do not overwrite them), and append the following script at the bottom:",[301,658,662],{"className":659,"code":660,"language":661,"meta":307,"style":307},"language-html shiki shiki-themes github-dark","\u003C!-- OpenCC Simplified-to-Traditional Conversion (Moodle RequireJS Isolated Edition) -->\n\u003Cscript>\n(function() {\n    \u002F\u002F 1. Run only when the user is in a Traditional Chinese locale (zh_tw \u002F zh_hk)\n    var isTraditional = document.body.classList.contains('lang-zh_tw') || \n                        document.body.classList.contains('lang-zh_hk');\n    if (!isTraditional) return;\n\n    \u002F\u002F 2. Isolate loading via fetch + new Function to avoid Moodle RequireJS define conflicts\n    function loadOpenCC() {\n        if (window._OpenCC) return Promise.resolve(window._OpenCC);\n        return fetch('https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fopencc-js@1.0.5\u002Fdist\u002Fumd\u002Ffull.js')\n            .then(function(res) { return res.text(); })\n            .then(function(code) {\n                var module = { exports: {} };\n                var fn = new Function('module', 'exports', 'define', code);\n                fn(module, module.exports, undefined);\n                window._OpenCC = module.exports;\n                return window._OpenCC;\n            });\n    }\n\n    loadOpenCC().then(function(OpenCC) {\n        \u002F\u002F cn -> twp: Mainland Simplified to Taiwan Traditional with regional phrases\n        var converter = OpenCC.Converter({ from: 'cn', to: 'twp' });\n        var reverseConverter = OpenCC.Converter({ from: 'tw', to: 'cn' });\n\n        \u002F\u002F 3. Walk and convert text nodes, skipping code blocks and input controls\n        function convertElement(node) {\n            if (!node) return;\n            var walker = document.createTreeWalker(\n                node,\n                NodeFilter.SHOW_TEXT,\n                {\n                    acceptNode: function(textNode) {\n                        var p = textNode.parentNode;\n                        if (!p) return NodeFilter.FILTER_REJECT;\n                        var tag = p.tagName;\n                        if (['SCRIPT', 'STYLE', 'CODE', 'PRE', 'TEXTAREA'].indexOf(tag) !== -1) {\n                            return NodeFilter.FILTER_REJECT;\n                        }\n                        if (tag === 'INPUT' || p.isContentEditable) {\n                            return NodeFilter.FILTER_REJECT;\n                        }\n                        return NodeFilter.FILTER_ACCEPT;\n                    }\n                }\n            );\n            var curr;\n            while ((curr = walker.nextNode())) {\n                curr.nodeValue = converter(curr.nodeValue);\n            }\n        }\n\n        convertElement(document.body);\n        document.title = converter(document.title);\n        document.querySelectorAll('input[placeholder]').forEach(function(el) {\n            el.placeholder = converter(el.placeholder);\n        });\n\n        \u002F\u002F 4. Observe dynamically loaded Moodle DOM nodes (collapsed sections, drawers, AJAX modals)\n        var debounceTimer = null;\n        var pendingNodes = [];\n        var observer = new MutationObserver(function(mutations) {\n            mutations.forEach(function(m) {\n                m.addedNodes.forEach(function(n) {\n                    if (n.nodeType === Node.ELEMENT_NODE) {\n                        pendingNodes.push(n);\n                    }\n                });\n            });\n            clearTimeout(debounceTimer);\n            debounceTimer = setTimeout(function() {\n                while (pendingNodes.length > 0) {\n                    convertElement(pendingNodes.shift());\n                }\n            }, 60);\n        });\n        observer.observe(document.body, { childList: true, subtree: true });\n\n        \u002F\u002F 5. Convert text inputs back to Simplified Chinese before form submission\n        document.addEventListener('submit', function(e) {\n            e.target.querySelectorAll('input[type=\"text\"], textarea').forEach(function(input) {\n                if (input.value) {\n                    input.value = reverseConverter(input.value);\n                }\n            });\n        }, true);\n    }).catch(function(err) {\n        console.error('[OpenCC] Load error:', err);\n    });\n})();\n\u003C\u002Fscript>\n","html",[36,663,664,670,683,695,700,733,748,767,773,778,788,810,826,858,876,890,924,948,965,974,980,986,991,1013,1019,1051,1076,1081,1087,1103,1120,1140,1146,1158,1164,1182,1196,1219,1232,1282,1294,1300,1320,1331,1336,1349,1355,1361,1367,1375,1395,1409,1415,1421,1426,1435,1448,1478,1491,1496,1501,1507,1522,1535,1561,1580,1599,1618,1630,1635,1641,1646,1655,1672,1692,1707,1712,1723,1728,1750,1755,1761,1785,1813,1822,1836,1841,1846,1856,1876,1893,1899,1905],{"__ignoreMap":307},[333,665,666],{"class":335,"line":336},[333,667,669],{"class":668},"sAwPA","\u003C!-- OpenCC Simplified-to-Traditional Conversion (Moodle RequireJS Isolated Edition) -->\n",[333,671,672,676,680],{"class":335,"line":342},[333,673,675],{"class":674},"s95oV","\u003C",[333,677,679],{"class":678},"s4JwU","script",[333,681,682],{"class":674},">\n",[333,684,685,688,692],{"class":335,"line":348},[333,686,687],{"class":674},"(",[333,689,691],{"class":690},"snl16","function",[333,693,694],{"class":674},"() {\n",[333,696,697],{"class":335,"line":354},[333,698,699],{"class":668},"    \u002F\u002F 1. Run only when the user is in a Traditional Chinese locale (zh_tw \u002F zh_hk)\n",[333,701,702,705,708,711,714,718,720,724,727,730],{"class":335,"line":360},[333,703,704],{"class":690},"    var",[333,706,707],{"class":674}," isTraditional ",[333,709,710],{"class":690},"=",[333,712,713],{"class":674}," document.body.classList.",[333,715,717],{"class":716},"svObZ","contains",[333,719,687],{"class":674},[333,721,723],{"class":722},"sU2Wk","'lang-zh_tw'",[333,725,726],{"class":674},") ",[333,728,729],{"class":690},"||",[333,731,732],{"class":674}," \n",[333,734,735,738,740,742,745],{"class":335,"line":549},[333,736,737],{"class":674},"                        document.body.classList.",[333,739,717],{"class":716},[333,741,687],{"class":674},[333,743,744],{"class":722},"'lang-zh_hk'",[333,746,747],{"class":674},");\n",[333,749,750,753,755,758,761,764],{"class":335,"line":555},[333,751,752],{"class":690},"    if",[333,754,53],{"class":674},[333,756,757],{"class":690},"!",[333,759,760],{"class":674},"isTraditional) ",[333,762,763],{"class":690},"return",[333,765,766],{"class":674},";\n",[333,768,769],{"class":335,"line":561},[333,770,772],{"emptyLinePlaceholder":771},true,"\n",[333,774,775],{"class":335,"line":567},[333,776,777],{"class":668},"    \u002F\u002F 2. Isolate loading via fetch + new Function to avoid Moodle RequireJS define conflicts\n",[333,779,780,783,786],{"class":335,"line":573},[333,781,782],{"class":690},"    function",[333,784,785],{"class":716}," loadOpenCC",[333,787,694],{"class":674},[333,789,790,793,796,798,802,804,807],{"class":335,"line":579},[333,791,792],{"class":690},"        if",[333,794,795],{"class":674}," (window._OpenCC) ",[333,797,763],{"class":690},[333,799,801],{"class":800},"sDLfK"," Promise",[333,803,392],{"class":674},[333,805,806],{"class":716},"resolve",[333,808,809],{"class":674},"(window._OpenCC);\n",[333,811,812,815,818,820,823],{"class":335,"line":585},[333,813,814],{"class":690},"        return",[333,816,817],{"class":716}," fetch",[333,819,687],{"class":674},[333,821,822],{"class":722},"'https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fopencc-js@1.0.5\u002Fdist\u002Fumd\u002Ffull.js'",[333,824,825],{"class":674},")\n",[333,827,829,832,835,837,839,841,845,848,850,853,855],{"class":335,"line":828},13,[333,830,831],{"class":674},"            .",[333,833,834],{"class":716},"then",[333,836,687],{"class":674},[333,838,691],{"class":690},[333,840,687],{"class":674},[333,842,844],{"class":843},"s9osk","res",[333,846,847],{"class":674},") { ",[333,849,763],{"class":690},[333,851,852],{"class":674}," res.",[333,854,306],{"class":716},[333,856,857],{"class":674},"(); })\n",[333,859,861,863,865,867,869,871,873],{"class":335,"line":860},14,[333,862,831],{"class":674},[333,864,834],{"class":716},[333,866,687],{"class":674},[333,868,691],{"class":690},[333,870,687],{"class":674},[333,872,36],{"class":843},[333,874,875],{"class":674},") {\n",[333,877,879,882,885,887],{"class":335,"line":878},15,[333,880,881],{"class":690},"                var",[333,883,884],{"class":674}," module ",[333,886,710],{"class":690},[333,888,889],{"class":674}," { exports: {} };\n",[333,891,893,895,898,900,903,906,908,911,913,916,918,921],{"class":335,"line":892},16,[333,894,881],{"class":690},[333,896,897],{"class":674}," fn ",[333,899,710],{"class":690},[333,901,902],{"class":690}," new",[333,904,905],{"class":716}," Function",[333,907,687],{"class":674},[333,909,910],{"class":722},"'module'",[333,912,65],{"class":674},[333,914,915],{"class":722},"'exports'",[333,917,65],{"class":674},[333,919,920],{"class":722},"'define'",[333,922,923],{"class":674},", code);\n",[333,925,927,930,932,934,936,938,940,942,944,946],{"class":335,"line":926},17,[333,928,929],{"class":716},"                fn",[333,931,687],{"class":674},[333,933,490],{"class":800},[333,935,65],{"class":674},[333,937,490],{"class":800},[333,939,392],{"class":674},[333,941,487],{"class":800},[333,943,65],{"class":674},[333,945,511],{"class":800},[333,947,747],{"class":674},[333,949,951,954,956,959,961,963],{"class":335,"line":950},18,[333,952,953],{"class":674},"                window._OpenCC ",[333,955,710],{"class":690},[333,957,958],{"class":800}," module",[333,960,392],{"class":674},[333,962,487],{"class":800},[333,964,766],{"class":674},[333,966,968,971],{"class":335,"line":967},19,[333,969,970],{"class":690},"                return",[333,972,973],{"class":674}," window._OpenCC;\n",[333,975,977],{"class":335,"line":976},20,[333,978,979],{"class":674},"            });\n",[333,981,983],{"class":335,"line":982},21,[333,984,985],{"class":674},"    }\n",[333,987,989],{"class":335,"line":988},22,[333,990,772],{"emptyLinePlaceholder":771},[333,992,994,997,1000,1002,1004,1006,1008,1011],{"class":335,"line":993},23,[333,995,996],{"class":716},"    loadOpenCC",[333,998,999],{"class":674},"().",[333,1001,834],{"class":716},[333,1003,687],{"class":674},[333,1005,691],{"class":690},[333,1007,687],{"class":674},[333,1009,1010],{"class":843},"OpenCC",[333,1012,875],{"class":674},[333,1014,1016],{"class":335,"line":1015},24,[333,1017,1018],{"class":668},"        \u002F\u002F cn -> twp: Mainland Simplified to Taiwan Traditional with regional phrases\n",[333,1020,1022,1025,1028,1030,1033,1036,1039,1042,1045,1048],{"class":335,"line":1021},25,[333,1023,1024],{"class":690},"        var",[333,1026,1027],{"class":674}," converter ",[333,1029,710],{"class":690},[333,1031,1032],{"class":674}," OpenCC.",[333,1034,1035],{"class":716},"Converter",[333,1037,1038],{"class":674},"({ from: ",[333,1040,1041],{"class":722},"'cn'",[333,1043,1044],{"class":674},", to: ",[333,1046,1047],{"class":722},"'twp'",[333,1049,1050],{"class":674}," });\n",[333,1052,1054,1056,1059,1061,1063,1065,1067,1070,1072,1074],{"class":335,"line":1053},26,[333,1055,1024],{"class":690},[333,1057,1058],{"class":674}," reverseConverter ",[333,1060,710],{"class":690},[333,1062,1032],{"class":674},[333,1064,1035],{"class":716},[333,1066,1038],{"class":674},[333,1068,1069],{"class":722},"'tw'",[333,1071,1044],{"class":674},[333,1073,1041],{"class":722},[333,1075,1050],{"class":674},[333,1077,1079],{"class":335,"line":1078},27,[333,1080,772],{"emptyLinePlaceholder":771},[333,1082,1084],{"class":335,"line":1083},28,[333,1085,1086],{"class":668},"        \u002F\u002F 3. Walk and convert text nodes, skipping code blocks and input controls\n",[333,1088,1090,1093,1096,1098,1101],{"class":335,"line":1089},29,[333,1091,1092],{"class":690},"        function",[333,1094,1095],{"class":716}," convertElement",[333,1097,687],{"class":674},[333,1099,1100],{"class":843},"node",[333,1102,875],{"class":674},[333,1104,1106,1109,1111,1113,1116,1118],{"class":335,"line":1105},30,[333,1107,1108],{"class":690},"            if",[333,1110,53],{"class":674},[333,1112,757],{"class":690},[333,1114,1115],{"class":674},"node) ",[333,1117,763],{"class":690},[333,1119,766],{"class":674},[333,1121,1123,1126,1129,1131,1134,1137],{"class":335,"line":1122},31,[333,1124,1125],{"class":690},"            var",[333,1127,1128],{"class":674}," walker ",[333,1130,710],{"class":690},[333,1132,1133],{"class":674}," document.",[333,1135,1136],{"class":716},"createTreeWalker",[333,1138,1139],{"class":674},"(\n",[333,1141,1143],{"class":335,"line":1142},32,[333,1144,1145],{"class":674},"                node,\n",[333,1147,1149,1152,1155],{"class":335,"line":1148},33,[333,1150,1151],{"class":674},"                NodeFilter.",[333,1153,1154],{"class":800},"SHOW_TEXT",[333,1156,1157],{"class":674},",\n",[333,1159,1161],{"class":335,"line":1160},34,[333,1162,1163],{"class":674},"                {\n",[333,1165,1167,1170,1173,1175,1177,1180],{"class":335,"line":1166},35,[333,1168,1169],{"class":716},"                    acceptNode",[333,1171,1172],{"class":674},": ",[333,1174,691],{"class":690},[333,1176,687],{"class":674},[333,1178,1179],{"class":843},"textNode",[333,1181,875],{"class":674},[333,1183,1185,1188,1191,1193],{"class":335,"line":1184},36,[333,1186,1187],{"class":690},"                        var",[333,1189,1190],{"class":674}," p ",[333,1192,710],{"class":690},[333,1194,1195],{"class":674}," textNode.parentNode;\n",[333,1197,1199,1202,1204,1206,1209,1211,1214,1217],{"class":335,"line":1198},37,[333,1200,1201],{"class":690},"                        if",[333,1203,53],{"class":674},[333,1205,757],{"class":690},[333,1207,1208],{"class":674},"p) ",[333,1210,763],{"class":690},[333,1212,1213],{"class":674}," NodeFilter.",[333,1215,1216],{"class":800},"FILTER_REJECT",[333,1218,766],{"class":674},[333,1220,1222,1224,1227,1229],{"class":335,"line":1221},38,[333,1223,1187],{"class":690},[333,1225,1226],{"class":674}," tag ",[333,1228,710],{"class":690},[333,1230,1231],{"class":674}," p.tagName;\n",[333,1233,1235,1237,1240,1243,1245,1248,1250,1253,1255,1258,1260,1263,1266,1269,1272,1275,1278,1280],{"class":335,"line":1234},39,[333,1236,1201],{"class":690},[333,1238,1239],{"class":674}," ([",[333,1241,1242],{"class":722},"'SCRIPT'",[333,1244,65],{"class":674},[333,1246,1247],{"class":722},"'STYLE'",[333,1249,65],{"class":674},[333,1251,1252],{"class":722},"'CODE'",[333,1254,65],{"class":674},[333,1256,1257],{"class":722},"'PRE'",[333,1259,65],{"class":674},[333,1261,1262],{"class":722},"'TEXTAREA'",[333,1264,1265],{"class":674},"].",[333,1267,1268],{"class":716},"indexOf",[333,1270,1271],{"class":674},"(tag) ",[333,1273,1274],{"class":690},"!==",[333,1276,1277],{"class":690}," -",[333,1279,235],{"class":800},[333,1281,875],{"class":674},[333,1283,1285,1288,1290,1292],{"class":335,"line":1284},40,[333,1286,1287],{"class":690},"                            return",[333,1289,1213],{"class":674},[333,1291,1216],{"class":800},[333,1293,766],{"class":674},[333,1295,1297],{"class":335,"line":1296},41,[333,1298,1299],{"class":674},"                        }\n",[333,1301,1303,1305,1308,1311,1314,1317],{"class":335,"line":1302},42,[333,1304,1201],{"class":690},[333,1306,1307],{"class":674}," (tag ",[333,1309,1310],{"class":690},"===",[333,1312,1313],{"class":722}," 'INPUT'",[333,1315,1316],{"class":690}," ||",[333,1318,1319],{"class":674}," p.isContentEditable) {\n",[333,1321,1323,1325,1327,1329],{"class":335,"line":1322},43,[333,1324,1287],{"class":690},[333,1326,1213],{"class":674},[333,1328,1216],{"class":800},[333,1330,766],{"class":674},[333,1332,1334],{"class":335,"line":1333},44,[333,1335,1299],{"class":674},[333,1337,1339,1342,1344,1347],{"class":335,"line":1338},45,[333,1340,1341],{"class":690},"                        return",[333,1343,1213],{"class":674},[333,1345,1346],{"class":800},"FILTER_ACCEPT",[333,1348,766],{"class":674},[333,1350,1352],{"class":335,"line":1351},46,[333,1353,1354],{"class":674},"                    }\n",[333,1356,1358],{"class":335,"line":1357},47,[333,1359,1360],{"class":674},"                }\n",[333,1362,1364],{"class":335,"line":1363},48,[333,1365,1366],{"class":674},"            );\n",[333,1368,1370,1372],{"class":335,"line":1369},49,[333,1371,1125],{"class":690},[333,1373,1374],{"class":674}," curr;\n",[333,1376,1378,1381,1384,1386,1389,1392],{"class":335,"line":1377},50,[333,1379,1380],{"class":690},"            while",[333,1382,1383],{"class":674}," ((curr ",[333,1385,710],{"class":690},[333,1387,1388],{"class":674}," walker.",[333,1390,1391],{"class":716},"nextNode",[333,1393,1394],{"class":674},"())) {\n",[333,1396,1398,1401,1403,1406],{"class":335,"line":1397},51,[333,1399,1400],{"class":674},"                curr.nodeValue ",[333,1402,710],{"class":690},[333,1404,1405],{"class":716}," converter",[333,1407,1408],{"class":674},"(curr.nodeValue);\n",[333,1410,1412],{"class":335,"line":1411},52,[333,1413,1414],{"class":674},"            }\n",[333,1416,1418],{"class":335,"line":1417},53,[333,1419,1420],{"class":674},"        }\n",[333,1422,1424],{"class":335,"line":1423},54,[333,1425,772],{"emptyLinePlaceholder":771},[333,1427,1429,1432],{"class":335,"line":1428},55,[333,1430,1431],{"class":716},"        convertElement",[333,1433,1434],{"class":674},"(document.body);\n",[333,1436,1438,1441,1443,1445],{"class":335,"line":1437},56,[333,1439,1440],{"class":674},"        document.title ",[333,1442,710],{"class":690},[333,1444,1405],{"class":716},[333,1446,1447],{"class":674},"(document.title);\n",[333,1449,1451,1454,1457,1459,1462,1464,1467,1469,1471,1473,1476],{"class":335,"line":1450},57,[333,1452,1453],{"class":674},"        document.",[333,1455,1456],{"class":716},"querySelectorAll",[333,1458,687],{"class":674},[333,1460,1461],{"class":722},"'input[placeholder]'",[333,1463,650],{"class":674},[333,1465,1466],{"class":716},"forEach",[333,1468,687],{"class":674},[333,1470,691],{"class":690},[333,1472,687],{"class":674},[333,1474,1475],{"class":843},"el",[333,1477,875],{"class":674},[333,1479,1481,1484,1486,1488],{"class":335,"line":1480},58,[333,1482,1483],{"class":674},"            el.placeholder ",[333,1485,710],{"class":690},[333,1487,1405],{"class":716},[333,1489,1490],{"class":674},"(el.placeholder);\n",[333,1492,1494],{"class":335,"line":1493},59,[333,1495,582],{"class":674},[333,1497,1499],{"class":335,"line":1498},60,[333,1500,772],{"emptyLinePlaceholder":771},[333,1502,1504],{"class":335,"line":1503},61,[333,1505,1506],{"class":668},"        \u002F\u002F 4. Observe dynamically loaded Moodle DOM nodes (collapsed sections, drawers, AJAX modals)\n",[333,1508,1510,1512,1515,1517,1520],{"class":335,"line":1509},62,[333,1511,1024],{"class":690},[333,1513,1514],{"class":674}," debounceTimer ",[333,1516,710],{"class":690},[333,1518,1519],{"class":800}," null",[333,1521,766],{"class":674},[333,1523,1525,1527,1530,1532],{"class":335,"line":1524},63,[333,1526,1024],{"class":690},[333,1528,1529],{"class":674}," pendingNodes ",[333,1531,710],{"class":690},[333,1533,1534],{"class":674}," [];\n",[333,1536,1538,1540,1543,1545,1547,1550,1552,1554,1556,1559],{"class":335,"line":1537},64,[333,1539,1024],{"class":690},[333,1541,1542],{"class":674}," observer ",[333,1544,710],{"class":690},[333,1546,902],{"class":690},[333,1548,1549],{"class":716}," MutationObserver",[333,1551,687],{"class":674},[333,1553,691],{"class":690},[333,1555,687],{"class":674},[333,1557,1558],{"class":843},"mutations",[333,1560,875],{"class":674},[333,1562,1564,1567,1569,1571,1573,1575,1578],{"class":335,"line":1563},65,[333,1565,1566],{"class":674},"            mutations.",[333,1568,1466],{"class":716},[333,1570,687],{"class":674},[333,1572,691],{"class":690},[333,1574,687],{"class":674},[333,1576,1577],{"class":843},"m",[333,1579,875],{"class":674},[333,1581,1583,1586,1588,1590,1592,1594,1597],{"class":335,"line":1582},66,[333,1584,1585],{"class":674},"                m.addedNodes.",[333,1587,1466],{"class":716},[333,1589,687],{"class":674},[333,1591,691],{"class":690},[333,1593,687],{"class":674},[333,1595,1596],{"class":843},"n",[333,1598,875],{"class":674},[333,1600,1602,1605,1608,1610,1613,1616],{"class":335,"line":1601},67,[333,1603,1604],{"class":690},"                    if",[333,1606,1607],{"class":674}," (n.nodeType ",[333,1609,1310],{"class":690},[333,1611,1612],{"class":674}," Node.",[333,1614,1615],{"class":800},"ELEMENT_NODE",[333,1617,875],{"class":674},[333,1619,1621,1624,1627],{"class":335,"line":1620},68,[333,1622,1623],{"class":674},"                        pendingNodes.",[333,1625,1626],{"class":716},"push",[333,1628,1629],{"class":674},"(n);\n",[333,1631,1633],{"class":335,"line":1632},69,[333,1634,1354],{"class":674},[333,1636,1638],{"class":335,"line":1637},70,[333,1639,1640],{"class":674},"                });\n",[333,1642,1644],{"class":335,"line":1643},71,[333,1645,979],{"class":674},[333,1647,1649,1652],{"class":335,"line":1648},72,[333,1650,1651],{"class":716},"            clearTimeout",[333,1653,1654],{"class":674},"(debounceTimer);\n",[333,1656,1658,1661,1663,1666,1668,1670],{"class":335,"line":1657},73,[333,1659,1660],{"class":674},"            debounceTimer ",[333,1662,710],{"class":690},[333,1664,1665],{"class":716}," setTimeout",[333,1667,687],{"class":674},[333,1669,691],{"class":690},[333,1671,694],{"class":674},[333,1673,1675,1678,1681,1684,1687,1690],{"class":335,"line":1674},74,[333,1676,1677],{"class":690},"                while",[333,1679,1680],{"class":674}," (pendingNodes.",[333,1682,1683],{"class":800},"length",[333,1685,1686],{"class":690}," >",[333,1688,1689],{"class":800}," 0",[333,1691,875],{"class":674},[333,1693,1695,1698,1701,1704],{"class":335,"line":1694},75,[333,1696,1697],{"class":716},"                    convertElement",[333,1699,1700],{"class":674},"(pendingNodes.",[333,1702,1703],{"class":716},"shift",[333,1705,1706],{"class":674},"());\n",[333,1708,1710],{"class":335,"line":1709},76,[333,1711,1360],{"class":674},[333,1713,1715,1718,1721],{"class":335,"line":1714},77,[333,1716,1717],{"class":674},"            }, ",[333,1719,1720],{"class":800},"60",[333,1722,747],{"class":674},[333,1724,1726],{"class":335,"line":1725},78,[333,1727,582],{"class":674},[333,1729,1731,1734,1737,1740,1743,1746,1748],{"class":335,"line":1730},79,[333,1732,1733],{"class":674},"        observer.",[333,1735,1736],{"class":716},"observe",[333,1738,1739],{"class":674},"(document.body, { childList: ",[333,1741,1742],{"class":800},"true",[333,1744,1745],{"class":674},", subtree: ",[333,1747,1742],{"class":800},[333,1749,1050],{"class":674},[333,1751,1753],{"class":335,"line":1752},80,[333,1754,772],{"emptyLinePlaceholder":771},[333,1756,1758],{"class":335,"line":1757},81,[333,1759,1760],{"class":668},"        \u002F\u002F 5. Convert text inputs back to Simplified Chinese before form submission\n",[333,1762,1764,1766,1769,1771,1774,1776,1778,1780,1783],{"class":335,"line":1763},82,[333,1765,1453],{"class":674},[333,1767,1768],{"class":716},"addEventListener",[333,1770,687],{"class":674},[333,1772,1773],{"class":722},"'submit'",[333,1775,65],{"class":674},[333,1777,691],{"class":690},[333,1779,687],{"class":674},[333,1781,1782],{"class":843},"e",[333,1784,875],{"class":674},[333,1786,1788,1791,1793,1795,1798,1800,1802,1804,1806,1808,1811],{"class":335,"line":1787},83,[333,1789,1790],{"class":674},"            e.target.",[333,1792,1456],{"class":716},[333,1794,687],{"class":674},[333,1796,1797],{"class":722},"'input[type=\"text\"], textarea'",[333,1799,650],{"class":674},[333,1801,1466],{"class":716},[333,1803,687],{"class":674},[333,1805,691],{"class":690},[333,1807,687],{"class":674},[333,1809,1810],{"class":843},"input",[333,1812,875],{"class":674},[333,1814,1816,1819],{"class":335,"line":1815},84,[333,1817,1818],{"class":690},"                if",[333,1820,1821],{"class":674}," (input.value) {\n",[333,1823,1825,1828,1830,1833],{"class":335,"line":1824},85,[333,1826,1827],{"class":674},"                    input.value ",[333,1829,710],{"class":690},[333,1831,1832],{"class":716}," reverseConverter",[333,1834,1835],{"class":674},"(input.value);\n",[333,1837,1839],{"class":335,"line":1838},86,[333,1840,1360],{"class":674},[333,1842,1844],{"class":335,"line":1843},87,[333,1845,979],{"class":674},[333,1847,1849,1852,1854],{"class":335,"line":1848},88,[333,1850,1851],{"class":674},"        }, ",[333,1853,1742],{"class":800},[333,1855,747],{"class":674},[333,1857,1859,1862,1865,1867,1869,1871,1874],{"class":335,"line":1858},89,[333,1860,1861],{"class":674},"    }).",[333,1863,1864],{"class":716},"catch",[333,1866,687],{"class":674},[333,1868,691],{"class":690},[333,1870,687],{"class":674},[333,1872,1873],{"class":843},"err",[333,1875,875],{"class":674},[333,1877,1879,1882,1885,1887,1890],{"class":335,"line":1878},90,[333,1880,1881],{"class":674},"        console.",[333,1883,1884],{"class":716},"error",[333,1886,687],{"class":674},[333,1888,1889],{"class":722},"'[OpenCC] Load error:'",[333,1891,1892],{"class":674},", err);\n",[333,1894,1896],{"class":335,"line":1895},91,[333,1897,1898],{"class":674},"    });\n",[333,1900,1902],{"class":335,"line":1901},92,[333,1903,1904],{"class":674},"})();\n",[333,1906,1908,1911,1913],{"class":335,"line":1907},93,[333,1909,1910],{"class":674},"\u003C\u002F",[333,1912,679],{"class":678},[333,1914,682],{"class":674},[25,1916,1917],{"start":354},[28,1918,1919,1920,1923,1924],{},"Click ",[31,1921,1922],{},"Save changes"," at the bottom of the page.\n",[160,1925,1926],{},[28,1927,1928,1932,1933,1936,1937,1940,1941,392],{},[1929,1930,1931],"em",{},"A quick browser-automation tip",": If you use a browser automation tool to click Save in Moodle admin, never run a bare ",[36,1934,1935],{},"document.querySelector('button[type=\"submit\"]').click()","—the global search button in Moodle's top navbar is also ",[36,1938,1939],{},"button[type=\"submit\"]"," and appears first in DOM order. Always scope the selector to ",[36,1942,1943],{},"form#adminsettings button[type=\"submit\"]",[100,1945,1947],{"id":1946},"step-3-verify-bilingual-switching","Step 3: Verify Bilingual Switching",[15,1949,1950],{},"Open any course page and switch between Simplified and Traditional Chinese in the top-right language menu:",[1952,1953,1954,1975],"table",{},[1955,1956,1957],"thead",{},[1958,1959,1960,1965,1970],"tr",{},[1961,1962,1964],"th",{"align":1963},"left","Check Item",[1961,1966,1967,1968,231],{"align":1963},"Simplified Locale (",[36,1969,139],{},[1961,1971,1972,1973,231],{"align":1963},"Traditional Locale (",[36,1974,143],{},[1976,1977,1978,1994,2013,2037,2062],"tbody",{},[1958,1979,1980,1986,1989],{},[1981,1982,1983],"td",{"align":1963},[31,1984,1985],{},"Course & Section Titles",[1981,1987,1988],{"align":1963},"第一单元：为什么要学习……",[1981,1990,1991],{"align":1963},[31,1992,1993],{},"第一單元：為什麼要學習……",[1958,1995,1996,2001,2008],{},[1981,1997,1998],{"align":1963},[31,1999,2000],{},"Resource Type Badges",[1981,2002,2003,2004,2007],{"align":1963},"课程指南（",[31,2005,2006],{},"文件","）",[1981,2009,2010],{"align":1963},[31,2011,2012],{},"課程指南（檔案）",[1958,2014,2015,2020,2029],{},[1981,2016,2017],{"align":1963},[31,2018,2019],{},"Media Activity Labels",[1981,2021,2022,2025,2026],{"align":1963},[31,2023,2024],{},"视频"," \u002F ",[31,2027,2028],{},"音频",[1981,2030,2031,2025,2034],{"align":1963},[31,2032,2033],{},"影片",[31,2035,2036],{},"音訊",[1958,2038,2039,2044,2054],{},[1981,2040,2041],{"align":1963},[31,2042,2043],{},"Common Terminology",[1981,2045,2046,2047,2050,2051],{"align":1963},"伙伴",[31,2048,2049],{},"项目"," \u002F 量身定",[31,2052,2053],{},"制",[1981,2055,2056,2050,2059],{"align":1963},[31,2057,2058],{},"夥伴專案",[31,2060,2061],{},"製",[1958,2063,2064,2069,2075],{},[1981,2065,2066],{"align":1963},[31,2067,2068],{},"Console Errors",[1981,2070,2071,2074],{"align":1963},[36,2072,2073],{},"0"," (script exits early, zero network requests)",[1981,2076,2077,2079],{"align":1963},[36,2078,2073],{}," (no RequireJS conflict errors)",[15,2081,2082,2083,2086,2087,65,2090,2093,2094,2097],{},"Thanks to OpenCC's ",[36,2084,2085],{},"from: 'cn', to: 'twp'"," regional phrase mode, not only are character glyphs converted, but cross-strait vocabulary differences such as ",[36,2088,2089],{},"文件\u002F檔案",[36,2091,2092],{},"视频\u002F影片",", and ",[36,2095,2096],{},"音频\u002F音訊"," are translated naturally as well.",[90,2099],{},[10,2101,2103],{"id":2102},"_5-summary-and-applicability-boundaries","5. Summary and Applicability Boundaries",[15,2105,2106,2107,2109,2110,2112],{},"Combining Moodle's official language pack with an ",[36,2108,48],{}," injection of ",[36,2111,52],{}," works well for Moodle sites authored in Simplified Chinese that want to serve Traditional Chinese learners with minimal maintenance overhead:",[160,2114,2115,2121],{},[28,2116,2117,2120],{},[31,2118,2119],{},"Best Fit",": Courses consisting primarily of web pages, external video links, and objective quiz questions (multiple choice, true\u002Ffalse, matching, and dropdown missing-word selection), where you want zero database changes and a 5-minute rollout.",[28,2122,2123,2126,2127],{},[31,2124,2125],{},"When Not to Rely on It Alone",":\n",[25,2128,2129,2132],{},[28,2130,2131],{},"Text baked into raster images or scanned PDFs cannot be converted by DOM text-node replacement.",[28,2133,2134,2135,2137,2138,2140,2141,2143],{},"If your quizzes rely heavily on free-text ",[36,2136,263],{}," or embedded ",[36,2139,267],{}," Cloze inputs: while Step 5 in the script attaches a capture-phase ",[36,2142,279],{}," listener to convert Traditional Chinese input back to Simplified Chinese, I have only verified page rendering so far and have not yet run an end-to-end grading test across Moodle's quiz submission and AJAX auto-save flows. Before running high-stakes exams with free-text inputs, test a full quiz submission with a student account or add Traditional Chinese synonyms directly to the question bank's accepted answers.",[2145,2146,2147],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":307,"searchDepth":342,"depth":342,"links":2149},[2150,2151,2157,2166,2171],{"id":12,"depth":342,"text":13},{"id":94,"depth":342,"text":95,"children":2152},[2153,2154,2156],{"id":102,"depth":348,"text":103},{"id":115,"depth":348,"text":2155},"2.2 Detecting Simplified vs. Traditional Chinese: Ignore \u003Chtml lang>, Check body.classList",{"id":208,"depth":348,"text":209},{"id":285,"depth":342,"text":2158,"children":2159},"3. Debugging Log: opencc-js Hijacked by RequireJS",[2160,2162,2164],{"id":312,"depth":348,"text":2161},"Round 1: Why Wasn't window.OpenCC Defined?",{"id":395,"depth":348,"text":2163},"Round 2: Why Temporarily Nulling Out window.define Crashed on Real Pages",{"id":442,"depth":348,"text":2165},"Final Solution: fetch + new Function to Force the CommonJS Branch",{"id":596,"depth":342,"text":597,"children":2167},[2168,2169,2170],{"id":600,"depth":348,"text":601},{"id":627,"depth":348,"text":628},{"id":1946,"depth":348,"text":1947},{"id":2102,"depth":342,"text":2103},"coder","2026-11-09","Built a Simplified Chinese AI course on Moodle and want to serve Traditional Chinese learners without maintaining a duplicate course? Combine Moodle language packs with a RequireJS-safe OpenCC frontend injection.","md",null,{},"\u002Fen\u002Fblog\u002Fmoodle-opencc-traditional-chinese",{"title":5,"description":2174},"moodle-opencc-traditional-chinese","publish","en\u002Fblog\u002Fmoodle-opencc-traditional-chinese",[2184,1010,2185,2186,2187],"Moodle","RequireJS","Frontend","i18n","QnXQWejB4xrbyad5iQPNZAzGY2C7K6vFZwejGXlZKKI",[2176,2190],{"title":2191,"path":2192,"stem":2193,"slug":2194,"date":2195,"children":-1},"Mac Troubleshooting: Why 'Auto Proxy Discovery' Causes Tailscale Connection Timeouts","\u002Fen\u002Fblog\u002Fmacos-auto-proxy-discovery-tailscale","en\u002Fblog\u002Fmacos-auto-proxy-discovery-tailscale","macos-auto-proxy-discovery-tailscale","2026-11-08",1794217079815]