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) <noreply@anthropic.com>
This commit is contained in:
Chaim
2026-04-08 22:18:16 +00:00
parent 5160741248
commit 09f0327055
2 changed files with 743 additions and 3 deletions

View File

@@ -739,3 +739,325 @@ html[dir="rtl"] [class*="arrow-left"] svg,
html[dir="rtl"] [class*="arrow-right"] svg { html[dir="rtl"] [class*="arrow-right"] svg {
transform: scaleX(-1); 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;
}

View File

@@ -99,6 +99,301 @@
"Configure": "הגדר", "Configure": "הגדר",
"Choose": "בחר", "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 --- // --- Action Buttons ---
"Save changes": "שמור שינויים", "Save changes": "שמור שינויים",
"Save Configuration": "שמור הגדרות", "Save Configuration": "שמור הגדרות",
@@ -503,6 +798,8 @@
"No recent agent runs.": "אין הרצות סוכנים אחרונות.", "No recent agent runs.": "אין הרצות סוכנים אחרונות.",
"No recent issues.": "אין משימות אחרונות.", "No recent issues.": "אין משימות אחרונות.",
"No sub-goals.": "אין תת-יעדים.", "No sub-goals.": "אין תת-יעדים.",
"Sub-Goals": "תת-יעדים",
"Sub Goal": "תת-יעד",
"No sub-issues.": "אין תת-משימות.", "No sub-issues.": "אין תת-משימות.",
"None": "ללא", "None": "ללא",
"Unassigned": "לא מוקצה", "Unassigned": "לא מוקצה",
@@ -739,14 +1036,65 @@
* Translate a text string if a translation exists. * Translate a text string if a translation exists.
* Uses exact match first, then case-insensitive. * 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) { function translate(text) {
const trimmed = text.trim(); const trimmed = text.trim().replace(/\s+/g, ' ');
if (!trimmed) return null; if (!trimmed) return null;
// Exact match // Exact match
if (T[trimmed] !== undefined) return T[trimmed]; if (T[trimmed] !== undefined) return T[trimmed];
// Case-insensitive match // Case-insensitive match
const lk = trimmed.toLowerCase(); const lk = trimmed.toLowerCase();
if (lowerMap[lk]) return T[lowerMap[lk]]; 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; return null;
} }
@@ -786,10 +1134,16 @@
const original = node.nodeValue; const original = node.nodeValue;
const translated = translate(original); const translated = translate(original);
if (translated !== null && translated !== original) { if (translated !== null && translated !== original) {
// 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; node.nodeValue = translated;
} }
} }
} }
}
/** /**
* Translate placeholder and aria-label attributes. * Translate placeholder and aria-label attributes.
@@ -820,9 +1174,25 @@
/** /**
* Full translation pass on the entire document. * 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() { function translateAll() {
translateTextNodes(document.body); translateTextNodes(document.body);
translateAttributes(document.body); translateAttributes(document.body);
translateButtonLabels(document.body);
// Translate page title // Translate page title
if (document.title) { if (document.title) {
const t = translate(document.title); const t = translate(document.title);
@@ -909,10 +1279,58 @@
setTimeout(translateAll, 4000); 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 // Start when DOM is ready
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", () => { init(); initCommentOrder(); });
} else { } else {
init(); init();
initCommentOrder();
} }
})(); })();