Browse Source

Merge pull request #234 from yeungalan/claude/wonderful-bohr-fZPTG

Add internationalization support to Task Scheduler module
Alan Yeung 2 tuần trước cách đây
mục cha
commit
bdb6f70010

+ 90 - 72
src/web/SystemAO/arsm/aecron.html

@@ -7,6 +7,7 @@
     <title>Tasks Scheduler</title>
     <script src="../../script/jquery.min.js"></script>
     <script src="../../script/ao_module.js"></script>
+    <script src="../../script/applocale.js"></script>
     <script src="../arsm/js/moment.min.js"></script>
     <style>
         * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -200,8 +201,8 @@
     <div id="ae-page-header">
         <img src="img/scheduler.png" onerror="this.style.display='none';">
         <div>
-            <div class="ae-title">Tasks Scheduler</div>
-            <div class="ae-subtitle">Schedule tasks to run while you are offline</div>
+            <div class="ae-title" locale="aecron/page/title">Tasks Scheduler</div>
+            <div class="ae-subtitle" locale="aecron/page/subtitle">Schedule tasks to run while you are offline</div>
         </div>
     </div>
 
@@ -209,56 +210,65 @@
     <div id="ae-status-banner">
         <div id="ae-status-icon"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/><path d="M12 7v5l3 2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
         <div id="ae-status-text">
-            <div class="ae-status-title">Checking permission…</div>
-            <div class="ae-status-desc">Please wait</div>
+            <div class="ae-status-title" locale="aecron/js/checking/title">Checking permission…</div>
+            <div class="ae-status-desc" locale="aecron/js/checking/desc">Please wait</div>
         </div>
     </div>
 
     <button id="ae-open-btn" style="display:none;" onclick="openSystemScheduler()">
-        <svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="vertical-align:-1px;margin-right:5px"><circle cx="8" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v1M8 13.5v1M1.5 8h1M13.5 8h1M3.6 3.6l.7.7M11.7 11.7l.7.7M3.6 12.4l.7-.7M11.7 4.3l.7-.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>Open System Scheduler
+        <svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="vertical-align:-1px;margin-right:5px"><circle cx="8" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8 1.5v1M8 13.5v1M1.5 8h1M13.5 8h1M3.6 3.6l.7.7M11.7 11.7l.7.7M3.6 12.4l.7-.7M11.7 4.3l.7-.7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
+        <span locale="aecron/btn/open">Open System Scheduler</span>
     </button>
 
     <!-- User's scheduled tasks -->
-    <div class="ae-section-label">Your Scheduled Tasks</div>
+    <div class="ae-section-label" locale="aecron/section/mytasks">Your Scheduled Tasks</div>
     <div class="ae-card">
         <table class="ae-table">
             <thead>
                 <tr>
-                    <th>Task / App</th>
-                    <th>Script</th>
-                    <th>Interval</th>
-                    <th>Base Time</th>
+                    <th locale="aecron/header/task-app">Task / App</th>
+                    <th locale="aecron/header/script">Script</th>
+                    <th locale="header/interval">Interval</th>
+                    <th locale="header/basetime">Base Time</th>
                 </tr>
             </thead>
             <tbody id="ae-task-list">
-                <tr class="ae-empty"><td colspan="4">Loading…</td></tr>
+                <tr class="ae-empty"><td colspan="4" locale="js/loading">Loading…</td></tr>
             </tbody>
         </table>
     </div>
 
     <!-- Admin: Group cron permissions (only shown to admins) -->
     <div id="ae-admin-section" style="display:none;">
-        <div class="ae-section-label">Group Cron Job Permissions</div>
+        <div class="ae-section-label" locale="aecron/section/groupperm">Group Cron Job Permissions</div>
         <div class="ae-card">
             <table class="ae-table">
                 <thead>
                     <tr>
-                        <th>Permission Group</th>
-                        <th style="width:160px;">Can Create Cron Jobs</th>
+                        <th locale="aecron/header/permgroup">Permission Group</th>
+                        <th style="width:160px;" locale="aecron/header/cancreate">Can Create Cron Jobs</th>
                     </tr>
                 </thead>
                 <tbody id="ae-group-perm-list">
-                    <tr class="ae-empty"><td colspan="2">Loading…</td></tr>
+                    <tr class="ae-empty"><td colspan="2" locale="js/loading">Loading…</td></tr>
                 </tbody>
             </table>
         </div>
-        <p style="font-size:12px;color:var(--ae-muted);margin-top:4px;">
+        <p style="font-size:12px;color:var(--ae-muted);margin-top:4px;" locale="aecron/admin/note">
             Administrator groups always have cron creation permission and cannot be restricted.
         </p>
     </div>
 </div>
 
 <script>
+    /* ── i18n helper ── */
+    function t(key, fallback) {
+        if (typeof applocale !== 'undefined') {
+            return applocale.getString(key, fallback);
+        }
+        return fallback;
+    }
+
     /* ── Theme detection ── */
     (function() {
         var root = document.getElementById('ae-root');
@@ -273,12 +283,12 @@
             }
         } catch(e) {}
         try {
-            var t = null;
-            if (typeof preferredTheme !== 'undefined') t = preferredTheme;
-            else if (parent && typeof parent.preferredTheme !== 'undefined') t = parent.preferredTheme;
-            if (!t) { try { t = localStorage.getItem('preferredTheme'); } catch(e) {} }
-            if (t) {
-                var isDark = t === 'dark' || t === 'darkTheme';
+            var theme = null;
+            if (typeof preferredTheme !== 'undefined') theme = preferredTheme;
+            else if (parent && typeof parent.preferredTheme !== 'undefined') theme = parent.preferredTheme;
+            if (!theme) { try { theme = localStorage.getItem('preferredTheme'); } catch(e) {} }
+            if (theme) {
+                var isDark = theme === 'dark' || theme === 'darkTheme';
                 root.classList.toggle('dark', isDark);
                 document.body.classList.toggle('dark', isDark);
             }
@@ -290,50 +300,55 @@
         var d = Math.floor(s / 86400);
         var h = Math.floor((s % 86400) / 3600);
         var m = Math.floor((s % 3600) / 60);
+        var sep = t('js/time/sep', ' ');
         var parts = [];
-        if (d) parts.push(d + (d === 1 ? " day" : " days"));
-        if (h) parts.push(h + (h === 1 ? " hour" : " hours"));
-        if (m) parts.push(m + (m === 1 ? " minute" : " minutes"));
-        return parts.length ? parts.join(" ") : s + " sec";
+        if (d) parts.push(d + t(d === 1 ? 'js/time/day1'    : 'js/time/days',    d === 1 ? ' day'    : ' days'));
+        if (h) parts.push(h + t(h === 1 ? 'js/time/hour1'   : 'js/time/hours',   h === 1 ? ' hour'   : ' hours'));
+        if (m) parts.push(m + t(m === 1 ? 'js/time/minute1' : 'js/time/minutes', m === 1 ? ' minute' : ' minutes'));
+        return parts.length ? parts.join(sep) : s + t('js/time/sec', ' sec');
     }
 
     /* ── Check user cron permission ── */
-    $.ajax({
-        url: "../../system/arsm/aecron/permission",
-        success: function(data) {
-            var banner = document.getElementById('ae-status-banner');
-            var icon   = document.getElementById('ae-status-icon');
-            var title  = banner.querySelector('.ae-status-title');
-            var desc   = banner.querySelector('.ae-status-desc');
-            if (data.CanCreate) {
-                banner.classList.add('permitted');
-                icon.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/><path d="M7 12.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
-                title.textContent = 'Cron Job Creation Enabled';
-                desc.textContent  = 'You are allowed to create and manage scheduled tasks.';
-            } else {
-                banner.classList.add('denied');
-                icon.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><rect x="6" y="11" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.6"/><path d="M8 11V7.5a4 4 0 018 0V11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
-                title.textContent = 'Cron Job Creation Disabled';
-                desc.textContent  = 'Your account does not have permission to create scheduled tasks. Contact an administrator.';
+    function checkPermission() {
+        $.ajax({
+            url: "../../system/arsm/aecron/permission",
+            success: function(data) {
+                var banner = document.getElementById('ae-status-banner');
+                var icon   = document.getElementById('ae-status-icon');
+                var title  = banner.querySelector('.ae-status-title');
+                var desc   = banner.querySelector('.ae-status-desc');
+                if (data.CanCreate) {
+                    banner.classList.add('permitted');
+                    icon.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.6"/><path d="M7 12.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
+                    title.textContent = t('aecron/js/status/enabled/title', 'Cron Job Creation Enabled');
+                    desc.textContent  = t('aecron/js/status/enabled/desc',  'You are allowed to create and manage scheduled tasks.');
+                } else {
+                    banner.classList.add('denied');
+                    icon.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><rect x="6" y="11" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.6"/><path d="M8 11V7.5a4 4 0 018 0V11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
+                    title.textContent = t('aecron/js/status/disabled/title', 'Cron Job Creation Disabled');
+                    desc.textContent  = t('aecron/js/status/disabled/desc',  'Your account does not have permission to create scheduled tasks. Contact an administrator.');
+                }
             }
-        }
-    });
+        });
+    }
 
     /* ── Check if user has access to Task Scheduler module (show Open button) ── */
-    $.ajax({
-        url: "../../system/modules/list",
-        success: function(data) {
-            var hasAccess = false;
-            if (Array.isArray(data)) {
-                data.forEach(function(m) {
-                    if (m.Name === "Tasks Scheduler") hasAccess = true;
-                });
-            }
-            if (hasAccess) {
-                document.getElementById('ae-open-btn').style.display = 'inline-flex';
+    function checkModuleAccess() {
+        $.ajax({
+            url: "../../system/modules/list",
+            success: function(data) {
+                var hasAccess = false;
+                if (Array.isArray(data)) {
+                    data.forEach(function(m) {
+                        if (m.Name === "Tasks Scheduler") hasAccess = true;
+                    });
+                }
+                if (hasAccess) {
+                    document.getElementById('ae-open-btn').style.display = 'inline-flex';
+                }
             }
-        }
-    });
+        });
+    }
 
     /* ── Load user's task list ── */
     function loadTaskList() {
@@ -343,25 +358,24 @@
                 var tbody = document.getElementById('ae-task-list');
                 tbody.innerHTML = "";
                 if (!data || data.length === 0) {
-                    tbody.innerHTML = '<tr class="ae-empty"><td colspan="4">No scheduled tasks registered.</td></tr>';
+                    tbody.innerHTML = '<tr class="ae-empty"><td colspan="4">' + t('aecron/js/empty/notask', 'No scheduled tasks registered.') + '</td></tr>';
                     return;
                 }
                 data.forEach(function(task) {
                     var scriptName = (task.ScriptVpath || "").split("/").pop();
                     var appBadge   = task.AppName ? '<span class="ae-app-badge">' + escapeHtml(task.AppName) + '</span>' : '';
-                    var nameCell   = escapeHtml(task.Name) + appBadge;
                     var tr = document.createElement('tr');
                     tr.innerHTML =
                         '<td><span title="' + escapeHtml(task.ScriptVpath || "") + '">' + escapeHtml(scriptName) + '</span>' + appBadge + '</td>' +
                         '<td style="color:var(--ae-muted);font-size:12px;">' + escapeHtml(task.ScriptVpath || "") + '</td>' +
-                        '<td>Every ' + parseSecondsToHuman(task.ExecutionInterval) + '</td>' +
+                        '<td>' + t('js/task/every', 'Every ') + parseSecondsToHuman(task.ExecutionInterval) + '</td>' +
                         '<td style="font-size:12px;">' + moment.unix(task.BaseTime).format('lll') + '</td>';
                     tbody.appendChild(tr);
                 });
             },
             error: function() {
                 document.getElementById('ae-task-list').innerHTML =
-                    '<tr class="ae-empty"><td colspan="4">Unable to load task list.</td></tr>';
+                    '<tr class="ae-empty"><td colspan="4">' + t('aecron/js/error/loadfail', 'Unable to load task list.') + '</td></tr>';
             }
         });
     }
@@ -375,7 +389,7 @@
                 var tbody = document.getElementById('ae-group-perm-list');
                 tbody.innerHTML = "";
                 if (!data || data.length === 0) {
-                    tbody.innerHTML = '<tr class="ae-empty"><td colspan="2">No permission groups found.</td></tr>';
+                    tbody.innerHTML = '<tr class="ae-empty"><td colspan="2">' + t('aecron/js/empty/nogroups', 'No permission groups found.') + '</td></tr>';
                     return;
                 }
                 data.forEach(function(entry) {
@@ -383,7 +397,7 @@
                     var checked = entry.CanCreateCronJob ? ' checked' : '';
                     var lockClass = isAdmin ? ' admin-locked' : '';
                     var adminNote = isAdmin
-                        ? '<div class="ae-group-admin-note">Administrator group — always enabled</div>'
+                        ? '<div class="ae-group-admin-note">' + t('aecron/js/admingroup/note', 'Administrator group — always enabled') + '</div>'
                         : '';
                     var tr = document.createElement('tr');
                     tr.innerHTML =
@@ -402,8 +416,8 @@
                     tbody.appendChild(tr);
                 });
             },
-            error: function(xhr) {
-                // Not admin or endpoint not available  silently skip
+            error: function() {
+                // Not admin or endpoint not available  silently skip
             }
         });
     }
@@ -416,9 +430,8 @@
             method: "POST",
             data: { group: groupName, allow: allow ? "true" : "false" },
             error: function() {
-                // Revert on failure
                 checkbox.checked = !allow;
-                alert("Failed to update group permission.");
+                alert(t('aecron/js/error/groupperm', 'Failed to update group permission.'));
             }
         });
     }
@@ -427,7 +440,7 @@
         ao_module_newfw({
             url: "SystemAO/arsm/scheduler.html",
             appicon: "SystemAO/arsm/img/scheduler.png",
-            title: "Task Scheduler"
+            title: t('aecron/page/title', 'Task Scheduler')
         });
     }
 
@@ -439,9 +452,14 @@
             .replace(/"/g, "&quot;");
     }
 
-    // Init
-    loadTaskList();
-    loadGroupPermissions();
+    /* ── Init ── */
+    applocale.init("../locale/scheduler.json", function() {
+        applocale.translate();
+        checkPermission();
+        checkModuleAccess();
+        loadTaskList();
+        loadGroupPermissions();
+    });
 </script>
 </body>
 </html>

+ 111 - 95
src/web/SystemAO/arsm/scheduler.html

@@ -6,6 +6,7 @@
     <title>Task Scheduler</title>
     <script src="../../script/jquery.min.js"></script>
     <script src="../../script/ao_module.js"></script>
+    <script src="../../script/applocale.js"></script>
     <script src="js/moment.min.js"></script>
     <style>
         *, *::before, *::after { box-sizing: border-box; }
@@ -360,24 +361,28 @@
     <div id="sidebar">
         <div id="sidebar-title">
             <img src="img/small_icon.png" onerror="this.style.display='none'">
-            Scheduler
+            <span locale="sidebar/title">Scheduler</span>
         </div>
         <div id="sidebar-nav">
             <div class="nav-item active" id="nav-mine"  onclick="showView('mine')">
-                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M5 4h7M5 8h7M5 12h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="2.5" cy="4" r="0.75" fill="currentColor"/><circle cx="2.5" cy="8" r="0.75" fill="currentColor"/><circle cx="2.5" cy="12" r="0.75" fill="currentColor"/></svg></span> My Tasks
+                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M5 4h7M5 8h7M5 12h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="2.5" cy="4" r="0.75" fill="currentColor"/><circle cx="2.5" cy="8" r="0.75" fill="currentColor"/><circle cx="2.5" cy="12" r="0.75" fill="currentColor"/></svg></span>
+                <span locale="nav/mine">My Tasks</span>
             </div>
             <div class="nav-item" id="nav-all"   onclick="showView('all')">
-                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="2" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/></svg></span> All Tasks
+                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="2" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="2" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/><rect x="9" y="9" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.4"/></svg></span>
+                <span locale="nav/all">All Tasks</span>
             </div>
             <div class="nav-item" id="nav-new"   onclick="showView('new')">
-                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span> New Task
+                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></span>
+                <span locale="nav/new">New Task</span>
             </div>
             <div class="nav-item" id="nav-remove" onclick="showView('remove')">
-                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M3 5h10M6 5V4h4v1M6.5 8v4M9.5 8v4M4 5l.9 9h6.2L12 5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span> Remove Task
+                <span class="nav-icon"><svg viewBox="0 0 16 16" fill="none"><path d="M3 5h10M6 5V4h4v1M6.5 8v4M9.5 8v4M4 5l.9 9h6.2L12 5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
+                <span locale="nav/remove">Remove Task</span>
             </div>
         </div>
         <div id="sidebar-filter">
-            <input id="filter-input" type="text" placeholder="Filter tasks…"
+            <input id="filter-input" type="text" placeholder="filter/placeholder"
                    oninput="applyFilter()" onkeydown="if(event.key==='Enter')applyFilter()">
         </div>
     </div>
@@ -387,102 +392,102 @@
 
         <!-- My Tasks -->
         <div id="view-mine" class="view active">
-            <div class="section-title">My Scheduled Tasks</div>
+            <div class="section-title" locale="view/mine/title">My Scheduled Tasks</div>
             <div class="card" id="card-mine">
                 <div class="task-row header">
-                    <span>Task / Script</span>
-                    <span>Path</span>
-                    <span>App</span>
-                    <span>Interval</span>
+                    <span locale="header/task-script">Task / Script</span>
+                    <span locale="header/path">Path</span>
+                    <span locale="header/app">App</span>
+                    <span locale="header/interval">Interval</span>
                 </div>
-                <div id="list-mine"><div class="empty-state">Loading…</div></div>
+                <div id="list-mine"><div class="empty-state" locale="js/loading">Loading…</div></div>
             </div>
         </div>
 
         <!-- All Tasks (admin) -->
         <div id="view-all" class="view">
-            <div class="section-title">All Scheduled Tasks</div>
+            <div class="section-title" locale="view/all/title">All Scheduled Tasks</div>
             <div class="card">
                 <div class="task-row all-cols header">
-                    <span>Task Name</span>
-                    <span>Creator</span>
-                    <span>Script Path</span>
-                    <span>App</span>
-                    <span>Interval</span>
-                    <span>Base Time</span>
+                    <span locale="header/taskname">Task Name</span>
+                    <span locale="header/creator">Creator</span>
+                    <span locale="header/scriptpath">Script Path</span>
+                    <span locale="header/app">App</span>
+                    <span locale="header/interval">Interval</span>
+                    <span locale="header/basetime">Base Time</span>
                 </div>
-                <div id="list-all"><div class="empty-state">Loading…</div></div>
+                <div id="list-all"><div class="empty-state" locale="js/loading">Loading…</div></div>
             </div>
         </div>
 
         <!-- New Task -->
         <div id="view-new" class="view">
-            <div class="section-title">New Scheduled Task</div>
+            <div class="section-title" locale="view/new/title">New Scheduled Task</div>
             <div id="noPermMessage">
                 <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;margin-right:6px;vertical-align:-2px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg>
-                Your account does not have permission to create scheduled tasks.
-                Ask an administrator to grant cron job access via <strong>System Settings &rarr; Tasks Scheduler</strong>.
+                <span locale="noperm/message">Your account does not have permission to create scheduled tasks.
+                Ask an administrator to grant cron job access via <strong>System Settings &rarr; Tasks Scheduler</strong>.</span>
             </div>
             <div id="newTaskForm">
                 <div class="form-card">
                     <div class="form-row">
-                        <label class="form-label">Task Name <span class="form-req">*</span></label>
+                        <label class="form-label"><span locale="form/taskname/label">Task Name</span> <span class="form-req">*</span></label>
                         <input class="form-input" id="taskname" type="text"
-                               placeholder="My Daily Task" maxlength="32" autocomplete="off">
-                        <div class="form-hint">Max 32 characters, must be unique.</div>
+                               placeholder="taskname/placeholder" maxlength="32" autocomplete="off">
+                        <div class="form-hint" locale="form/taskname/hint">Max 32 characters, must be unique.</div>
                     </div>
                     <div class="form-row">
-                        <label class="form-label">Description</label>
+                        <label class="form-label" locale="form/desc/label">Description</label>
                         <input class="form-input" id="desc" type="text"
-                               placeholder="What does this task do?" autocomplete="off">
+                               placeholder="desc/placeholder" autocomplete="off">
                     </div>
                     <div class="form-row">
-                        <label class="form-label">Script Path <span class="form-req">*</span></label>
+                        <label class="form-label"><span locale="form/scriptpath/label">Script Path</span> <span class="form-req">*</span></label>
                         <div class="input-with-btn">
                             <input class="form-input" id="scriptpath" type="text"
-                                   placeholder="user:/path/to/script.agi" autocomplete="off"
+                                   placeholder="scriptpath/placeholder" autocomplete="off"
                                    oninput="checkExt(this.value)">
-                            <button class="btn" onclick="openFileSelector()">Browse…</button>
+                            <button class="btn" onclick="openFileSelector()" locale="form/browse">Browse…</button>
                         </div>
                         <div class="warn-bar" id="extWarn">
                             <svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;vertical-align:-2px;margin-right:4px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg>
-                            This file extension may not be supported. Only .agi and .js files are executed.
+                            <span locale="form/extwarn">This file extension may not be supported. Only .agi and .js files are executed.</span>
                         </div>
                     </div>
                     <div class="form-row">
-                        <label class="form-label">Run Every <span class="form-req">*</span></label>
+                        <label class="form-label"><span locale="form/runevery/label">Run Every</span> <span class="form-req">*</span></label>
                         <div class="form-row-inline">
                             <input class="form-input" id="intervalvalue" type="number"
                                    value="1" min="1" style="max-width:90px;">
                             <select class="form-input" id="intervalunit" style="max-width:160px;">
-                                <option value="60">Minutes</option>
-                                <option value="3600">Hours</option>
-                                <option value="86400" selected>Days</option>
-                                <option value="2628333">Months (approx.)</option>
+                                <option value="60" locale="form/unit/minutes">Minutes</option>
+                                <option value="3600" locale="form/unit/hours">Hours</option>
+                                <option value="86400" selected locale="form/unit/days">Days</option>
+                                <option value="2628333" locale="form/unit/months">Months (approx.)</option>
                             </select>
                         </div>
-                        <div class="form-hint">Month is approximated as 2 628 333 seconds.</div>
+                        <div class="form-hint" locale="form/month/hint">Month is approximated as 2 628 333 seconds.</div>
                     </div>
                     <div class="form-row">
-                        <label class="form-label">Align to <span class="form-req">*</span></label>
+                        <label class="form-label"><span locale="form/alignto/label">Align to</span> <span class="form-req">*</span></label>
                         <select class="form-input" id="intervalbase">
-                            <option value="now">Now (current minute)</option>
-                            <option value="hour">Start of the Hour</option>
-                            <option value="day">Start of the Day</option>
-                            <option value="month">Start of the Month</option>
-                            <option value="year">Start of the Year</option>
-                            <option value="mon">Monday midnight</option>
-                            <option value="tue">Tuesday midnight</option>
-                            <option value="wed">Wednesday midnight</option>
-                            <option value="thu">Thursday midnight</option>
-                            <option value="fri">Friday midnight</option>
-                            <option value="sat">Saturday midnight</option>
-                            <option value="sun">Sunday midnight</option>
+                            <option value="now" locale="form/base/now">Now (current minute)</option>
+                            <option value="hour" locale="form/base/hour">Start of the Hour</option>
+                            <option value="day" locale="form/base/day">Start of the Day</option>
+                            <option value="month" locale="form/base/month">Start of the Month</option>
+                            <option value="year" locale="form/base/year">Start of the Year</option>
+                            <option value="mon" locale="form/base/mon">Monday midnight</option>
+                            <option value="tue" locale="form/base/tue">Tuesday midnight</option>
+                            <option value="wed" locale="form/base/wed">Wednesday midnight</option>
+                            <option value="thu" locale="form/base/thu">Thursday midnight</option>
+                            <option value="fri" locale="form/base/fri">Friday midnight</option>
+                            <option value="sat" locale="form/base/sat">Saturday midnight</option>
+                            <option value="sun" locale="form/base/sun">Sunday midnight</option>
                         </select>
-                        <div class="form-hint">Sets the reference point for interval alignment (useful for weekly/monthly schedules).</div>
+                        <div class="form-hint" locale="form/base/hint">Sets the reference point for interval alignment (useful for weekly/monthly schedules).</div>
                     </div>
-                    <button class="btn primary" onclick="submitNewTask()">Create Task</button>
-                    <span style="margin-left:10px; font-size:12px; color:var(--text-muted);">
+                    <button class="btn primary" onclick="submitNewTask()" locale="form/submit">Create Task</button>
+                    <span style="margin-left:10px; font-size:12px; color:var(--text-muted);" locale="form/required/hint">
                         Fields with <span class="form-req">*</span> are required.
                     </span>
                 </div>
@@ -491,18 +496,18 @@
 
         <!-- Remove Task -->
         <div id="view-remove" class="view">
-            <div class="section-title">Remove Scheduled Tasks</div>
+            <div class="section-title" locale="view/remove/title">Remove Scheduled Tasks</div>
             <div class="card">
-                <div class="danger-header"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;vertical-align:-2px;margin-right:5px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg>Removal is permanent and cannot be undone.</div>
+                <div class="danger-header"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;vertical-align:-2px;margin-right:5px"><path d="M8 2L1.5 14h13L8 2z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M8 7v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="12" r="0.8" fill="currentColor"/></svg><span locale="danger/message">Removal is permanent and cannot be undone.</span></div>
                 <div class="task-row remove-cols header">
-                    <span>Task Name</span>
-                    <span>Creator</span>
-                    <span>Script Path</span>
-                    <span>App</span>
-                    <span>Interval</span>
-                    <span>Action</span>
+                    <span locale="header/taskname">Task Name</span>
+                    <span locale="header/creator">Creator</span>
+                    <span locale="header/scriptpath">Script Path</span>
+                    <span locale="header/app">App</span>
+                    <span locale="header/interval">Interval</span>
+                    <span locale="header/action">Action</span>
                 </div>
-                <div id="list-remove"><div class="empty-state">Loading…</div></div>
+                <div id="list-remove"><div class="empty-state" locale="js/loading">Loading…</div></div>
             </div>
         </div>
 
@@ -517,6 +522,14 @@
         document.body.classList.toggle('dark', c !== 'whiteTheme');
     });
 
+    /* ── i18n helper ── */
+    function t(key, fallback) {
+        if (typeof applocale !== 'undefined') {
+            return applocale.getString(key, fallback);
+        }
+        return fallback;
+    }
+
     /* ── Navigation ── */
     var currentView = 'mine';
     function showView(name) {
@@ -539,7 +552,7 @@
             m = Math.floor(s % 3600 / 60),
             sec = s % 60;
         var parts = [];
-        if (d)   parts.push(d   + (d   === 1 ? 'd' : 'd'));
+        if (d)   parts.push(d   + 'd');
         if (h)   parts.push(h   + 'h');
         if (m)   parts.push(m   + 'min');
         if (sec) parts.push(sec + 's');
@@ -591,18 +604,18 @@
             var el = document.getElementById('list-mine');
             var rows = (data || []).filter(matchFilter);
             if (!rows.length) {
-                el.innerHTML = '<div class="empty-state">No scheduled tasks found.</div>';
+                el.innerHTML = '<div class="empty-state">' + t('js/empty/notfound', 'No scheduled tasks found.') + '</div>';
                 return;
             }
-            el.innerHTML = rows.map(function(t) {
-                var script = (t.ScriptVpath || '').split('/').pop();
+            el.innerHTML = rows.map(function(task) {
+                var script = (task.ScriptVpath || '').split('/').pop();
                 return '<div class="task-row">'
-                    + '<span class="task-name" title="' + esc(t.Name) + '">' + esc(t.Name)
-                    + (t.Description ? '<br><small style="color:var(--text-muted);font-weight:400">' + esc(t.Description) + '</small>' : '')
+                    + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name)
+                    + (task.Description ? '<br><small style="color:var(--text-muted);font-weight:400">' + esc(task.Description) + '</small>' : '')
                     + '</span>'
-                    + '<span class="task-script" title="' + esc(t.ScriptVpath) + '">' + esc(t.ScriptVpath) + '</span>'
-                    + '<span>' + appBadge(t.AppName) + '</span>'
-                    + '<span class="task-interval">Every ' + fmtInterval(t.ExecutionInterval) + '</span>'
+                    + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
+                    + '<span>' + appBadge(task.AppName) + '</span>'
+                    + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
                     + '</div>';
             }).join('');
         });
@@ -614,17 +627,17 @@
             var el = document.getElementById('list-all');
             var rows = (data || []).filter(matchFilter);
             if (!rows.length) {
-                el.innerHTML = '<div class="empty-state">No scheduled tasks found.</div>';
+                el.innerHTML = '<div class="empty-state">' + t('js/empty/notfound', 'No scheduled tasks found.') + '</div>';
                 return;
             }
-            el.innerHTML = rows.map(function(t) {
+            el.innerHTML = rows.map(function(task) {
                 return '<div class="task-row all-cols">'
-                    + '<span class="task-name" title="' + esc(t.Name) + '">' + esc(t.Name) + '</span>'
-                    + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(t.Creator) + '</span>'
-                    + '<span class="task-script" title="' + esc(t.ScriptVpath) + '">' + esc(t.ScriptVpath) + '</span>'
-                    + '<span>' + appBadge(t.AppName) + '</span>'
-                    + '<span class="task-interval">Every ' + fmtInterval(t.ExecutionInterval) + '</span>'
-                    + '<span class="task-basetime">' + fmtTime(t.BaseTime) + '</span>'
+                    + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name) + '</span>'
+                    + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(task.Creator) + '</span>'
+                    + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
+                    + '<span>' + appBadge(task.AppName) + '</span>'
+                    + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
+                    + '<span class="task-basetime">' + fmtTime(task.BaseTime) + '</span>'
                     + '</div>';
             }).join('');
         });
@@ -636,27 +649,27 @@
             var el = document.getElementById('list-remove');
             var rows = (data || []).filter(matchFilter);
             if (!rows.length) {
-                el.innerHTML = '<div class="empty-state">No scheduled tasks.</div>';
+                el.innerHTML = '<div class="empty-state">' + t('js/empty/none', 'No scheduled tasks.') + '</div>';
                 return;
             }
-            el.innerHTML = rows.map(function(t) {
+            el.innerHTML = rows.map(function(task) {
                 return '<div class="task-row remove-cols">'
-                    + '<span class="task-name" title="' + esc(t.Name) + '">' + esc(t.Name) + '</span>'
-                    + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(t.Creator) + '</span>'
-                    + '<span class="task-script" title="' + esc(t.ScriptVpath) + '">' + esc(t.ScriptVpath) + '</span>'
-                    + '<span>' + appBadge(t.AppName) + '</span>'
-                    + '<span class="task-interval">Every ' + fmtInterval(t.ExecutionInterval) + '</span>'
-                    + '<span><button class="btn danger sm" onclick="removeTask(' + JSON.stringify(esc(t.Name)) + ')">Remove</button></span>'
+                    + '<span class="task-name" title="' + esc(task.Name) + '">' + esc(task.Name) + '</span>'
+                    + '<span style="color:var(--text-dim);font-size:12.5px">' + esc(task.Creator) + '</span>'
+                    + '<span class="task-script" title="' + esc(task.ScriptVpath) + '">' + esc(task.ScriptVpath) + '</span>'
+                    + '<span>' + appBadge(task.AppName) + '</span>'
+                    + '<span class="task-interval">' + t('js/task/every', 'Every ') + fmtInterval(task.ExecutionInterval) + '</span>'
+                    + '<span><button class="btn danger sm" onclick="removeTask(' + JSON.stringify(esc(task.Name)) + ')">' + t('js/btn/remove', 'Remove') + '</button></span>'
                     + '</div>';
             }).join('');
         });
     }
 
     function removeTask(name) {
-        if (!confirm('Remove task "' + name + '"? This cannot be undone.')) return;
+        if (!confirm(t('js/confirm/remove/pre', 'Remove task "') + name + t('js/confirm/remove/post', '"? This cannot be undone.'))) return;
         $.post('../../system/arsm/aecron/remove', { name: name }, function(data) {
             if (data && data.error) { alert(data.error); return; }
-            toast('Task removed.');
+            toast(t('js/toast/removed', 'Task removed.'));
             loadRemove();
         });
     }
@@ -718,9 +731,9 @@
         var interval   = parseFloat($('#intervalvalue').val()) * parseFloat($('#intervalunit').val());
         var base       = baseUnix($('#intervalbase').val());
 
-        if (!name) { alert('Task name is required.'); return; }
-        if (!scriptPath) { alert('Script path is required.'); return; }
-        if (!interval || interval < 60) { alert('Interval must be at least 1 minute.'); return; }
+        if (!name) { alert(t('js/alert/noname', 'Task name is required.')); return; }
+        if (!scriptPath) { alert(t('js/alert/nopath', 'Script path is required.')); return; }
+        if (!interval || interval < 60) { alert(t('js/alert/interval', 'Interval must be at least 1 minute.')); return; }
 
         $.post('../../system/arsm/aecron/add', {
             name: name, desc: desc, path: scriptPath,
@@ -730,13 +743,16 @@
             $('#taskname').val('');
             $('#desc').val('');
             $('#scriptpath').val('');
-            toast('Task created!');
+            toast(t('js/toast/created', 'Task created!'));
             showView('mine');
         });
     }
 
     /* ── Init ── */
-    loadMine();
+    applocale.init("../locale/scheduler.json", function() {
+        applocale.translate();
+        loadMine();
+    });
 </script>
 </body>
 </html>

+ 624 - 0
src/web/SystemAO/locale/scheduler.json

@@ -0,0 +1,624 @@
+{
+    "author": "tobychui",
+    "version": "1.0",
+    "keys": {
+        "zh-tw": {
+            "name": "繁體中文(台灣)",
+            "fwtitle": "任務排程器",
+            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings": {
+                "sidebar/title": "排程管理員",
+                "nav/mine": "我的任務",
+                "nav/all": "所有任務",
+                "nav/new": "新增任務",
+                "nav/remove": "移除任務",
+                "view/mine/title": "我的排程任務",
+                "header/task-script": "任務 / 腳本",
+                "header/path": "路徑",
+                "header/app": "應用程式",
+                "header/interval": "執行間隔",
+                "view/all/title": "所有排程任務",
+                "header/taskname": "任務名稱",
+                "header/creator": "建立者",
+                "header/scriptpath": "腳本路徑",
+                "header/basetime": "基準時間",
+                "header/action": "動作",
+                "view/new/title": "新增排程任務",
+                "noperm/message": "您的帳戶沒有建立排程任務的權限。請聯絡系統管理員,在<strong>系統設定 &rarr; 任務排程器</strong>中授予 cron 任務存取權限。",
+                "form/taskname/label": "任務名稱",
+                "form/taskname/hint": "最多 32 個字元,必須唯一。",
+                "form/desc/label": "說明",
+                "form/scriptpath/label": "腳本路徑",
+                "form/browse": "瀏覽…",
+                "form/extwarn": "此檔案副檔名可能不受支援。僅執行 .agi 和 .js 檔案。",
+                "form/runevery/label": "執行頻率",
+                "form/unit/minutes": "分鐘",
+                "form/unit/hours": "小時",
+                "form/unit/days": "天",
+                "form/unit/months": "月(約略值)",
+                "form/month/hint": "月份約為 2,628,333 秒。",
+                "form/alignto/label": "對齊至",
+                "form/base/now": "現在(當前分鐘)",
+                "form/base/hour": "本小時開始",
+                "form/base/day": "本日開始",
+                "form/base/month": "本月開始",
+                "form/base/year": "本年開始",
+                "form/base/mon": "週一午夜",
+                "form/base/tue": "週二午夜",
+                "form/base/wed": "週三午夜",
+                "form/base/thu": "週四午夜",
+                "form/base/fri": "週五午夜",
+                "form/base/sat": "週六午夜",
+                "form/base/sun": "週日午夜",
+                "form/base/hint": "設定間隔對齊的參考時間點(適用於每週/每月排程)。",
+                "form/submit": "建立任務",
+                "form/required/hint": "標有 <span class=\"form-req\">*</span> 的欄位為必填。",
+                "view/remove/title": "移除排程任務",
+                "danger/message": "移除操作是永久性的,無法復原。",
+                "js/loading": "載入中…",
+                "js/empty/notfound": "找不到排程任務。",
+                "js/empty/none": "沒有排程任務。",
+                "js/task/every": "每隔 ",
+                "js/btn/remove": "移除",
+                "js/confirm/remove/pre": "移除任務「",
+                "js/confirm/remove/post": "」?此操作無法復原。",
+                "js/toast/removed": "任務已移除。",
+                "js/alert/noname": "任務名稱為必填。",
+                "js/alert/nopath": "腳本路徑為必填。",
+                "js/alert/interval": "執行間隔至少需要 1 分鐘。",
+                "js/toast/created": "任務已建立!",
+                "js/time/day1": "天",
+                "js/time/days": "天",
+                "js/time/hour1": "小時",
+                "js/time/hours": "小時",
+                "js/time/minute1": "分鐘",
+                "js/time/minutes": "分鐘",
+                "js/time/sec": "秒",
+                "js/time/sep": "",
+                "aecron/page/title": "任務排程器",
+                "aecron/page/subtitle": "在您離線時執行排程任務",
+                "aecron/btn/open": "開啟系統排程器",
+                "aecron/section/mytasks": "我的排程任務",
+                "aecron/header/task-app": "任務 / 應用程式",
+                "aecron/header/script": "腳本",
+                "aecron/section/groupperm": "群組 Cron 任務權限",
+                "aecron/header/permgroup": "權限群組",
+                "aecron/header/cancreate": "可建立 Cron 任務",
+                "aecron/admin/note": "管理員群組始終擁有 cron 任務建立權限,且無法被限制。",
+                "aecron/js/checking/title": "正在檢查權限…",
+                "aecron/js/checking/desc": "請稍候",
+                "aecron/js/status/enabled/title": "Cron 任務建立已啟用",
+                "aecron/js/status/enabled/desc": "您可以建立和管理排程任務。",
+                "aecron/js/status/disabled/title": "Cron 任務建立已停用",
+                "aecron/js/status/disabled/desc": "您的帳戶沒有建立排程任務的權限。請聯絡管理員。",
+                "aecron/js/empty/notask": "沒有已登記的排程任務。",
+                "aecron/js/error/loadfail": "無法載入任務清單。",
+                "aecron/js/empty/nogroups": "找不到權限群組。",
+                "aecron/js/admingroup/note": "管理員群組 — 始終啟用",
+                "aecron/js/error/groupperm": "更新群組權限失敗。"
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "篩選任務…",
+                "taskname/placeholder": "我的每日任務",
+                "desc/placeholder": "此任務的功能說明",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        },
+        "zh-hk": {
+            "name": "繁體中文(香港)",
+            "fwtitle": "任務排程器",
+            "fontFamily": "\"Microsoft JhengHei\",\"SimHei\", \"Apple LiGothic Medium\", \"STHeiti\"",
+            "strings": {
+                "sidebar/title": "排程管理員",
+                "nav/mine": "我的任務",
+                "nav/all": "所有任務",
+                "nav/new": "新增任務",
+                "nav/remove": "移除任務",
+                "view/mine/title": "我的排程任務",
+                "header/task-script": "任務 / 腳本",
+                "header/path": "路徑",
+                "header/app": "應用程式",
+                "header/interval": "執行間隔",
+                "view/all/title": "所有排程任務",
+                "header/taskname": "任務名稱",
+                "header/creator": "建立者",
+                "header/scriptpath": "腳本路徑",
+                "header/basetime": "基準時間",
+                "header/action": "動作",
+                "view/new/title": "新增排程任務",
+                "noperm/message": "您的帳戶沒有建立排程任務的權限。請聯絡系統管理員,在<strong>系統設定 &rarr; 任務排程器</strong>中授予 cron 任務存取權限。",
+                "form/taskname/label": "任務名稱",
+                "form/taskname/hint": "最多 32 個字元,必須唯一。",
+                "form/desc/label": "說明",
+                "form/scriptpath/label": "腳本路徑",
+                "form/browse": "瀏覽…",
+                "form/extwarn": "此檔案副檔名可能不受支援。僅執行 .agi 和 .js 檔案。",
+                "form/runevery/label": "執行頻率",
+                "form/unit/minutes": "分鐘",
+                "form/unit/hours": "小時",
+                "form/unit/days": "天",
+                "form/unit/months": "月(約略值)",
+                "form/month/hint": "月份約為 2,628,333 秒。",
+                "form/alignto/label": "對齊至",
+                "form/base/now": "現在(當前分鐘)",
+                "form/base/hour": "本小時開始",
+                "form/base/day": "本日開始",
+                "form/base/month": "本月開始",
+                "form/base/year": "本年開始",
+                "form/base/mon": "週一午夜",
+                "form/base/tue": "週二午夜",
+                "form/base/wed": "週三午夜",
+                "form/base/thu": "週四午夜",
+                "form/base/fri": "週五午夜",
+                "form/base/sat": "週六午夜",
+                "form/base/sun": "週日午夜",
+                "form/base/hint": "設定間隔對齊的參考時間點(適用於每週/每月排程)。",
+                "form/submit": "建立任務",
+                "form/required/hint": "標有 <span class=\"form-req\">*</span> 的欄位為必填。",
+                "view/remove/title": "移除排程任務",
+                "danger/message": "移除操作是永久性的,無法復原。",
+                "js/loading": "載入中…",
+                "js/empty/notfound": "找不到排程任務。",
+                "js/empty/none": "沒有排程任務。",
+                "js/task/every": "每隔 ",
+                "js/btn/remove": "移除",
+                "js/confirm/remove/pre": "移除任務「",
+                "js/confirm/remove/post": "」?此操作無法復原。",
+                "js/toast/removed": "任務已移除。",
+                "js/alert/noname": "任務名稱為必填。",
+                "js/alert/nopath": "腳本路徑為必填。",
+                "js/alert/interval": "執行間隔至少需要 1 分鐘。",
+                "js/toast/created": "任務已建立!",
+                "js/time/day1": "天",
+                "js/time/days": "天",
+                "js/time/hour1": "小時",
+                "js/time/hours": "小時",
+                "js/time/minute1": "分鐘",
+                "js/time/minutes": "分鐘",
+                "js/time/sec": "秒",
+                "js/time/sep": "",
+                "aecron/page/title": "任務排程器",
+                "aecron/page/subtitle": "在您離線時執行排程任務",
+                "aecron/btn/open": "開啟系統排程器",
+                "aecron/section/mytasks": "我的排程任務",
+                "aecron/header/task-app": "任務 / 應用程式",
+                "aecron/header/script": "腳本",
+                "aecron/section/groupperm": "群組 Cron 任務權限",
+                "aecron/header/permgroup": "權限群組",
+                "aecron/header/cancreate": "可建立 Cron 任務",
+                "aecron/admin/note": "管理員群組始終擁有 cron 任務建立權限,且無法被限制。",
+                "aecron/js/checking/title": "正在檢查權限…",
+                "aecron/js/checking/desc": "請稍候",
+                "aecron/js/status/enabled/title": "Cron 任務建立已啟用",
+                "aecron/js/status/enabled/desc": "您可以建立和管理排程任務。",
+                "aecron/js/status/disabled/title": "Cron 任務建立已停用",
+                "aecron/js/status/disabled/desc": "您的帳戶沒有建立排程任務的權限。請聯絡管理員。",
+                "aecron/js/empty/notask": "沒有已登記的排程任務。",
+                "aecron/js/error/loadfail": "無法載入任務清單。",
+                "aecron/js/empty/nogroups": "找不到權限群組。",
+                "aecron/js/admingroup/note": "管理員群組 — 始終啟用",
+                "aecron/js/error/groupperm": "更新群組權限失敗。"
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "篩選任務…",
+                "taskname/placeholder": "我的每日任務",
+                "desc/placeholder": "此任務的功能說明",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        },
+        "zh-cn": {
+            "name": "简体中文",
+            "fwtitle": "任务计划程序",
+            "fontFamily": "\"Microsoft YaHei\",\"SimHei\", \"PingFangSC-Medium\", \"STHeiti\"",
+            "strings": {
+                "sidebar/title": "任务计划程序",
+                "nav/mine": "我的任务",
+                "nav/all": "全部任务",
+                "nav/new": "新建任务",
+                "nav/remove": "删除任务",
+                "view/mine/title": "我的计划任务",
+                "header/task-script": "任务 / 脚本",
+                "header/path": "路径",
+                "header/app": "应用",
+                "header/interval": "执行间隔",
+                "view/all/title": "所有计划任务",
+                "header/taskname": "任务名称",
+                "header/creator": "创建者",
+                "header/scriptpath": "脚本路径",
+                "header/basetime": "基准时间",
+                "header/action": "操作",
+                "view/new/title": "新建计划任务",
+                "noperm/message": "您的账户没有创建计划任务的权限。请联系管理员,在<strong>系统设置 &rarr; 任务计划程序</strong>中授予权限。",
+                "form/taskname/label": "任务名称",
+                "form/taskname/hint": "最多 32 个字符,必须唯一。",
+                "form/desc/label": "描述",
+                "form/scriptpath/label": "脚本路径",
+                "form/browse": "浏览…",
+                "form/extwarn": "此文件扩展名可能不受支持。仅支持执行 .agi 和 .js 文件。",
+                "form/runevery/label": "运行频率",
+                "form/unit/minutes": "分钟",
+                "form/unit/hours": "小时",
+                "form/unit/days": "天",
+                "form/unit/months": "月(近似值)",
+                "form/month/hint": "月份约为 2,628,333 秒。",
+                "form/alignto/label": "对齐至",
+                "form/base/now": "现在(当前分钟)",
+                "form/base/hour": "本小时开始",
+                "form/base/day": "本日开始",
+                "form/base/month": "本月开始",
+                "form/base/year": "本年开始",
+                "form/base/mon": "周一午夜",
+                "form/base/tue": "周二午夜",
+                "form/base/wed": "周三午夜",
+                "form/base/thu": "周四午夜",
+                "form/base/fri": "周五午夜",
+                "form/base/sat": "周六午夜",
+                "form/base/sun": "周日午夜",
+                "form/base/hint": "设置间隔对齐的参考时间点(适用于每周/每月计划)。",
+                "form/submit": "创建任务",
+                "form/required/hint": "带 <span class=\"form-req\">*</span> 的字段为必填项。",
+                "view/remove/title": "删除计划任务",
+                "danger/message": "删除操作是永久性的,无法撤销。",
+                "js/loading": "加载中…",
+                "js/empty/notfound": "未找到计划任务。",
+                "js/empty/none": "没有计划任务。",
+                "js/task/every": "每隔 ",
+                "js/btn/remove": "删除",
+                "js/confirm/remove/pre": "删除任务「",
+                "js/confirm/remove/post": "」?此操作无法撤销。",
+                "js/toast/removed": "任务已删除。",
+                "js/alert/noname": "任务名称为必填项。",
+                "js/alert/nopath": "脚本路径为必填项。",
+                "js/alert/interval": "执行间隔至少需要 1 分钟。",
+                "js/toast/created": "任务已创建!",
+                "js/time/day1": "天",
+                "js/time/days": "天",
+                "js/time/hour1": "小时",
+                "js/time/hours": "小时",
+                "js/time/minute1": "分钟",
+                "js/time/minutes": "分钟",
+                "js/time/sec": "秒",
+                "js/time/sep": "",
+                "aecron/page/title": "任务计划程序",
+                "aecron/page/subtitle": "在您离线时执行计划任务",
+                "aecron/btn/open": "打开系统计划程序",
+                "aecron/section/mytasks": "我的计划任务",
+                "aecron/header/task-app": "任务 / 应用",
+                "aecron/header/script": "脚本",
+                "aecron/section/groupperm": "群组 Cron 任务权限",
+                "aecron/header/permgroup": "权限群组",
+                "aecron/header/cancreate": "可创建 Cron 任务",
+                "aecron/admin/note": "管理员组始终拥有 cron 任务创建权限,且无法被限制。",
+                "aecron/js/checking/title": "正在检查权限…",
+                "aecron/js/checking/desc": "请稍候",
+                "aecron/js/status/enabled/title": "Cron 任务创建已启用",
+                "aecron/js/status/enabled/desc": "您可以创建和管理计划任务。",
+                "aecron/js/status/disabled/title": "Cron 任务创建已停用",
+                "aecron/js/status/disabled/desc": "您的账户没有创建计划任务的权限。请联系管理员。",
+                "aecron/js/empty/notask": "没有已注册的计划任务。",
+                "aecron/js/error/loadfail": "无法加载任务列表。",
+                "aecron/js/empty/nogroups": "未找到权限组。",
+                "aecron/js/admingroup/note": "管理员组 — 始终启用",
+                "aecron/js/error/groupperm": "更新组权限失败。"
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "筛选任务…",
+                "taskname/placeholder": "我的每日任务",
+                "desc/placeholder": "此任务的功能说明",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        },
+        "en-us": {
+            "name": "English (US)",
+            "fwtitle": "Task Scheduler",
+            "fontFamily": "Arial, Helvetica, sans-serif",
+            "strings": {
+                "sidebar/title": "Scheduler",
+                "nav/mine": "My Tasks",
+                "nav/all": "All Tasks",
+                "nav/new": "New Task",
+                "nav/remove": "Remove Task",
+                "view/mine/title": "My Scheduled Tasks",
+                "header/task-script": "Task / Script",
+                "header/path": "Path",
+                "header/app": "App",
+                "header/interval": "Interval",
+                "view/all/title": "All Scheduled Tasks",
+                "header/taskname": "Task Name",
+                "header/creator": "Creator",
+                "header/scriptpath": "Script Path",
+                "header/basetime": "Base Time",
+                "header/action": "Action",
+                "view/new/title": "New Scheduled Task",
+                "noperm/message": "Your account does not have permission to create scheduled tasks. Ask an administrator to grant cron job access via <strong>System Settings &rarr; Tasks Scheduler</strong>.",
+                "form/taskname/label": "Task Name",
+                "form/taskname/hint": "Max 32 characters, must be unique.",
+                "form/desc/label": "Description",
+                "form/scriptpath/label": "Script Path",
+                "form/browse": "Browse…",
+                "form/extwarn": "This file extension may not be supported. Only .agi and .js files are executed.",
+                "form/runevery/label": "Run Every",
+                "form/unit/minutes": "Minutes",
+                "form/unit/hours": "Hours",
+                "form/unit/days": "Days",
+                "form/unit/months": "Months (approx.)",
+                "form/month/hint": "Month is approximated as 2 628 333 seconds.",
+                "form/alignto/label": "Align to",
+                "form/base/now": "Now (current minute)",
+                "form/base/hour": "Start of the Hour",
+                "form/base/day": "Start of the Day",
+                "form/base/month": "Start of the Month",
+                "form/base/year": "Start of the Year",
+                "form/base/mon": "Monday midnight",
+                "form/base/tue": "Tuesday midnight",
+                "form/base/wed": "Wednesday midnight",
+                "form/base/thu": "Thursday midnight",
+                "form/base/fri": "Friday midnight",
+                "form/base/sat": "Saturday midnight",
+                "form/base/sun": "Sunday midnight",
+                "form/base/hint": "Sets the reference point for interval alignment (useful for weekly/monthly schedules).",
+                "form/submit": "Create Task",
+                "form/required/hint": "Fields with <span class=\"form-req\">*</span> are required.",
+                "view/remove/title": "Remove Scheduled Tasks",
+                "danger/message": "Removal is permanent and cannot be undone.",
+                "js/loading": "Loading…",
+                "js/empty/notfound": "No scheduled tasks found.",
+                "js/empty/none": "No scheduled tasks.",
+                "js/task/every": "Every ",
+                "js/btn/remove": "Remove",
+                "js/confirm/remove/pre": "Remove task \"",
+                "js/confirm/remove/post": "\"? This cannot be undone.",
+                "js/toast/removed": "Task removed.",
+                "js/alert/noname": "Task name is required.",
+                "js/alert/nopath": "Script path is required.",
+                "js/alert/interval": "Interval must be at least 1 minute.",
+                "js/toast/created": "Task created!",
+                "js/time/day1": " day",
+                "js/time/days": " days",
+                "js/time/hour1": " hour",
+                "js/time/hours": " hours",
+                "js/time/minute1": " minute",
+                "js/time/minutes": " minutes",
+                "js/time/sec": " sec",
+                "js/time/sep": " ",
+                "aecron/page/title": "Tasks Scheduler",
+                "aecron/page/subtitle": "Schedule tasks to run while you are offline",
+                "aecron/btn/open": "Open System Scheduler",
+                "aecron/section/mytasks": "Your Scheduled Tasks",
+                "aecron/header/task-app": "Task / App",
+                "aecron/header/script": "Script",
+                "aecron/section/groupperm": "Group Cron Job Permissions",
+                "aecron/header/permgroup": "Permission Group",
+                "aecron/header/cancreate": "Can Create Cron Jobs",
+                "aecron/admin/note": "Administrator groups always have cron creation permission and cannot be restricted.",
+                "aecron/js/checking/title": "Checking permission…",
+                "aecron/js/checking/desc": "Please wait",
+                "aecron/js/status/enabled/title": "Cron Job Creation Enabled",
+                "aecron/js/status/enabled/desc": "You are allowed to create and manage scheduled tasks.",
+                "aecron/js/status/disabled/title": "Cron Job Creation Disabled",
+                "aecron/js/status/disabled/desc": "Your account does not have permission to create scheduled tasks. Contact an administrator.",
+                "aecron/js/empty/notask": "No scheduled tasks registered.",
+                "aecron/js/error/loadfail": "Unable to load task list.",
+                "aecron/js/empty/nogroups": "No permission groups found.",
+                "aecron/js/admingroup/note": "Administrator group — always enabled",
+                "aecron/js/error/groupperm": "Failed to update group permission."
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "Filter tasks…",
+                "taskname/placeholder": "My Daily Task",
+                "desc/placeholder": "What does this task do?",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        },
+        "ja-jp": {
+            "name": "日本語",
+            "fwtitle": "タスクスケジューラ",
+            "fontFamily": "\"Meiryo UI\", \"Arial Unicode MS\", \"Hiragino Kaku Gothic Pro\"",
+            "strings": {
+                "sidebar/title": "タスクスケジューラ",
+                "nav/mine": "マイタスク",
+                "nav/all": "すべてのタスク",
+                "nav/new": "新規タスク",
+                "nav/remove": "タスクを削除",
+                "view/mine/title": "マイスケジュールタスク",
+                "header/task-script": "タスク / スクリプト",
+                "header/path": "パス",
+                "header/app": "アプリ",
+                "header/interval": "実行間隔",
+                "view/all/title": "すべてのスケジュールタスク",
+                "header/taskname": "タスク名",
+                "header/creator": "作成者",
+                "header/scriptpath": "スクリプトパス",
+                "header/basetime": "基準時刻",
+                "header/action": "操作",
+                "view/new/title": "新規スケジュールタスク",
+                "noperm/message": "アカウントにスケジュールタスクを作成する権限がありません。<strong>システム設定 &rarr; タスクスケジューラ</strong>から管理者に権限の付与を依頼してください。",
+                "form/taskname/label": "タスク名",
+                "form/taskname/hint": "最大 32 文字、一意である必要があります。",
+                "form/desc/label": "説明",
+                "form/scriptpath/label": "スクリプトパス",
+                "form/browse": "参照…",
+                "form/extwarn": "このファイル拡張子はサポートされていない可能性があります。実行できるのは .agi および .js ファイルのみです。",
+                "form/runevery/label": "実行間隔",
+                "form/unit/minutes": "分",
+                "form/unit/hours": "時間",
+                "form/unit/days": "日",
+                "form/unit/months": "月(概算)",
+                "form/month/hint": "1ヶ月は約 2,628,333 秒です。",
+                "form/alignto/label": "整列基準",
+                "form/base/now": "現在(現在の分)",
+                "form/base/hour": "時間の開始",
+                "form/base/day": "日の開始",
+                "form/base/month": "月の開始",
+                "form/base/year": "年の開始",
+                "form/base/mon": "月曜日の深夜",
+                "form/base/tue": "火曜日の深夜",
+                "form/base/wed": "水曜日の深夜",
+                "form/base/thu": "木曜日の深夜",
+                "form/base/fri": "金曜日の深夜",
+                "form/base/sat": "土曜日の深夜",
+                "form/base/sun": "日曜日の深夜",
+                "form/base/hint": "間隔整列の基準点を設定します(週次/月次スケジュールに便利)。",
+                "form/submit": "タスクを作成",
+                "form/required/hint": "<span class=\"form-req\">*</span> のフィールドは必須です。",
+                "view/remove/title": "スケジュールタスクを削除",
+                "danger/message": "削除は永久的で、元に戻すことができません。",
+                "js/loading": "読み込み中…",
+                "js/empty/notfound": "スケジュールタスクが見つかりません。",
+                "js/empty/none": "スケジュールタスクはありません。",
+                "js/task/every": "毎 ",
+                "js/btn/remove": "削除",
+                "js/confirm/remove/pre": "タスク「",
+                "js/confirm/remove/post": "」を削除しますか?この操作は元に戻せません。",
+                "js/toast/removed": "タスクを削除しました。",
+                "js/alert/noname": "タスク名は必須です。",
+                "js/alert/nopath": "スクリプトパスは必須です。",
+                "js/alert/interval": "実行間隔は1分以上にする必要があります。",
+                "js/toast/created": "タスクを作成しました!",
+                "js/time/day1": "日",
+                "js/time/days": "日",
+                "js/time/hour1": "時間",
+                "js/time/hours": "時間",
+                "js/time/minute1": "分",
+                "js/time/minutes": "分",
+                "js/time/sec": "秒",
+                "js/time/sep": "",
+                "aecron/page/title": "タスクスケジューラ",
+                "aecron/page/subtitle": "オフライン中にタスクを実行するよう設定します",
+                "aecron/btn/open": "システムスケジューラを開く",
+                "aecron/section/mytasks": "マイスケジュールタスク",
+                "aecron/header/task-app": "タスク / アプリ",
+                "aecron/header/script": "スクリプト",
+                "aecron/section/groupperm": "グループ Cron 権限",
+                "aecron/header/permgroup": "権限グループ",
+                "aecron/header/cancreate": "Cron タスクの作成可否",
+                "aecron/admin/note": "管理者グループは常に Cron タスク作成権限を持ち、制限することはできません。",
+                "aecron/js/checking/title": "権限を確認中…",
+                "aecron/js/checking/desc": "しばらくお待ちください",
+                "aecron/js/status/enabled/title": "Cron タスク作成が有効",
+                "aecron/js/status/enabled/desc": "スケジュールタスクを作成・管理できます。",
+                "aecron/js/status/disabled/title": "Cron タスク作成が無効",
+                "aecron/js/status/disabled/desc": "アカウントにスケジュールタスクを作成する権限がありません。管理者に連絡してください。",
+                "aecron/js/empty/notask": "スケジュールタスクは登録されていません。",
+                "aecron/js/error/loadfail": "タスクリストを読み込めません。",
+                "aecron/js/empty/nogroups": "権限グループが見つかりません。",
+                "aecron/js/admingroup/note": "管理者グループ — 常に有効",
+                "aecron/js/error/groupperm": "グループ権限の更新に失敗しました。"
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "タスクを絞り込む…",
+                "taskname/placeholder": "毎日のタスク",
+                "desc/placeholder": "このタスクの説明",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        },
+        "ko-kr": {
+            "name": "한국어",
+            "fwtitle": "작업 스케줄러",
+            "fontFamily": "\"Malgun Gothic\", \"Apple SD Gothic Neo\"",
+            "strings": {
+                "sidebar/title": "작업 스케줄러",
+                "nav/mine": "내 작업",
+                "nav/all": "모든 작업",
+                "nav/new": "새 작업",
+                "nav/remove": "작업 삭제",
+                "view/mine/title": "내 예약 작업",
+                "header/task-script": "작업 / 스크립트",
+                "header/path": "경로",
+                "header/app": "앱",
+                "header/interval": "실행 간격",
+                "view/all/title": "모든 예약 작업",
+                "header/taskname": "작업 이름",
+                "header/creator": "작성자",
+                "header/scriptpath": "스크립트 경로",
+                "header/basetime": "기준 시간",
+                "header/action": "작업",
+                "view/new/title": "새 예약 작업",
+                "noperm/message": "계정에 예약 작업을 만들 권한이 없습니다. <strong>시스템 설정 &rarr; 작업 스케줄러</strong>에서 관리자에게 권한 부여를 요청하세요.",
+                "form/taskname/label": "작업 이름",
+                "form/taskname/hint": "최대 32자, 고유해야 합니다.",
+                "form/desc/label": "설명",
+                "form/scriptpath/label": "스크립트 경로",
+                "form/browse": "찾아보기…",
+                "form/extwarn": "이 파일 확장자는 지원되지 않을 수 있습니다. .agi 및 .js 파일만 실행됩니다.",
+                "form/runevery/label": "실행 주기",
+                "form/unit/minutes": "분",
+                "form/unit/hours": "시간",
+                "form/unit/days": "일",
+                "form/unit/months": "월 (근사값)",
+                "form/month/hint": "1개월은 약 2,628,333초입니다.",
+                "form/alignto/label": "정렬 기준",
+                "form/base/now": "지금 (현재 분)",
+                "form/base/hour": "시간 시작",
+                "form/base/day": "일 시작",
+                "form/base/month": "월 시작",
+                "form/base/year": "연 시작",
+                "form/base/mon": "월요일 자정",
+                "form/base/tue": "화요일 자정",
+                "form/base/wed": "수요일 자정",
+                "form/base/thu": "목요일 자정",
+                "form/base/fri": "금요일 자정",
+                "form/base/sat": "토요일 자정",
+                "form/base/sun": "일요일 자정",
+                "form/base/hint": "간격 정렬의 기준점을 설정합니다 (주간/월간 스케줄에 유용).",
+                "form/submit": "작업 만들기",
+                "form/required/hint": "<span class=\"form-req\">*</span> 표시된 필드는 필수입니다.",
+                "view/remove/title": "예약 작업 삭제",
+                "danger/message": "삭제는 영구적이며 취소할 수 없습니다.",
+                "js/loading": "로딩 중…",
+                "js/empty/notfound": "예약 작업을 찾을 수 없습니다.",
+                "js/empty/none": "예약 작업이 없습니다.",
+                "js/task/every": "매 ",
+                "js/btn/remove": "삭제",
+                "js/confirm/remove/pre": "작업 「",
+                "js/confirm/remove/post": "」을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
+                "js/toast/removed": "작업이 삭제되었습니다.",
+                "js/alert/noname": "작업 이름은 필수입니다.",
+                "js/alert/nopath": "스크립트 경로는 필수입니다.",
+                "js/alert/interval": "실행 간격은 최소 1분이어야 합니다.",
+                "js/toast/created": "작업이 생성되었습니다!",
+                "js/time/day1": "일",
+                "js/time/days": "일",
+                "js/time/hour1": "시간",
+                "js/time/hours": "시간",
+                "js/time/minute1": "분",
+                "js/time/minutes": "분",
+                "js/time/sec": "초",
+                "js/time/sep": "",
+                "aecron/page/title": "작업 스케줄러",
+                "aecron/page/subtitle": "오프라인 상태에서 작업을 실행하도록 예약합니다",
+                "aecron/btn/open": "시스템 스케줄러 열기",
+                "aecron/section/mytasks": "내 예약 작업",
+                "aecron/header/task-app": "작업 / 앱",
+                "aecron/header/script": "스크립트",
+                "aecron/section/groupperm": "그룹 Cron 권한",
+                "aecron/header/permgroup": "권한 그룹",
+                "aecron/header/cancreate": "Cron 작업 생성 가능",
+                "aecron/admin/note": "관리자 그룹은 항상 Cron 작업 생성 권한을 가지며 제한할 수 없습니다.",
+                "aecron/js/checking/title": "권한 확인 중…",
+                "aecron/js/checking/desc": "잠시 기다려 주세요",
+                "aecron/js/status/enabled/title": "Cron 작업 생성 활성화",
+                "aecron/js/status/enabled/desc": "예약 작업을 생성하고 관리할 수 있습니다.",
+                "aecron/js/status/disabled/title": "Cron 작업 생성 비활성화",
+                "aecron/js/status/disabled/desc": "계정에 예약 작업을 생성할 권한이 없습니다. 관리자에게 문의하세요.",
+                "aecron/js/empty/notask": "등록된 예약 작업이 없습니다.",
+                "aecron/js/error/loadfail": "작업 목록을 불러올 수 없습니다.",
+                "aecron/js/empty/nogroups": "권한 그룹을 찾을 수 없습니다.",
+                "aecron/js/admingroup/note": "관리자 그룹 — 항상 활성화",
+                "aecron/js/error/groupperm": "그룹 권한 업데이트에 실패했습니다."
+            },
+            "titles": {},
+            "placeholder": {
+                "filter/placeholder": "작업 필터…",
+                "taskname/placeholder": "나의 매일 작업",
+                "desc/placeholder": "이 작업의 기능 설명",
+                "scriptpath/placeholder": "user:/path/to/script.agi"
+            }
+        }
+    }
+}