From 09f03270554fea412b9ff7e570a51ce021a60416 Mon Sep 17 00:00:00 2001 From: Chaim Date: Wed, 8 Apr 2026 22:18:16 +0000 Subject: [PATCH] Update Hebrew RTL and translation patches - translate-he.js: Add tab button translations (Sub-Goals, Projects, Issues etc. with counts), fix whitespace normalization for matching, add translateButtonLabels() for React tab buttons - rtl-override.css: Properties panel RTL fixes (row-reverse, text-align), timeline reverse order (newest first + comment box at top), truncation and panel width adjustments Co-Authored-By: Claude Opus 4.6 (1M context) --- assets/rtl-override.css | 322 ++++++++++++++++++++++++++++++ assets/translate-he.js | 424 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 743 insertions(+), 3 deletions(-) diff --git a/assets/rtl-override.css b/assets/rtl-override.css index 7fbe084..5609685 100644 --- a/assets/rtl-override.css +++ b/assets/rtl-override.css @@ -739,3 +739,325 @@ html[dir="rtl"] [class*="arrow-left"] svg, html[dir="rtl"] [class*="arrow-right"] svg { transform: scaleX(-1); } + +/* -------------------------------------------------------------------------- + COMMENT HEADER — agent name + date row RTL alignment + -------------------------------------------------------------------------- */ +html[dir="rtl"] .flex.items-center.justify-between.mb-1 { + direction: rtl; +} + +html[dir="rtl"] [href*="/agents/"] { + direction: rtl; +} + +/* -------------------------------------------------------------------------- + ACTIVITY LOG — RTL alignment + -------------------------------------------------------------------------- */ +html[dir="rtl"] [id^="activity-"] { + direction: rtl; + text-align: right; +} + +html[dir="rtl"] [id^="activity-"] .flex-1 { + text-align: right; +} + +html[dir="rtl"] [id^="activity-"] .flex.items-start.gap-2\.5 { + flex-direction: row-reverse; +} + +/* -------------------------------------------------------------------------- + TABLES inside comments — RTL alignment + -------------------------------------------------------------------------- */ +html[dir="rtl"] .paperclip-markdown table { + direction: rtl; + text-align: right; +} + +html[dir="rtl"] .paperclip-markdown table th, +html[dir="rtl"] .paperclip-markdown table td { + text-align: right; +} + +html[dir="rtl"] .paperclip-markdown { + direction: rtl; + text-align: right; +} + +/* -------------------------------------------------------------------------- + GLOBAL RTL — all flex rows with gap should be RTL + -------------------------------------------------------------------------- */ +html[dir="rtl"] [id^="run-"], +html[dir="rtl"] [id^="activity-"], +html[dir="rtl"] [id^="comment-"] { + direction: rtl; +} + +html[dir="rtl"] [id^="run-"] .flex, +html[dir="rtl"] [id^="activity-"] .flex, +html[dir="rtl"] [id^="comment-"] .flex { + direction: rtl; +} + +html[dir="rtl"] [id^="run-"] .flex.items-center { + flex-direction: row-reverse; +} + +html[dir="rtl"] [id^="run-"] .flex.items-center.gap-2\.5 { + flex-direction: row-reverse; +} + +html[dir="rtl"] .min-w-0.flex-1 { + text-align: right; + direction: rtl; +} + +/* Inner flex rows inside run/activity items — text flows RTL */ +html[dir="rtl"] .min-w-0.flex-1 > .flex { + direction: rtl; + justify-content: flex-start; +} + +/* The outer row: avatar on right, content on left */ +html[dir="rtl"] .flex.items-center.gap-2\.5.py-1\.5 { + direction: rtl; +} + +/* Same for items-start variant (activity log) */ +html[dir="rtl"] .flex.items-start.gap-2\.5.py-1\.5 { + direction: rtl; +} + +/* -------------------------------------------------------------------------- + ACTIVITY TAB — global RTL for all activity content + -------------------------------------------------------------------------- */ +html[dir="rtl"] [id*="-content-activity"] { + direction: rtl; + text-align: right; +} + +html[dir="rtl"] [id*="-content-activity"] .flex { + direction: rtl; +} + +html[dir="rtl"] [id*="-content-activity"] .flex.items-center, +html[dir="rtl"] [id*="-content-activity"] .flex.items-start { + direction: rtl; +} + +html[dir="rtl"] [id*="-content-activity"] .min-w-0 { + text-align: right; +} + +/* -------------------------------------------------------------------------- + PROPERTIES PANEL — global RTL + -------------------------------------------------------------------------- */ +html[dir="rtl"] [data-slot="sheet-content"], +html[dir="rtl"] [role="dialog"] { + direction: rtl; + text-align: right; +} + +html[dir="rtl"] [data-slot="sheet-content"] .flex, +html[dir="rtl"] [role="dialog"] .flex { + direction: rtl; +} + +html[dir="rtl"] [data-slot="sheet-content"] label, +html[dir="rtl"] [role="dialog"] label { + text-align: right; +} + +/* -------------------------------------------------------------------------- + GLOBAL CATCH-ALL — any remaining LTR flex containers in main content + -------------------------------------------------------------------------- */ +html[dir="rtl"] main { + direction: rtl; +} + +html[dir="rtl"] main .flex.items-center { + direction: rtl; +} + +html[dir="rtl"] main .flex.items-start { + direction: rtl; +} + +html[dir="rtl"] main .min-w-0 { + text-align: right; +} + +html[dir="rtl"] main .space-y-1\.5, +html[dir="rtl"] main .space-y-2, +html[dir="rtl"] main .space-y-3, +html[dir="rtl"] main .space-y-4 { + direction: rtl; + text-align: right; +} + +/* -------------------------------------------------------------------------- + PROPERTIES SIDEBAR — the p-4 container with property rows + -------------------------------------------------------------------------- */ +html[dir="rtl"] .p-4 .space-y-4, +html[dir="rtl"] .p-4 .space-y-1 { + direction: rtl; + text-align: right; +} + +/* Property row: label (w-20) on right, value on left */ +html[dir="rtl"] .p-4 .flex.items-center.gap-3.py-1\.5 { + direction: rtl; +} + +/* The label span */ +html[dir="rtl"] .p-4 .shrink-0.w-20 { + text-align: right; +} + +/* -------------------------------------------------------------------------- + BREADCRUMB — RTL direction + flip chevron separator + -------------------------------------------------------------------------- */ +html[dir="rtl"] [data-slot="breadcrumb"] { + direction: rtl; +} + +html[dir="rtl"] [data-slot="breadcrumb-list"] { + direction: rtl; +} + +html[dir="rtl"] [data-slot="breadcrumb-separator"] svg { + transform: scaleX(-1); +} + +/* Top header bar */ +html[dir="rtl"] .border-b.border-border .flex.items-center { + direction: rtl; +} + +/* -------------------------------------------------------------------------- + FILTER BAR — the "For [assignee] in [project]" row + -------------------------------------------------------------------------- */ +html[dir="rtl"] .overflow-x-auto .inline-flex.items-center.gap-2 { + direction: rtl; +} + +/* -------------------------------------------------------------------------- + PLUGIN PAGE + CARDS — RTL for all card content + -------------------------------------------------------------------------- */ +html[dir="rtl"] [data-slot="tabs"][dir="ltr"] { + direction: rtl !important; +} + +html[dir="rtl"] [data-slot="card"], +html[dir="rtl"] [data-slot="card-header"], +html[dir="rtl"] [data-slot="card-content"], +html[dir="rtl"] [data-slot="card-title"] { + direction: rtl; + text-align: right; +} + +html[dir="rtl"] [data-slot="card-content"] .flex.justify-between { + direction: rtl; +} + +html[dir="rtl"] [data-slot="card-content"] .flex.items-center.justify-between { + direction: rtl; +} + +html[dir="rtl"] [data-slot="card-content"] .grid { + direction: rtl; +} + +html[dir="rtl"] [data-slot="card-description"] { + text-align: right; +} + +/* Status change row: reverse order so old→new reads RTL */ +html[dir="rtl"] .flex.flex-wrap.items-center.gap-2.text-sm { + direction: rtl; +} + +/* Flip the arrow in status change rows */ +html[dir="rtl"] .flex.flex-wrap.items-center.gap-2.text-sm .lucide-arrow-right { + transform: scaleX(-1); +} + +/* -------------------------------------------------------------------------- + PROPERTIES PANEL (aside) - RTL fixes + -------------------------------------------------------------------------- */ + +/* Fix truncation direction: show start of text, not end */ +html[dir="rtl"] aside .truncate { + direction: rtl; + text-align: right; + text-overflow: ellipsis; +} + +/* Widen properties panel to fill the gap */ +html[dir="rtl"] aside.hidden.md\:flex { + width: 460px !important; +} +html[dir="rtl"] aside .w-80 { + width: 460px; + min-width: 460px; +} + +/* Ensure property labels don't shrink too much */ +html[dir="rtl"] aside .shrink-0.w-20 { + width: 5.5rem; +} + +/* Properties panel rows: reverse flex so label is on the right */ +html[dir="rtl"] aside .flex.items-center.gap-3.py-1\.5 { + flex-direction: row-reverse; +} + +/* Property value containers: align right */ +html[dir="rtl"] aside .flex.items-center.gap-1\.5.min-w-0.flex-1 { + flex-direction: row-reverse; + justify-content: flex-start; +} + +/* Property buttons inside panel: reverse icon+text order */ +html[dir="rtl"] aside .flex.items-center.gap-1\.5.min-w-0.flex-1 button { + flex-direction: row-reverse; +} + +/* Panel header */ +html[dir="rtl"] aside .flex.items-center.justify-between { + flex-direction: row-reverse; +} + +/* Panel text alignment */ +html[dir="rtl"] aside .text-sm { + text-align: right; +} + +html[dir="rtl"] aside .text-xs.text-muted-foreground.shrink-0 { + text-align: right; +} + +/* -------------------------------------------------------------------------- + TIMELINE + COMMENT BOX - comment box at top, newest first + -------------------------------------------------------------------------- */ + +/* The tab content container that holds timeline + comment box: reverse order */ +html[dir="rtl"] [role="tabpanel"][id*="content-comments"] > .space-y-4 { + display: flex; + flex-direction: column-reverse; +} + +/* Also target the parent of timeline + comment box */ +html[dir="rtl"] main .space-y-6 > .space-y-4:last-child { + display: flex; + flex-direction: column-reverse; + gap: 1rem; +} + +/* Reverse timeline items so newest appears first */ +html[dir="rtl"] main .space-y-4 > .space-y-4 { + display: flex; + flex-direction: column-reverse; + gap: 1rem; +} diff --git a/assets/translate-he.js b/assets/translate-he.js index 31ba3ba..8048e4a 100644 --- a/assets/translate-he.js +++ b/assets/translate-he.js @@ -99,6 +99,301 @@ "Configure": "הגדר", "Choose": "בחר", + // --- Activity Log --- + "updated this task": "עדכן משימה זו", + "created this task": "יצר משימה זו", + "created this issue": "יצר משימה זו", + "updated this issue": "עדכן משימה זו", + "commented on this task": "הגיב על משימה זו", + "commented on this issue": "הגיב על משימה זו", + "assigned this task": "הקצה משימה זו", + "assigned this issue": "הקצה משימה זו", + "changed status": "שינה סטטוס", + "changed priority": "שינה עדיפות", + "unassigned this task": "ביטל הקצאת משימה", + "checked out this task": "לקח משימה", + "released this task": "שחרר משימה", + "just now": "הרגע", + "1m ago": "לפני דקה", + "2m ago": "לפני 2 דקות", + "3m ago": "לפני 3 דקות", + "5m ago": "לפני 5 דקות", + "10m ago": "לפני 10 דקות", + "15m ago": "לפני 15 דקות", + "20m ago": "לפני 20 דקות", + "25m ago": "לפני 25 דקות", + "28m ago": "לפני 28 דקות", + "30m ago": "לפני 30 דקות", + "45m ago": "לפני 45 דקות", + "1h ago": "לפני שעה", + "2h ago": "לפני שעתיים", + "3h ago": "לפני 3 שעות", + "yesterday": "אתמול", + "You": "אני", + "Copy as markdown": "העתק כ-markdown", + + // --- Run Statuses --- + "succeeded": "הצליח", + "failed": "נכשל", + "running": "רץ", + "timed_out": "חרג מזמן", + "queued": "בתור", + "skipped": "דולג", + "blocked": "חסום", + "completed": "הושלם", + "cancelled": "בוטל", + "started": "התחיל", + + // --- Activity Log Actions --- + "started a run": "התחיל ריצה", + "completed a run": "השלים ריצה", + "checked out": "לקח לטיפול", + "released": "שחרר", + "moved to": "עבר ל", + "changed status to": "שינה סטטוס ל", + "assigned to": "הוקצה ל", + "unassigned from": "בוטלה הקצאה מ", + "added a comment": "הוסיף תגובה", + "ran": "הריץ", + "set priority": "קבע עדיפות", + "set status": "קבע סטטוס", + + // --- Run Details --- + "Transcript": "תמליל", + "No transcript captured": "לא נקלט תמליל", + "No transcript captured.": "לא נקלט תמליל.", + "Run details": "פרטי ריצה", + "Duration": "משך", + "Tokens": "טוקנים", + "Cost": "עלות", + "Model": "מודל", + "Turns": "סיבובים", + "Exit code": "קוד יציאה", + "Error": "שגיאה", + "Heartbeat": "פעימה", + "Heartbeats": "פעימות", + "Latest run": "ריצה אחרונה", + "All runs": "כל הריצות", + "No runs yet": "אין ריצות עדיין", + "No runs yet.": "אין ריצות עדיין.", + "Run": "ריצה", + "Runs": "ריצות", + "Input tokens": "טוקני קלט", + "Output tokens": "טוקני פלט", + "Cache read": "קריאה ממטמון", + "Cache write": "כתיבה למטמון", + "Total cost": "עלות כוללת", + "Session": "סשן", + "Agent": "סוכן", + "Source": "מקור", + "Trigger": "טריגר", + "on_demand": "לפי דרישה", + "manual": "ידני", + "timer": "טיימר", + "assignment": "הקצאה", + "automation": "אוטומציה", + + // --- Filter/Context --- + "For": "עבור", + "for": "עבור", + "in": "ב", + "In": "ב", + "of": "של", + "to": "ל", + "by": "על ידי", + "from": "מ", + "with": "עם", + "and": "ו", + "or": "או", + "on": "ב", + "at": "ב", + "as": "בתור", + "via": "דרך", + "No results": "אין תוצאות", + "No results.": "אין תוצאות.", + "No items": "אין פריטים", + "No data": "אין נתונים", + + // --- Settings Page --- + "General Settings": "הגדרות כלליות", + "Server Configuration": "הגדרות שרת", + "Deployment Mode": "מצב פריסה", + "Exposure": "חשיפה", + "Allowed Hostnames": "שמות מארח מורשים", + "Public Base URL": "כתובת URL ציבורית", + "Disable Sign Up": "ביטול הרשמה", + "Disable sign up": "ביטול הרשמה", + "Backup Settings": "הגדרות גיבוי", + "Backup Interval": "מרווח גיבוי", + "Retention Days": "ימי שמירה", + "Database Mode": "מצב מסד נתונים", + "Embedded PostgreSQL": "PostgreSQL מובנה", + "Storage Provider": "ספק אחסון", + "Local Disk": "דיסק מקומי", + "Secret Provider": "ספק סודות", + "Logging Mode": "מצב לוגים", + "Log Directory": "תיקיית לוגים", + "Save Changes": "שמור שינויים", + "Configuration saved": "ההגדרות נשמרו", + "Configuration saved.": "ההגדרות נשמרו.", + "Restart required": "נדרש הפעלה מחדש", + "Instance": "מופע", + "Server": "שרת", + "Auth": "אימות", + "Authentication": "אימות", + "Sign-up": "הרשמה", + "Allowed": "מורשה", + "Port": "פורט", + "Host": "מארח", + "public": "ציבורי", + "private": "פרטי", + "authenticated": "מאומת", + "enabled": "מופעל", + "disabled": "מושבת", + "Database": "מסד נתונים", + "Backups": "גיבויים", + "Storage": "אחסון", + "Secrets": "סודות", + "Logging": "לוגים", + "Server port": "פורט שרת", + "Listen host": "מארח האזנה", + "Base URL": "כתובת בסיס", + "Interval (minutes)": "מרווח (דקות)", + "Retention (days)": "שמירה (ימים)", + "Data directory": "תיקיית נתונים", + "Backup directory": "תיקיית גיבוי", + "Key file path": "נתיב קובץ מפתח", + "Mode": "מצב", + "Provider": "ספק", + "Path": "נתיב", + "Danger Zone": "אזור סכנה", + + // --- Experimental Page --- + "Opt into features that are still being evaluated before they become default behavior.": "הצטרף לפיצ׳רים שעדיין בהערכה לפני שהם הופכים לברירת מחדל.", + "Enable Isolated Workspaces": "הפעל סביבות עבודה מבודדות", + "Show execution workspace controls in project configuration and allow isolated workspace behavior for new and existing issue runs.": "הצג אפשרויות סביבת עבודה בהגדרות פרויקט ואפשר התנהגות מבודדת לריצות חדשות וקיימות.", + "Auto-Restart Dev Server When Idle": "הפעלה מחדש אוטומטית של שרת פיתוח בזמן חוסר פעילות", + "In `pnpm dev:once`, wait for all queued and running local agent runs to finish, then restart the server automatically when backend changes or migrations make the current boot stale.": "ב-pnpm dev:once, המתן לסיום כל ריצות הסוכנים בתור וברצף, ואז הפעל מחדש אוטומטית כששינויים בשרת הופכים את האתחול הנוכחי למיושן.", + + // --- Plugin Status Page --- + "Runtime Dashboard": "לוח בקרה תפעולי", + "Worker process, scheduled jobs, and webhook deliveries": "תהליך עובד, משימות מתוזמנות ומשלוחי webhook", + "Worker Process": "תהליך עובד", + "Pending RPCs": "קריאות ממתינות", + "Uptime": "זמן פעילות", + "Recent Job Runs": "ריצות אחרונות", + "Recent Webhook Deliveries": "משלוחי webhook אחרונים", + "No webhook deliveries recorded yet.": "טרם נרשמו משלוחי webhook.", + "Last checked": "בדיקה אחרונה", + "Health Status": "מצב בריאות", + "Overall": "כולל", + "ready": "מוכן", + "error": "שגיאה", + "registry": "רישום", + "manifest": "מניפסט", + "error_state": "מצב שגיאה", + "Permissions": "הרשאות", + "Details": "פרטים", + "Plugin ID": "מזהה תוסף", + "Plugin Key": "מפתח תוסף", + "NPM Package": "חבילת NPM", + "Version": "גרסה", + "Configuration": "הגדרות", + "Status": "סטטוס", + + // --- Action Buttons --- + "Disable All": "השבת הכל", + "Enable All": "הפעל הכל", + "Disable all": "השבת הכל", + "Enable all": "הפעל הכל", + "Select All": "בחר הכל", + "Select all": "בחר הכל", + "Deselect All": "בטל בחירה", + "Deselect all": "בטל בחירה", + "Clear All": "נקה הכל", + "Clear all": "נקה הכל", + "Remove All": "הסר הכל", + "Remove all": "הסר הכל", + "Delete All": "מחק הכל", + "Delete all": "מחק הכל", + "Refresh": "רענן", + "Reload": "טען מחדש", + "Load More": "טען עוד", + "Load more": "טען עוד", + "Show More": "הצג עוד", + "Show more": "הצג עוד", + "Show Less": "הצג פחות", + "Show less": "הצג פחות", + "View All": "הצג הכל", + "View all": "הצג הכל", + "Expand All": "הרחב הכל", + "Expand all": "הרחב הכל", + "Collapse All": "צמצם הכל", + "Collapse all": "צמצם הכל", + "Copy ID": "העתק מזהה", + "Copy URL": "העתק כתובת", + "Go to": "עבור אל", + "Jump to": "קפוץ אל", + "Mark all": "סמן הכל", + "Danger zone": "אזור סכנה", + "Reset": "איפוס", + "Reset instance": "איפוס מופע", + "This action cannot be undone.": "לא ניתן לבטל פעולה זו.", + "Are you sure?": "האם אתה בטוח?", + + // --- Settings: General Tab --- + "Configure instance-wide defaults that affect how operator-visible logs are displayed.": "הגדר ברירות מחדל ברמת המופע שמשפיעות על אופן הצגת הלוגים.", + "Censor username in logs": "הסתר שם משתמש בלוגים", + "Hide the username segment in home-directory paths and similar operator-visible log output. Standalone username mentions outside of paths are not yet masked in the live transcript view. This is off by default.": "הסתר את שם המשתמש בנתיבי תיקיות ובפלט לוגים. אזכורי שם משתמש עצמאיים מחוץ לנתיבים עדיין אינם מוסתרים בתצוגת התמליל החי. מושבת כברירת מחדל.", + + // --- Settings: Keyboard Shortcuts --- + "Keyboard Shortcuts": "קיצורי מקלדת", + "Enable app keyboard shortcuts, including inbox navigation and global shortcuts like creating issues or toggling panels. This is off by default.": "הפעל קיצורי מקלדת באפליקציה, כולל ניווט בתיבת הדואר וקיצורים גלובליים כמו יצירת משימות או מעבר בין פנלים. מושבת כברירת מחדל.", + + // --- Settings: AI Feedback --- + "AI Feedback Sharing": "שיתוף משוב AI", + "Control whether thumbs up and thumbs down votes can send the voted AI output to Paperclip Labs. Votes are always saved locally.": "קבע אם הצבעות אגודל למעלה ולמטה ישלחו את פלט ה-AI ל-Paperclip Labs. ההצבעות תמיד נשמרות מקומית.", + "Read our Terms of Service": "קרא את תנאי השירות שלנו", + "No default is saved yet. The next thumbs up or thumbs down choice will ask once and then save the answer here.": "טרם נשמרה בחירת ברירת מחדל. ההצבעה הבאה תשאל פעם אחת ותשמור את התשובה כאן.", + "Always allow": "אפשר תמיד", + "Always Allow": "אפשר תמיד", + "Share voted AI outputs automatically.": "שתף פלטי AI שנבחרו אוטומטית.", + "Never allow": "אל תאפשר", + "Never Allow": "אל תאפשר", + "Keep voted AI outputs local only.": "שמור פלטי AI שנבחרו מקומית בלבד.", + "To retest the first-use prompt in local dev, remove the feedbackDataSharingPreference key from the instance_settings.general JSON row for this instance, or set it back to \"prompt\". Unset and \"prompt\" both mean no default has been chosen yet.": "לבדיקה מחדש של ההנחיה הראשונית, הסר את המפתח feedbackDataSharingPreference מטבלת instance_settings או החזר ל-prompt.", + + // --- Tabs & Sections --- + "Sub-issues": "תת-משימות", + "Subissues": "תת-משימות", + "sub-issues": "תת-משימות", + "Work Products": "תוצרי עבודה", + "Linked": "מקושר", + "Related": "קשור", + "Parent": "משימת אב", + "Timeline": "ציר זמן", + "History": "היסטוריה", + "Approvals": "אישורים", + + // --- Comments Section --- + "No comments yet": "אין תגובות עדיין", + "No comments yet.": "אין תגובות עדיין.", + "Add a comment": "הוסף תגובה", + "Write a comment": "כתוב תגובה", + "Write a comment...": "כתוב תגובה...", + "Type a comment": "הקלד תגובה", + "Type a comment...": "הקלד תגובה...", + + // --- Activity Section --- + "No activity yet": "אין פעילות עדיין", + "No activity yet.": "אין פעילות עדיין.", + + // --- Sub-issues Section --- + "No sub-issues": "אין תת-משימות", + "No sub-issues.": "אין תת-משימות.", + "Create sub-issue": "צור תת-משימה", + "Add sub-issue": "הוסף תת-משימה", + // --- Action Buttons --- "Save changes": "שמור שינויים", "Save Configuration": "שמור הגדרות", @@ -503,6 +798,8 @@ "No recent agent runs.": "אין הרצות סוכנים אחרונות.", "No recent issues.": "אין משימות אחרונות.", "No sub-goals.": "אין תת-יעדים.", + "Sub-Goals": "תת-יעדים", + "Sub Goal": "תת-יעד", "No sub-issues.": "אין תת-משימות.", "None": "ללא", "Unassigned": "לא מוקצה", @@ -739,14 +1036,65 @@ * Translate a text string if a translation exists. * Uses exact match first, then case-insensitive. */ + // Regex patterns for dynamic time strings + const TIME_PATTERNS = [ + [/^(\d+)m ago$/, (m) => `לפני ${m[1]} דקות`], + [/^(\d+)h ago$/, (m) => `לפני ${m[1]} שעות`], + [/^(\d+)d ago$/, (m) => `לפני ${m[1]} ימים`], + [/^(\d+)s ago$/, (m) => `לפני ${m[1]} שניות`], + [/^(\d+) minutes? ago$/, (m) => `לפני ${m[1]} דקות`], + [/^(\d+) hours? ago$/, (m) => `לפני ${m[1]} שעות`], + [/^(\d+) days? ago$/, (m) => `לפני ${m[1]} ימים`], + [/^just now$/, () => "הרגע"], + [/^yesterday$/, () => "אתמול"], + // Tabs with counts + [/^Sub-Goals\s*\((\d+)\)$/, (m) => `תת-יעדים (${m[1]})`], + [/^Projects\s*\((\d+)\)$/, (m) => `פרויקטים (${m[1]})`], + [/^Issues\s*\((\d+)\)$/, (m) => `משימות (${m[1]})`], + [/^Agents\s*\((\d+)\)$/, (m) => `סוכנים (${m[1]})`], + [/^Members\s*\((\d+)\)$/, (m) => `חברים (${m[1]})`], + [/^Comments\s*\((\d+)\)$/, (m) => `תגובות (${m[1]})`], + [/^Documents\s*\((\d+)\)$/, (m) => `מסמכים (${m[1]})`], + [/^Routines\s*\((\d+)\)$/, (m) => `שגרות (${m[1]})`], + [/^Skills\s*\((\d+)\)$/, (m) => `כישורים (${m[1]})`], + [/^Labels\s*\((\d+)\)$/, (m) => `תוויות (${m[1]})`], + ]; + + // Regex patterns for date strings + const DATE_MONTHS = { + "Jan": "ינו׳", "Feb": "פבר׳", "Mar": "מרץ", "Apr": "אפר׳", + "May": "מאי", "Jun": "יוני", "Jul": "יולי", "Aug": "אוג׳", + "Sep": "ספט׳", "Oct": "אוק׳", "Nov": "נוב׳", "Dec": "דצמ׳" + }; + function translate(text) { - const trimmed = text.trim(); + const trimmed = text.trim().replace(/\s+/g, ' '); if (!trimmed) return null; // Exact match if (T[trimmed] !== undefined) return T[trimmed]; // Case-insensitive match const lk = trimmed.toLowerCase(); if (lowerMap[lk]) return T[lowerMap[lk]]; + // Regex time patterns + for (const [pattern, replacer] of TIME_PATTERNS) { + const match = trimmed.match(pattern); + if (match) return replacer(match); + } + // Date format with time: "Apr 7, 2026, 6:02 PM" → "7 אפר׳ 2026, 18:02" + const dateTimeMatch = trimmed.match(/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+),\s+(\d{4}),\s+(\d+):(\d+)\s+(AM|PM)$/); + if (dateTimeMatch) { + const [, mon, day, year, hr, min, ampm] = dateTimeMatch; + let hour = parseInt(hr); + if (ampm === "PM" && hour !== 12) hour += 12; + if (ampm === "AM" && hour === 12) hour = 0; + return `${day} ${DATE_MONTHS[mon] || mon} ${year}, ${hour.toString().padStart(2,"0")}:${min}`; + } + // Date format without time: "Apr 7, 2026" → "7 אפר׳ 2026" + const dateMatch = trimmed.match(/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+),\s+(\d{4})$/); + if (dateMatch) { + const [, mon, day, year] = dateMatch; + return `${day} ${DATE_MONTHS[mon] || mon} ${year}`; + } return null; } @@ -786,7 +1134,13 @@ const original = node.nodeValue; const translated = translate(original); if (translated !== null && translated !== original) { - node.nodeValue = translated; + // Add space when text follows a number in a sibling span (e.g. "0" + "active" → "0 פעיל") + const prev = node.previousSibling; + if (prev && prev.nodeType === 1 && /^\d+$/.test(prev.textContent.trim())) { + node.nodeValue = " " + translated; + } else { + node.nodeValue = translated; + } } } } @@ -820,9 +1174,25 @@ /** * Full translation pass on the entire document. */ + /** + * Translate tab buttons and other elements whose textContent contains + * dynamic patterns like "Sub-Goals (0)" that text node walking misses. + */ + function translateButtonLabels(root) { + const buttons = root.querySelectorAll('button[role="tab"], button[data-slot="tabs-trigger"]'); + buttons.forEach(function(btn) { + const text = btn.textContent.trim().replace(/\s+/g, ' '); + const t = translate(text); + if (t && t !== text) { + btn.textContent = t; + } + }); + } + function translateAll() { translateTextNodes(document.body); translateAttributes(document.body); + translateButtonLabels(document.body); // Translate page title if (document.title) { const t = translate(document.title); @@ -909,10 +1279,58 @@ setTimeout(translateAll, 4000); } + // ========================================================================= + // COMMENT ORDER — newest first + // ========================================================================= + function reverseComments() { + // Find comment containers by anchor IDs + const commentAnchors = document.querySelectorAll('[id^="comment-"]'); + if (!commentAnchors.length) return; + + // Group by parent container + const parents = new Set(); + commentAnchors.forEach(el => { + const parent = el.parentElement; + if (parent && !parent.dataset.reversed && parent.children.length > 1) { + parents.add(parent); + } + }); + + parents.forEach(parent => { + const children = Array.from(parent.children); + children.reverse().forEach(child => parent.appendChild(child)); + parent.dataset.reversed = "true"; + }); + } + + // Re-run on route changes (SPA) + let lastUrl = location.href; + const urlObserver = new MutationObserver(() => { + if (location.href !== lastUrl) { + lastUrl = location.href; + // Reset reversed flags on navigation + document.querySelectorAll('[data-reversed]').forEach(el => { + delete el.dataset.reversed; + }); + setTimeout(reverseComments, 500); + } + }); + + function initCommentOrder() { + urlObserver.observe(document.body, { childList: true, subtree: true }); + // Also run after each mutation to catch late-loading comments + const commentMutObs = new MutationObserver(() => { + setTimeout(reverseComments, 200); + }); + commentMutObs.observe(document.body, { childList: true, subtree: true }); + setTimeout(reverseComments, 1000); + } + // Start when DOM is ready if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); + document.addEventListener("DOMContentLoaded", () => { init(); initCommentOrder(); }); } else { init(); + initCommentOrder(); } })();