diff --git a/artifacts/teaboy-os/public/opengraph.jpg b/artifacts/teaboy-os/public/opengraph.jpg index 530741e0..7dfb16c8 100644 Binary files a/artifacts/teaboy-os/public/opengraph.jpg and b/artifacts/teaboy-os/public/opengraph.jpg differ diff --git a/artifacts/teaboy-os/src/lib/i18n-format.ts b/artifacts/teaboy-os/src/lib/i18n-format.ts new file mode 100644 index 00000000..30e6c1fc --- /dev/null +++ b/artifacts/teaboy-os/src/lib/i18n-format.ts @@ -0,0 +1,101 @@ +/** + * Locale-aware date/number formatters that ALWAYS render Latin (0-9) digits, + * even when the active language is Arabic. Month and weekday names still + * localize to Arabic as expected. + */ + +function withLatn(locale: string): string { + return locale.includes("-u-") ? `${locale}-nu-latn` : `${locale}-u-nu-latn`; +} + +function baseLocale(lang: string): string { + return lang === "ar" ? "ar" : "en-US"; +} + +export function formatTime( + date: Date | string | number, + lang: string, + options: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" }, +): string { + const d = date instanceof Date ? date : new Date(date); + return new Intl.DateTimeFormat(withLatn(baseLocale(lang)), { + ...options, + hour12: false, + numberingSystem: "latn", + }).format(d); +} + +export function formatDate( + date: Date | string | number, + lang: string, + options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "long", + year: "numeric", + }, +): string { + const d = date instanceof Date ? date : new Date(date); + return new Intl.DateTimeFormat(withLatn(baseLocale(lang)), { + ...options, + numberingSystem: "latn", + }).format(d); +} + +export function formatHijri( + date: Date | string | number, + lang: string, + options: Intl.DateTimeFormatOptions = { + day: "numeric", + month: "long", + year: "numeric", + }, +): string { + const d = date instanceof Date ? date : new Date(date); + const loc = lang === "ar" ? "ar-u-ca-islamic-umalqura-nu-latn" : "en-u-ca-islamic-umalqura-nu-latn"; + return new Intl.DateTimeFormat(loc, { + ...options, + numberingSystem: "latn", + }).format(d); +} + +export function formatWeekday( + date: Date | string | number, + lang: string, + weekday: "long" | "short" | "narrow" = "long", +): string { + const d = date instanceof Date ? date : new Date(date); + return new Intl.DateTimeFormat(withLatn(baseLocale(lang)), { + weekday, + numberingSystem: "latn", + }).format(d); +} + +export function formatDateTime( + date: Date | string | number, + lang: string, +): string { + const d = date instanceof Date ? date : new Date(date); + return new Intl.DateTimeFormat(withLatn(baseLocale(lang)), { + dateStyle: "medium", + timeStyle: "short", + numberingSystem: "latn", + hour12: false, + }).format(d); +} + +export function formatNumber(value: number, lang: string): string { + return new Intl.NumberFormat(withLatn(baseLocale(lang)), { + numberingSystem: "latn", + }).format(value); +} + +/** + * Localized time-of-day greeting key. Uses the local hour to pick one of + * "morning", "afternoon", or "evening". Caller provides the i18n strings. + */ +export function greetingKey(date: Date = new Date()): "morning" | "afternoon" | "evening" { + const h = date.getHours(); + if (h < 12) return "morning"; + if (h < 18) return "afternoon"; + return "evening"; +} diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 706bff69..466080a4 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -35,7 +35,20 @@ "rights": "جميع الحقوق محفوظة." }, "home": { - "myApps": "تطبيقاتي" + "myApps": "تطبيقاتي", + "noApps": "لا توجد تطبيقات بعد", + "noAppsHint": "ستظهر هنا التطبيقات التي تملك صلاحية الوصول إليها.", + "greeting": { + "morning": "صباح الخير، {{name}}", + "afternoon": "مساء الخير، {{name}}", + "evening": "مساء الخير، {{name}}" + }, + "stats": { + "apps": "التطبيقات", + "services": "الخدمات", + "messages": "الرسائل", + "alerts": "التنبيهات" + } }, "services": { "title": "الخدمات", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index bfc6b059..eb4a5492 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -35,7 +35,20 @@ "rights": "All rights reserved." }, "home": { - "myApps": "My Apps" + "myApps": "My Apps", + "noApps": "No apps yet", + "noAppsHint": "Apps you have access to will appear here.", + "greeting": { + "morning": "Good morning, {{name}}", + "afternoon": "Good afternoon, {{name}}", + "evening": "Good evening, {{name}}" + }, + "stats": { + "apps": "Apps", + "services": "Services", + "messages": "Messages", + "alerts": "Alerts" + } }, "services": { "title": "Services", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 70f682cc..d1c1deb7 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -36,6 +36,7 @@ import { Switch } from "@/components/ui/switch"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { useToast } from "@/hooks/use-toast"; import { cn } from "@/lib/utils"; +import { formatDate as formatDateUtil, formatWeekday as formatWeekdayUtil } from "@/lib/i18n-format"; type AdminSection = "dashboard" | "apps" | "services" | "users" | "settings"; @@ -778,15 +779,10 @@ function DashboardSection({ ? [...apps].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""))[0] : undefined; - const dateLocale = lang === "ar" ? "ar" : "en"; const formatDate = (iso?: string) => { if (!iso) return ""; try { - return new Date(iso).toLocaleDateString(dateLocale, { - year: "numeric", - month: "short", - day: "numeric", - }); + return formatDateUtil(iso, lang, { year: "numeric", month: "short", day: "numeric" }); } catch { return ""; } @@ -814,9 +810,7 @@ function DashboardSection({ const maxSignup = Math.max(1, ...signups.map((s) => s.count)); const dayLabel = (iso: string) => { try { - return new Date(iso + "T00:00:00Z").toLocaleDateString(dateLocale, { - weekday: "short", - }); + return formatWeekdayUtil(new Date(iso + "T00:00:00Z"), lang, "short"); } catch { return ""; } diff --git a/artifacts/teaboy-os/src/pages/chat.tsx b/artifacts/teaboy-os/src/pages/chat.tsx index 78de046b..c6d49a28 100644 --- a/artifacts/teaboy-os/src/pages/chat.tsx +++ b/artifacts/teaboy-os/src/pages/chat.tsx @@ -16,6 +16,7 @@ import { io } from "socket.io-client"; import { ArrowRight, ArrowLeft, MessageCircle, Plus, Send } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { formatTime } from "@/lib/i18n-format"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -265,10 +266,7 @@ export default function ChatPage() { {msg.content}