From d231cca447e157f8aa8b33632802f9e78baa68b8 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Tue, 30 Jun 2026 17:57:04 +0300 Subject: [PATCH 01/15] fix: pagina facultati --- .../icons/svg/arrow-to-bottom-stroke.svg | 1 + Frontend/Mobile/assets/icons/svg/file.svg | 1 + .../src/app/(public)/acasa/categorie.tsx | 2 +- .../src/app/(public)/acasa/categorie.web.tsx | 2 +- .../src/app/(public)/acasa/vizualizare.tsx | 10 ++- .../src/app/(public)/anunt/[id].web.tsx | 1 + .../src/app/(public)/eveniment/[id].web.tsx | 1 + .../components/ui/display/article-detail.tsx | 7 ++ .../components/ui/display/file-attachment.tsx | 73 +++++++++++++++++++ 9 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 Frontend/Mobile/assets/icons/svg/arrow-to-bottom-stroke.svg create mode 100644 Frontend/Mobile/assets/icons/svg/file.svg create mode 100644 Frontend/Mobile/src/components/ui/display/file-attachment.tsx diff --git a/Frontend/Mobile/assets/icons/svg/arrow-to-bottom-stroke.svg b/Frontend/Mobile/assets/icons/svg/arrow-to-bottom-stroke.svg new file mode 100644 index 00000000..a109b886 --- /dev/null +++ b/Frontend/Mobile/assets/icons/svg/arrow-to-bottom-stroke.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Mobile/assets/icons/svg/file.svg b/Frontend/Mobile/assets/icons/svg/file.svg new file mode 100644 index 00000000..259d60e5 --- /dev/null +++ b/Frontend/Mobile/assets/icons/svg/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index e8a9d3b2..4f9cabdd 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -120,7 +120,7 @@ export default function CategoryScreen() { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), title: item.name || "Titlu necunoscut", - image: item.image_url || undefined, + image: item.logo_url || undefined, address: item.address || "Adresă necunoscută", phone: item.phone || "", website: item.website_url || "", diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index 4c61d794..adaf2959 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -115,7 +115,7 @@ export default function CategoryScreen() { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), title: item.name || "Titlu necunoscut", - image: item.image_url || undefined, + image: item.logo_url || undefined, address: item.address || "Adresă necunoscută", phone: item.phone || "", website: item.website_url || "", diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index 5f278a43..c59cee84 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -18,6 +18,7 @@ import PhoneIcon from "@/assets/icons/svg/phone.svg"; import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg"; import { CategoryTag } from "@/components/ui/display/news-card"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; +import { FileAttachments } from "@/components/ui/display/file-attachment"; const DAY_NAMES = ["", "Luni", "Marți", "Miercuri", "Joi", "Vineri", "Sâmbătă", "Duminică"]; @@ -135,6 +136,7 @@ function VizualizareScreen() { author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, + files: item.files || [], }; } } else if (initialTipPagina === "Facultate") { @@ -145,7 +147,7 @@ function VizualizareScreen() { id: item.id.toString(), type: "Facultate", title: item.name || "Titlu necunoscut", - image: item.image_url || "", + image: item.logo_url || "", address: item.address || "Adresă necunoscută", phone: item.phone || "", website: item.website_url || "", @@ -187,7 +189,7 @@ function VizualizareScreen() { if (match && isMounted) { let mappedItem: any = null; if (isFaculty) { - mappedItem = { id: match.id.toString(), type: "Facultate", title: match.name || "Titlu necunoscut", image: match.image_url || "", address: match.address || "Adresă necunoscută", phone: match.phone || "", website: match.website_url || "", content: match.description || "Conținut necunoscut" }; + mappedItem = { id: match.id.toString(), type: "Facultate", title: match.name || "Titlu necunoscut", image: match.logo_url || "", address: match.address || "Adresă necunoscută", phone: match.phone || "", website: match.website_url || "", content: match.description || "Conținut necunoscut" }; } else if (isFacility) { mappedItem = { id: match.id.toString(), type: "Facilitate", title: match.name || "Titlu necunoscut", image: match.image_url || "", content: match.description || "", schedules: match.schedules || [] }; } else { @@ -487,6 +489,10 @@ function VizualizareScreen() { {content || "Conținut necunoscut"} + + {(tipPagina === "Anunț" || tipPagina === "Eveniment") && ( + + )} diff --git a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx index 7d26f82a..fc41f2a1 100644 --- a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx @@ -96,6 +96,7 @@ export default function AnuntScreen() { date={date} posted_at={date} author={author} + files={item.files || []} /> ); diff --git a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx index cd02e3cf..d0bb675b 100644 --- a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx @@ -107,6 +107,7 @@ export default function EvenimentScreen() { time_start={time_start} time_end={time_end} date={date} + files={ev.files || []} /> ); diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index b505e672..49c0267e 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -27,6 +27,7 @@ import CalendarIcon from "@/assets/icons/svg/calendar.svg"; import LocationIcon from "@/assets/icons/svg/location.svg"; import PhoneIcon from "@/assets/icons/svg/phone.svg"; import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg"; +import { FileAttachments, type FileItem } from "@/components/ui/display/file-attachment"; // Latimea coloanei din dreapta (sidebar) cand layout-ul e pe doua coloane. const SIDEBAR_WIDTH = 340; @@ -50,6 +51,7 @@ export interface ArticleDetailProps { website?: string; date?: string; author?: string; + files?: FileItem[]; } export function ArticleDetail({ @@ -69,6 +71,7 @@ export function ArticleDetail({ website = "", date = "", author = "", + files, }: ArticleDetailProps) { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; @@ -337,6 +340,10 @@ export function ArticleDetail({ {content || "Conținutul nu este disponibil."} + + {(tipPagina === "Anunț" || tipPagina === "Eveniment") && ( + + )} {/* Dreapta: 3 carduri Noutăți, una sub alta. */} diff --git a/Frontend/Mobile/src/components/ui/display/file-attachment.tsx b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx new file mode 100644 index 00000000..3fa93108 --- /dev/null +++ b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx @@ -0,0 +1,73 @@ +import { Platform, Linking, TouchableOpacity, View, Text } from "react-native"; +import { Colors, ColorScheme, Spacing } from "@/constants/theme"; +import { Typography } from "@/constants/typography"; +import { useColorScheme } from "@/hooks/use-color-scheme"; +import FileIcon from "@/assets/icons/svg/file.svg"; +import DownloadIcon from "@/assets/icons/svg/arrow-to-bottom-stroke.svg"; + +export type FileItem = { name: string; url: string }; + +function download(url: string) { + if (Platform.OS === "web") { + window.open(url, "_blank", "noopener,noreferrer"); + } else { + Linking.openURL(url); + } +} + +function FileCard({ file }: { file: FileItem }) { + const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; + const theme = Colors[themeName]; + + return ( + download(file.url)} + activeOpacity={0.7} + style={{ alignItems: "center", width: 104 }} + > + + + + {file.name} + + + + + + + ); +} + +export function FileAttachments({ files }: { files?: FileItem[] }) { + const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; + const theme = Colors[themeName]; + + if (!files?.length) return null; + + return ( + + Fișiere atașate + + {files.map((f, i) => ( + + ))} + + + ); +} \ No newline at end of file From e71d3af7972cad661662a43204ecbf3280a6a9ea Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Tue, 30 Jun 2026 18:37:07 +0300 Subject: [PATCH 02/15] deleted: notificari --- .../src/app/(onboarding)/notificari.tsx | 79 +---- .../Mobile/src/app/(public)/acasa/_layout.tsx | 5 - .../Mobile/src/app/(public)/acasa/index.tsx | 178 +---------- .../src/app/(public)/acasa/notificari.tsx | 156 +--------- .../src/app/(public)/acasa/vizualizare.tsx | 1 + .../app/(public)/acasa/vizualizare.web.tsx | 14 +- .../Mobile/src/app/(public)/more/setari.tsx | 40 +-- .../src/app/(public)/more/setari.web.tsx | 38 +-- Frontend/Mobile/src/app/index.tsx | 2 +- .../components/ui/display/article-detail.tsx | 25 +- .../components/ui/display/notificare-card.tsx | 74 +---- .../ui/display/notificare-card.web.tsx | 101 +----- .../ui/navigation/notification-menu.tsx | 288 +----------------- .../components/ui/navigation/web-navbar.tsx | 18 +- 14 files changed, 46 insertions(+), 973 deletions(-) diff --git a/Frontend/Mobile/src/app/(onboarding)/notificari.tsx b/Frontend/Mobile/src/app/(onboarding)/notificari.tsx index 6116ff0c..a09f8fd1 100644 --- a/Frontend/Mobile/src/app/(onboarding)/notificari.tsx +++ b/Frontend/Mobile/src/app/(onboarding)/notificari.tsx @@ -1,77 +1,10 @@ +// Disabled — notifications onboarding removed +import { useEffect } from "react"; import { useRouter } from "expo-router"; -import { View, Text, Pressable, StyleSheet } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { LinearGradient } from "expo-linear-gradient"; -import { Image } from "expo-image"; -import { Spacing, ColorScheme, Colors } from "@/constants/theme"; -import { Typography } from "@/constants/typography"; -import { useColorScheme } from "@/hooks/use-color-scheme"; -import * as Notifications from "expo-notifications"; -import NotificationSvg from "@/assets/instructions/notification.svg"; +import { View } from "react-native"; export default function NotificariScreen() { const router = useRouter(); - const insets = useSafeAreaInsets(); - const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; - const theme = Colors[themeName]; - - const handleContinue = async () => { - await Notifications.requestPermissionsAsync(); - router.push("/(onboarding)/locatie"); - }; - - return ( - - - - - - - Nu rata - - - Stai la zi cu noile activități din campusul universitar. - - - - - - - - - ({ - height: 56, - borderRadius: Spacing.md, - justifyContent: "center", - alignItems: "center", - backgroundColor: theme.primary, - opacity: pressed ? 0.85 : 1, - })} - onPress={handleContinue} - > - Continuă - - - - - - - - - - ); -} \ No newline at end of file + useEffect(() => { router.replace("/(onboarding)/locatie" as any); }, []); + return ; +} diff --git a/Frontend/Mobile/src/app/(public)/acasa/_layout.tsx b/Frontend/Mobile/src/app/(public)/acasa/_layout.tsx index 31e162a7..04671c6b 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/_layout.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/_layout.tsx @@ -53,11 +53,6 @@ export default function LineupLayout() { }} /> - - ); } diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index e3e34061..fccb79ab 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -1,11 +1,9 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; -import React, { useState, useEffect, useCallback } from "react"; -import { View, Text, ScrollView, RefreshControl, Platform, Pressable, Alert, StyleSheet } from "react-native"; -import Animated, { useSharedValue, withTiming, useAnimatedStyle, useAnimatedProps, interpolateColor } from "react-native-reanimated"; +import React, { useState, useEffect } from "react"; +import { View, Text, ScrollView, RefreshControl, Alert } from "react-native"; +import Animated, { useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useRouter, useFocusEffect } from "expo-router"; -import { LinearGradient } from "expo-linear-gradient"; -import { MOCK_NOTIFICARI } from "./notificari"; +import { useRouter } from "expo-router"; import { Colors, ColorScheme, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; @@ -17,57 +15,14 @@ import { getFormattedDate, parseRomanianDate, isoToRomanianDateStr, getTodayRoma import api, { storage } from "@/services/api"; import { ErrorState } from "@/components/ui/display/error-state"; import { HomeSkeleton, CarouselSkeleton } from "@/components/ui/display/skeletons"; -import { InteractiveGlass } from "@/components/ui/layout/interactive-glass"; -import BellIcon from "@/assets/icons/svg/bell.svg"; - -const AnimatedBell = Animated.createAnimatedComponent(BellIcon); - -function hexToRgba(hex: string, alpha: number) { - const cleanHex = hex.replace("#", ""); - let r = 0, g = 0, b = 0; - if (cleanHex.length === 3) { - r = parseInt(cleanHex[0] + cleanHex[0], 16); - g = parseInt(cleanHex[1] + cleanHex[1], 16); - b = parseInt(cleanHex[2] + cleanHex[2], 16); - } else if (cleanHex.length === 6) { - r = parseInt(cleanHex.substring(0, 2), 16); - g = parseInt(cleanHex.substring(2, 4), 16); - b = parseInt(cleanHex.substring(4, 6), 16); - } - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -} - export default function HomeScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; - const headerBgColor = themeName === "light" ? ColorScheme.pureBlack : ColorScheme.pureWhite; const insets = useSafeAreaInsets(); const router = useRouter(); - const [unreadCount, setUnreadCount] = useState(0); - - useFocusEffect( - useCallback(() => { - storage.getItem('read_notification_ids').then((val) => { - let readSet = new Set(); - if (val) { - try { - const ids = JSON.parse(val); - if (Array.isArray(ids)) { - readSet = new Set(ids); - } - } catch (e) { - console.error(e); - } - } - const count = MOCK_NOTIFICARI.filter(n => !readSet.has(n.id)).length; - setUnreadCount(count); - }); - }, []) - ); - const [noutati, setNoutati] = useState([]); const [evenimente, setEvenimente] = useState([]); const [facultati, setFacultati] = useState([]); @@ -75,28 +30,11 @@ export default function HomeScreen() { const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); const [refreshing, setRefreshing] = useState(false); - const headerAnim = useSharedValue(0); const scrollY = useSharedValue(0); - const isPastThreshold = React.useRef(false); - - const headerFadeStyle = useAnimatedStyle(() => ({ - opacity: headerAnim.value, - })); - const bellAnimatedProps = useAnimatedProps(() => ({ - color: interpolateColor(headerAnim.value, [0, 1], [ColorScheme.white, theme.text]), - })); const handleScroll = (event: any) => { const offsetY = event.nativeEvent.contentOffset.y; scrollY.set(offsetY); - const headerHeight = insets.top + 56; - const threshold = HERO_HEIGHT - headerHeight; - const isPast = offsetY >= threshold; - - if (isPast !== isPastThreshold.current) { - isPastThreshold.current = isPast; - headerAnim.set(withTiming(isPast ? 1 : 0, { duration: 250 })); - } }; const fetchApiData = async () => { @@ -333,10 +271,6 @@ export default function HomeScreen() { }); }; - const handleNotificationsPress = () => { - router.push("/(public)/acasa/notificari"); - }; - const activeNoutati = noutati; const activeEvenimente = evenimente; const activeFacultati = facultati; @@ -360,110 +294,6 @@ export default function HomeScreen() { return ( - {/* Fixed Header */} - - - - - - - - {Platform.OS === 'ios' ? ( - [{ opacity: pressed ? 0.85 : 1 }]} - > - - - - {unreadCount > 0 && ( - - - {unreadCount > 9 ? "9+" : unreadCount} - - - )} - - ) : ( - [ - { - opacity: pressed ? 0.85 : 1, - width: 45, - height: 45, - borderRadius: 22.5, - backgroundColor: theme.primary, - alignItems: "center", - justifyContent: "center" - } - ]} - > - - {unreadCount > 0 && ( - - - {unreadCount > 9 ? "9+" : unreadCount} - - - )} - - )} - - >(new Set()); - - useEffect(() => { - storage.getItem('read_notification_ids').then((val) => { - if (val) { - try { - const ids = JSON.parse(val); - if (Array.isArray(ids)) { - setReadIds(new Set(ids)); - } - } catch (e) { - console.error(e); - } - } - }); - }, []); - - const markAllRead = () => { - const allIds = new Set(MOCK_NOTIFICARI.map((n) => n.id)); - setReadIds(allIds); - storage.setItem('read_notification_ids', JSON.stringify(Array.from(allIds))); - }; - - return ( - - ( - router.back()} style={{ padding: Spacing.xs }}> - - - ), - headerRight: () => ( - - {({ pressed }) => ( - - )} - - ), - }} - /> - - - {MOCK_NOTIFICARI.map((item) => ( - { - const updated = new Set(readIds).add(item.id); - setReadIds(updated); - storage.setItem('read_notification_ids', JSON.stringify(Array.from(updated))); - if (item.actiune) { - const isWeb = item.actiune.startsWith("http://") || item.actiune.startsWith("https://"); - const isInternalDomain = item.actiune.includes("inside.ugal.ro"); - if (isWeb && !isInternalDomain) { - Linking.openURL(item.actiune).catch((err) => console.error("Couldn't open URL", err)); - } else { - router.push(item.actiune as any); - } - } - }} - /> - ))} - - - ); -} \ No newline at end of file +// Disabled — notifications screen removed +import { View } from "react-native"; +export const MOCK_NOTIFICARI: any[] = []; +export default function NotificariScreen() { return ; } diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index c59cee84..bbdbcf0e 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -472,6 +472,7 @@ function VizualizareScreen() { + Program: {formatSchedules(itemData.schedules).map((line: string, i: number) => ( {line} ))} diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index a6c4187c..790fe86e 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { View, Text, ScrollView, Linking, TouchableOpacity, Alert, useWindowDimensions, StyleSheet, type LayoutChangeEvent, ActivityIndicator } from "react-native"; +import { View, Text, ScrollView, Linking, TouchableOpacity, useWindowDimensions, StyleSheet, type LayoutChangeEvent, ActivityIndicator } from "react-native"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useLocalSearchParams, useRouter } from "expo-router"; @@ -284,14 +284,9 @@ function VizualizareScreen() { }; const handleCall = () => { - Alert.alert( - "Contact Facultate", - `Doriți să apelați numărul ${phone}?`, - [ - { text: "Anulează", style: "cancel" }, - { text: "Sună", onPress: () => Linking.openURL(`tel:${phone}`) }, - ] - ); + if (window.confirm(`Doriți să apelați numărul ${phone}?`)) { + Linking.openURL(`tel:${phone}`); + } }; const displayDateValue = category === "Noutăți" @@ -507,6 +502,7 @@ function VizualizareScreen() { + Program: {formatSchedules(itemData.schedules).map((line: string, i: number) => ( {line} ))} diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index 0a443de2..cb69d11e 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -41,9 +41,6 @@ export default function SettingsScreen() { return unsubscribe; }, []); - const [notifyStaff, setNotifyStaff] = useState(true); - const [notifyStiri, setNotifyStiri] = useState(true); - const languages = [ { code: "ro", label: "Română" }, { code: "en", label: "English" }, @@ -191,42 +188,7 @@ export default function SettingsScreen() { - {/* SECȚIUNEA 2: NOTIFICĂRI */} - - - Notificări - - - - {/* Notificare 1 (Alerte Campus) */} - - - Alerte Campus - - - - - {/* Notificare 2 (Știri & Anunțuri) */} - - - Știri & Anunțuri - - - - - - - {/* SECȚIUNEA 3: DESPRE & ACȚIUNI */} + {/* SECȚIUNEA 2: DESPRE & ACȚIUNI */} Asistență & Info diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index bd3c1d69..16778d15 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -36,9 +36,6 @@ export default function SettingsScreen() { return unsubscribe; }, []); - const [notifyStaff, setNotifyStaff] = useState(true); - const [notifyStiri, setNotifyStiri] = useState(true); - const languages = [ { code: "ro", label: "Română" }, { code: "en", label: "English" }, @@ -192,40 +189,7 @@ export default function SettingsScreen() { - {/* SECȚIUNEA 2: NOTIFICĂRI */} - - - Notificări - - - - {/* Notificare 1 (Alerte Campus) */} - - - Alerte Campus - - - - - {/* Notificare 2 (Știri & Anunțuri) */} - - - Știri & Anunțuri - - - - - - - {/* SECȚIUNEA 3: DESPRE & ACȚIUNI */} + {/* SECȚIUNEA 2: DESPRE & ACȚIUNI */} Asistență & Info diff --git a/Frontend/Mobile/src/app/index.tsx b/Frontend/Mobile/src/app/index.tsx index dece2fe2..a282a216 100644 --- a/Frontend/Mobile/src/app/index.tsx +++ b/Frontend/Mobile/src/app/index.tsx @@ -16,7 +16,7 @@ export default function SplashScreen() { if (hasSeenOnboarding === "true") { router.replace("/(public)/acasa"); } else { - router.replace("/(onboarding)/notificari"); + router.replace("/(onboarding)/locatie"); } } }; diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index 49c0267e..1d51a88a 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -7,7 +7,7 @@ // // Folosita doar din fisiere web (.web.tsx), deci nu intra in bundle-ul de mobil. import { useState, useEffect } from "react"; -import { View, Text, ScrollView, Linking, TouchableOpacity, Alert, useWindowDimensions, StyleSheet, type LayoutChangeEvent } from "react-native"; +import { View, Text, ScrollView, Linking, TouchableOpacity, useWindowDimensions, StyleSheet, type LayoutChangeEvent } from "react-native"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; @@ -182,14 +182,9 @@ export function ArticleDetail({ }; const handleCall = () => { - Alert.alert( - "Contact Facultate", - `Doriți să apelați numărul ${phone}?`, - [ - { text: "Anulează", style: "cancel" }, - { text: "Sună", onPress: () => Linking.openURL(`tel:${phone}`) }, - ] - ); + if (window.confirm(`Doriți să apelați numărul ${phone}?`)) { + Linking.openURL(`tel:${phone}`); + } }; const formattedDate = getFormattedDate(date || posted_at); @@ -271,10 +266,10 @@ export function ArticleDetail({ - + De pe {date_start || "N/A"} {time_start || ""} - + Până la {date_end || "N/A"} {time_end || ""} @@ -282,7 +277,7 @@ export function ArticleDetail({ - + {location || "Locație nespecificată"} @@ -299,7 +294,7 @@ export function ArticleDetail({ Adresă - + {address || "Nespecificată"} @@ -311,7 +306,7 @@ export function ArticleDetail({ Telefon - {phone} + {phone} @@ -323,7 +318,7 @@ export function ArticleDetail({ Website Linking.openURL(website)}> - {website} + {website} diff --git a/Frontend/Mobile/src/components/ui/display/notificare-card.tsx b/Frontend/Mobile/src/components/ui/display/notificare-card.tsx index d8cb2357..bd64f23a 100644 --- a/Frontend/Mobile/src/components/ui/display/notificare-card.tsx +++ b/Frontend/Mobile/src/components/ui/display/notificare-card.tsx @@ -1,70 +1,4 @@ -import React from "react"; -import { Pressable, View, Text } from "react-native"; -import { Colors, Spacing } from "@/constants/theme"; -import { Typography } from "@/constants/typography"; - -export interface Notificare { - id: string; - data: string; - titlu: string; - continut: string; - actiune?: string; -} - -interface NotificareCardProps { - item: Notificare; - theme: typeof Colors.light | typeof Colors.dark; - isUnread?: boolean; - onPress?: () => void; - showDivider?: boolean; - hoverBg?: string; - unreadBg?: string; -} - -export function NotificareCard({ item, theme, isUnread = false, onPress }: NotificareCardProps) { - const CardContent = ( - - - - {item.data} - - {isUnread && ( - - )} - - - {item.titlu} - - - {item.continut} - - - ); - - if (onPress) { - return ( - ({ - paddingVertical: Spacing.lg, - opacity: pressed ? 0.7 : 1, - })} - > - {CardContent} - - ); - } - - return ( - - {CardContent} - - ); -} +// Disabled — notification card removed +import { View } from "react-native"; +export interface Notificare { id: string; data: string; titlu: string; continut: string; actiune?: string; } +export function NotificareCard() { return ; } diff --git a/Frontend/Mobile/src/components/ui/display/notificare-card.web.tsx b/Frontend/Mobile/src/components/ui/display/notificare-card.web.tsx index add51f54..bd64f23a 100644 --- a/Frontend/Mobile/src/components/ui/display/notificare-card.web.tsx +++ b/Frontend/Mobile/src/components/ui/display/notificare-card.web.tsx @@ -1,97 +1,4 @@ -import React from "react"; -import { Pressable, View, Text } from "react-native"; -import { Colors, Spacing } from "@/constants/theme"; -import { Typography } from "@/constants/typography"; - -export interface Notificare { - id: string; - data: string; - titlu: string; - continut: string; - actiune?: string; -} - -interface NotificareCardProps { - item: Notificare; - theme: typeof Colors.light | typeof Colors.dark; - isUnread?: boolean; - onPress?: () => void; - showDivider?: boolean; - hoverBg?: string; - unreadBg?: string; -} - -export function NotificareCard({ - item, - theme, - isUnread = false, - onPress, - showDivider = false, - hoverBg, - unreadBg, -}: NotificareCardProps) { - - const CardContent = ( - - - - {item.data} - - {isUnread && ( - - )} - - - {item.titlu} - - - {item.continut} - - - ); - - if (onPress) { - return ( - [ - { - paddingHorizontal: Spacing.lg, - paddingVertical: Spacing.md, - }, - isUnread && unreadBg ? { backgroundColor: unreadBg } : null, - (pressed || hovered) && hoverBg ? { backgroundColor: hoverBg } : null, - ]} - > - {CardContent} - - ); - } - - return ( - - {CardContent} - - ); -} +// Disabled — notification card removed +import { View } from "react-native"; +export interface Notificare { id: string; data: string; titlu: string; continut: string; actiune?: string; } +export function NotificareCard() { return ; } diff --git a/Frontend/Mobile/src/components/ui/navigation/notification-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/notification-menu.tsx index 4dd151b2..89e0283c 100644 --- a/Frontend/Mobile/src/components/ui/navigation/notification-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/notification-menu.tsx @@ -1,285 +1,3 @@ -// Meniu de notificari (DOAR web — importat doar din web-navbar.tsx, care la randul -// lui e importat doar din _layout.web.tsx, deci nu intra in bundle-ul de mobil). -// Acelasi pattern ca ThemeMenu / ProfileMenu: -// - trigger = iconita clopotel (bell.svg), cu fundal plin cand e deschis -// - sub el cade, cu fade + slide, un panou dreptunghiular (colturi drepte + shadow) -// ca celelalte dropdown-uri din navbar -// - lista de notificari e scrollabila (limitam inaltimea cu maxHeight) -// - badge cu numarul de necitite peste clopotel; "Marcheaza toate ca citite" goleste -// - se inchide la scroll-ul PAGINII (ca ThemeMenu), DAR nu si la scroll-ul din -// interiorul listei (altfel panoul s-ar inchide cand incerci sa derulezi) -// -// Culorile sunt legate de tema (theme.card / theme.text / ...), deci panoul reactioneaza -// la dark mode (spre deosebire de ThemeMenu / ProfileMenu, care raman albe — vezi nota -// din web-navbar daca vrem sa le aliniem). -// -// Datele sunt mock-uite local (aceeasi forma `Notificare` ca ecranul de mobil -// acasa/notificari.tsx). Nu importam din fisierul de mobil ca sa nu-l atingem; cand -// va exista un endpoint, se inlocuieste doar `MOCK_NOTIFICARI` cu un fetch. -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Pressable, View, Text, ScrollView, useWindowDimensions, Linking } from "react-native"; -import Animated, { useSharedValue, withTiming, useAnimatedStyle, interpolate, Extrapolation, Easing } from "react-native-reanimated"; -import { ColorScheme, Spacing, Colors } from "@/constants/theme"; -import { Typography } from "@/constants/typography"; -import { useColorScheme } from "@/hooks/use-color-scheme"; -import { WEB_COMPACT_BREAKPOINT } from "@/components/ui/layout/web-container"; -import BellIcon from "@/assets/icons/svg/bell.svg"; -import { useRouter } from "expo-router"; -import { NotificareCard, Notificare } from "@/components/ui/display/notificare-card"; -import { storage } from "@/services/api"; -import { MOCK_NOTIFICARI } from "@/app/(public)/acasa/notificari"; - -// MOCK_NOTIFICARI is imported from notificari.tsx - -export function NotificationMenu({ - open: controlledOpen, - onToggle, - onClose, -}: { - open?: boolean; - onToggle?: () => void; - onClose?: () => void; -}) { - const router = useRouter(); - const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; - const theme = Colors[themeName]; - const isDark = themeName === "dark"; - const { width } = useWindowDimensions(); - const isCompact = width < WEB_COMPACT_BREAKPOINT; - - // Culori luate direct din tema (conform specificatiilor). - const dividerColor = theme.border; - const rowBorder = theme.border; - const hoverBg = theme.background; - const unreadBg = theme.background; - - const [localOpen, setLocalOpen] = useState(false); - const open = controlledOpen !== undefined ? controlledOpen : localOpen; - const anim = useSharedValue(0); // 0 = inchis, 1 = deschis - - // Ref catre cardul panoului (pe web = nodul DOM), ca sa stim daca un scroll vine - // din interiorul listei -> in acel caz NU inchidem panoul. - const panelRef = useRef(null); - - // Ref catre containerul trigger-ului. Pe ecran compact panoul devine o "foaie" - // pozitionata `fixed` fata de ecran (nu fata de clopotel), ca sa nu iasa in afara. - // Masuram marginea de jos a barei din trigger ca sa stim de unde incepe foaia - // (asa prindem corect inaltimea barei + safe-area, fara constante hardcodate). - const triggerRef = useRef(null); - const [sheetTop, setSheetTop] = useState(0); - useLayoutEffect(() => { - if (!open || !isCompact) return; - const node = triggerRef.current as unknown as HTMLElement | null; - if (node && typeof node.getBoundingClientRect === "function") { - setSheetTop(node.getBoundingClientRect().bottom + Spacing.xs); - } - }, [open, isCompact, width]); - - // Citite/necitite tinute local (id-urile celor citite). La inceput toate sunt necitite. - const [readIds, setReadIds] = useState>(new Set()); - const unreadCount = MOCK_NOTIFICARI.filter((n) => !readIds.has(n.id)).length; - - useEffect(() => { - storage.getItem('read_notification_ids').then((val) => { - if (val) { - try { - const ids = JSON.parse(val); - if (Array.isArray(ids)) { - setReadIds(new Set(ids)); - } - } catch (e) { - console.error(e); - } - } - }); - }, [open]); - - const toggle = () => { - if (onToggle) onToggle(); - else setLocalOpen((v) => !v); - }; - - const close = () => { - if (onClose) onClose(); - else setLocalOpen(false); - }; - - useEffect(() => { - anim.set(withTiming(open ? 1 : 0, { - duration: open ? 280 : 200, - easing: Easing.out(Easing.cubic), - })); - }, [open, anim]); - - // Inchide la scroll-ul PAGINII, dar ignora scroll-ul din interiorul panoului - // (lista de notificari) — altfel panoul s-ar inchide imediat ce incerci sa derulezi. - // Faza de CAPTURE ca sa prindem scroll-ul din ScrollView-ul oricarei pagini. - useEffect(() => { - if (!open) return; - const closeOnScroll = (e: Event) => { - const node = panelRef.current as unknown as HTMLElement | null; - if (node && e.target instanceof Node && node.contains(e.target)) return; - close(); - }; - document.addEventListener("scroll", closeOnScroll, true); - return () => document.removeEventListener("scroll", closeOnScroll, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open]); - - const dropStyle = useAnimatedStyle(() => ({ - opacity: anim.value, - transform: [{ translateY: interpolate(anim.value, [0, 1], [-8, 0], Extrapolation.CLAMP) }], - })); - - // Latimea panoului pe ecran lat (dropdown ancorat la dreapta sub clopotel). - const panelWidth = Math.min(360, width - 2 * Spacing.lg); - - // Pe ecran lat: dropdown clasic, ancorat la dreapta sub clopotel. - // Pe ecran compact: "foaie" pozitionata `fixed` fata de ecran, cu margini egale - // stanga/dreapta, ca sa nu iasa niciodata in afara (clopotelul nu e la marginea - // ecranului, deci `right: 0` fata de el ar impinge panoul mult spre stanga). - const panelPosition = isCompact - ? ({ position: "fixed", top: sheetTop, left: Spacing.lg, right: Spacing.lg } as any) - : ({ position: "absolute", top: "100%", right: 0, width: panelWidth } as const); - - const markAllRead = () => { - const allIds = new Set(MOCK_NOTIFICARI.map((n) => n.id)); - setReadIds(allIds); - storage.setItem('read_notification_ids', JSON.stringify(Array.from(allIds))); - }; - - return ( - - {/* Trigger: clopotel + badge cu numarul de necitite. */} - 0 ? `Notificări, ${unreadCount} necitite` : "Notificări"} - style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} - > - - - - {unreadCount > 0 && ( - - - {unreadCount > 9 ? "9+" : unreadCount} - - - )} - - - - {/* Panoul de notificari, ancorat la dreapta sub clopotel (ca la profil/tema). */} - - - {/* Antet: titlu + actiune "marcheaza toate ca citite". */} - - Notificări - {unreadCount > 0 && ( - - {({ pressed, hovered }: any) => ( - - Marchează toate ca citite - - )} - - )} - - - - - {/* Lista scrollabila (limitam inaltimea ca panoul sa nu depaseasca ecranul). - overscrollBehavior: contain -> scroll-ul listei nu "scapa" la pagina cand - ajunge la capat. */} - {MOCK_NOTIFICARI.length === 0 ? ( - - Nu ai notificări. - - ) : ( - - {MOCK_NOTIFICARI.map((item, idx) => { - const isUnread = !readIds.has(item.id); - return ( - { - const updated = new Set(readIds).add(item.id); - setReadIds(updated); - storage.setItem('read_notification_ids', JSON.stringify(Array.from(updated))); - if (item.actiune) { - const isWeb = item.actiune.startsWith("http://") || item.actiune.startsWith("https://"); - const isInternalDomain = item.actiune.includes("inside.ugal.ro"); - if (isWeb && !isInternalDomain) { - Linking.openURL(item.actiune).catch((err) => console.error("Couldn't open URL", err)); - } else { - router.push(item.actiune as any); - } - close(); - } - }} - showDivider={idx > 0} - hoverBg={hoverBg} - unreadBg={unreadBg} - /> - ); - })} - - )} - - - - ); -} +// Disabled — notification menu removed +import { View } from "react-native"; +export function NotificationMenu() { return ; } diff --git a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx index b7688d21..cdbf8428 100644 --- a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx @@ -26,7 +26,6 @@ import { WebContainer, WEB_COMPACT_BREAKPOINT } from "@/components/ui/layout/web import { ThemeMenu } from "@/components/ui/navigation/theme-menu"; import { ThemeToggle } from "@/components/ui/navigation/theme-toggle"; import { ProfileMenu, DASHBOARD_URL } from "@/components/ui/navigation/profile-menu"; -import { NotificationMenu } from "@/components/ui/navigation/notification-menu"; import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; import api, { getAuthToken, logout } from "@/services/api"; @@ -95,7 +94,7 @@ export function WebNavbar() { const zoom = width > WebContentMaxWidth ? Math.min(width / WebContentMaxWidth, WebMaxScale) : 1; const [menuOpen, setMenuOpen] = useState(false); - const [activeMenu, setActiveMenu] = useState<"theme" | "profile" | "notifications" | null>(null); + const [activeMenu, setActiveMenu] = useState<"theme" | "profile" | null>(null); // Hide-on-scroll: bara ascunsa la scroll in jos, vizibila la scroll in sus. const [hidden, setHidden] = useState(false); // Pozitia de scroll de la ultimul event (ref, ca sa o putem reseta la navigare). @@ -283,16 +282,8 @@ export function WebNavbar() { {isCompact ? ( - /* Ecran ingust: clopotel de notificari + buton hamburger. */ + /* Ecran ingust: buton hamburger. */ - { - setMenuOpen(false); // nu tinem ambele panouri deschise simultan - setActiveMenu(activeMenu === "notifications" ? null : "notifications"); - }} - onClose={() => activeMenu === "notifications" && setActiveMenu(null)} - /> { setActiveMenu(null); // inchide panoul de notificari la deschiderea meniului @@ -384,11 +375,6 @@ export function WebNavbar() { })} - setActiveMenu(activeMenu === "notifications" ? null : "notifications")} - onClose={() => activeMenu === "notifications" && setActiveMenu(null)} - /> Date: Tue, 30 Jun 2026 19:31:49 +0300 Subject: [PATCH 03/15] redesign: pagina login --- Frontend/Mobile/src/app/(auth)/_layout.tsx | 4 +- Frontend/Mobile/src/app/(auth)/index.web.tsx | 73 ++++++- .../components/ui/navigation/theme-menu.tsx | 191 +++++++++++------- .../components/ui/navigation/web-navbar.tsx | 2 +- 4 files changed, 184 insertions(+), 86 deletions(-) diff --git a/Frontend/Mobile/src/app/(auth)/_layout.tsx b/Frontend/Mobile/src/app/(auth)/_layout.tsx index 51251356..bf5a9afb 100644 --- a/Frontend/Mobile/src/app/(auth)/_layout.tsx +++ b/Frontend/Mobile/src/app/(auth)/_layout.tsx @@ -1,6 +1,6 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import { Stack, useRouter } from 'expo-router'; -import { Pressable } from "react-native"; +import { Pressable, Platform } from "react-native"; import { Colors, Spacing } from '@/constants/theme'; import CloseIcon from "@/assets/icons/svg/x.svg"; @@ -14,7 +14,7 @@ export default function AuthLayout() { = WEB_COMPACT_BREAKPOINT; + const [logoRotate, setLogoRotate] = useState({ x: 0, y: 0 }); + const logoPanelRef = useRef(null); + + const handleMouseMove = (e: any) => { + const rect = e.currentTarget.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + const rotY = ((e.clientX - cx) / (rect.width / 2)) * 20; + const rotX = -((e.clientY - cy) / (rect.height / 2)) * 20; + setLogoRotate({ x: rotX, y: rotY }); + }; + + const handleMouseLeave = () => setLogoRotate({ x: 0, y: 0 }); + return ( + {/* Floating Close Button */} + router.back()} + style={({ pressed }) => ({ + position: "absolute", + top: Spacing.xl, + right: Spacing.xl, + zIndex: 10, + padding: Spacing.xs, + opacity: pressed ? 0.6 : 1, + })} + > + + + + {/* Stânga: formularul de logare */} - + Autentificare @@ -163,6 +201,31 @@ export default function LoginScreen() { + + {/* Dreapta: logo mare (doar desktop) */} + {isDesktop && ( + + + + )} ); diff --git a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx index 8c149631..c37b277c 100644 --- a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx @@ -1,10 +1,3 @@ -// Meniu de tema (DOAR web). Trigger = iconita cog (rotita). La apasare: -// - cog-ul se roteste (90deg) -// - sub el apare, cu fade + slide, un meniu dreptunghiular (ca la anunturi) cu optiunile: -// - Luminos -// - Întunecat -// - Implicit dispozitivului -// Se inchide la selectie sau la a doua apasare pe cog. Trebuie montat intr-un . import { useEffect, useState } from "react"; import { Pressable, View, Text } from "react-native"; import Animated, { useSharedValue, withTiming, useAnimatedStyle, interpolate, Extrapolation, Easing } from "react-native-reanimated"; @@ -12,7 +5,31 @@ import { ColorScheme, Spacing, Colors } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { useThemeContext } from "@/contexts/theme-context"; import { useColorScheme } from "@/hooks/use-color-scheme"; +import { settingsStore } from "@/utils/settings-store"; import CogIcon from "@/assets/icons/svg/cog.svg"; +import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; + +const THEMES = [ + { id: "system" as const, label: "Sistem" }, + { id: "light" as const, label: "Luminos" }, + { id: "dark" as const, label: "Întunecat" }, +]; + +const LANGUAGES = [ + { code: "ro", label: "Română" }, + { code: "en", label: "English" }, + { code: "es", label: "Español" }, + { code: "fr", label: "Français" }, + { code: "de", label: "Deutsch" }, + { code: "it", label: "Italiano" }, +]; + +const SHADOW = { + shadowColor: ColorScheme.pureBlack, + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.12, + shadowRadius: 16, +}; export function ThemeMenu({ solid = true, @@ -27,38 +44,33 @@ export function ThemeMenu({ }) { const [localOpen, setLocalOpen] = useState(false); const open = controlledOpen !== undefined ? controlledOpen : localOpen; - const anim = useSharedValue(0); // 0 = inchis, 1 = deschis + const [subMenu, setSubMenu] = useState<"tema" | "limba" | null>(null); + const anim = useSharedValue(0); + const subAnim = useSharedValue(0); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const { themeMode, setThemeMode } = useThemeContext(); + const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); - const toggle = () => { - if (onToggle) { - onToggle(); - } else { - setLocalOpen(!localOpen); - } - }; + useEffect(() => settingsStore.subscribe(() => setSelectedLang(settingsStore.getLang())), []); + const toggle = () => onToggle ? onToggle() : setLocalOpen((v) => !v); const close = () => { - if (onClose) { - onClose(); - } else { - setLocalOpen(false); - } + setSubMenu(null); + onClose ? onClose() : setLocalOpen(false); }; + useEffect(() => { if (!open) setSubMenu(null); }, [open]); + useEffect(() => { - anim.set(withTiming(open ? 1 : 0, { - duration: open ? 280 : 200, - easing: Easing.out(Easing.cubic), - })); + anim.set(withTiming(open ? 1 : 0, { duration: open ? 280 : 200, easing: Easing.out(Easing.cubic) })); }, [open, anim]); - // Cand meniul e deschis si pagina e derulata, il inchidem inapoi (cu animatia - // inversa). Scroll-ul se intampla in ScrollView-ul paginii, deci ascultam in - // faza de CAPTURE pe document, ca sa prindem orice container care deruleaza. + useEffect(() => { + subAnim.set(withTiming(subMenu ? 1 : 0, { duration: 200, easing: Easing.out(Easing.cubic) })); + }, [subMenu, subAnim]); + useEffect(() => { if (!open) return; const closeOnScroll = () => close(); @@ -74,28 +86,27 @@ export function ThemeMenu({ opacity: anim.value, transform: [{ translateY: interpolate(anim.value, [0, 1], [-8, 0], Extrapolation.CLAMP) }], })); + const subStyle = useAnimatedStyle(() => ({ + opacity: subAnim.value, + transform: [{ translateX: interpolate(subAnim.value, [0, 1], [8, 0], Extrapolation.CLAMP) }], + })); - const options = [ - { id: "light" as const, label: "Luminos" }, - { id: "dark" as const, label: "Întunecat" }, - { id: "system" as const, label: "Implicit dispozitivului" } - ]; + const themeLabel = THEMES.find((t) => t.id === themeMode)?.label ?? themeMode; + const langLabel = LANGUAGES.find((l) => l.code === selectedLang)?.label ?? selectedLang; return ( - {/* Trigger: cog (rotund, border alb ca sa se vada pe navbarul albastru). */} ({ opacity: pressed ? 0.6 : 1 })} > - {/* Dropdown: cardul de setări temă, aliniat la dreapta sub iconiță (ca la profil). - Colturi drepte (borderRadius: 0), shadow si aspect identic cu dropdown-ul de anunturi. */} + {/* Panoul principal */} - - {options.map((opt) => { - const isSelected = themeMode === opt.id; + + {(subMenu === "tema" ? THEMES : LANGUAGES).map((opt: any) => { + const isSelected = subMenu === "tema" ? themeMode === opt.id : selectedLang === opt.code; + return ( + { + if (subMenu === "tema") setThemeMode(opt.id); + else settingsStore.setLang(opt.code); + setSubMenu(null); + }} + accessibilityRole="button" + style={({ pressed, hovered }: any) => [ + { paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md }, + (pressed || hovered || isSelected) && { backgroundColor: "rgba(0,0,0,0.05)" }, + ]} + > + {({ pressed, hovered }: any) => ( + + {opt.label} + + )} + + ); + })} + + + + {/* Panoul principal: Temă curentă + Limbă curentă */} + + {[ + { label: "Temă curentă", value: themeLabel, sub: "tema" as const }, + { label: "Limbă curentă", value: langLabel, sub: "limba" as const }, + ].map((item, i) => { + const isActive = subMenu === item.sub; return ( { - setThemeMode(opt.id); - close(); - }} + key={item.sub} + onPress={() => setSubMenu(isActive ? null : item.sub)} accessibilityRole="button" style={({ pressed, hovered }: any) => [ { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, + borderTopWidth: i > 0 ? 1 : 0, + borderTopColor: "rgba(0,0,0,0.08)", }, - (pressed || hovered || isSelected) && { backgroundColor: "rgba(0, 0, 0, 0.05)" }, + (pressed || hovered || isActive) && { backgroundColor: "rgba(0,0,0,0.05)" }, ]} > {({ pressed, hovered }: any) => ( - - {opt.label} - + <> + + {item.label}:{" "} + + {item.value} + + + + )} ); @@ -173,4 +208,4 @@ export function ThemeMenu({ ); -} +} \ No newline at end of file diff --git a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx index cdbf8428..43e51587 100644 --- a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx @@ -169,7 +169,7 @@ export function WebNavbar() { } // Doar pagina de acasa (index) are hero -> transparent pana la scroll. - const isHome = pathname === "/acasa" || pathname === "/"; + const isHome = pathname === "/acasa" || pathname === "/" || pathname === "/(auth)" || pathname.startsWith("/auth"); // Bara e solida daca: nu suntem pe hero / s-a derulat / panoul hamburger e deschis. const solid = !isHome || scrolled || (isCompact && menuOpen); From 2ee88bba886808893dffebc84bd1f23287cc8d20 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Tue, 30 Jun 2026 21:00:27 +0300 Subject: [PATCH 04/15] feat: traduceri --- Frontend/Mobile/package-lock.json | 84 ++++++ Frontend/Mobile/package.json | 2 + Frontend/Mobile/src/app/(auth)/index.tsx | 22 +- Frontend/Mobile/src/app/(auth)/index.web.tsx | 22 +- .../Mobile/src/app/(onboarding)/locatie.tsx | 9 +- Frontend/Mobile/src/app/(public)/_layout.tsx | 12 +- .../src/app/(public)/acasa/categorie.tsx | 43 ++-- .../Mobile/src/app/(public)/acasa/index.tsx | 55 ++-- .../src/app/(public)/acasa/index.web.tsx | 51 ++-- .../src/app/(public)/acasa/vizualizare.tsx | 78 +++--- .../app/(public)/acasa/vizualizare.web.tsx | 94 +++---- .../Mobile/src/app/(public)/cantina/index.tsx | 22 +- .../src/app/(public)/cantina/index.web.tsx | 20 +- Frontend/Mobile/src/app/(public)/harta.tsx | 10 +- .../Mobile/src/app/(public)/harta.web.tsx | 10 +- .../Mobile/src/app/(public)/more/index.tsx | 16 +- .../src/app/(public)/more/index.web.tsx | 6 +- .../Mobile/src/app/(public)/more/limba.tsx | 13 +- .../src/app/(public)/more/limba.web.tsx | 13 +- .../Mobile/src/app/(public)/more/setari.tsx | 25 +- .../src/app/(public)/more/setari.web.tsx | 25 +- .../Mobile/src/app/(public)/more/tema.tsx | 27 +- .../Mobile/src/app/(public)/more/tema.web.tsx | 27 +- .../src/app/(public)/sesizari/_layout.tsx | 16 +- .../src/app/(public)/sesizari/adauga.tsx | 33 +-- .../src/app/(public)/sesizari/adauga.web.tsx | 35 +-- .../src/app/(public)/sesizari/detalii.tsx | 50 ++-- .../src/app/(public)/sesizari/detalii.web.tsx | 2 + .../src/app/(public)/sesizari/index.tsx | 20 +- .../src/app/(public)/sesizari/index.web.tsx | 36 +-- Frontend/Mobile/src/app/_layout.tsx | 1 + Frontend/Mobile/src/app/ace.tsx | 17 +- .../components/ui/display/article-detail.tsx | 44 ++-- .../src/components/ui/display/error-state.tsx | 8 +- .../components/ui/display/file-attachment.tsx | 4 +- .../src/components/ui/layout/ace.web.tsx | 20 +- .../components/ui/navigation/profile-menu.tsx | 10 +- .../components/ui/navigation/theme-menu.tsx | 14 +- .../components/ui/navigation/theme-toggle.tsx | 4 +- .../components/ui/navigation/web-navbar.tsx | 98 ++++--- .../Mobile/src/contexts/theme-context.tsx | 21 +- Frontend/Mobile/src/i18n/index.ts | 18 ++ Frontend/Mobile/src/i18n/locales/en.json | 243 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/ro.json | 243 ++++++++++++++++++ Frontend/Mobile/src/utils/date.ts | 4 +- Frontend/Mobile/src/utils/settings-store.ts | 2 + 46 files changed, 1166 insertions(+), 463 deletions(-) create mode 100644 Frontend/Mobile/src/i18n/index.ts create mode 100644 Frontend/Mobile/src/i18n/locales/en.json create mode 100644 Frontend/Mobile/src/i18n/locales/ro.json diff --git a/Frontend/Mobile/package-lock.json b/Frontend/Mobile/package-lock.json index ba6b9f64..cc8b1074 100644 --- a/Frontend/Mobile/package-lock.json +++ b/Frontend/Mobile/package-lock.json @@ -31,9 +31,11 @@ "expo-symbols": "~56.0.6", "expo-system-ui": "~56.0.5", "expo-web-browser": "~56.0.5", + "i18next": "^26.3.4", "maplibre-gl": "^5.24.0", "react": "19.2.3", "react-dom": "19.2.3", + "react-i18next": "^17.0.8", "react-native": "0.85.3", "react-native-gesture-handler": "~2.31.1", "react-native-keyboard-controller": "1.21.6", @@ -8017,6 +8019,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -8065,6 +8076,34 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "26.3.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz", + "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -10879,6 +10918,33 @@ "react": ">=17.0.0" } }, + "node_modules/react-i18next": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", + "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", @@ -12759,6 +12825,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -12815,6 +12890,15 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/Frontend/Mobile/package.json b/Frontend/Mobile/package.json index f77851cc..196dc65e 100644 --- a/Frontend/Mobile/package.json +++ b/Frontend/Mobile/package.json @@ -26,9 +26,11 @@ "expo-symbols": "~56.0.6", "expo-system-ui": "~56.0.5", "expo-web-browser": "~56.0.5", + "i18next": "^26.3.4", "maplibre-gl": "^5.24.0", "react": "19.2.3", "react-dom": "19.2.3", + "react-i18next": "^17.0.8", "react-native": "0.85.3", "react-native-gesture-handler": "~2.31.1", "react-native-keyboard-controller": "1.21.6", diff --git a/Frontend/Mobile/src/app/(auth)/index.tsx b/Frontend/Mobile/src/app/(auth)/index.tsx index 7147266e..758490cb 100644 --- a/Frontend/Mobile/src/app/(auth)/index.tsx +++ b/Frontend/Mobile/src/app/(auth)/index.tsx @@ -6,12 +6,14 @@ import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { KeyboardProvider } from 'react-native-keyboard-controller'; import { useAuth } from "@/contexts/auth-context"; +import { useTranslation } from 'react-i18next'; export default function LoginScreen() { const router = useRouter(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const { login } = useAuth(); + const { t } = useTranslation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -22,15 +24,15 @@ export default function LoginScreen() { const newErrors: { email?: string; password?: string } = {}; if (!email) { - newErrors.email = "Email-ul este obligatoriu."; + newErrors.email = t('auth.emailRequired'); } else if (!/\S+@\S+\.\S+/.test(email)) { - newErrors.email = "Formatul email-ului este invalid."; + newErrors.email = t('auth.emailInvalid'); } if (!password) { - newErrors.password = "Parola este obligatorie."; + newErrors.password = t('auth.passwordRequired'); } else if (password.length < 6) { - newErrors.password = "Parola trebuie să aibă cel puțin 6 caractere."; + newErrors.password = t('auth.passwordTooShort'); } setErrors(newErrors); @@ -47,7 +49,7 @@ export default function LoginScreen() { } catch (err: any) { setErrors(prev => ({ ...prev, - general: err.message || "Email-ul sau parola sunt incorecte." + general: err.message || t('auth.invalidCredentials') })); } finally { setSubmitting(false); @@ -67,9 +69,9 @@ export default function LoginScreen() { > - Autentificare + {t('auth.title')} - Introdu datele pentru a intra în cont + {t('auth.subtitle')} @@ -82,7 +84,7 @@ export default function LoginScreen() { {/* Email Input */} - Email + {t('auth.email')} { @@ -113,7 +115,7 @@ export default function LoginScreen() { {/* Password Input */} - Parolă + {t('auth.password')} { @@ -158,7 +160,7 @@ export default function LoginScreen() { {submitting ? ( ) : ( - Autentificare + {t('auth.login')} )} diff --git a/Frontend/Mobile/src/app/(auth)/index.web.tsx b/Frontend/Mobile/src/app/(auth)/index.web.tsx index 85304247..6322c0dc 100644 --- a/Frontend/Mobile/src/app/(auth)/index.web.tsx +++ b/Frontend/Mobile/src/app/(auth)/index.web.tsx @@ -9,6 +9,7 @@ import { KeyboardProvider } from 'react-native-keyboard-controller'; import { useAuth } from "@/contexts/auth-context"; import { WEB_COMPACT_BREAKPOINT } from "@/components/ui/layout/web-container"; import CloseIcon from "@/assets/icons/svg/x.svg"; +import { useTranslation } from 'react-i18next'; const LOGO = require("@/assets/images/logo.png"); @@ -17,6 +18,7 @@ export default function LoginScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const { login } = useAuth(); + const { t } = useTranslation(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -27,15 +29,15 @@ export default function LoginScreen() { const newErrors: { email?: string; password?: string } = {}; if (!email) { - newErrors.email = "Email-ul este obligatoriu."; + newErrors.email = t('auth.emailRequired'); } else if (!/\S+@\S+\.\S+/.test(email)) { - newErrors.email = "Formatul email-ului este invalid."; + newErrors.email = t('auth.emailInvalid'); } if (!password) { - newErrors.password = "Parola este obligatorie."; + newErrors.password = t('auth.passwordRequired'); } else if (password.length < 6) { - newErrors.password = "Parola trebuie să aibă cel puțin 6 caractere."; + newErrors.password = t('auth.passwordTooShort'); } setErrors(newErrors); @@ -52,7 +54,7 @@ export default function LoginScreen() { } catch (err: any) { setErrors(prev => ({ ...prev, - general: err.message || "Email-ul sau parola sunt incorecte." + general: err.message || t('auth.invalidCredentials') })); } finally { setSubmitting(false); @@ -104,9 +106,9 @@ export default function LoginScreen() { > - Autentificare + {t('auth.title')} - Introdu datele pentru a intra în cont + {t('auth.subtitle')} @@ -119,7 +121,7 @@ export default function LoginScreen() { {/* Email Input */} - Email + {t('auth.email')} { @@ -150,7 +152,7 @@ export default function LoginScreen() { {/* Password Input */} - Parolă + {t('auth.password')} { @@ -195,7 +197,7 @@ export default function LoginScreen() { {submitting ? ( ) : ( - Autentificare + {t('auth.login')} )} diff --git a/Frontend/Mobile/src/app/(onboarding)/locatie.tsx b/Frontend/Mobile/src/app/(onboarding)/locatie.tsx index 53c9b3c5..f6cc1cb3 100644 --- a/Frontend/Mobile/src/app/(onboarding)/locatie.tsx +++ b/Frontend/Mobile/src/app/(onboarding)/locatie.tsx @@ -7,6 +7,7 @@ import { Spacing, ColorScheme, Colors } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { storage } from "@/services/api"; +import { useTranslation } from 'react-i18next'; import * as Location from "expo-location"; import LocationSvg from "@/assets/instructions/location.svg"; @@ -16,6 +17,8 @@ export default function LocatieScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); + const handleContinue = async () => { await Location.requestForegroundPermissionsAsync(); await storage.setItem("has_seen_onboarding", "true"); @@ -42,10 +45,10 @@ export default function LocatieScreen() { }}> - Explorează campusul + {t('onboarding.exploreTitle')} - Descoperă clădirile și facilitățile campusului universitar direct pe hartă. + {t('onboarding.exploreDesc')} @@ -65,7 +68,7 @@ export default function LocatieScreen() { })} onPress={handleContinue} > - Continuă + {t('onboarding.continue')} diff --git a/Frontend/Mobile/src/app/(public)/_layout.tsx b/Frontend/Mobile/src/app/(public)/_layout.tsx index 7b31273b..03039a46 100644 --- a/Frontend/Mobile/src/app/(public)/_layout.tsx +++ b/Frontend/Mobile/src/app/(public)/_layout.tsx @@ -6,12 +6,14 @@ import { Colors } from '@/constants/theme'; import { useNavigation } from 'expo-router'; import { Ace } from '@/components/ui/layout/ace'; import { Typography } from '@/constants/typography'; +import { useTranslation } from 'react-i18next'; export default function TabLayout() { const navigation = useNavigation(); const themeName = (useColorScheme() ?? 'light') as keyof typeof Colors; const theme = Colors[themeName]; const activeColor = theme.primary; + const { t } = useTranslation(); return ( @@ -52,7 +54,7 @@ export default function TabLayout() { }} renderingMode='template' /> - Acasă + {t('nav.home')} @@ -63,7 +65,7 @@ export default function TabLayout() { }} renderingMode='template' /> - Hartă + {t('nav.map')} @@ -74,7 +76,7 @@ export default function TabLayout() { }} renderingMode='template' /> - Cantină + {t('nav.canteen')} @@ -85,7 +87,7 @@ export default function TabLayout() { }} renderingMode='template' /> - Sesizări + {t('nav.reports')} @@ -96,7 +98,7 @@ export default function TabLayout() { }} renderingMode='template' /> - Mai multe + {t('nav.more')} diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index 4f9cabdd..45a69110 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -11,6 +11,7 @@ import { CategoryHeader, FilterItem } from "@/components/ui/display/category-hea import { getFormattedDate, isoToRomanianDateStr } from "@/utils/date"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; import { NewsListSkeleton } from "@/components/ui/display/skeletons"; import { ErrorState } from "@/components/ui/display/error-state"; @@ -26,6 +27,13 @@ export default function CategoryScreen() { opacity: interpolate(scrollY.value, [50, 90], [0, 1], Extrapolation.CLAMP), })); + const { t, i18n } = useTranslation(); + const displayTitle = categoryTitle === "Noutăți" ? t('home.news') + : categoryTitle === "Evenimente" ? t('home.events') + : categoryTitle === "Facultăți" ? t('home.faculties') + : categoryTitle === "Facilități" ? t('home.facilities') + : (categoryTitle as string) || t('category.fallback'); + const [selectedFacultyId, setSelectedFacultyId] = useState(null); const [data, setData] = useState([]); const [page, setPage] = useState(1); @@ -41,7 +49,7 @@ export default function CategoryScreen() { setHasError(false); const success = await fetchData(1, true); if (!success && data.length > 0) { - Alert.alert("Eroare la actualizare", "Nu s-au putut reîmprospăta datele pentru această categorie. Te rugăm să verifici conexiunea la internet."); + Alert.alert(t('common.updateError'), t('category.refreshError')); } setPage(1); setRefreshing(false); @@ -65,7 +73,7 @@ export default function CategoryScreen() { }, []); const facultyFilters: FilterItem[] = [ - { id: null, title: "Toate Facultățile", abbreviation: "Toate" }, + { id: null, title: t('category.allFaculties'), abbreviation: t('category.all') }, ...faculties.map(f => ({ id: f.id.toString(), title: f.name, @@ -87,24 +95,25 @@ export default function CategoryScreen() { page: pageToFetch, size: 20, announcement_type: type, - faculty_id: selectedFacultyId || undefined + faculty_id: selectedFacultyId || undefined, + lang: i18n.language, } }); - + if (response.data && response.data.items) { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", - category: categoryTitle, - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: displayTitle, + date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", - location: item.location_name || "Locație necunoscută", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), + location: item.location_name || t('common.unknownLocation'), created_at: item.created_at, updated_at: item.updated_at, })); @@ -119,12 +128,12 @@ export default function CategoryScreen() { if (response.data && response.data.items) { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.logo_url || undefined, - address: item.address || "Adresă necunoscută", + address: item.address || t('common.unknownAddress'), phone: item.phone || "", website: item.website_url || "", - content: item.description || "Conținut necunoscut", + content: item.description || t('common.unknownContent'), })); } } else if (categoryTitle === "Facilități") { @@ -137,7 +146,7 @@ export default function CategoryScreen() { if (response.data && response.data.items) { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.image_url || undefined, content: item.description || "", })); @@ -171,7 +180,7 @@ export default function CategoryScreen() { }, 0); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedFacultyId, categoryTitle]); + }, [selectedFacultyId, categoryTitle, i18n.language]); const handlePress = (item: any) => { let type: string | undefined = undefined; @@ -237,7 +246,7 @@ export default function CategoryScreen() { ]} numberOfLines={1} > - {(categoryTitle as string) || "Categorie"} + {displayTitle} ), @@ -258,7 +267,7 @@ export default function CategoryScreen() { > - Nu există elemente în această categorie. + {t('category.empty')} )} diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index fccb79ab..8b1782e8 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -13,6 +13,7 @@ import { NewsCard } from "@/components/ui/display/news-card"; import { HeroSlideshow, HERO_HEIGHT } from "@/components/ui/display/hero-slideshow"; import { getFormattedDate, parseRomanianDate, isoToRomanianDateStr, getTodayRomanianDate } from "@/utils/date"; import api, { storage } from "@/services/api"; +import { useTranslation } from 'react-i18next'; import { ErrorState } from "@/components/ui/display/error-state"; import { HomeSkeleton, CarouselSkeleton } from "@/components/ui/display/skeletons"; @@ -23,6 +24,7 @@ export default function HomeScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); + const { t, i18n } = useTranslation(); const [noutati, setNoutati] = useState([]); const [evenimente, setEvenimente] = useState([]); const [facultati, setFacultati] = useState([]); @@ -47,24 +49,25 @@ export default function HomeScreen() { page: 1, size: 50, announcement_type: undefined, - faculty_id: undefined + faculty_id: undefined, + lang: i18n.language, } }); if (response.data && response.data.items) { const apiItems = response.data.items; - + await storage.setItem('cached_announcements', JSON.stringify(apiItems)); const apiNoutati = apiItems .filter((item: any) => item.type === "NOUTATE") .map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", - category: "Noutăți", - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: t('home.news'), + date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), created_at: item.created_at, })); @@ -72,17 +75,17 @@ export default function HomeScreen() { .filter((item: any) => item.type === "EVENIMENT") .map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", - category: "Evenimente", - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: t('home.events'), + date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", - location: item.location_name || "Locație necunoscută", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), + location: item.location_name || t('common.unknownLocation'), created_at: item.created_at, })); @@ -206,7 +209,7 @@ export default function HomeScreen() { const isPageEmpty = noutati.length === 0 && evenimente.length === 0 && facultati.length === 0 && facilitati.length === 0; if (!success && !isPageEmpty) { - Alert.alert("Eroare la actualizare", "Nu s-au putut reîmprospăta datele de pe ecranul principal. Te rugăm să verifici conexiunea la internet."); + Alert.alert(t('common.updateError'), t('home.refreshError')); } const elapsed = Date.now() - start; @@ -220,7 +223,7 @@ export default function HomeScreen() { // eslint-disable-next-line react-hooks/set-state-in-effect fetchApiData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [i18n.language]); const announcementsForHero = [...noutati] .sort((a, b) => parseRomanianDate(b.date).getTime() - parseRomanianDate(a.date).getTime()) @@ -324,15 +327,15 @@ export default function HomeScreen() { <> {activeNoutati.length === 0 ? ( - Noutăți - Nu s-au putut găsi noutăți. + {t('home.news')} + {t('home.noNews')} ) : ( item.id} - viewAllHref="/(public)/acasa/categorie?title=Noutăți" + viewAllHref={`/(public)/acasa/categorie?title=Noutăți`} renderItem={({ item, index }) => ( - Evenimente - Nu s-au putut găsi evenimente. + {t('home.events')} + {t('home.noEvents')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Evenimente" @@ -374,12 +377,12 @@ export default function HomeScreen() { {activeFacultati.length === 0 ? ( - Facultăți - Nu s-au putut găsi facultăți. + {t('home.faculties')} + {t('home.noFaculties')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facultăți" @@ -397,12 +400,12 @@ export default function HomeScreen() { {activeFacilitati.length === 0 ? ( - Facilități - Nu s-au putut găsi facilități. + {t('home.facilities')} + {t('home.noFacilities')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facilități" diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 5cb9e1a5..d0ac78fb 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -16,6 +16,7 @@ import { NAVBAR_HEIGHT } from "@/components/ui/navigation/web-navbar"; import { getFormattedDate, parseRomanianDate, isoToRomanianDateStr, getTodayRomanianDate } from "@/utils/date"; import { useWebScrollAware } from "@/contexts/web-scroll-context"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; import { ErrorState } from "@/components/ui/display/error-state"; import { HomeSkeleton } from "@/components/ui/display/skeletons"; import { Seo } from "@/components/seo"; @@ -30,6 +31,7 @@ export default function HomeScreen() { // Navbar transparent pana trece de hero. Pragul = inaltimea hero-ului minus navbar. const scrollProps = useWebScrollAware(HERO_HEIGHT - NAVBAR_HEIGHT); + const { t, i18n } = useTranslation(); const [noutati, setNoutati] = useState([]); const [evenimente, setEvenimente] = useState([]); const [facultati, setFacultati] = useState([]); @@ -47,22 +49,23 @@ export default function HomeScreen() { page: 1, size: 50, announcement_type: undefined, - faculty_id: undefined + faculty_id: undefined, + lang: i18n.language, } }); if (response.data && response.data.items) { const apiItems = response.data.items; - + const apiNoutati = apiItems .filter((item: any) => item.type === "NOUTATE") .map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", - category: "Noutăți", - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: t('home.news'), + date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), created_at: item.created_at, })); @@ -70,17 +73,17 @@ export default function HomeScreen() { .filter((item: any) => item.type === "EVENIMENT") .map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", - category: "Evenimente", - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: t('home.events'), + date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", - location: item.location_name || "Locație necunoscută", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), + location: item.location_name || t('common.unknownLocation'), created_at: item.created_at, })); @@ -163,7 +166,7 @@ export default function HomeScreen() { }; run(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [i18n.language]); // Ultimele 3 anunturi (Noutăți), cele mai recente primele, pentru hero. const announcementsForHero = [...noutati] @@ -286,15 +289,15 @@ export default function HomeScreen() { {activeNoutati.length === 0 ? ( - Noutăți + {t('home.news')} - {hasError ? "Nu s-au putut încărca noutățile." : "Nu există noutăți."} + {hasError ? t('home.loadErrorNews') : t('home.emptyNews')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Noutăți" @@ -315,15 +318,15 @@ export default function HomeScreen() { {activeEvenimente.length === 0 ? ( - Evenimente + {t('home.events')} - {hasError ? "Nu s-au putut încărca evenimentele." : "Nu există evenimente."} + {hasError ? t('home.loadErrorEvents') : t('home.emptyEvents')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Evenimente" @@ -344,15 +347,15 @@ export default function HomeScreen() { {activeFacultati.length === 0 ? ( - Facultăți + {t('home.faculties')} - {hasError ? "Nu s-au putut încărca facultățile." : "Nu există facultăți."} + {hasError ? t('home.loadErrorFaculties') : t('home.emptyFaculties')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facultăți" @@ -371,15 +374,15 @@ export default function HomeScreen() { {activeFacilitati.length === 0 ? ( - Facilități + {t('home.facilities')} - {hasError ? "Nu s-au putut încărca facilitățile." : "Nu există facilități."} + {hasError ? t('home.loadErrorFacilities') : t('home.emptyFacilities')} ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facilități" diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index bbdbcf0e..a034a618 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -9,6 +9,7 @@ import { Colors, ColorScheme, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { getFormattedDate, getReadingTime, isoToRomanianDateStr } from "@/utils/date"; import api, { storage } from "@/services/api"; +import { useTranslation } from 'react-i18next'; import { VizualizareSkeleton } from "@/components/ui/display/skeletons"; import { ErrorState } from "@/components/ui/display/error-state"; @@ -20,9 +21,7 @@ import { CategoryTag } from "@/components/ui/display/news-card"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import { FileAttachments } from "@/components/ui/display/file-attachment"; -const DAY_NAMES = ["", "Luni", "Marți", "Miercuri", "Joi", "Vineri", "Sâmbătă", "Duminică"]; - -function formatSchedules(schedules: any[]): string[] { +function formatSchedules(schedules: any[], t: (key: string) => string): string[] { if (!schedules || schedules.length === 0) return []; const sorted = [...schedules].sort((a, b) => a.day_of_week - b.day_of_week); const groups: string[] = []; @@ -40,8 +39,8 @@ function formatSchedules(schedules: any[]): string[] { const timeRange = `${start.open_time.slice(0, 5)} - ${start.close_time.slice(0, 5)}`; groups.push( j - i === 1 - ? `${DAY_NAMES[start.day_of_week]}: ${timeRange}` - : `${DAY_NAMES[start.day_of_week]} - ${DAY_NAMES[end.day_of_week]}: ${timeRange}` + ? `${t(`days.${start.day_of_week}`)}: ${timeRange}` + : `${t(`days.${start.day_of_week}`)} - ${t(`days.${end.day_of_week}`)}: ${timeRange}` ); i = j; } @@ -53,6 +52,7 @@ function VizualizareScreen() { const id = params.id as string; const router = useRouter(); const { width } = useWindowDimensions(); + const { t, i18n } = useTranslation(); const [scrolledPast, setScrolledPast] = useState(false); const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); @@ -116,17 +116,17 @@ function VizualizareScreen() { if (isNumeric) { if (initialTipPagina === "Eveniment" || initialTipPagina === "Anunț") { - const res = await api.get(`/announcements/${numericId}`); + const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; fetchedItem = { id: item.id.toString(), type: item.type === "NOUTATE" ? "Anunț" : "Eveniment", - title: item.title || "Titlu necunoscut", - category: item.type === "NOUTATE" ? "Noutăți" : "Evenimente", - content: item.content || "Conținut necunoscut", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: item.type === "NOUTATE" ? t('home.news') : t('home.events'), + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), image: item.image_url || "", - location: item.location_name || "Locație necunoscută", + location: item.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", @@ -146,12 +146,12 @@ function VizualizareScreen() { fetchedItem = { id: item.id.toString(), type: "Facultate", - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.logo_url || "", - address: item.address || "Adresă necunoscută", + address: item.address || t('common.unknownAddress'), phone: item.phone || "", website: item.website_url || "", - content: item.description || "Conținut necunoscut", + content: item.description || t('common.unknownContent'), }; } } else if (initialTipPagina === "Facilitate") { @@ -161,7 +161,7 @@ function VizualizareScreen() { fetchedItem = { id: item.id.toString(), type: "Facilitate", - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.image_url || "", content: item.description || "", schedules: item.schedules || [], @@ -189,11 +189,11 @@ function VizualizareScreen() { if (match && isMounted) { let mappedItem: any = null; if (isFaculty) { - mappedItem = { id: match.id.toString(), type: "Facultate", title: match.name || "Titlu necunoscut", image: match.logo_url || "", address: match.address || "Adresă necunoscută", phone: match.phone || "", website: match.website_url || "", content: match.description || "Conținut necunoscut" }; + mappedItem = { id: match.id.toString(), type: "Facultate", title: match.name || t('common.unknownTitle'), image: match.logo_url || "", address: match.address || t('common.unknownAddress'), phone: match.phone || "", website: match.website_url || "", content: match.description || t('common.unknownContent') }; } else if (isFacility) { - mappedItem = { id: match.id.toString(), type: "Facilitate", title: match.name || "Titlu necunoscut", image: match.image_url || "", content: match.description || "", schedules: match.schedules || [] }; + mappedItem = { id: match.id.toString(), type: "Facilitate", title: match.name || t('common.unknownTitle'), image: match.image_url || "", content: match.description || "", schedules: match.schedules || [] }; } else { - mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: match.title || "Titlu necunoscut", category: match.type === "NOUTATE" ? "Noutăți" : "Evenimente", content: match.content || "Conținut necunoscut", image: match.image_url || "", location: match.location_name || "Locație necunoscută", date_start: isoToRomanianDateStr(match.start_date) || "", date_end: isoToRomanianDateStr(match.end_date) || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(match.created_at) || "", date: isoToRomanianDateStr(match.start_date) || "Dată necunoscută", author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; + mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: match.title || t('common.unknownTitle'), category: match.type === "NOUTATE" ? t('home.news') : t('home.events'), content: match.content || t('common.unknownContent'), image: match.image_url || "", location: match.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(match.start_date) || "", date_end: isoToRomanianDateStr(match.end_date) || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(match.created_at) || "", date: isoToRomanianDateStr(match.start_date) || t('common.unknownDate'), author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; } setItemData(mappedItem); setLoading(false); @@ -213,7 +213,7 @@ function VizualizareScreen() { interactionTask.cancel(); } }; - }, [id, initialTipPagina, retryKey]); + }, [id, initialTipPagina, retryKey, i18n.language]); const onRefresh = () => { setRefreshing(true); @@ -251,11 +251,11 @@ function VizualizareScreen() { const handleCall = () => { Alert.alert( - tipPagina === "Facultate" ? "Contact Facultate" : "Contact Facilitate", - `Doriți să apelați numărul ${phone}?`, + tipPagina === "Facultate" ? t('detail.callFaculty') : t('detail.callFacility'), + t('detail.callConfirm', { phone }), [ - { text: "Anulează", style: "cancel" }, - { text: "Sună", onPress: () => Linking.openURL(`tel:${phone}`) } + { text: t('detail.cancel'), style: "cancel" }, + { text: t('detail.call'), onPress: () => Linking.openURL(`tel:${phone}`) } ] ); }; @@ -299,7 +299,7 @@ function VizualizareScreen() { }} /> setRetryKey(prev => prev + 1)} /> @@ -322,7 +322,7 @@ function VizualizareScreen() { ), headerShadowVisible: false, headerTintColor: scrolledPast ? theme.text : ColorScheme.white, - headerTitle: scrolledPast ? (title || "Detalii") : "", + headerTitle: scrolledPast ? (title || t('detail.details')) : "", headerLeft: () => ( router.back()} @@ -362,7 +362,7 @@ function VizualizareScreen() { ) : ( - {category || (tipPagina === "Facultate" ? "Facultate" : tipPagina === "Facilitate" ? "Facilitate" : "Categorie")} + {category || (tipPagina === "Facultate" ? t('common.faculty') : tipPagina === "Facilitate" ? t('common.facility') : t('common.category'))} )} @@ -379,7 +379,7 @@ function VizualizareScreen() { {isUpdated && formattedUpdateDate ? ( - Actualizat: {formattedUpdateDate} + {t('detail.updated')} {formattedUpdateDate} ) : null} @@ -388,7 +388,7 @@ function VizualizareScreen() { {tipPagina === "Eveniment" && ( - Informații eveniment + {t('detail.eventInfo')} @@ -396,10 +396,10 @@ function VizualizareScreen() { - De pe {date_start || "Dată de început necunoscută"} {time_start || ""} + {t('detail.from')} {date_start || t('common.unknownStartDate')} {time_start || ""} - Până la {date_end || "Dată de sfârșit necunoscută"} {time_end || ""} + {t('detail.until')} {date_end || t('common.unknownEndDate')} {time_end || ""} @@ -418,16 +418,16 @@ function VizualizareScreen() { {tipPagina === "Facultate" && ( - Contact și Locație + {t('detail.contact')} - Adresă + {t('detail.address')} - {address || "Adresă necunoscută"} + {address || t('common.unknownAddress')} @@ -436,7 +436,7 @@ function VizualizareScreen() { - Telefon + {t('detail.phone')} {phone} @@ -450,7 +450,7 @@ function VizualizareScreen() { - Website + {t('detail.website')} Linking.openURL(website as string)}> {website} @@ -463,17 +463,17 @@ function VizualizareScreen() { )} - {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || []).length > 0 && ( + {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || [], t).length > 0 && ( - Informații facilitate + {t('detail.facilityInfo')} - Program: - {formatSchedules(itemData.schedules).map((line: string, i: number) => ( + {t('detail.schedule')} + {formatSchedules(itemData.schedules, t).map((line: string, i: number) => ( {line} ))} @@ -484,7 +484,7 @@ function VizualizareScreen() { - {tipPagina === "Eveniment" ? "Despre eveniment" : tipPagina === "Facultate" ? "Despre facultate" : tipPagina === "Facilitate" ? "Despre facilitate" : "Detalii"} + {tipPagina === "Eveniment" ? t('detail.aboutEvent') : tipPagina === "Facultate" ? t('detail.aboutFaculty') : tipPagina === "Facilitate" ? t('detail.aboutFacility') : t('detail.details')} {content || "Conținut necunoscut"} diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index 790fe86e..caef686d 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -14,6 +14,7 @@ import { CompactCard } from "@/components/ui/display/home-highlights"; import { NewsCard, CategoryTag } from "@/components/ui/display/news-card"; import { Seo } from "@/components/seo"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; import { ErrorState } from "@/components/ui/display/error-state"; import CalendarIcon from "@/assets/icons/svg/calendar.svg"; @@ -21,9 +22,7 @@ import LocationIcon from "@/assets/icons/svg/location.svg"; import PhoneIcon from "@/assets/icons/svg/phone.svg"; import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg"; -const DAY_NAMES = ["", "Luni", "Marți", "Miercuri", "Joi", "Vineri", "Sâmbătă", "Duminică"]; - -function formatSchedules(schedules: any[]): string[] { +function formatSchedules(schedules: any[], t: (key: string) => string): string[] { if (!schedules || schedules.length === 0) return []; const sorted = [...schedules].sort((a, b) => a.day_of_week - b.day_of_week); const groups: string[] = []; @@ -41,8 +40,8 @@ function formatSchedules(schedules: any[]): string[] { const timeRange = `${start.open_time.slice(0, 5)} - ${start.close_time.slice(0, 5)}`; groups.push( j - i === 1 - ? `${DAY_NAMES[start.day_of_week]}: ${timeRange}` - : `${DAY_NAMES[start.day_of_week]} - ${DAY_NAMES[end.day_of_week]}: ${timeRange}` + ? `${t(`days.${start.day_of_week}`)}: ${timeRange}` + : `${t(`days.${start.day_of_week}`)} - ${t(`days.${end.day_of_week}`)}: ${timeRange}` ); i = j; } @@ -57,6 +56,7 @@ const TWO_COL_BREAKPOINT = 900; function VizualizareScreen() { const params = useLocalSearchParams(); const id = params.id as string; + const { t, i18n } = useTranslation(); const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); const [retryKey, setRetryKey] = useState(0); @@ -107,7 +107,7 @@ function VizualizareScreen() { } } else { try { - const res = await api.get('/announcements/', { params: { page: 1, size: 20 } }); + const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language } }); if (res.data?.items && isMounted) setRelatedPool(res.data.items); } catch (err) { console.warn('[API] Error loading related announcements:', err); @@ -132,23 +132,23 @@ function VizualizareScreen() { if (isNumeric) { try { if (initialTipPagina === "Eveniment" || initialTipPagina === "Anunț") { - const res = await api.get(`/announcements/${numericId}`); + const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; fetchedItem = { id: item.id.toString(), type: item.type === "NOUTATE" ? "Anunț" : "Eveniment", - title: item.title || "Titlu necunoscut", - category: item.type === "NOUTATE" ? "Noutăți" : "Evenimente", - content: item.content || "Conținut necunoscut", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: item.type === "NOUTATE" ? t('home.news') : t('home.events'), + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), image: item.image_url || "", - location: item.location_name || "Locație necunoscută", + location: item.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || "Dată necunoscută", + date: isoToRomanianDateStr(item.start_date) || t('common.unknownDate'), author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, @@ -161,12 +161,12 @@ function VizualizareScreen() { fetchedItem = { id: item.id.toString(), type: "Facultate", - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.image_url || "", - address: item.address || "Adresă necunoscută", + address: item.address || t('common.unknownAddress'), phone: item.phone || "", website: item.website_url || "", - content: item.description || "Conținut necunoscut", + content: item.description || t('common.unknownContent'), }; } } else if (initialTipPagina === "Facilitate") { @@ -176,7 +176,7 @@ function VizualizareScreen() { fetchedItem = { id: item.id.toString(), type: "Facilitate", - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.image_url || "", content: item.description || "", schedules: item.schedules || [], @@ -203,7 +203,7 @@ function VizualizareScreen() { return () => { isMounted = false; }; - }, [id, initialTipPagina, retryKey]); + }, [id, initialTipPagina, retryKey, i18n.language]); const title = itemData?.title || ""; const category = itemData?.category || ""; @@ -228,17 +228,17 @@ function VizualizareScreen() { .map((item: any) => ({ id: item.id.toString(), type: item.type === "NOUTATE" ? "Anunț" : "Eveniment", - title: item.title || "Titlu necunoscut", - category: item.type === "NOUTATE" ? "Noutăți" : "Evenimente", - content: item.content || "Conținut necunoscut", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), + category: item.type === "NOUTATE" ? t('home.news') : t('home.events'), + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), image: item.image_url || "", - location: item.location_name || "Locație necunoscută", + location: item.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || "Dată necunoscută", + date: isoToRomanianDateStr(item.start_date) || t('common.unknownDate'), author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, @@ -260,7 +260,7 @@ function VizualizareScreen() { .slice(0, 3) .map((f: any) => ({ id: f.id.toString(), - title: f.name || "Titlu necunoscut", + title: f.name || t('common.unknownTitle'), image: f.image_url || "", })); @@ -295,7 +295,7 @@ function VizualizareScreen() { const formattedDate = getFormattedDate(displayDateValue as string); const readingTime = getReadingTime(content as string); - const dateDisplay = category === "Noutăți" ? (formattedDate ? `${formattedDate} | ${readingTime}` : "Dată necunoscută") : (formattedDate || "Dată necunoscută"); + const dateDisplay = category === "Noutăți" ? (formattedDate ? `${formattedDate} | ${readingTime}` : "Dată necunoscută") : (formattedDate || t('common.unknownDate')); const createdTime = itemData?.created_at ? new Date(itemData.created_at).getTime() : 0; const updatedTime = itemData?.updated_at ? new Date(itemData.updated_at).getTime() : 0; @@ -314,7 +314,7 @@ function VizualizareScreen() { return ( setRetryKey(prev => prev + 1)} /> @@ -325,11 +325,11 @@ function VizualizareScreen() { // duce la lista categoriei respective; ultimul (titlul) nu e clickabil. const crumbCategory = (category as string) || (tipPagina as string); const crumbs: Crumb[] = [ - { label: "Acasă", href: "/(public)/acasa" }, + { label: t('common.home'), href: "/(public)/acasa" }, ...(crumbCategory ? [{ label: crumbCategory, href: `/(public)/acasa/categorie?title=${encodeURIComponent(crumbCategory)}` }] : []), - { label: (title as string) || "Titlu necunoscut" }, + { label: (title as string) || t('common.unknownTitle') }, ]; // SEO: descriere din continut + date structurate (Event / NewsArticle). @@ -358,7 +358,7 @@ function VizualizareScreen() { return ( - + {jsonLd ? ( ) : null} @@ -397,7 +397,7 @@ function VizualizareScreen() { {...({ "aria-level": 1 } as any)} style={[Typography.Heading2, { color: ColorScheme.white }]} > - {title || "Titlu necunoscut"} + {title || t('common.unknownTitle')} @@ -419,7 +419,7 @@ function VizualizareScreen() { {isUpdated && formattedUpdateDate ? ( - Actualizat: {formattedUpdateDate} + {t('detail.updated')} {formattedUpdateDate} ) : null} @@ -427,16 +427,16 @@ function VizualizareScreen() { {tipPagina === "Eveniment" && ( - Informații eveniment + {t('detail.eventInfo')} - De pe {date_start || "Dată de început necunoscută"} {time_start || ""} + {t('detail.from')} {date_start || t('common.unknownStartDate')} {time_start || ""} - Până la {date_end || "Dată de sfârșit necunoscută"} {time_end || ""} + {t('detail.until')} {date_end || t('common.unknownEndDate')} {time_end || ""} @@ -444,7 +444,7 @@ function VizualizareScreen() { - {location || "Locație necunoscută"} + {location || t('common.unknownLocation')} @@ -454,14 +454,14 @@ function VizualizareScreen() { {tipPagina === "Facultate" && ( - Contact și Locație + {t('detail.contact')} - Adresă + {t('detail.address')} - {address || "Adresă necunoscută"} + {address || t('common.unknownAddress')} @@ -470,7 +470,7 @@ function VizualizareScreen() { - Telefon + {t('detail.phone')} {phone} @@ -482,7 +482,7 @@ function VizualizareScreen() { - Website + {t('detail.website')} Linking.openURL(website as string)}> {website} @@ -493,17 +493,17 @@ function VizualizareScreen() { )} - {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || []).length > 0 && ( + {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || [], t).length > 0 && ( - Informații facilitate + {t('detail.facilityInfo')} - Program: - {formatSchedules(itemData.schedules).map((line: string, i: number) => ( + {t('detail.schedule')} + {formatSchedules(itemData.schedules, t).map((line: string, i: number) => ( {line} ))} @@ -514,10 +514,10 @@ function VizualizareScreen() { - {tipPagina === "Eveniment" ? "Despre eveniment" : tipPagina === "Facultate" ? "Despre facultate" : tipPagina === "Facilitate" ? "Despre facilitate" : "Detalii"} + {tipPagina === "Eveniment" ? t('detail.aboutEvent') : tipPagina === "Facultate" ? t('detail.aboutFaculty') : tipPagina === "Facilitate" ? t('detail.aboutFacility') : t('detail.details')} - {content || "Conținut necunoscut"} + {content || t('common.unknownContent')} @@ -525,7 +525,7 @@ function VizualizareScreen() { {/* Dreapta: 3 carduri Noutăți, una sub alta. */} {sidebarItems.length > 0 && ( - Articole similare + {t('detail.relatedArticles')} {sidebarItems.map((item) => ( openItem(item)} /> ))} @@ -536,7 +536,7 @@ function VizualizareScreen() { {/* Jos, sub tot: 3 carduri pe un rand. */} {bottomItems.length > 0 && ( - Mai multe + {t('detail.more')} {bottomCardWidth > 0 && bottomItems.map((item: any) => ( tipPagina === "Facilitate" ? ( diff --git a/Frontend/Mobile/src/app/(public)/cantina/index.tsx b/Frontend/Mobile/src/app/(public)/cantina/index.tsx index 054ec07f..c8bdc26c 100644 --- a/Frontend/Mobile/src/app/(public)/cantina/index.tsx +++ b/Frontend/Mobile/src/app/(public)/cantina/index.tsx @@ -10,6 +10,7 @@ import { MenuItem } from "@/components/ui/navigation/menu-item"; import api, { storage } from "@/services/api"; import { CantinaMenuSkeleton } from "@/components/ui/display/skeletons"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; function formatCategoryName(name: string): string { if (!name) return ""; @@ -32,16 +33,17 @@ export default function CantinaScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); + const { t } = useTranslation(); const [menuData, setMenuData] = useState([]); const [categoriesList, setCategoriesList] = useState([]); const daysFilter = useMemo(() => { const allDays = [ - { id: "luni", title: "Luni" }, - { id: "marti", title: "Marți" }, - { id: "miercuri", title: "Miercuri" }, - { id: "joi", title: "Joi" }, - { id: "vineri", title: "Vineri" }, + { id: "luni", title: t('days.1') }, + { id: "marti", title: t('days.2') }, + { id: "miercuri", title: t('days.3') }, + { id: "joi", title: t('days.4') }, + { id: "vineri", title: t('days.5') }, ]; const now = new Date(); @@ -57,9 +59,9 @@ export default function CantinaScreen() { return sortedDays.map((day, index) => ({ ...day, - title: index === 0 ? "Azi" : day.title, + title: index === 0 ? t('canteen.today') : day.title, })); - }, []); + }, [t]); const [selectedDay, setSelectedDay] = useState(daysFilter[0].id); const [loading, setLoading] = useState(true); @@ -124,7 +126,7 @@ export default function CantinaScreen() { console.warn('[API] Error refreshing daily menus data:', err); setHasError(true); if (menuData.length > 0) { - Alert.alert("Eroare la actualizare", "Nu s-a putut reîmprospăta meniul cantinei. Te rugăm să verifici conexiunea la internet."); + Alert.alert(t('common.updateError'), t('canteen.updateError')); } } }; @@ -184,7 +186,7 @@ export default function CantinaScreen() { return ( id && setSelectedDay(id)} @@ -207,7 +209,7 @@ export default function CantinaScreen() { {currentMenu.length === 0 ? ( - Nu există meniu disponibil pentru această zi. + {t('canteen.empty')} ) : ( diff --git a/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx b/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx index bac52499..8c84efa7 100644 --- a/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx @@ -13,6 +13,7 @@ import { Expandable } from "@/components/ui/layout/expandable"; import { MenuItem } from "@/components/ui/navigation/menu-item"; import api from "@/services/api"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; function formatCategoryName(name: string): string { if (!name) return ""; @@ -36,16 +37,17 @@ export default function CantinaScreen() { const theme = Colors[themeName]; const insets = useSafeAreaInsets(); const contentTop = useWebContentTop(); + const { t } = useTranslation(); const [menuData, setMenuData] = useState([]); const [categoriesList, setCategoriesList] = useState([]); const daysFilter = useMemo(() => { const allDays = [ - { id: "luni", title: "Luni" }, - { id: "marti", title: "Marți" }, - { id: "miercuri", title: "Miercuri" }, - { id: "joi", title: "Joi" }, - { id: "vineri", title: "Vineri" }, + { id: "luni", title: t('days.1') }, + { id: "marti", title: t('days.2') }, + { id: "miercuri", title: t('days.3') }, + { id: "joi", title: t('days.4') }, + { id: "vineri", title: t('days.5') }, ]; const now = new Date(); @@ -61,9 +63,9 @@ export default function CantinaScreen() { return sortedDays.map((day, index) => ({ ...day, - title: index === 0 ? "Azi" : day.title, + title: index === 0 ? t('canteen.today') : day.title, })); - }, []); + }, [t]); const [selectedDay, setSelectedDay] = useState(daysFilter[0].id); const [openCategories, setOpenCategories] = useState>({ @@ -172,7 +174,7 @@ export default function CantinaScreen() { > { if (id) { setSelectedDay(id); setOpenCategories({ "Meniul zilei": true }); } }} @@ -187,7 +189,7 @@ export default function CantinaScreen() { {currentMenu.length === 0 ? ( - Nu există meniu disponibil pentru această zi. + {t('canteen.empty')} ) : ( diff --git a/Frontend/Mobile/src/app/(public)/harta.tsx b/Frontend/Mobile/src/app/(public)/harta.tsx index eec7a9e3..35d8869d 100644 --- a/Frontend/Mobile/src/app/(public)/harta.tsx +++ b/Frontend/Mobile/src/app/(public)/harta.tsx @@ -8,6 +8,7 @@ import { CategoryHeader } from '@/components/ui/display/category-header'; import api, { storage } from '@/services/api'; import { ErrorState } from '@/components/ui/display/error-state'; import * as Location from 'expo-location'; +import { useTranslation } from 'react-i18next'; export default function HartaScreen() { const [selectedFacultyId, setSelectedFacultyId] = useState(null); @@ -17,6 +18,7 @@ export default function HartaScreen() { const insets = useSafeAreaInsets(); const themeName = (useColorScheme() ?? 'light') as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); const [hasError, setHasError] = useState(false); const loadData = useCallback(async () => { @@ -76,14 +78,14 @@ export default function HartaScreen() { const facultyFilters = useMemo(() => { return [ - { id: null, title: 'Toate locațiile' }, - { id: 'f8', title: 'Facilități' }, + { id: null, title: t('map.allLocations') }, + { id: 'f8', title: t('map.facilities') }, ...faculties.map(f => ({ id: f.id.toString(), title: f.abbreviation || f.name })) ]; - }, [faculties]); + }, [faculties, t]); const mappedBuildings = useMemo(() => { return locations.map((item: any) => ({ @@ -112,7 +114,7 @@ export default function HartaScreen() { paddingTop: insets.top + Spacing.md, }}> (lastKnownUserLocation); @@ -136,14 +138,14 @@ export default function HartaScreen() { const facultyFilters = useMemo(() => { return [ - { id: null, title: "Toate locațiile" }, - { id: "f8", title: "Facilități" }, + { id: null, title: t('map.allLocations') }, + { id: "f8", title: t('map.facilities') }, ...faculties.map((f) => ({ id: f.id.toString(), title: f.abbreviation || f.name })) ]; - }, [faculties]); + }, [faculties, t]); const mappedBuildings = useMemo(() => { return locations.map((item: any) => ({ @@ -176,7 +178,7 @@ export default function HartaScreen() { /> ([]); @@ -95,12 +97,12 @@ export default function MoreScreen() { onPress={async () => { if (isAuthenticated) { Alert.alert( - "Profilul tău", - "Ești deja conectat în cont. Vrei să te deconectezi?", + t('more.profileTitle'), + t('more.profileLoggedIn'), [ - { text: "Anulează", style: "cancel" }, + { text: t('more.cancel'), style: "cancel" }, { - text: "Deconectare", + text: t('more.logout'), style: "destructive", onPress: async () => { await logout(); @@ -147,7 +149,7 @@ export default function MoreScreen() { }} numberOfLines={2} > - {isAuthenticated ? "Deconectează-te" : "Conectare"} + {isAuthenticated ? t('more.disconnect') : t('more.login')} @@ -185,14 +187,14 @@ export default function MoreScreen() { }} numberOfLines={2} > - Setări + {t('more.settings')} {/* Section Title */} - Vizitează Galați + {t('more.visitGalati')} {/* Categories Grid - 3 items per row */} diff --git a/Frontend/Mobile/src/app/(public)/more/index.web.tsx b/Frontend/Mobile/src/app/(public)/more/index.web.tsx index 02c349cc..239b494d 100644 --- a/Frontend/Mobile/src/app/(public)/more/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/index.web.tsx @@ -9,6 +9,7 @@ import { CategoryHeader } from "@/components/ui/display/category-header"; import { useWebContentTop } from "@/hooks/use-web-content-top"; import { Typography } from "@/constants/typography"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; // Import local SVGs import BusIcon from "@/assets/icons/svg/bus.svg"; @@ -24,6 +25,7 @@ export default function MoreScreen() { const insets = useSafeAreaInsets(); const contentTop = useWebContentTop(); const router = useRouter(); + const { t } = useTranslation(); const [categories, setCategories] = useState([]); useEffect(() => { @@ -76,13 +78,13 @@ export default function MoreScreen() { }} > - + {/* Section Title */} - Vizitează Galați + {t('more.visitGalati')} {/* Categories Grid - 3 items per row */} diff --git a/Frontend/Mobile/src/app/(public)/more/limba.tsx b/Frontend/Mobile/src/app/(public)/more/limba.tsx index cf4b0a23..f92820f0 100644 --- a/Frontend/Mobile/src/app/(public)/more/limba.tsx +++ b/Frontend/Mobile/src/app/(public)/more/limba.tsx @@ -9,6 +9,7 @@ import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import CloseIcon from "@/assets/icons/svg/x.svg"; @@ -27,8 +28,8 @@ export default function LanguageScreen() { opacity: interpolate(scrollY.value, [50, 90], [0, 1], Extrapolation.CLAMP), })); - // Read current selected language from settings store - const currentLang = settingsStore.getLang(); + const { t, i18n } = useTranslation(); + const currentLang = i18n.language; const languages = [ { code: "ro", label: "Română" }, @@ -78,7 +79,7 @@ export default function LanguageScreen() { ]} numberOfLines={1} > - Limbă aplicație + {t('language.title')} ), @@ -94,11 +95,11 @@ export default function LanguageScreen() { onScroll={scrollHandler} scrollEventThrottle={16} > - + - Selectează limba + {t('language.select')} @@ -133,7 +134,7 @@ export default function LanguageScreen() { fontSize: 16 }} > - (Selectat) + {t('language.selected')} )} diff --git a/Frontend/Mobile/src/app/(public)/more/limba.web.tsx b/Frontend/Mobile/src/app/(public)/more/limba.web.tsx index 18cc0de8..fe2ff957 100644 --- a/Frontend/Mobile/src/app/(public)/more/limba.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/limba.web.tsx @@ -10,6 +10,7 @@ import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { WebContainer } from "@/components/ui/layout/web-container"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import CloseIcon from "@/assets/icons/svg/x.svg"; @@ -23,8 +24,8 @@ export default function LanguageScreen() { const [scrollY] = useState(() => new Animated.Value(0)); - // Read current selected language from settings store - const currentLang = settingsStore.getLang(); + const { t, i18n } = useTranslation(); + const currentLang = i18n.language; const languages = [ { code: "ro", label: "Română" }, @@ -80,7 +81,7 @@ export default function LanguageScreen() { ]} numberOfLines={1} > - Limbă aplicație + {t('language.title')} ), @@ -101,11 +102,11 @@ export default function LanguageScreen() { > - + - Selectează limba + {t('language.select')} @@ -140,7 +141,7 @@ export default function LanguageScreen() { fontSize: 16 }} > - (Selectat) + {t('language.selected')} )} diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index cb69d11e..ab914452 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -10,6 +10,7 @@ import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import GlobeIcon from "@/assets/icons/svg/globe-europe.svg"; @@ -29,6 +30,8 @@ export default function SettingsScreen() { opacity: interpolate(scrollY.value, [50, 90], [0, 1], Extrapolation.CLAMP), })); + const { t, i18n } = useTranslation(); + // Local settings states driven by settingsStore const [selectedTheme, setSelectedTheme] = useState(() => settingsStore.getTheme()); const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); @@ -95,7 +98,7 @@ export default function SettingsScreen() { ]} numberOfLines={1} > - Setări + {t('settings.title')} ), @@ -111,20 +114,20 @@ export default function SettingsScreen() { onScroll={scrollHandler} scrollEventThrottle={16} > - + {/* SECȚIUNEA 1: ASPECT & LIMBĂ */} - Aspect & Limbă + {t('settings.appearanceLang')} {/* Opțiune Temă */} - Temă aplicație + {t('settings.themeApp')} {/* Buton navigare temă - navighează la tema.tsx */} - Temă curentă: - {selectedTheme === "system" ? "Sistem" : selectedTheme === "light" ? "Luminos" : "Întunecat"} + {t('settings.currentTheme')} + {selectedTheme === "system" ? t('theme.system') : selectedTheme === "light" ? t('theme.light') : t('theme.dark')} - Limbă aplicație + {t('language.title')} {/* Buton navigare limbă - navighează la limba.tsx */} - Limbă curentă: {languages.find((l) => l.code === selectedLang)?.label || "Română"} + {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || "Română"} - Asistență & Info + {t('settings.supportInfo')} @@ -206,7 +209,7 @@ export default function SettingsScreen() { opacity: pressed ? 0.6 : 1 })} > - Vizitează Website UGAL + {t('settings.visitWebsite')} @@ -218,7 +221,7 @@ export default function SettingsScreen() { InsideUGAL v0.1.0 - Creat pentru studenții Universității „Dunărea de Jos” din Galați + {t('settings.appSlogan')} diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index 16778d15..2da14abf 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -10,6 +10,7 @@ import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { WebContainer } from "@/components/ui/layout/web-container"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import GlobeIcon from "@/assets/icons/svg/globe-europe.svg"; @@ -24,6 +25,8 @@ export default function SettingsScreen() { const [scrollY] = useState(() => new Animated.Value(0)); + const { t, i18n } = useTranslation(); + // Local settings states driven by settingsStore const [selectedTheme, setSelectedTheme] = useState(() => settingsStore.getTheme()); const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); @@ -91,7 +94,7 @@ export default function SettingsScreen() { ]} numberOfLines={1} > - Setări + {t('settings.title')} ), @@ -112,20 +115,20 @@ export default function SettingsScreen() { > - + {/* SECȚIUNEA 1: ASPECT & LIMBĂ */} - Aspect & Limbă + {t('settings.appearanceLang')} {/* Opțiune Temă */} - Temă aplicație + {t('settings.themeApp')} {/* Buton navigare temă - navighează la tema.tsx */} - Temă curentă: - {selectedTheme === "system" ? "Sistem" : selectedTheme === "light" ? "Luminos" : "Întunecat"} + {t('settings.currentTheme')} + {selectedTheme === "system" ? t('theme.system') : selectedTheme === "light" ? t('theme.light') : t('theme.dark')} - Limbă aplicație + {t('language.title')} {/* Buton navigare limbă - navighează la limba.tsx */} - Limbă curentă: {languages.find((l) => l.code === selectedLang)?.label || "Română"} + {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || "Română"} - Asistență & Info + {t('settings.supportInfo')} @@ -207,7 +210,7 @@ export default function SettingsScreen() { opacity: pressed ? 0.6 : 1 })} > - Vizitează Website UGAL + {t('settings.visitWebsite')} @@ -219,7 +222,7 @@ export default function SettingsScreen() { InsideUGAL v0.1.0 - Creat pentru studenții Universității „Dunărea de Jos” din Galați + {t('settings.appSlogan')} diff --git a/Frontend/Mobile/src/app/(public)/more/tema.tsx b/Frontend/Mobile/src/app/(public)/more/tema.tsx index 69b94bd5..718620a3 100644 --- a/Frontend/Mobile/src/app/(public)/more/tema.tsx +++ b/Frontend/Mobile/src/app/(public)/more/tema.tsx @@ -9,6 +9,7 @@ import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import CloseIcon from "@/assets/icons/svg/x.svg"; @@ -27,13 +28,15 @@ export default function ThemeScreen() { opacity: interpolate(scrollY.value, [50, 90], [0, 1], Extrapolation.CLAMP), })); + const { t } = useTranslation(); + // Read current selected theme from settings store const currentTheme = settingsStore.getTheme(); const themes = [ - { code: "system", label: "Sistem" }, - { code: "light", label: "Luminos" }, - { code: "dark", label: "Întunecat" } + { code: "system", label: t('theme.system') }, + { code: "light", label: t('theme.light') }, + { code: "dark", label: t('theme.dark') }, ]; const handleSelectTheme = (code: string) => { @@ -75,7 +78,7 @@ export default function ThemeScreen() { ]} numberOfLines={1} > - Temă aplicație + {t('theme.title')} ), @@ -91,20 +94,20 @@ export default function ThemeScreen() { onScroll={scrollHandler} scrollEventThrottle={16} > - + - Selectează tema + {t('theme.select')} - {themes.map((t) => { - const isSelected = currentTheme === t.code; + {themes.map((themeItem) => { + const isSelected = currentTheme === themeItem.code; return ( handleSelectTheme(t.code)} + key={themeItem.code} + onPress={() => handleSelectTheme(themeItem.code)} style={({ pressed }) => ({ flexDirection: "row", justifyContent: "space-between", @@ -120,7 +123,7 @@ export default function ThemeScreen() { fontSize: 18 }} > - {t.label} + {themeItem.label} {isSelected && ( - (Selectat) + {t('language.selected')} )} diff --git a/Frontend/Mobile/src/app/(public)/more/tema.web.tsx b/Frontend/Mobile/src/app/(public)/more/tema.web.tsx index 973ce788..c1d9b9e6 100644 --- a/Frontend/Mobile/src/app/(public)/more/tema.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/tema.web.tsx @@ -10,6 +10,7 @@ import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { WebContainer } from "@/components/ui/layout/web-container"; import { settingsStore } from "@/utils/settings-store"; +import { useTranslation } from 'react-i18next'; import CloseIcon from "@/assets/icons/svg/x.svg"; @@ -23,13 +24,15 @@ export default function ThemeScreen() { const [scrollY] = useState(() => new Animated.Value(0)); + const { t } = useTranslation(); + // Read current selected theme from settings store const currentTheme = settingsStore.getTheme(); const themes = [ - { code: "system", label: "Sistem" }, - { code: "light", label: "Luminos" }, - { code: "dark", label: "Întunecat" } + { code: "system", label: t('theme.system') }, + { code: "light", label: t('theme.light') }, + { code: "dark", label: t('theme.dark') }, ]; const handleSelectTheme = (code: string) => { @@ -77,7 +80,7 @@ export default function ThemeScreen() { ]} numberOfLines={1} > - Temă aplicație + {t('theme.title')} ), @@ -98,20 +101,20 @@ export default function ThemeScreen() { > - + - Selectează tema + {t('theme.select')} - {themes.map((t) => { - const isSelected = currentTheme === t.code; + {themes.map((themeItem) => { + const isSelected = currentTheme === themeItem.code; return ( handleSelectTheme(t.code)} + key={themeItem.code} + onPress={() => handleSelectTheme(themeItem.code)} style={({ pressed }) => ({ flexDirection: "row", justifyContent: "space-between", @@ -127,7 +130,7 @@ export default function ThemeScreen() { fontSize: 18 }} > - {t.label} + {themeItem.label} {isSelected && ( - (Selectat) + {t('language.selected')} )} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/_layout.tsx b/Frontend/Mobile/src/app/(public)/sesizari/_layout.tsx index 52d3296f..5220ed0a 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/_layout.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/_layout.tsx @@ -9,6 +9,7 @@ import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import PlusIcon from "@/assets/icons/svg/plus.svg"; import { CategoryHeader, FilterItem } from "@/components/ui/display/category-header"; import { InteractiveGlass } from "@/components/ui/layout/interactive-glass"; +import { useTranslation } from 'react-i18next'; export const unstable_settings = { initialRouteName: "index", @@ -20,6 +21,7 @@ export default function SesizariLayout() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); + const { t } = useTranslation(); const activeFilter = (params.filter as string) || "toate"; const scalePlusAnim = useSharedValue(1); @@ -36,11 +38,11 @@ export default function SesizariLayout() { }; const filters: FilterItem[] = [ - { id: "toate", title: "Toate sesizările" }, - { id: "mele", title: "Sesizările mele" }, - { id: "active", title: "Active" }, - { id: "respinse", title: "Respinse" }, - { id: "finalizate", title: "Finalizate" }, + { id: "toate", title: t('reports.all') }, + { id: "mele", title: t('reports.mine') }, + { id: "active", title: t('reports.active') }, + { id: "respinse", title: t('reports.rejected') }, + { id: "finalizate", title: t('reports.completed') }, ]; return ( @@ -63,7 +65,7 @@ export default function SesizariLayout() { header: () => ( { @@ -122,7 +124,7 @@ export default function SesizariLayout() { name="adauga" options={{ headerShown: true, - headerTitle: "Sesizare nouă", + headerTitle: t('reports.newReport'), headerLeft: () => ( router.back()} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx b/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx index a2943480..f8a0d1c1 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx @@ -10,6 +10,7 @@ import XIcon from "@/assets/icons/svg/x.svg"; import ImagesIcon from "@/assets/icons/svg/images.svg"; import * as ImagePicker from "expo-image-picker"; import api, { storage } from "@/services/api"; +import { useTranslation } from 'react-i18next'; interface LocationPillProps { label: string; @@ -70,9 +71,10 @@ export default function AdaugaSesizareScreen() { const router = useRouter(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); const [locations, setLocations] = useState([]); - const [location, setLocation] = useState("Exterior"); + const [location, setLocation] = useState(t('reports.exterior')); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [photos, setPhotos] = useState([]); @@ -107,19 +109,20 @@ export default function AdaugaSesizareScreen() { const buildingsList = useMemo(() => { const list = locations.map(loc => loc.name); - return list.length > 0 ? [...Array.from(new Set(list)), "Exterior"] : ["Exterior"]; - }, [locations]); + const exterior = t('reports.exterior'); + return list.length > 0 ? [...Array.from(new Set(list)), exterior] : [exterior]; + }, [locations, t]); const validate = () => { const newErrors: { title?: string; description?: string; photos?: string } = {}; if (!title.trim()) { - newErrors.title = "Titlul este obligatoriu."; + newErrors.title = t('reports.titleRequired'); } if (!description.trim()) { - newErrors.description = "Descrierea este obligatorie."; + newErrors.description = t('reports.descRequired'); } if (photos.length === 0) { - newErrors.photos = "Este obligatoriu să adăugați o fotografie."; + newErrors.photos = t('reports.photoRequired'); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -129,7 +132,7 @@ export default function AdaugaSesizareScreen() { const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permissionResult.granted) { - alert("Permisiunea de acces la fotografii este necesară pentru a adăuga o poză!"); + alert(t('reports.photoPermission')); return; } @@ -150,7 +153,7 @@ export default function AdaugaSesizareScreen() { const handleRemovePhoto = (index: number) => { setPhotos([]); - setErrors({ ...errors, photos: "Este obligatoriu să adăugați o fotografie." }); + setErrors({ ...errors, photos: t('reports.photoRequired') }); }; const handleSubmit = async () => { @@ -207,7 +210,7 @@ export default function AdaugaSesizareScreen() { console.warn('[API] Error creating complaint:', err); setErrors(prev => ({ ...prev, - general: err.message || "A apărut o eroare la trimiterea sesizării. Te rugăm să încerci din nou." + general: err.message || t('reports.submitError') })); } finally { setSubmitting(false); @@ -233,7 +236,7 @@ export default function AdaugaSesizareScreen() { )} - Locație + {t('reports.location')} {buildingsList.map((bldg) => ( - Titlu + {t('reports.titleField')} { @@ -278,7 +281,7 @@ export default function AdaugaSesizareScreen() { - Descriere detaliată + {t('reports.descDetailed')} { @@ -310,7 +313,7 @@ export default function AdaugaSesizareScreen() { )} - Adaugă o fotografie + {t('reports.addPhoto')} {photos.length < 1 && ( - Adaugă poză + {t('reports.addPhotoBtn')} )} @@ -403,7 +406,7 @@ export default function AdaugaSesizareScreen() { {submitting ? ( ) : ( - Trimite sesizarea + {t('reports.submit')} )} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx index a0adc20c..653a403b 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx @@ -12,6 +12,7 @@ import api, { storage } from "@/services/api"; import XIcon from "@/assets/icons/svg/x.svg"; import ImagesIcon from "@/assets/icons/svg/images.svg"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; +import { useTranslation } from 'react-i18next'; interface LocationPillProps { label: string; @@ -53,11 +54,12 @@ export default function AdaugaSesizareScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); + const { t } = useTranslation(); const [locations, setLocations] = useState([]); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); - const [location, setLocation] = useState("Exterior"); + const [location, setLocation] = useState(t('reports.exterior')); const [photos, setPhotos] = useState([]); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState<{ title?: string; description?: string; photos?: string; general?: string }>({}); @@ -90,19 +92,20 @@ export default function AdaugaSesizareScreen() { const buildingsList = useMemo(() => { const list = locations.map(loc => loc.name); - return list.length > 0 ? [...Array.from(new Set(list)), "Exterior"] : ["Exterior"]; - }, [locations]); + const exterior = t('reports.exterior'); + return list.length > 0 ? [...Array.from(new Set(list)), exterior] : [exterior]; + }, [locations, t]); const validate = () => { const newErrors: { title?: string; description?: string; photos?: string } = {}; if (!title.trim()) { - newErrors.title = "Titlul este obligatoriu."; + newErrors.title = t('reports.titleRequired'); } if (!description.trim()) { - newErrors.description = "Descrierea este obligatorie."; + newErrors.description = t('reports.descRequired'); } if (photos.length === 0) { - newErrors.photos = "Este obligatoriu să adăugați o fotografie."; + newErrors.photos = t('reports.photoRequired'); } setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -112,7 +115,7 @@ export default function AdaugaSesizareScreen() { const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permissionResult.granted) { - alert("Permisiunea de acces la fotografii este necesară pentru a adăuga o poză!"); + alert(t('reports.photoPermission')); return; } @@ -133,7 +136,7 @@ export default function AdaugaSesizareScreen() { const handleRemovePhoto = (index: number) => { setPhotos([]); - setErrors({ ...errors, photos: "Este obligatoriu să adăugați o fotografie." }); + setErrors({ ...errors, photos: t('reports.photoRequired') }); }; const handleSubmit = async () => { @@ -183,7 +186,7 @@ export default function AdaugaSesizareScreen() { console.warn('[API] Error creating complaint:', err); setErrors(prev => ({ ...prev, - general: err.message || "A apărut o eroare la trimiterea sesizării. Te rugăm să încerci din nou." + general: err.message || t('reports.submitError') })); } finally { setSubmitting(false); @@ -218,7 +221,7 @@ export default function AdaugaSesizareScreen() { > - Sesizare nouă + {t('reports.newReport')} @@ -229,7 +232,7 @@ export default function AdaugaSesizareScreen() { )} - Locație + {t('reports.location')} {buildingsList.map((bldg) => ( - Titlu + {t('reports.titleField')} { @@ -274,7 +277,7 @@ export default function AdaugaSesizareScreen() { - Descriere detaliată + {t('reports.descDetailed')} { @@ -307,7 +310,7 @@ export default function AdaugaSesizareScreen() { )} - Adaugă o fotografie + {t('reports.addPhoto')} {photos.length < 1 && ( - Adaugă poză + {t('reports.addPhotoBtn')} )} @@ -400,7 +403,7 @@ export default function AdaugaSesizareScreen() { {submitting ? ( ) : ( - Trimite sesizarea + {t('reports.submit')} )} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx index 487b12fe..2734022c 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx @@ -12,6 +12,7 @@ import { CAROUSEL_CARD_MARGIN } from "@/components/ui/display/carousel/carousel. import { CategoryHeader } from "@/components/ui/display/category-header"; import api, { storage, resolveImageUrl } from "@/services/api"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; import LocationIcon from "@/assets/icons/svg/location.svg"; import CalendarIcon from "@/assets/icons/svg/calendar.svg"; @@ -49,7 +50,8 @@ export default function SesizareDetaliiScreen() { const insets = useSafeAreaInsets(); const id = params.id as string; - + const { t } = useTranslation(); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [report, setReport] = useState(null); @@ -89,22 +91,22 @@ export default function SesizareDetaliiScreen() { const item = res.data; setReport({ id: item.id.toString(), - title: item.title || "Titlu lipsă", - description: item.description || "Nicio descriere adăugată.", - category: "General", - location: locationMap.get(item.location_id) || "Locație nespecificată", + title: item.title || t('reports.missingTitle'), + description: item.description || t('reports.noDescription'), + category: t('reports.general'), + location: locationMap.get(item.location_id) || t('reports.unknownLocation'), status: mapApiStatus(item.status), - date: item.created_at || "Dată nespecificată", + date: item.created_at || t('common.unknownDate'), image: resolveImageUrl(item.image_url) || "", }); } else { - setError("Sesizarea nu a putut fi găsită."); + setError(t('reports.notFound')); } setLoading(false); } catch (err: any) { setLoading(false); console.error("[API] Error fetching complaint detail:", err); - setError(err.message || "A apărut o eroare la încărcarea sesizării."); + setError(err.message || t('reports.loadError')); } }, [id]); @@ -117,11 +119,11 @@ export default function SesizareDetaliiScreen() { const title = report?.title || ""; const description = report?.description || ""; - const location = report?.location || "Locație nespecificată"; + const location = report?.location || t('reports.unknownLocation'); const status = report?.status || "active"; - const date = report?.date || "Dată nespecificată"; + const date = report?.date || t('common.unknownDate'); - const statusLabel = status === "active" ? "Activă" : status === "respinse" ? "Respinsă" : "Soluționată"; + const statusLabel = status === "active" ? t('reports.statusActive') : status === "respinse" ? t('reports.statusRejected') : t('reports.statusCompleted'); const [modalVisible, setModalVisible] = useState(false); const [selectedImage, setSelectedImage] = useState(null); @@ -141,25 +143,25 @@ export default function SesizareDetaliiScreen() { switch (status) { case "active": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "În curs de analiză", desc: "Un administrator evaluează detaliile problemei.", active: true, completed: true }, - { title: "Soluționare finalizată", desc: "Echipa va interveni pentru a remedia situația.", completed: false }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2ActiveTitle'), desc: t('reports.step2ActiveDesc'), active: true, completed: true }, + { title: t('reports.step3ActiveTitle'), desc: t('reports.step3ActiveDesc'), completed: false }, ]; case "respinse": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "Respinsă", desc: "Solicitarea a fost respinsă de către echipa administrativă.", completed: true, isError: true }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2RejectedTitle'), desc: t('reports.step2RejectedDesc'), completed: true, isError: true }, ]; case "finalizate": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "În analiză administrativă", desc: "Problema a fost procesată cu succes.", completed: true }, - { title: "Soluționată", desc: "Problema a fost rezolvată în teren de personalul tehnic.", completed: true, isSuccess: true }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2CompletedTitle'), desc: t('reports.step2CompletedDesc'), completed: true }, + { title: t('reports.step3CompletedTitle'), desc: t('reports.step3CompletedDesc'), completed: true, isSuccess: true }, ]; default: return []; } - }, [status, date]); + }, [status, date, t]); if (loading) { return ( @@ -212,7 +214,7 @@ export default function SesizareDetaliiScreen() { ), }} /> - + ); } @@ -250,7 +252,7 @@ export default function SesizareDetaliiScreen() { - Informații sesizare + {t('reports.infoTitle')} @@ -279,7 +281,7 @@ export default function SesizareDetaliiScreen() { - Descriere problemă + {t('reports.descSection')} {description} @@ -309,7 +311,7 @@ export default function SesizareDetaliiScreen() { - Istoric progres + {t('reports.progressTitle')} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx index 02b53317..84a552d2 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx @@ -14,6 +14,7 @@ import { WebContainer } from "@/components/ui/layout/web-container"; import { Breadcrumbs } from "@/components/ui/navigation/breadcrumbs"; import api, { storage, resolveImageUrl } from "@/services/api"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; import LocationIcon from "@/assets/icons/svg/location.svg"; import CalendarIcon from "@/assets/icons/svg/calendar.svg"; @@ -49,6 +50,7 @@ export default function SesizareDetaliiScreen() { const insets = useSafeAreaInsets(); const id = params.id as string; + const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.tsx index e974a559..17074eb8 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.tsx @@ -10,6 +10,7 @@ import api, { storage, resolveImageUrl } from "@/services/api"; import { useAuth } from "@/contexts/auth-context"; import { SesizariListSkeleton } from "@/components/ui/display/skeletons"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; type FilterType = "toate" | "mele" | "active" | "respinse" | "finalizate"; @@ -34,6 +35,7 @@ export default function SesizariScreen() { const params = useLocalSearchParams(); const { isAuthenticated, user, isLoading: authLoading } = useAuth(); + const { t } = useTranslation(); const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); @@ -45,7 +47,7 @@ export default function SesizariScreen() { setRefreshing(true); const success = await loadData(); if (!success && reports.length > 0) { - Alert.alert("Eroare la actualizare", "Nu s-au putut reîmprospăta sesizările. Te rugăm să verifici conexiunea la internet."); + Alert.alert(t('common.updateError'), t('reports.updateError')); } setRefreshing(false); }; @@ -117,10 +119,10 @@ export default function SesizariScreen() { id: item.id.toString(), title: item.title, description: item.description, - category: "General", + category: t('reports.general'), status: mapApiStatus(item.status), date: item.created_at, - location: locationMap.get(item.location_id) || "Locație nespecificată", + location: locationMap.get(item.location_id) || t('reports.unknownLocation'), isUserReport: myProfileId ? item.user_id === myProfileId : false, image: resolveImageUrl(item.image_url) || undefined, })); @@ -130,7 +132,7 @@ export default function SesizariScreen() { } catch (err: any) { setLoading(false); console.warn('[API] Error fetching complaints:', err); - setError(err.message || "A apărut o eroare la încărcarea sesizărilor."); + setError(err.message || t('reports.loadErrorGeneral')); return false; } }; @@ -196,16 +198,16 @@ export default function SesizariScreen() { return ( - Trebuie să fii conectat + {t('reports.loginRequired')} - Conectează-te pentru a trimite sau vizualiza sesizările tale. + {t('reports.loginDesc')} router.push("/(auth)")} style={{ backgroundColor: theme.primary, paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, borderRadius: Spacing.md }} > - Conectare + {t('reports.login')} ); @@ -229,10 +231,10 @@ export default function SesizariScreen() { ListEmptyComponent={ - Nicio sesizare în această secțiune + {t('reports.empty')} - Momentan nu există înregistrări. + {t('reports.emptyDesc')} } diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx index ae2d98e9..e488c98c 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx @@ -15,17 +15,10 @@ import PlusIcon from "@/assets/icons/svg/plus.svg"; import api, { storage, resolveImageUrl } from "@/services/api"; import { useAuth } from "@/contexts/auth-context"; import { ErrorState } from "@/components/ui/display/error-state"; +import { useTranslation } from 'react-i18next'; type FilterType = "toate" | "mele" | "active" | "respinse" | "finalizate"; -const filters: FilterItem[] = [ - { id: "toate", title: "Toate sesizările" }, - { id: "mele", title: "Sesizările mele" }, - { id: "active", title: "Active" }, - { id: "respinse", title: "Respinse" }, - { id: "finalizate", title: "Finalizate" }, -]; - function mapApiStatus(apiStatus: string): "active" | "respinse" | "finalizate" { switch (apiStatus) { case 'respins': @@ -46,6 +39,15 @@ export default function SesizariScreen() { const router = useRouter(); const { isAuthenticated, user, isLoading: authLoading } = useAuth(); + const { t } = useTranslation(); + + const filters: FilterItem[] = [ + { id: "toate", title: t('reports.all') }, + { id: "mele", title: t('reports.mine') }, + { id: "active", title: t('reports.active') }, + { id: "respinse", title: t('reports.rejected') }, + { id: "finalizate", title: t('reports.completed') }, + ]; const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); @@ -121,10 +123,10 @@ export default function SesizariScreen() { id: item.id.toString(), title: item.title, description: item.description, - category: "General", + category: t('reports.general'), status: mapApiStatus(item.status), date: item.created_at, - location: locationMap.get(item.location_id) || "Locație nespecificată", + location: locationMap.get(item.location_id) || t('reports.unknownLocation'), isUserReport: myProfileId ? item.user_id === myProfileId : false, image: resolveImageUrl(item.image_url) || undefined, })); @@ -133,7 +135,7 @@ export default function SesizariScreen() { } catch (err: any) { setLoading(false); console.warn('[API] Error fetching complaints:', err); - setError(err.message || "A apărut o eroare la încărcarea sesizărilor."); + setError(err.message || t('reports.loadErrorGeneral')); } }; @@ -187,7 +189,7 @@ export default function SesizariScreen() { > setActiveFilter((id as FilterType) || "mele")} @@ -214,16 +216,16 @@ export default function SesizariScreen() { {!isAuthenticated ? ( - Trebuie să fii conectat + {t('reports.loginRequired')} - Conectează-te pentru a trimite sau vizualiza sesizările tale. + {t('reports.loginDesc')} router.push("/(auth)")} style={{ backgroundColor: theme.primary, paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, borderRadius: Spacing.md }} > - Conectare + {t('reports.login')} ) : loading && reports.length === 0 ? ( @@ -245,10 +247,10 @@ export default function SesizariScreen() { {filteredData.length === 0 && ( - Nicio sesizare în această secțiune + {t('reports.empty')} - Momentan nu există înregistrări. + {t('reports.emptyDesc')} )} diff --git a/Frontend/Mobile/src/app/_layout.tsx b/Frontend/Mobile/src/app/_layout.tsx index adc87268..7db35969 100644 --- a/Frontend/Mobile/src/app/_layout.tsx +++ b/Frontend/Mobile/src/app/_layout.tsx @@ -7,6 +7,7 @@ import 'react-native-reanimated'; import { useColorScheme } from "@/hooks/use-color-scheme"; import { View } from 'react-native'; import { AuthProvider } from '@/contexts/auth-context'; +import '@/i18n'; SplashScreen.preventAutoHideAsync(); diff --git a/Frontend/Mobile/src/app/ace.tsx b/Frontend/Mobile/src/app/ace.tsx index 908da49f..ce35931e 100644 --- a/Frontend/Mobile/src/app/ace.tsx +++ b/Frontend/Mobile/src/app/ace.tsx @@ -23,6 +23,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { NewsCard } from '@/components/ui/display/news-card'; import { ace } from '@/services/api'; +import { useTranslation } from 'react-i18next'; import CloseIcon from '@/assets/icons/svg/x.svg'; import MessagePlusIcon from '@/assets/icons/svg/message-plus.svg'; @@ -120,6 +121,7 @@ interface ChatInputProps { function ChatInput({ onSend, theme, themeWhite }: ChatInputProps) { const insets = useSafeAreaInsets(); + const { t } = useTranslation(); const [inputText, setInputText] = useState(''); const bottomPad = useSharedValue(Math.max(insets.bottom, Spacing.md)); const padStyle = useAnimatedStyle(() => ({ paddingBottom: bottomPad.value })); @@ -180,7 +182,7 @@ function ChatInput({ onSend, theme, themeWhite }: ChatInputProps) { [...prev, { id: generateMsgId(), text: typeof parsed.error === 'string' ? parsed.error : 'Eroare la asistent.', sender: 'ai', timestamp: new Date() }]); + setMessages(prev => [...prev, { id: generateMsgId(), text: typeof parsed.error === 'string' ? parsed.error : t('ace.assistantError'), sender: 'ai', timestamp: new Date() }]); return; } const tok = parsed.token ?? parsed.content ?? ''; @@ -417,7 +420,7 @@ export default function AceScreen() { if (tokens.length === 0) { setIsTyping(false); - setMessages(prev => [...prev, { id: generateMsgId(), text: 'Nu am primit un răspuns valid.', sender: 'ai', timestamp: new Date() }]); + setMessages(prev => [...prev, { id: generateMsgId(), text: t('ace.noValidResponse'), sender: 'ai', timestamp: new Date() }]); return; } @@ -439,7 +442,7 @@ export default function AceScreen() { }) .catch(() => { setIsTyping(false); - setMessages(prev => [...prev, { id: generateMsgId(), text: 'A apărut o eroare. Vă rugăm să încercați din nou.', sender: 'ai', timestamp: new Date() }]); + setMessages(prev => [...prev, { id: generateMsgId(), text: t('ace.errorOccurred'), sender: 'ai', timestamp: new Date() }]); scrollToBottom(); }); }; @@ -579,10 +582,10 @@ export default function AceScreen() { style={{ marginBottom: Spacing.xs }} /> - Cu ce te pot ajuta azi? + {t('ace.howCanIHelp')} - Întreabă-mă despre evenimente, cantină, hartă sau sesizări. + {t('ace.promptSuggestion')} ) : null} diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index 1d51a88a..072c8d57 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -18,6 +18,7 @@ import { Typography } from "@/constants/typography"; import { getFormattedDate, getReadingTime, isoToRomanianDateStr } from "@/utils/date"; import { WebContainer } from "@/components/ui/layout/web-container"; import { Breadcrumbs, type Crumb } from "@/components/ui/navigation/breadcrumbs"; +import { useTranslation } from "react-i18next"; import { CompactCard } from "@/components/ui/display/home-highlights"; import { NewsCard, CategoryTag } from "@/components/ui/display/news-card"; import { eventHref, anuntHref } from "@/utils/article-url"; @@ -78,6 +79,7 @@ export function ArticleDetail({ const insets = useSafeAreaInsets(); const router = useRouter(); const { width } = useWindowDimensions(); + const { t } = useTranslation(); const twoCol = width >= TWO_COL_BREAKPOINT; const tipPagina = type || "Eveniment"; @@ -182,7 +184,7 @@ export function ArticleDetail({ }; const handleCall = () => { - if (window.confirm(`Doriți să apelați numărul ${phone}?`)) { + if (window.confirm(t('detail.callConfirm', { phone }))) { Linking.openURL(`tel:${phone}`); } }; @@ -195,11 +197,11 @@ export function ArticleDetail({ // duce la lista categoriei respective; ultimul (titlul) nu e clickabil. const crumbCategory = category || tipPagina; const crumbs: Crumb[] = [ - { label: "Acasă", href: "/(public)/acasa" }, + { label: t('common.home'), href: "/(public)/acasa" }, ...(crumbCategory - ? [{ label: crumbCategory, href: `/(public)/acasa/categorie?title=${encodeURIComponent(crumbCategory)}` }] + ? [{ label: crumbCategory === "Evenimente" ? t('home.events') : crumbCategory === "Noutăți" ? t('home.news') : crumbCategory === "Facultăți" ? t('home.faculties') : crumbCategory === "Facilități" ? t('home.facilities') : crumbCategory, href: `/(public)/acasa/categorie?title=${encodeURIComponent(crumbCategory)}` }] : []), - { label: title || "Articol" }, + { label: title || t('common.article') }, ]; return ( @@ -209,7 +211,7 @@ export function ArticleDetail({ @@ -230,7 +232,7 @@ export function ArticleDetail({ ) : ( - {category || (tipPagina === "Facultate" ? "Facultate" : "Categorie")} + {category || (tipPagina === "Facultate" ? t('common.faculty') : t('common.category'))} )} - {title || "Titlu"} + {title || t('common.unknownTitle')} @@ -255,22 +257,22 @@ export function ArticleDetail({ {tipPagina !== "Facultate" && ( - {[dateDisplay, author].filter(Boolean).join(" · ") || "Dată necunoscută"} + {[dateDisplay, author].filter(Boolean).join(" · ") || t('common.unknownDate')} )} {tipPagina === "Eveniment" && ( - Informații eveniment + {t('detail.eventInfo')} - De pe {date_start || "N/A"} {time_start || ""} + {t('detail.from')} {date_start || "N/A"} {time_start || ""} - Până la {date_end || "N/A"} {time_end || ""} + {t('detail.until')} {date_end || "N/A"} {time_end || ""} @@ -278,7 +280,7 @@ export function ArticleDetail({ - {location || "Locație nespecificată"} + {location || t('common.unknownLocation')} @@ -288,14 +290,14 @@ export function ArticleDetail({ {tipPagina === "Facultate" && ( - Contact și Locație + {t('detail.contact')} - Adresă + {t('detail.address')} - {address || "Nespecificată"} + {address || t('common.unknownAddress')} @@ -304,7 +306,7 @@ export function ArticleDetail({ - Telefon + {t('detail.phone')} {phone} @@ -316,7 +318,7 @@ export function ArticleDetail({ - Website + {t('detail.website')} Linking.openURL(website)}> {website} @@ -329,10 +331,10 @@ export function ArticleDetail({ - {tipPagina === "Eveniment" ? "Despre eveniment" : tipPagina === "Facultate" ? "Despre facultate" : "Detalii anunț"} + {tipPagina === "Eveniment" ? t('detail.aboutEvent') : tipPagina === "Facultate" ? t('detail.aboutFaculty') : t('detail.details')} - {content || "Conținutul nu este disponibil."} + {content || t('common.unknownContent')} @@ -344,7 +346,7 @@ export function ArticleDetail({ {/* Dreapta: 3 carduri Noutăți, una sub alta. */} {sidebarItems.length > 0 && ( - Articole similare + {t('detail.relatedArticles')} {sidebarItems.map((item) => ( openItem(item)} /> ))} @@ -355,7 +357,7 @@ export function ArticleDetail({ {/* Jos, sub tot: 3 carduri pe un rand. */} {relatedItems.length > 0 && ( - Mai multe + {t('detail.more')} {bottomCardWidth > 0 && relatedItems.map((item) => ( diff --git a/Frontend/Mobile/src/components/ui/display/error-state.tsx b/Frontend/Mobile/src/components/ui/display/error-state.tsx index a8bc471e..b8829b9c 100644 --- a/Frontend/Mobile/src/components/ui/display/error-state.tsx +++ b/Frontend/Mobile/src/components/ui/display/error-state.tsx @@ -5,6 +5,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import AlertIcon from "@/assets/icons/svg/alert-octagon.svg"; +import { useTranslation } from "react-i18next"; interface ErrorStateProps { message?: string; @@ -17,6 +18,7 @@ export function ErrorState({ title, message, onRetry, style }: ErrorStateProps) const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); + const { t } = useTranslation(); return ( - {title || "Ups! A intervenit o eroare..."} + {title || t('common.errorTitle')} - {message || "Nu s-a putut realiza conexiunea cu serverul. Te rugăm să încerci din nou."} + {message || t('common.connectionError')} {onRetry && ( - Reîncearcă + {t('common.retry')} )} diff --git a/Frontend/Mobile/src/components/ui/display/file-attachment.tsx b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx index 3fa93108..d42e71c4 100644 --- a/Frontend/Mobile/src/components/ui/display/file-attachment.tsx +++ b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx @@ -4,6 +4,7 @@ import { Typography } from "@/constants/typography"; import { useColorScheme } from "@/hooks/use-color-scheme"; import FileIcon from "@/assets/icons/svg/file.svg"; import DownloadIcon from "@/assets/icons/svg/arrow-to-bottom-stroke.svg"; +import { useTranslation } from "react-i18next"; export type FileItem = { name: string; url: string }; @@ -57,12 +58,13 @@ function FileCard({ file }: { file: FileItem }) { export function FileAttachments({ files }: { files?: FileItem[] }) { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); if (!files?.length) return null; return ( - Fișiere atașate + {t('common.attachedFiles')} {files.map((f, i) => ( diff --git a/Frontend/Mobile/src/components/ui/layout/ace.web.tsx b/Frontend/Mobile/src/components/ui/layout/ace.web.tsx index 0d25001f..1f7399f6 100644 --- a/Frontend/Mobile/src/components/ui/layout/ace.web.tsx +++ b/Frontend/Mobile/src/components/ui/layout/ace.web.tsx @@ -27,6 +27,7 @@ import { Spacing, ColorScheme } from '@/constants/theme'; import { Typography } from '@/constants/typography'; import { NewsCard } from '@/components/ui/display/news-card'; import { streamAce } from '@/services/ace-stream'; +import { useTranslation } from 'react-i18next'; import Svg, { Circle, Defs, LinearGradient as SvgLinearGradient, Stop } from 'react-native-svg'; @@ -107,6 +108,7 @@ export function Ace() { const theme = useTheme(); const router = useRouter(); const { width, height } = useWindowDimensions(); + const { t } = useTranslation(); const [open, setOpen] = useState(false); const [messages, setMessages] = useState([]); @@ -192,7 +194,7 @@ export function Ace() { setMessages((prev) => prev.map((m) => m.id === aiMsgId && !m.text - ? { ...m, text: '⚠️ Asistentul nu a returnat niciun răspuns. Încearcă din nou mai târziu.' } + ? { ...m, text: t('ace.noResponse') } : m ) ); @@ -364,7 +366,7 @@ export function Ace() { Ace - Asistent virtual + {t('ace.virtualAssistant')} @@ -372,7 +374,7 @@ export function Ace() { ({ opacity: pressed ? 0.6 : 1, padding: Spacing.xs })} > @@ -381,7 +383,7 @@ export function Ace() { setOpen(false)} accessibilityRole="button" - accessibilityLabel="Închide chat-ul" + accessibilityLabel={t('ace.closeChat')} hitSlop={8} style={({ pressed }: any) => ({ opacity: pressed ? 0.6 : 1, padding: Spacing.xs })} > @@ -405,10 +407,10 @@ export function Ace() { - Cu ce te pot ajuta azi? + {t('ace.howCanIHelp')} - Întreabă-mă despre evenimente, cantină, hartă sau sesizări. + {t('ace.promptSuggestion')} ) : ( @@ -434,7 +436,7 @@ export function Ace() { ({ width: 44, height: 44, @@ -476,7 +478,7 @@ export function Ace() { setOpen((v) => !v)} accessibilityRole="button" - accessibilityLabel={open ? 'Închide asistentul Ace' : 'Deschide asistentul Ace'} + accessibilityLabel={open ? t('ace.closeAssistant') : t('ace.openAssistant')} style={({ pressed }: any) => ({ width: FAB_SIZE, height: FAB_SIZE, diff --git a/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx index 8732c678..92ce244f 100644 --- a/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx @@ -8,6 +8,7 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import UserIcon from "@/assets/icons/svg/user.svg"; import { useAuth } from "@/contexts/auth-context"; import { Config } from "@/constants/config"; +import { useTranslation } from "react-i18next"; export const DASHBOARD_URL = Config.DASHBOARD_URL || ""; @@ -24,6 +25,7 @@ export function ProfileMenu({ const theme = Colors[themeName]; const router = useRouter(); const { isAuthenticated, user, logout } = useAuth(); + const { t } = useTranslation(); const [localOpen, setLocalOpen] = useState(false); const open = controlledOpen !== undefined ? controlledOpen : localOpen; @@ -102,7 +104,7 @@ export function ProfileMenu({ onPress={handleProfilePress} hitSlop={8} accessibilityRole="link" - accessibilityLabel="Autentificare" + accessibilityLabel={isAuthenticated ? t('more.profileTitle') : t('navbar.login')} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} > - {isAuthenticated ? (user?.name || "Utilizator") : "Neautentificat"} + {isAuthenticated ? (user?.name || t('common.user')) : t('navbar.unauthenticated')} {isAuthenticated && user?.email ? ( @@ -170,11 +172,11 @@ export function ProfileMenu({ {isAuthenticated ? ( - Deconectare + {t('navbar.logout')} ) : ( - Autentificare + {t('navbar.login')} )} diff --git a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx index c37b277c..b4eac47a 100644 --- a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx @@ -8,6 +8,7 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import { settingsStore } from "@/utils/settings-store"; import CogIcon from "@/assets/icons/svg/cog.svg"; import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; +import { useTranslation } from "react-i18next"; const THEMES = [ { id: "system" as const, label: "Sistem" }, @@ -52,6 +53,7 @@ export function ThemeMenu({ const theme = Colors[themeName]; const { themeMode, setThemeMode } = useThemeContext(); const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); + const { t } = useTranslation(); useEffect(() => settingsStore.subscribe(() => setSelectedLang(settingsStore.getLang())), []); @@ -91,7 +93,7 @@ export function ThemeMenu({ transform: [{ translateX: interpolate(subAnim.value, [0, 1], [8, 0], Extrapolation.CLAMP) }], })); - const themeLabel = THEMES.find((t) => t.id === themeMode)?.label ?? themeMode; + const themeLabel = themeMode === 'system' ? t('theme.system') : themeMode === 'light' ? t('theme.light') : themeMode === 'dark' ? t('theme.dark') : themeMode; const langLabel = LANGUAGES.find((l) => l.code === selectedLang)?.label ?? selectedLang; return ( @@ -100,7 +102,7 @@ export function ThemeMenu({ onPress={toggle} hitSlop={8} accessibilityRole="button" - accessibilityLabel="Setări" + accessibilityLabel={t('settings.title')} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} > - {opt.label} + {subMenu === "tema" + ? (opt.id === "system" ? t('theme.system') : opt.id === "light" ? t('theme.light') : opt.id === "dark" ? t('theme.dark') : opt.label) + : opt.label} )} @@ -163,8 +167,8 @@ export function ThemeMenu({ {/* Panoul principal: Temă curentă + Limbă curentă */} {[ - { label: "Temă curentă", value: themeLabel, sub: "tema" as const }, - { label: "Limbă curentă", value: langLabel, sub: "limba" as const }, + { label: t('theme.current'), value: themeLabel, sub: "tema" as const }, + { label: t('language.current'), value: langLabel, sub: "limba" as const }, ].map((item, i) => { const isActive = subMenu === item.sub; return ( diff --git a/Frontend/Mobile/src/components/ui/navigation/theme-toggle.tsx b/Frontend/Mobile/src/components/ui/navigation/theme-toggle.tsx index e38ede1a..44cc8074 100644 --- a/Frontend/Mobile/src/components/ui/navigation/theme-toggle.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/theme-toggle.tsx @@ -3,6 +3,7 @@ import { ColorScheme, Spacing } from "@/constants/theme"; import { useThemeContext } from "@/contexts/theme-context"; import SunIcon from "@/assets/icons/svg/sun.svg"; import MoonIcon from "@/assets/icons/svg/moon.svg"; +import { useTranslation } from "react-i18next"; interface ThemeToggleProps { /** Culoarea iconitei. Implicit alb (pe gri-ul navbarului). */ @@ -21,6 +22,7 @@ interface ThemeToggleProps { export function ThemeToggle({ color = ColorScheme.white, backgroundColor = "#272727", borderColor, size = 24 }: ThemeToggleProps) { const { scheme, toggleTheme } = useThemeContext(); const isDark = scheme === "dark"; + const { t } = useTranslation(); // In dark afisam soarele (apesi -> light); in light afisam luna. const Icon = isDark ? SunIcon : MoonIcon; @@ -30,7 +32,7 @@ export function ThemeToggle({ color = ColorScheme.white, backgroundColor = "#272 onPress={toggleTheme} hitSlop={8} accessibilityRole="button" - accessibilityLabel={isDark ? "Comuta pe tema deschisa" : "Comuta pe tema intunecata"} + accessibilityLabel={isDark ? t('theme.switchToLight') : t('theme.switchToDark')} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} > activ doar pe potrivire exacta (Acasă, ca sa nu se aprinda si pe // sub-paginile /acasa/...). `dropdown` -> element cu sub-meniu la hover (Anunțuri). type NavItem = { - label: string; + labelKey: string; href?: string; match: string; exact?: boolean; - dropdown?: { label: string; href: string }[]; + dropdown?: { labelKey: string; href: string }[]; }; const LINKS: NavItem[] = [ - { label: "Acasă", href: "/(public)/acasa", match: "/acasa", exact: true }, + { labelKey: "nav.home", href: "/(public)/acasa", match: "/acasa", exact: true }, { - label: "Anunțuri", + labelKey: "navbar.announcements", match: "/acasa/categorie", dropdown: [ - { label: "Noutăți", href: "/(public)/acasa/categorie?title=Noutăți" }, - { label: "Evenimente", href: "/(public)/acasa/categorie?title=Evenimente" }, + { labelKey: "navbar.news", href: "/(public)/acasa/categorie?title=Noutăți" }, + { labelKey: "navbar.events", href: "/(public)/acasa/categorie?title=Evenimente" }, ], }, - { label: "Hartă", href: "/(public)/harta", match: "/harta" }, - { label: "Cantină", href: "/(public)/cantina", match: "/cantina" }, - { label: "Sesizări", href: "/(public)/sesizari", match: "/sesizari" }, - { label: "Mai multe", href: "/(public)/more", match: "/more" }, + { labelKey: "nav.map", href: "/(public)/harta", match: "/harta" }, + { labelKey: "nav.canteen", href: "/(public)/cantina", match: "/cantina" }, + { labelKey: "nav.reports", href: "/(public)/sesizari", match: "/sesizari" }, + { labelKey: "nav.more", href: "/(public)/more", match: "/more" }, ]; // Activ daca pathname-ul se potriveste cu segmentul link-ului. @@ -87,6 +88,7 @@ export function WebNavbar() { const pathname = usePathname(); const { width } = useWindowDimensions(); const insets = useSafeAreaInsets(); + const { t } = useTranslation(); const isCompact = width < WEB_COMPACT_BREAKPOINT; // Pe ecrane late WebContainer-ul scaleaza continutul (zoom), deci bara e vizual @@ -275,7 +277,7 @@ export function WebNavbar() { router.push("/(public)/acasa")} accessibilityRole="link" - accessibilityLabel="InsideUGAL — Acasă" + accessibilityLabel={"InsideUGAL — " + t('navbar.home')} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > @@ -291,7 +293,7 @@ export function WebNavbar() { }} hitSlop={8} accessibilityRole="button" - accessibilityLabel={menuOpen ? "Închide meniul" : "Deschide meniul"} + accessibilityLabel={menuOpen ? t('navbar.closeMenu') : t('navbar.openMenu')} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1, padding: Spacing.xs })} > @@ -307,7 +309,7 @@ export function WebNavbar() { if (link.dropdown) { return ( @@ -323,7 +325,7 @@ export function WebNavbar() { gap: 2, })} > - {link.label} + {t(link.labelKey)} @@ -350,7 +352,7 @@ export function WebNavbar() { { color: (pressed || hovered) ? theme.primary : ColorScheme.black }, ]} > - {sub.label} + {t(sub.labelKey)} )} @@ -369,7 +371,7 @@ export function WebNavbar() { {...({ dataSet: { navlink: "true", active: isActive ? "true" : "false" } } as any)} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1, alignItems: "center", justifyContent: "center" })} > - {link.label} + {t(link.labelKey)} ); })} @@ -392,7 +394,7 @@ export function WebNavbar() { - {/* Panou hamburger (doar ecran ingust): lista verticala de link-uri + tema. */} + {/* Panou hamburger (doar ecran ingust): lista verticala de link-uri + Setări. */} {isCompact && ( + {link.dropdown.map((sub) => ( [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]} > - - {sub.label} + + {t(sub.labelKey)} ))} @@ -440,32 +442,41 @@ export function WebNavbar() { > - {link.label} + {t(link.labelKey)} ); })} - {/* Rand de tema: eticheta + comutator soare/luna. */} - - Temă - - + {/* Buton Setări */} + { + setMenuOpen(false); + router.push("/(public)/more/setari"); + }} + style={styles.panelLink} + > + + {t('settings.title')} + + {/* Profil (mobil): cine e conectat + Dashboard (daca are acces) + Deconectare. */} - - {isAuthenticated ? (user?.name || "Utilizator") : "Neautentificat"} - + {isAuthenticated ? ( + + {user?.name || t("common.user")} + + ) : null} {isAuthenticated && user?.email ? ( {user.email} @@ -480,7 +491,7 @@ export function WebNavbar() { }} style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]} > - + Dashboard @@ -496,8 +507,8 @@ export function WebNavbar() { }} style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]} > - - Deconectare + + {t('navbar.logout')} ) : ( @@ -508,8 +519,8 @@ export function WebNavbar() { }} style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]} > - - Autentificare + + {t('navbar.login')} )} @@ -596,6 +607,13 @@ const styles = StyleSheet.create({ borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: "rgba(255,255,255,0.25)", }, + panelRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: Spacing.lg, + paddingVertical: Spacing.xs, + }, panelProfile: { paddingHorizontal: Spacing.lg, paddingTop: Spacing.sm, diff --git a/Frontend/Mobile/src/contexts/theme-context.tsx b/Frontend/Mobile/src/contexts/theme-context.tsx index 1c8985b7..69e2ebe1 100644 --- a/Frontend/Mobile/src/contexts/theme-context.tsx +++ b/Frontend/Mobile/src/contexts/theme-context.tsx @@ -1,5 +1,6 @@ -import { createContext, useContext, useState, type ReactNode } from "react"; +import { createContext, useContext, useState, useEffect, type ReactNode } from "react"; import { useColorScheme as useSystemColorScheme } from "react-native"; +import { settingsStore } from "@/utils/settings-store"; type Scheme = "light" | "dark"; export type ThemeMode = "light" | "dark" | "system"; @@ -16,24 +17,30 @@ const ThemeContext = createContext(null); /** * Sursa de adevar pentru tema (DOAR pe web). * - * Porneste din tema sistemului; dupa prima comutare foloseste alegerea - * utilizatorului. Alegerea nu e salvata intre restarturi (deocamdata). - * Fisierul e importat exclusiv din fisiere .web — mobilul nu il atinge. + * Sincronizată cu settingsStore pentru a menține tema unitară între + * interfața web generală și setările din interiorul modulului mobil. */ export function ThemeProvider({ children }: { children: ReactNode }) { const system = useSystemColorScheme(); - const [themeMode, setThemeModeState] = useState("system"); + const [themeMode, setThemeModeState] = useState(() => settingsStore.getTheme()); + + useEffect(() => { + const unsubscribe = settingsStore.subscribe(() => { + setThemeModeState(settingsStore.getTheme()); + }); + return unsubscribe; + }, []); const scheme: Scheme = themeMode === "system" ? (system === "dark" ? "dark" : "light") : themeMode; const setThemeMode = (mode: ThemeMode) => { - setThemeModeState(mode); + settingsStore.setTheme(mode); }; const toggleTheme = () => { - setThemeModeState(scheme === "dark" ? "light" : "dark"); + settingsStore.setTheme(scheme === "dark" ? "light" : "dark"); }; return ( diff --git a/Frontend/Mobile/src/i18n/index.ts b/Frontend/Mobile/src/i18n/index.ts new file mode 100644 index 00000000..3965773e --- /dev/null +++ b/Frontend/Mobile/src/i18n/index.ts @@ -0,0 +1,18 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { settingsStore } from '@/utils/settings-store'; + +import ro from './locales/ro.json'; +import en from './locales/en.json'; + +i18n.use(initReactI18next).init({ + resources: { + ro: { translation: ro }, + en: { translation: en }, + }, + lng: settingsStore.getLang(), + fallbackLng: 'en', + interpolation: { escapeValue: false }, +}); + +export default i18n; diff --git a/Frontend/Mobile/src/i18n/locales/en.json b/Frontend/Mobile/src/i18n/locales/en.json new file mode 100644 index 00000000..a28c0c4b --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/en.json @@ -0,0 +1,243 @@ +{ + "home": { + "news": "News", + "events": "Events", + "faculties": "Faculties", + "facilities": "Facilities", + "recent": "Recent", + "noNews": "No news found.", + "noEvents": "No events found.", + "noFaculties": "No faculties found.", + "noFacilities": "No facilities found.", + "loadErrorNews": "Could not load news.", + "loadErrorEvents": "Could not load events.", + "loadErrorFaculties": "Could not load faculties.", + "loadErrorFacilities": "Could not load facilities.", + "emptyNews": "No news available.", + "emptyEvents": "No events available.", + "emptyFaculties": "No faculties available.", + "emptyFacilities": "No facilities available.", + "refreshError": "Could not refresh data. Please check your internet connection." + }, + "category": { + "empty": "No items in this category.", + "allFaculties": "All Faculties", + "all": "All", + "refreshError": "Could not refresh data for this category. Please check your internet connection.", + "fallback": "Category" + }, + "detail": { + "eventInfo": "Event information", + "facilityInfo": "Facility information", + "schedule": "Schedule:", + "from": "From", + "until": "Until", + "aboutEvent": "About the event", + "aboutFaculty": "About the faculty", + "aboutFacility": "About the facility", + "details": "Details", + "contact": "Contact & Location", + "address": "Address", + "phone": "Phone", + "website": "Website", + "updated": "Updated:", + "relatedArticles": "Similar articles", + "more": "More", + "loadError": "Page details could not be loaded.", + "notFound": "Details not found.", + "callFaculty": "Faculty Contact", + "callFacility": "Facility Contact", + "callConfirm": "Do you want to call {{phone}}?", + "cancel": "Cancel", + "call": "Call" + }, + "common": { + "unknownTitle": "Unknown title", + "unknownContent": "Unknown content", + "unknownDate": "Unknown date", + "unknownLocation": "Unknown location", + "unknownAddress": "Unknown address", + "unknownStartDate": "Unknown start date", + "unknownEndDate": "Unknown end date", + "home": "Home", + "news": "News", + "events": "Events", + "faculty": "Faculty", + "facility": "Facility", + "category": "Category", + "article": "Article", + "updateError": "Update error", + "selected": "(Selected)", + "retry": "Retry", + "connectionError": "Could not connect to the server. Please try again.", + "user": "User", + "dashboard": "Dashboard", + "attachedFiles": "Attached files", + "noItems": "No items in this category.", + "loading": "Loading...", + "errorTitle": "Oops! Something went wrong..." + }, + "days": { + "1": "Monday", + "2": "Tuesday", + "3": "Wednesday", + "4": "Thursday", + "5": "Friday", + "6": "Saturday", + "7": "Sunday" + }, + "language": { + "title": "App Language", + "header": "Language", + "select": "Select language", + "selected": "(Selected)", + "current": "Current language" + }, + "nav": { + "home": "Home", + "map": "Map", + "canteen": "Cafeteria", + "reports": "Complaints", + "more": "More" + }, + "more": { + "title": "More", + "visitGalati": "Visit Galați", + "login": "Sign In", + "disconnect": "Sign Out", + "settings": "Settings", + "profileTitle": "Your Profile", + "profileLoggedIn": "You're already signed in. Do you want to sign out?", + "cancel": "Cancel", + "logout": "Sign Out" + }, + "settings": { + "title": "Settings", + "appearanceLang": "Appearance & Language", + "themeApp": "App Theme", + "currentTheme": "Current theme:", + "currentLang": "Current language:", + "supportInfo": "Help & Info", + "visitWebsite": "Visit UGAL Website", + "appSlogan": "Created for students of the \"Dunărea de Jos\" University of Galați" + }, + "theme": { + "title": "App Theme", + "header": "Theme", + "select": "Select theme", + "system": "System", + "light": "Light", + "dark": "Dark", + "current": "Current theme", + "switchToLight": "Switch to light theme", + "switchToDark": "Switch to dark theme" + }, + "canteen": { + "title": "Cafeteria", + "today": "Today", + "empty": "No menu available for this day.", + "updateError": "Could not refresh the cafeteria menu. Please check your internet connection." + }, + "map": { + "title": "Map", + "allLocations": "All Locations", + "facilities": "Facilities" + }, + "reports": { + "title": "Complaints", + "all": "All Complaints", + "mine": "My Complaints", + "active": "Active", + "rejected": "Rejected", + "completed": "Resolved", + "newReport": "New Complaint", + "updateError": "Could not refresh complaints. Please check your internet connection.", + "loginRequired": "You need to be signed in", + "loginDesc": "Sign in to submit or view your complaints.", + "login": "Sign In", + "empty": "No complaints in this section", + "emptyDesc": "There are no records at the moment.", + "unknownLocation": "Unknown location", + "location": "Location", + "titleField": "Title", + "descDetailed": "Detailed Description", + "addPhoto": "Add a photo", + "addPhotoBtn": "Add photo", + "submit": "Submit Complaint", + "photoPermission": "Photo library access is required to add a photo!", + "titleRequired": "Title is required.", + "descRequired": "Description is required.", + "photoRequired": "A photo is required.", + "submitError": "An error occurred while submitting the complaint. Please try again.", + "infoTitle": "Complaint Information", + "descSection": "Problem Description", + "progressTitle": "Progress History", + "statusActive": "Active", + "statusRejected": "Rejected", + "statusCompleted": "Resolved", + "missingTitle": "Missing title", + "noDescription": "No description added.", + "notFound": "Complaint could not be found.", + "loadError": "An error occurred while loading the complaint.", + "loadErrorGeneral": "An error occurred while loading complaints.", + "step1Title": "Complaint registered", + "step1Desc": "The complaint has been saved in the system.", + "step2ActiveTitle": "Under review", + "step2ActiveDesc": "An administrator is evaluating the issue details.", + "step3ActiveTitle": "Resolution complete", + "step3ActiveDesc": "The team will intervene to fix the situation.", + "step2RejectedTitle": "Rejected", + "step2RejectedDesc": "The request was rejected by the administrative team.", + "step2CompletedTitle": "Under administrative review", + "step2CompletedDesc": "The issue was successfully processed.", + "step3CompletedTitle": "Resolved", + "step3CompletedDesc": "The issue was resolved on-site by the technical staff.", + "general": "General", + "exterior": "Exterior" + }, + "auth": { + "title": "Sign In", + "subtitle": "Enter your credentials to access your account", + "email": "Email", + "password": "Password", + "login": "Sign In", + "emailRequired": "Email is required.", + "emailInvalid": "Invalid email format.", + "passwordRequired": "Password is required.", + "passwordTooShort": "Password must be at least 6 characters.", + "invalidCredentials": "Incorrect email or password." + }, + "onboarding": { + "exploreTitle": "Explore the Campus", + "exploreDesc": "Discover the buildings and facilities of the university campus directly on the map.", + "continue": "Continue" + }, + "navbar": { + "unauthenticated": "Not signed in", + "theme": "Theme", + "logout": "Sign Out", + "login": "Sign In", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "home": "Home", + "announcements": "Announcements", + "news": "News", + "events": "Events" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Virtual Assistant", + "placeholder": "Ask a question...", + "noValidResponse": "Did not receive a valid response.", + "errorOccurred": "An error occurred. Please try again.", + "assistantError": "Assistant error.", + "howCanIHelp": "How can I help you today?", + "promptSuggestion": "Ask me about events, the cafeteria, the map, or complaints.", + "noResponse": "⚠️ The assistant did not return any response. Please try again later.", + "newConversation": "New conversation", + "closeChat": "Close chat", + "sendMessage": "Send message", + "openAssistant": "Open Ace assistant", + "closeAssistant": "Close Ace assistant" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/ro.json b/Frontend/Mobile/src/i18n/locales/ro.json new file mode 100644 index 00000000..bd047924 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/ro.json @@ -0,0 +1,243 @@ +{ + "home": { + "news": "Noutăți", + "events": "Evenimente", + "faculties": "Facultăți", + "facilities": "Facilități", + "recent": "Recente", + "noNews": "Nu s-au putut găsi noutăți.", + "noEvents": "Nu s-au putut găsi evenimente.", + "noFaculties": "Nu s-au putut găsi facultăți.", + "noFacilities": "Nu s-au putut găsi facilități.", + "loadErrorNews": "Nu s-au putut încărca noutățile.", + "loadErrorEvents": "Nu s-au putut încărca evenimentele.", + "loadErrorFaculties": "Nu s-au putut încărca facultățile.", + "loadErrorFacilities": "Nu s-au putut încărca facilitățile.", + "emptyNews": "Nu există noutăți.", + "emptyEvents": "Nu există evenimente.", + "emptyFaculties": "Nu există facultăți.", + "emptyFacilities": "Nu există facilități.", + "refreshError": "Nu s-au putut reîmprospăta datele de pe ecranul principal. Te rugăm să verifici conexiunea la internet." + }, + "category": { + "empty": "Nu există elemente în această categorie.", + "allFaculties": "Toate Facultățile", + "all": "Toate", + "refreshError": "Nu s-au putut reîmprospăta datele pentru această categorie. Te rugăm să verifici conexiunea la internet.", + "fallback": "Categorie" + }, + "detail": { + "eventInfo": "Informații eveniment", + "facilityInfo": "Informații facilitate", + "schedule": "Program:", + "from": "De pe", + "until": "Până la", + "aboutEvent": "Despre eveniment", + "aboutFaculty": "Despre facultate", + "aboutFacility": "Despre facilitate", + "details": "Detalii", + "contact": "Contact și Locație", + "address": "Adresă", + "phone": "Telefon", + "website": "Website", + "updated": "Actualizat:", + "relatedArticles": "Articole similare", + "more": "Mai multe", + "loadError": "Detaliile pentru această pagină nu au putut fi încărcate.", + "notFound": "Detaliile nu au putut fi găsite.", + "callFaculty": "Contact Facultate", + "callFacility": "Contact Facilitate", + "callConfirm": "Doriți să apelați numărul {{phone}}?", + "cancel": "Anulează", + "call": "Sună" + }, + "common": { + "unknownTitle": "Titlu necunoscut", + "unknownContent": "Conținut necunoscut", + "unknownDate": "Dată necunoscută", + "unknownLocation": "Locație necunoscută", + "unknownAddress": "Adresă necunoscută", + "unknownStartDate": "Dată de început necunoscută", + "unknownEndDate": "Dată de sfârșit necunoscută", + "home": "Acasă", + "news": "Noutăți", + "events": "Evenimente", + "faculty": "Facultate", + "facility": "Facilitate", + "category": "Categorie", + "article": "Articol", + "updateError": "Eroare la actualizare", + "selected": "(Selectat)", + "retry": "Reîncearcă", + "connectionError": "Nu s-a putut realiza conexiunea cu serverul. Te rugăm să încerci din nou.", + "user": "Utilizator", + "dashboard": "Dashboard", + "attachedFiles": "Fișiere atașate", + "noItems": "Nu există elemente în această categorie.", + "loading": "Se încarcă...", + "errorTitle": "Ups! A intervenit o eroare..." + }, + "days": { + "1": "Luni", + "2": "Marți", + "3": "Miercuri", + "4": "Joi", + "5": "Vineri", + "6": "Sâmbătă", + "7": "Duminică" + }, + "language": { + "title": "Limbă aplicație", + "header": "Limbă", + "select": "Selectează limba", + "selected": "(Selectat)", + "current": "Limbă curentă" + }, + "nav": { + "home": "Acasă", + "map": "Hartă", + "canteen": "Cantină", + "reports": "Sesizări", + "more": "Mai multe" + }, + "more": { + "title": "Mai multe", + "visitGalati": "Vizitează Galați", + "login": "Conectare", + "disconnect": "Deconectează-te", + "settings": "Setări", + "profileTitle": "Profilul tău", + "profileLoggedIn": "Ești deja conectat în cont. Vrei să te deconectezi?", + "cancel": "Anulează", + "logout": "Deconectare" + }, + "settings": { + "title": "Setări", + "appearanceLang": "Aspect & Limbă", + "themeApp": "Temă aplicație", + "currentTheme": "Temă curentă:", + "currentLang": "Limbă curentă:", + "supportInfo": "Asistență & Info", + "visitWebsite": "Vizitează Website UGAL", + "appSlogan": "Creat pentru studenții Universității „Dunărea de Jos” din Galați" + }, + "theme": { + "title": "Temă aplicație", + "header": "Temă", + "select": "Selectează tema", + "system": "Sistem", + "light": "Luminos", + "dark": "Întunecat", + "current": "Temă curentă", + "switchToLight": "Comută pe tema deschisă", + "switchToDark": "Comută pe tema întunecată" + }, + "canteen": { + "title": "Cantina", + "today": "Azi", + "empty": "Nu există meniu disponibil pentru această zi.", + "updateError": "Nu s-a putut reîmprospăta meniul cantinei. Te rugăm să verifici conexiunea la internet." + }, + "map": { + "title": "Hartă", + "allLocations": "Toate locațiile", + "facilities": "Facilități" + }, + "reports": { + "title": "Sesizări", + "all": "Toate sesizările", + "mine": "Sesizările mele", + "active": "Active", + "rejected": "Respinse", + "completed": "Finalizate", + "newReport": "Sesizare nouă", + "updateError": "Nu s-au putut reîmprospăta sesizările. Te rugăm să verifici conexiunea la internet.", + "loginRequired": "Trebuie să fii conectat", + "loginDesc": "Conectează-te pentru a trimite sau vizualiza sesizările tale.", + "login": "Conectare", + "empty": "Nicio sesizare în această secțiune", + "emptyDesc": "Momentan nu există înregistrări.", + "unknownLocation": "Locație nespecificată", + "location": "Locație", + "titleField": "Titlu", + "descDetailed": "Descriere detaliată", + "addPhoto": "Adaugă o fotografie", + "addPhotoBtn": "Adaugă poză", + "submit": "Trimite sesizarea", + "photoPermission": "Permisiunea de acces la fotografii este necesară pentru a adăuga o poză!", + "titleRequired": "Titlul este obligatoriu.", + "descRequired": "Descrierea este obligatorie.", + "photoRequired": "Este obligatoriu să adăugați o fotografie.", + "submitError": "A apărut o eroare la trimiterea sesizării. Te rugăm să încerci din nou.", + "infoTitle": "Informații sesizare", + "descSection": "Descriere problemă", + "progressTitle": "Istoric progres", + "statusActive": "Activă", + "statusRejected": "Respinsă", + "statusCompleted": "Soluționată", + "missingTitle": "Titlu lipsă", + "noDescription": "Nicio descriere adăugată.", + "notFound": "Sesizarea nu a putut fi găsită.", + "loadError": "A apărut o eroare la încărcarea sesizării.", + "loadErrorGeneral": "A apărut o eroare la încărcarea sesizărilor.", + "step1Title": "Sesizare înregistrată", + "step1Desc": "Sesizarea a fost salvată în sistem.", + "step2ActiveTitle": "În curs de analiză", + "step2ActiveDesc": "Un administrator evaluează detaliile problemei.", + "step3ActiveTitle": "Soluționare finalizată", + "step3ActiveDesc": "Echipa va interveni pentru a remedia situația.", + "step2RejectedTitle": "Respinsă", + "step2RejectedDesc": "Solicitarea a fost respinsă de către echipa administrativă.", + "step2CompletedTitle": "În analiză administrativă", + "step2CompletedDesc": "Problema a fost procesată cu succes.", + "step3CompletedTitle": "Soluționată", + "step3CompletedDesc": "Problema a fost rezolvată în teren de personalul tehnic.", + "general": "General", + "exterior": "Exterior" + }, + "auth": { + "title": "Autentificare", + "subtitle": "Introdu datele pentru a intra în cont", + "email": "Email", + "password": "Parolă", + "login": "Autentificare", + "emailRequired": "Email-ul este obligatoriu.", + "emailInvalid": "Formatul email-ului este invalid.", + "passwordRequired": "Parola este obligatorie.", + "passwordTooShort": "Parola trebuie să aibă cel puțin 6 caractere.", + "invalidCredentials": "Email-ul sau parola sunt incorecte." + }, + "onboarding": { + "exploreTitle": "Explorează campusul", + "exploreDesc": "Descoperă clădirile și facilitățile campusului universitar direct pe hartă.", + "continue": "Continuă" + }, + "navbar": { + "unauthenticated": "Neautentificat", + "theme": "Temă", + "logout": "Deconectare", + "login": "Autentificare", + "openMenu": "Deschide meniul", + "closeMenu": "Închide meniul", + "home": "Acasă", + "announcements": "Anunțuri", + "news": "Noutăți", + "events": "Evenimente" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Asistent virtual", + "placeholder": "Pune o întrebare...", + "noValidResponse": "Nu am primit un răspuns valid.", + "errorOccurred": "A apărut o eroare. Vă rugăm să încercați din nou.", + "assistantError": "Eroare la asistent.", + "howCanIHelp": "Cu ce te pot ajuta azi?", + "promptSuggestion": "Întreabă-mă despre evenimente, cantină, hartă sau sesizări.", + "noResponse": "⚠️ Asistentul nu a returnat niciun răspuns. Încearcă din nou mai târziu.", + "newConversation": "Conversație nouă", + "closeChat": "Închide chat-ul", + "sendMessage": "Trimite mesajul", + "openAssistant": "Deschide asistentul Ace", + "closeAssistant": "Închide asistentul Ace" + } +} diff --git a/Frontend/Mobile/src/utils/date.ts b/Frontend/Mobile/src/utils/date.ts index d7e6bfd0..b82b1745 100644 --- a/Frontend/Mobile/src/utils/date.ts +++ b/Frontend/Mobile/src/utils/date.ts @@ -37,10 +37,10 @@ export const getFormattedDate = (dateStr?: string) => { /** * Calculează timpul de citire estimat bazat pe lungimea textului */ -export const getReadingTime = (text?: string) => { - if (!text) return "1 min citire"; +export const getReadingTime = (text?: string, formatter?: (minutes: number) => string) => { const words = text.split(/\s+/).length; const minutes = Math.max(1, Math.ceil(words / 200)); + if (formatter) return formatter(minutes); return `${minutes} min citire`; }; diff --git a/Frontend/Mobile/src/utils/settings-store.ts b/Frontend/Mobile/src/utils/settings-store.ts index 17ea5ce7..9a3c0bd2 100644 --- a/Frontend/Mobile/src/utils/settings-store.ts +++ b/Frontend/Mobile/src/utils/settings-store.ts @@ -19,6 +19,8 @@ class SettingsStore { setLang(lang: string) { this.lang = lang; this.notify(); + // Lazy import to avoid circular dependency with i18n init reading settingsStore + import('@/i18n').then((mod) => mod.default.changeLanguage(lang)); } subscribe(listener: () => void) { From 379801c62bb54085b58e95c836cd008454045b21 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Wed, 1 Jul 2026 10:49:17 +0300 Subject: [PATCH 05/15] fix: more translations --- .../src/app/(public)/acasa/categorie.web.tsx | 12 +- .../Mobile/src/app/(public)/acasa/index.tsx | 10 +- .../src/app/(public)/acasa/index.web.tsx | 8 +- .../app/(public)/acasa/vizualizare.web.tsx | 10 +- .../src/app/(public)/anunt/[id].web.tsx | 4 +- .../src/app/(public)/eveniment/[id].web.tsx | 4 +- .../src/app/(public)/sesizari/detalii.web.tsx | 6 +- .../components/ui/display/article-detail.tsx | 10 +- .../ui/display/carousel/carousel.tsx | 4 +- .../ui/display/carousel/carousel.web.tsx | 4 +- .../src/components/ui/display/news-card.tsx | 4 +- .../components/ui/display/news-card.web.tsx | 30 ++-- Frontend/Mobile/src/i18n/locales/en.json | 5 +- Frontend/Mobile/src/i18n/locales/ro.json | 5 +- Frontend/Mobile/src/services/api.ts | 5 + Frontend/Mobile/src/utils/date.ts | 163 +++++++----------- Frontend/Mobile/src/utils/settings-store.ts | 54 +++++- 17 files changed, 191 insertions(+), 147 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index adaf2959..7fa84711 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -17,9 +17,11 @@ import { Seo } from "@/components/seo"; import { eventHref, anuntHref } from "@/utils/article-url"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import api from "@/services/api"; +import { useTranslation } from "react-i18next"; export default function CategoryScreen() { const { title: categoryTitle } = useLocalSearchParams(); + const { t } = useTranslation(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); @@ -36,9 +38,15 @@ export default function CategoryScreen() { const [hasError, setHasError] = useState(false); const [faculties, setFaculties] = useState([]); + const categoryLabel = categoryTitle === "Noutăți" ? t('home.news') + : categoryTitle === "Evenimente" ? t('home.events') + : categoryTitle === "Facultăți" ? t('home.faculties') + : categoryTitle === "Facilități" ? t('home.facilities') + : (categoryTitle as string) || t('category.fallback'); + const crumbs: Crumb[] = [ - { label: "Acasă", href: "/(public)/acasa" }, - { label: (categoryTitle as string) || "Categorie" } + { label: t('common.home'), href: "/(public)/acasa" }, + { label: categoryLabel } ]; // Fetch faculties list for filter options diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index 8b1782e8..8886d5c7 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -100,12 +100,12 @@ export default function HomeScreen() { if (cached) { const apiItems = JSON.parse(cached); setNoutati(apiItems.filter((i: any) => i.type === "NOUTATE").map((i: any) => ({ - id: i.id.toString(), title: i.title || "Titlu necunoscut", category: "Noutăți", + id: i.id.toString(), title: i.title || t('common.unknownTitle'), category: t('home.news'), date: isoToRomanianDateStr(i.created_at) || "Dată necunoscută", author: i.author_name || "", image: i.image_url || undefined, content: i.content || "Conținut necunoscut", created_at: i.created_at, }))); setEvenimente(apiItems.filter((i: any) => i.type === "EVENIMENT").map((i: any) => ({ - id: i.id.toString(), title: i.title || "Titlu necunoscut", category: "Evenimente", + id: i.id.toString(), title: i.title || t('common.unknownTitle'), category: t('home.events'), date: isoToRomanianDateStr(i.created_at) || "Dată necunoscută", date_start: isoToRomanianDateStr(i.start_date) || "", date_end: isoToRomanianDateStr(i.end_date) || "", time_start: i.start_date ? new Date(i.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", @@ -230,16 +230,16 @@ export default function HomeScreen() { .slice(0, 3) .map(item => ({ ...item, - category: item.category || "Noutăți" + category: item.category || t('home.news') })); const heroItems = announcementsForHero.length > 0 ? announcementsForHero : [ { id: "default_hero", title: "InsideUGAL", - category: "Universitate", + category: t('common.university'), date: getTodayRomanianDate(), - author: "Platforma ta universitară", + author: t('common.universityPlatform'), image: null } ]; diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index d0ac78fb..267186c6 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -177,9 +177,9 @@ export default function HomeScreen() { { id: "default_hero", title: "InsideUGAL", - category: "Universitate", + category: t('common.university'), date: getTodayRomanianDate(), - author: "Platforma ta universitară", + author: t('common.universityPlatform'), image: null } ]; @@ -197,11 +197,11 @@ export default function HomeScreen() { const handlePress = (item: any) => { if (item.id === "default_hero") return; // Evenimentele si anunturile au URL curat; restul raman pe vizualizare. - if (item.category === "Evenimente") { + if (item.category === t('home.events')) { router.push(eventHref(item) as any); return; } - if (item.category === "Noutăți") { + if (item.category === t('home.news')) { router.push(anuntHref(item) as any); return; } diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index caef686d..645d349b 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -278,7 +278,7 @@ function VizualizareScreen() { pathname: "/(public)/acasa/vizualizare", params: { id: item.id, - type: item.category === "Evenimente" ? "Eveniment" : "Anunț", + type: item.category === t('home.events') ? "Eveniment" : "Anunț", }, }); }; @@ -289,13 +289,13 @@ function VizualizareScreen() { } }; - const displayDateValue = category === "Noutăți" - ? (posted_at && posted_at !== "Dată necunoscută" ? posted_at : "") - : (date && date !== "Dată necunoscută" ? date : posted_at); + const displayDateValue = category === t('home.news') + ? (posted_at && posted_at !== t('common.unknownDate') ? posted_at : "") + : (date && date !== t('common.unknownDate') ? date : posted_at); const formattedDate = getFormattedDate(displayDateValue as string); const readingTime = getReadingTime(content as string); - const dateDisplay = category === "Noutăți" ? (formattedDate ? `${formattedDate} | ${readingTime}` : "Dată necunoscută") : (formattedDate || t('common.unknownDate')); + const dateDisplay = category === t('home.news') ? (formattedDate ? `${formattedDate} | ${readingTime}` : t('common.unknownDate')) : (formattedDate || t('common.unknownDate')); const createdTime = itemData?.created_at ? new Date(itemData.created_at).getTime() : 0; const updatedTime = itemData?.updated_at ? new Date(itemData.updated_at).getTime() : 0; diff --git a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx index fc41f2a1..1dc5bcf7 100644 --- a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx @@ -9,6 +9,7 @@ import { parseEventId, allAnuntParams } from "@/utils/article-url"; import api from "@/services/api"; import { isoToRomanianDateStr } from "@/utils/date"; import { Colors, Spacing } from "@/constants/theme"; +import { useTranslation } from "react-i18next"; // Pre-generează paginile la build export function generateStaticParams() { @@ -16,6 +17,7 @@ export function generateStaticParams() { } export default function AnuntScreen() { + const { t } = useTranslation(); const params = useLocalSearchParams(); const id = parseEventId(params.id); const [item, setItem] = useState(null); @@ -90,7 +92,7 @@ export default function AnuntScreen() { (null); @@ -98,7 +100,7 @@ export default function EvenimentScreen() { { - if (item.category === "Evenimente") { + if (item.category === t('home.events')) { router.push(eventHref(item) as any); return; } - if (item.category === "Noutăți") { + if (item.category === t('home.news')) { router.push(anuntHref(item) as any); return; } router.push({ pathname: "/(public)/acasa/vizualizare", params: { - type: item.category === "Evenimente" ? "Eveniment" : "Anunț", + type: item.category === t('home.events') ? "Eveniment" : "Anunț", title: item.title, category: item.category, content: item.content, @@ -191,7 +191,7 @@ export function ArticleDetail({ const formattedDate = getFormattedDate(date || posted_at); const readingTime = getReadingTime(content); - const dateDisplay = category === "Noutăți" ? `${formattedDate} | ${readingTime}` : formattedDate; + const dateDisplay = category === t('home.news') ? `${formattedDate} | ${readingTime}` : formattedDate; // Breadcrumbs: Acasă / [categorie sau tip] / [titlu]. Segmentul de categorie // duce la lista categoriei respective; ultimul (titlul) nu e clickabil. diff --git a/Frontend/Mobile/src/components/ui/display/carousel/carousel.tsx b/Frontend/Mobile/src/components/ui/display/carousel/carousel.tsx index ece662af..65b5b9e0 100644 --- a/Frontend/Mobile/src/components/ui/display/carousel/carousel.tsx +++ b/Frontend/Mobile/src/components/ui/display/carousel/carousel.tsx @@ -5,11 +5,13 @@ import { Typography } from "@/constants/typography"; import { Colors, Spacing } from "@/constants/theme"; import { CAROUSEL_CARD_WIDTH, CAROUSEL_CARD_MARGIN, CarouselProps } from "./carousel.shared"; import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; +import { useTranslation } from "react-i18next"; export function Carousel({ data, renderItem, keyExtractor, title, viewAllHref }: CarouselProps) { const router = useRouter(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); return ( @@ -32,7 +34,7 @@ export function Carousel({ data, renderItem, keyExtractor, title, viewAllHref opacity: pressed ? 0.7 : 1, })} > - Vezi mai multe + {t('common.viewAll')} diff --git a/Frontend/Mobile/src/components/ui/display/carousel/carousel.web.tsx b/Frontend/Mobile/src/components/ui/display/carousel/carousel.web.tsx index d78b5f92..ff3b1288 100644 --- a/Frontend/Mobile/src/components/ui/display/carousel/carousel.web.tsx +++ b/Frontend/Mobile/src/components/ui/display/carousel/carousel.web.tsx @@ -5,11 +5,13 @@ import { Typography } from "@/constants/typography"; import { Colors, Spacing } from "@/constants/theme"; import { CAROUSEL_CARD_WIDTH, CAROUSEL_CARD_MARGIN, CarouselProps } from "./carousel.shared"; import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; +import { useTranslation } from "react-i18next"; export function Carousel({ data, renderItem, keyExtractor, title, viewAllHref }: CarouselProps) { const router = useRouter(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; + const { t } = useTranslation(); // Fisier doar pentru web: pe ecran lat aratam bara de scroll (stilizata in global.css) const { width: windowWidth } = useWindowDimensions(); @@ -36,7 +38,7 @@ export function Carousel({ data, renderItem, keyExtractor, title, viewAllHref opacity: pressed ? 0.7 : 1, })} > - Vezi mai multe + {t('common.viewAll')} diff --git a/Frontend/Mobile/src/components/ui/display/news-card.tsx b/Frontend/Mobile/src/components/ui/display/news-card.tsx index b55a58c9..88133432 100644 --- a/Frontend/Mobile/src/components/ui/display/news-card.tsx +++ b/Frontend/Mobile/src/components/ui/display/news-card.tsx @@ -5,6 +5,7 @@ import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; import { Typography } from "@/constants/typography"; import { Colors, Spacing, ColorScheme } from "@/constants/theme"; +import { useTranslation } from "react-i18next"; const DEFAULT_IMAGE = require("@/assets/images/campus-stiintei.png"); @@ -23,7 +24,8 @@ export interface NewsCardProps { } export function CategoryTag({ category }: { category: string }) { - const isEvent = category === "Evenimente"; + const { t } = useTranslation(); + const isEvent = category === t('home.events'); return ( - + + + - + + + = 500) { + return "Serverul este temporar indisponibil. Vă rugăm să reîncercați mai târziu."; + } + // Fallback for technical strings containing raw supabase or json information if (lowerMsg.includes("supabase") || lowerMsg.includes("{") || lowerMsg.includes("status_code")) { return "A apărut o eroare de autentificare. Vă rugăm să reîncercați."; diff --git a/Frontend/Mobile/src/utils/date.ts b/Frontend/Mobile/src/utils/date.ts index b82b1745..39fc22c5 100644 --- a/Frontend/Mobile/src/utils/date.ts +++ b/Frontend/Mobile/src/utils/date.ts @@ -1,117 +1,78 @@ -/** - * Formatează o dată din formatul "21 septembrie 2026" în "Luni, 21 sept." - */ -export const getFormattedDate = (dateStr?: string) => { - if (!dateStr) return ""; - - let targetDateStr = dateStr; - // Check if it is an ISO date string (contains T or format YYYY-MM-DD) - if (dateStr.includes("T") || /^\d{4}-\d{2}-\d{2}/.test(dateStr)) { - targetDateStr = isoToRomanianDateStr(dateStr); - } - - const parts = targetDateStr.split(" "); - if (parts.length < 2) return targetDateStr; - - const day = parts[0]; - const monthName = parts[1].toLowerCase(); - const year = parts[2] || "2026"; - - const monthRO: Record = { - 'ianuarie': 0, 'februarie': 1, 'martie': 2, 'aprilie': 3, 'mai': 4, 'iunie': 5, - 'iulie': 6, 'august': 7, 'septembrie': 8, 'octombrie': 9, 'noiembrie': 10, 'decembrie': 11 - }; - const shortMonths = ['ian.', 'feb.', 'mar.', 'apr.', 'mai', 'iun.', 'iul.', 'aug.', 'sept.', 'oct.', 'nov.', 'dec.']; - const dayNames = ['Duminică', 'Luni', 'Marți', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă']; - - const monthIndex = monthRO[monthName]; - if (monthIndex === undefined) return targetDateStr; - - const d = new Date(parseInt(year), monthIndex, parseInt(day)); - const dayOfWeek = dayNames[d.getDay()]; - const shortMonth = shortMonths[monthIndex]; - - return `${dayOfWeek}, ${day} ${shortMonth} ${year}`; -}; +import { settingsStore } from '@/utils/settings-store'; -/** - * Calculează timpul de citire estimat bazat pe lungimea textului - */ -export const getReadingTime = (text?: string, formatter?: (minutes: number) => string) => { - const words = text.split(/\s+/).length; - const minutes = Math.max(1, Math.ceil(words / 200)); - if (formatter) return formatter(minutes); - return `${minutes} min citire`; +const MONTH_RO: Record = { + 'ianuarie': 0, 'februarie': 1, 'martie': 2, 'aprilie': 3, 'mai': 4, 'iunie': 5, + 'iulie': 6, 'august': 7, 'septembrie': 8, 'octombrie': 9, 'noiembrie': 10, 'decembrie': 11, }; -/** - * Convertește o dată din formatul "21 septembrie 2026" și un timp opțional "10:00" într-un obiect Date - */ -export const parseRomanianDate = (dateStr?: string, timeStr?: string) => { - if (!dateStr || dateStr.toLowerCase().includes("necunoscut")) return new Date(0); - - // Eliminăm eventualele caractere suplimentare (ex: virgule) - const cleanDateStr = dateStr.replace(",", ""); - const parts = cleanDateStr.trim().split(/\s+/); - - if (parts.length < 2) return new Date(0); - - const day = parseInt(parts[0]); - if (isNaN(day)) return new Date(0); +function parseAnyDate(dateStr: string): Date | null { + if (!dateStr) return null; - const monthName = parts[1].toLowerCase(); - - // Dacă anul lipsește, presupunem anul curent sau 2026 conform contextului proiectului - const year = parseInt(parts[2] || "2026"); + if (/^\d{4}-\d{2}-\d{2}/.test(dateStr) || dateStr.includes('T')) { + const d = new Date(dateStr); + return isNaN(d.getTime()) ? null : d; + } - const monthRO: Record = { - 'ianuarie': 0, 'februarie': 1, 'martie': 2, 'aprilie': 3, 'mai': 4, 'iunie': 5, - 'iulie': 6, 'august': 7, 'septembrie': 8, 'octombrie': 9, 'noiembrie': 10, 'decembrie': 11 - }; + // Works for English Intl output like "June 16, 2026" or "16 June 2026" + const native = new Date(dateStr); + if (!isNaN(native.getTime())) return native; - const monthIndex = monthRO[monthName] !== undefined ? monthRO[monthName] : 0; - - let hours = 0; - let minutes = 0; - if (timeStr) { - // Suport pentru formate ca "10:00" sau "10" - const timeParts = timeStr.split(":"); - hours = parseInt(timeParts[0]) || 0; - minutes = parseInt(timeParts[1]) || 0; + // Romanian fallback: "16 iunie 2026" + const parts = dateStr.replace(',', '').trim().split(/\s+/); + if (parts.length >= 2) { + const day = parseInt(parts[0]); + const monthIdx = MONTH_RO[parts[1].toLowerCase()]; + const year = parseInt(parts[2] || String(new Date().getFullYear())); + if (!isNaN(day) && monthIdx !== undefined && !isNaN(year)) { + return new Date(year, monthIdx, day); } + } - return new Date(year, monthIndex, day, hours, minutes); -}; - -export const getTodayRomanianDate = (): string => { - const months = [ - "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", - "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie" - ]; - const d = new Date(); - const day = d.getDate(); - const month = months[d.getMonth()]; - const year = d.getFullYear(); - return `${day} ${month} ${year}`; -}; + return null; +} -/** - * Convertește o dată ISO (ex: "2026-06-16T12:00:00Z") în format text românesc ("16 iunie 2026") - */ -export const isoToRomanianDateStr = (isoStr?: string): string => { - if (!isoStr) return ""; +export const isoToRomanianDateStr = (isoStr?: string, lang?: string): string => { + if (!isoStr) return ''; try { const d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - const months = [ - "ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", - "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie" - ]; - const day = d.getDate(); - const month = months[d.getMonth()]; - const year = d.getFullYear(); - return `${day} ${month} ${year}`; + return new Intl.DateTimeFormat(lang ?? settingsStore.getLang(), { + day: 'numeric', month: 'long', year: 'numeric', + }).format(d); } catch { return isoStr; } }; + +export const getFormattedDate = (dateStr?: string, lang?: string): string => { + if (!dateStr) return ''; + const d = parseAnyDate(dateStr); + if (!d || isNaN(d.getTime())) return dateStr; + return new Intl.DateTimeFormat(lang ?? settingsStore.getLang(), { + weekday: 'long', day: 'numeric', month: 'short', year: 'numeric', + }).format(d); +}; + +export const getReadingTime = (text?: string, formatter?: (minutes: number) => string): string => { + const words = (text ?? '').split(/\s+/).length; + const minutes = Math.max(1, Math.ceil(words / 200)); + if (formatter) return formatter(minutes); + return `${minutes} min`; +}; + +export const parseRomanianDate = (dateStr?: string, timeStr?: string): Date => { + if (!dateStr || dateStr.toLowerCase().includes('necunoscut')) return new Date(0); + const d = parseAnyDate(dateStr); + if (!d) return new Date(0); + if (timeStr) { + const [h, m] = timeStr.split(':').map(Number); + d.setHours(h || 0, m || 0); + } + return d; +}; + +export const getTodayRomanianDate = (lang?: string): string => { + return new Intl.DateTimeFormat(lang ?? settingsStore.getLang(), { + day: 'numeric', month: 'long', year: 'numeric', + }).format(new Date()); +}; \ No newline at end of file diff --git a/Frontend/Mobile/src/utils/settings-store.ts b/Frontend/Mobile/src/utils/settings-store.ts index 9a3c0bd2..38c91329 100644 --- a/Frontend/Mobile/src/utils/settings-store.ts +++ b/Frontend/Mobile/src/utils/settings-store.ts @@ -1,14 +1,60 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Platform } from 'react-native'; + +const SUPPORTED_LANGS = ['ro', 'en']; +const KEY_LANG = 'settings_lang'; +const KEY_THEME = 'settings_theme'; + +function getDeviceLang(): string { + const locale = Intl.DateTimeFormat().resolvedOptions().locale; + const lang = locale.split('-')[0]; + return SUPPORTED_LANGS.includes(lang) ? lang : 'en'; +} + +function lsRead(key: string): string | null { + try { + return typeof window !== 'undefined' ? window.localStorage.getItem(key) : null; + } catch { + return null; + } +} + +function lsWrite(key: string, value: string) { + try { + if (typeof window !== 'undefined') window.localStorage.setItem(key, value); + } catch { /* quota exceeded etc */ } +} + class SettingsStore { - private theme: "system" | "light" | "dark" = "system"; - private lang: string = "ro"; + private theme: "system" | "light" | "dark" = (lsRead(KEY_THEME) as any) || "system"; + private lang: string = lsRead(KEY_LANG) || getDeviceLang(); private listeners: Set<() => void> = new Set(); + constructor() { + if (Platform.OS !== 'web') { + AsyncStorage.multiGet([KEY_LANG, KEY_THEME]).then(([[, lang], [, theme]]) => { + let changed = false; + if (lang) { this.lang = lang; changed = true; } + if (theme && ['system', 'light', 'dark'].includes(theme)) { + this.theme = theme as any; + changed = true; + } + if (changed) { + this.notify(); + import('@/i18n').then((mod) => mod.default.changeLanguage(this.lang)); + } + }); + } + } + getTheme() { return this.theme; } setTheme(theme: "system" | "light" | "dark") { this.theme = theme; + lsWrite(KEY_THEME, theme); + if (Platform.OS !== 'web') AsyncStorage.setItem(KEY_THEME, theme); this.notify(); } @@ -18,6 +64,8 @@ class SettingsStore { setLang(lang: string) { this.lang = lang; + lsWrite(KEY_LANG, lang); + if (Platform.OS !== 'web') AsyncStorage.setItem(KEY_LANG, lang); this.notify(); // Lazy import to avoid circular dependency with i18n init reading settingsStore import('@/i18n').then((mod) => mod.default.changeLanguage(lang)); @@ -35,4 +83,4 @@ class SettingsStore { } } -export const settingsStore = new SettingsStore(); +export const settingsStore = new SettingsStore(); \ No newline at end of file From 511ec0e47cf58f281ec558740cf5b0d97159272b Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Wed, 1 Jul 2026 11:35:26 +0300 Subject: [PATCH 06/15] fix: carousel --- .../Mobile/src/app/(public)/acasa/index.tsx | 8 ++--- .../src/app/(public)/acasa/index.web.tsx | 16 +++++----- .../app/(public)/acasa/vizualizare.web.tsx | 11 +++++-- .../Mobile/src/app/(public)/harta.web.tsx | 14 +-------- .../src/app/(public)/sesizari/adauga.web.tsx | 13 ++------- .../src/app/(public)/sesizari/detalii.web.tsx | 9 ++---- .../src/app/(public)/sesizari/index.web.tsx | 7 +---- .../components/ui/display/article-detail.tsx | 29 +++++++------------ 8 files changed, 36 insertions(+), 71 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index 8886d5c7..46af00e2 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -274,10 +274,10 @@ export default function HomeScreen() { }); }; - const activeNoutati = noutati; - const activeEvenimente = evenimente; - const activeFacultati = facultati; - const activeFacilitati = facilitati; + const activeNoutati = noutati.slice(0, 3); + const activeEvenimente = evenimente.slice(0, 3); + const activeFacultati = facultati.slice(0, 3); + const activeFacilitati = facilitati.slice(0, 3); const isPageEmpty = noutati.length === 0 && evenimente.length === 0 && facultati.length === 0 && facilitati.length === 0; diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 267186c6..28d50e36 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -298,7 +298,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Noutăți" renderItem={({ item, index }) => ( @@ -308,7 +308,7 @@ export default function HomeScreen() { date={getFormattedDate(item.date)} author={item.author} image={item.image} - marginRight={index === activeNoutati.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === Math.min(activeNoutati.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handlePress(item)} /> )} @@ -327,7 +327,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Evenimente" renderItem={({ item, index }) => ( @@ -337,7 +337,7 @@ export default function HomeScreen() { date={getFormattedDate(item.date)} author={item.author} image={item.image} - marginRight={index === activeEvenimente.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === Math.min(activeEvenimente.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handlePress(item)} /> )} @@ -356,7 +356,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facultăți" renderItem={({ item, index }) => ( @@ -364,7 +364,7 @@ export default function HomeScreen() { variant="square" title={item.title} image={item.image} - marginRight={index === activeFacultati.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === Math.min(activeFacultati.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handleFacultyPress(item)} /> )} @@ -383,7 +383,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facilități" renderItem={({ item, index }) => ( @@ -391,7 +391,7 @@ export default function HomeScreen() { variant="square" title={item.title} image={item.image} - marginRight={index === activeFacilitati.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === Math.min(activeFacilitati.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handleFacilityPress(item)} /> )} diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index 645d349b..ccdaba4c 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -284,7 +284,7 @@ function VizualizareScreen() { }; const handleCall = () => { - if (window.confirm(`Doriți să apelați numărul ${phone}?`)) { + if (window.confirm(t('detail.callConfirm', { phone }))) { Linking.openURL(`tel:${phone}`); } }; @@ -324,10 +324,15 @@ function VizualizareScreen() { // Breadcrumbs: Acasă / [categorie sau tip] / [titlu]. Segmentul de categorie // duce la lista categoriei respective; ultimul (titlul) nu e clickabil. const crumbCategory = (category as string) || (tipPagina as string); + const TIP_LABELS: Record = { + "Facultate": t('common.faculty'), + "Facilitate": t('common.facility'), + }; + const crumbLabel = TIP_LABELS[crumbCategory] || crumbCategory; const crumbs: Crumb[] = [ { label: t('common.home'), href: "/(public)/acasa" }, ...(crumbCategory - ? [{ label: crumbCategory, href: `/(public)/acasa/categorie?title=${encodeURIComponent(crumbCategory)}` }] + ? [{ label: crumbLabel, href: `/(public)/acasa/categorie?title=${encodeURIComponent(crumbCategory)}` }] : []), { label: (title as string) || t('common.unknownTitle') }, ]; @@ -389,7 +394,7 @@ function VizualizareScreen() { ) : ( - {category || (tipPagina === "Facultate" ? "Facultate" : "Categorie")} + {category || TIP_LABELS[tipPagina] || t('common.category')} )} { async function loadLocations() { try { - const cached = await storage.getItem('cached_facilities'); - if (cached) { - const parsed = JSON.parse(cached); - setLocations(parsed); - if (parsed.length > 0) { - setLocation(parsed[0].name); - } - } const res = await api.get('/locations/', { params: { page: 1, size: 50 } }); if (res.data?.items) { setLocations(res.data.items); - await storage.setItem('cached_facilities', JSON.stringify(res.data.items)); - if (res.data.items.length > 0 && !cached) { + if (res.data.items.length > 0) { setLocation(res.data.items[0].name); } } diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx index 41b161eb..500956fc 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx @@ -12,7 +12,7 @@ import { CAROUSEL_CARD_MARGIN } from "@/components/ui/display/carousel/carousel. import { CategoryHeader } from "@/components/ui/display/category-header"; import { WebContainer } from "@/components/ui/layout/web-container"; import { Breadcrumbs } from "@/components/ui/navigation/breadcrumbs"; -import api, { storage, resolveImageUrl } from "@/services/api"; +import api, { resolveImageUrl } from "@/services/api"; import { ErrorState } from "@/components/ui/display/error-state"; import { useTranslation } from 'react-i18next'; @@ -67,17 +67,12 @@ export default function SesizareDetaliiScreen() { setLoading(true); setError(null); - // 1. Fetch locations for mapping (using cached first) + // 1. Fetch locations for mapping let locationsData: any[] = []; - const cachedLocs = await storage.getItem('cached_facilities'); - if (cachedLocs) { - locationsData = JSON.parse(cachedLocs); - } try { const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; - await storage.setItem('cached_facilities', JSON.stringify(locsRes.data.items)); } } catch (locError) { console.warn('[API] Could not fetch fresh locations for complaint detail web:', locError); diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx index e488c98c..3293a7ea 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx @@ -12,7 +12,7 @@ import { SesizariListSkeleton } from "@/components/ui/display/skeletons"; import { Seo } from "@/components/seo"; import { SesizareCard, Sesizare } from "@/components/ui/display/sesizare-card"; import PlusIcon from "@/assets/icons/svg/plus.svg"; -import api, { storage, resolveImageUrl } from "@/services/api"; +import api, { resolveImageUrl } from "@/services/api"; import { useAuth } from "@/contexts/auth-context"; import { ErrorState } from "@/components/ui/display/error-state"; import { useTranslation } from 'react-i18next'; @@ -64,15 +64,10 @@ export default function SesizariScreen() { } // 1. Fetch/load locations to build a map of id -> name let locationsData: any[] = []; - const cachedLocs = await storage.getItem('cached_facilities'); - if (cachedLocs) { - locationsData = JSON.parse(cachedLocs); - } try { const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; - await storage.setItem('cached_facilities', JSON.stringify(locsRes.data.items)); } } catch (locError) { console.warn('[API] Could not fetch fresh locations for complaints:', locError); diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index 2c4e2fa2..dd8fe5be 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -22,7 +22,7 @@ import { useTranslation } from "react-i18next"; import { CompactCard } from "@/components/ui/display/home-highlights"; import { NewsCard, CategoryTag } from "@/components/ui/display/news-card"; import { eventHref, anuntHref } from "@/utils/article-url"; -import api, { storage } from "@/services/api"; +import api from "@/services/api"; import CalendarIcon from "@/assets/icons/svg/calendar.svg"; import LocationIcon from "@/assets/icons/svg/location.svg"; @@ -79,7 +79,7 @@ export function ArticleDetail({ const insets = useSafeAreaInsets(); const router = useRouter(); const { width } = useWindowDimensions(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const twoCol = width >= TWO_COL_BREAKPOINT; const tipPagina = type || "Eveniment"; @@ -90,18 +90,9 @@ export function ArticleDetail({ let isMounted = true; const loadRelated = async () => { try { - let cachedStr = await storage.getItem('cached_announcements'); - let items = []; - if (cachedStr) { - items = JSON.parse(cachedStr); - } else { - const res = await api.get('/announcements/', { params: { page: 1, size: 20 } }); - if (res.data?.items) { - items = res.data.items; - } - } - if (isMounted) { - setRelatedPool(items); + const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language } }); + if (res.data?.items && isMounted) { + setRelatedPool(res.data.items); } } catch (err) { console.warn('[ArticleDetail] Error loading related announcements:', err); @@ -111,7 +102,7 @@ export function ArticleDetail({ return () => { isMounted = false; }; - }, []); + }, [i18n.language]); // Anunturi inrudite: prioritizam aceeasi categorie ca articolul curent, apoi // completam cu restul. Excludem articolul curent (dupa titlu). Sidebar-ul ia @@ -121,17 +112,17 @@ export function ArticleDetail({ .map((item: any) => ({ id: item.id.toString(), type: item.type === "NOUTATE" ? "Anunț" : "Eveniment", - title: item.title || "Titlu necunoscut", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: item.type === "NOUTATE" ? t('home.news') : t('home.events'), - content: item.content || "Conținut necunoscut", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), image: item.image_url || "", - location: item.location_name || "Locație necunoscută", + location: item.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || "Dată necunoscută", + date: isoToRomanianDateStr(item.start_date) || t('common.unknownDate'), author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, From b7c076257b5700d5b72b2c9137c8b2dcf6baf29a Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Wed, 1 Jul 2026 12:28:06 +0300 Subject: [PATCH 07/15] fix: carousel facultati/facilitati --- Frontend/Mobile/src/app/(public)/acasa/index.tsx | 4 ++-- Frontend/Mobile/src/app/(public)/acasa/index.web.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index 46af00e2..a825e612 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -276,8 +276,8 @@ export default function HomeScreen() { const activeNoutati = noutati.slice(0, 3); const activeEvenimente = evenimente.slice(0, 3); - const activeFacultati = facultati.slice(0, 3); - const activeFacilitati = facilitati.slice(0, 3); + const activeFacultati = facultati; + const activeFacilitati = facilitati; const isPageEmpty = noutati.length === 0 && evenimente.length === 0 && facultati.length === 0 && facilitati.length === 0; diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 28d50e36..9c8252d7 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -356,7 +356,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facultăți" renderItem={({ item, index }) => ( @@ -364,7 +364,7 @@ export default function HomeScreen() { variant="square" title={item.title} image={item.image} - marginRight={index === Math.min(activeFacultati.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === activeFacultati.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handleFacultyPress(item)} /> )} @@ -383,7 +383,7 @@ export default function HomeScreen() { ) : ( item.id} viewAllHref="/(public)/acasa/categorie?title=Facilități" renderItem={({ item, index }) => ( @@ -391,7 +391,7 @@ export default function HomeScreen() { variant="square" title={item.title} image={item.image} - marginRight={index === Math.min(activeFacilitati.length, 3) - 1 ? 0 : CAROUSEL_CARD_MARGIN} + marginRight={index === activeFacilitati.length - 1 ? 0 : CAROUSEL_CARD_MARGIN} onPress={() => handleFacilityPress(item)} /> )} From 6eddefe9ba25d12dee6f30fe200c2bab5cb1e3e0 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Wed, 1 Jul 2026 16:01:50 +0300 Subject: [PATCH 08/15] fix: vizualizare imagine facultate --- Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index ccdaba4c..d41e5f0b 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -162,7 +162,7 @@ function VizualizareScreen() { id: item.id.toString(), type: "Facultate", title: item.name || t('common.unknownTitle'), - image: item.image_url || "", + image: item.logo_url || "", address: item.address || t('common.unknownAddress'), phone: item.phone || "", website: item.website_url || "", From e2dd3c1172806ae7861958a64db268cdd64376fb Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Wed, 1 Jul 2026 16:20:38 +0300 Subject: [PATCH 09/15] fix: skeleton loading harta --- Frontend/Mobile/src/components/map/map.web.tsx | 7 ++----- Frontend/Mobile/src/components/ui/display/skeletons.tsx | 5 +++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Frontend/Mobile/src/components/map/map.web.tsx b/Frontend/Mobile/src/components/map/map.web.tsx index a3814b64..b148d7fa 100644 --- a/Frontend/Mobile/src/components/map/map.web.tsx +++ b/Frontend/Mobile/src/components/map/map.web.tsx @@ -6,6 +6,7 @@ import { cleanMapStyle } from '@/utils/map-helper'; import { createRoot, flushSync } from 'react-dom/client'; import { MapPin } from './map-pin'; import { UserLocationPin } from './user-location-pin'; +import { MapSkeleton } from '@/components/ui/display/skeletons'; interface MapProps { themeName: 'light' | 'dark'; @@ -221,11 +222,7 @@ export default function Map({ themeName, selectedFacultyId, onFacultySelect, bui }, []); if (!mapStyle) { - return ( -
- Se încarcă harta... -
- ); + return ; } return ( diff --git a/Frontend/Mobile/src/components/ui/display/skeletons.tsx b/Frontend/Mobile/src/components/ui/display/skeletons.tsx index 1d4fbd59..4063a487 100644 --- a/Frontend/Mobile/src/components/ui/display/skeletons.tsx +++ b/Frontend/Mobile/src/components/ui/display/skeletons.tsx @@ -268,6 +268,11 @@ const styles = StyleSheet.create({ }, }); +// ── Harta ──────────────────────────────────────────────────────────────────────── +export function MapSkeleton() { + return ; +} + // ── Vizualizare (pagina de detalii) ───────────────────────────────────────────── export function VizualizareSkeleton() { return ( From 9bf030bdeb2af6c632ff58f058f1d17746731d74 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 16:28:01 +0300 Subject: [PATCH 10/15] feature: more languages --- .../src/app/(public)/acasa/categorie.tsx | 2 +- .../src/app/(public)/acasa/categorie.web.tsx | 2 +- .../Mobile/src/app/(public)/acasa/index.tsx | 4 +- .../src/app/(public)/acasa/index.web.tsx | 6 +- .../src/app/(public)/acasa/vizualizare.tsx | 8 +- .../app/(public)/acasa/vizualizare.web.tsx | 18 +- .../Mobile/src/app/(public)/more/setari.tsx | 38 ++- .../src/app/(public)/more/setari.web.tsx | 10 +- Frontend/Mobile/src/constants/languages.ts | 18 ++ Frontend/Mobile/src/i18n/index.ts | 28 ++ Frontend/Mobile/src/i18n/locales/ar.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/de.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/el.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/es.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/fr.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/hi.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/it.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/ja.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/ko.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/ru.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/tr.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/uk.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/vi.json | 246 ++++++++++++++++++ Frontend/Mobile/src/i18n/locales/zh.json | 246 ++++++++++++++++++ 24 files changed, 3539 insertions(+), 39 deletions(-) create mode 100644 Frontend/Mobile/src/constants/languages.ts create mode 100644 Frontend/Mobile/src/i18n/locales/ar.json create mode 100644 Frontend/Mobile/src/i18n/locales/de.json create mode 100644 Frontend/Mobile/src/i18n/locales/el.json create mode 100644 Frontend/Mobile/src/i18n/locales/es.json create mode 100644 Frontend/Mobile/src/i18n/locales/fr.json create mode 100644 Frontend/Mobile/src/i18n/locales/hi.json create mode 100644 Frontend/Mobile/src/i18n/locales/it.json create mode 100644 Frontend/Mobile/src/i18n/locales/ja.json create mode 100644 Frontend/Mobile/src/i18n/locales/ko.json create mode 100644 Frontend/Mobile/src/i18n/locales/ru.json create mode 100644 Frontend/Mobile/src/i18n/locales/tr.json create mode 100644 Frontend/Mobile/src/i18n/locales/uk.json create mode 100644 Frontend/Mobile/src/i18n/locales/vi.json create mode 100644 Frontend/Mobile/src/i18n/locales/zh.json diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index 45a69110..bf2c6f74 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -105,7 +105,7 @@ export default function CategoryScreen() { id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: displayTitle, - date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), + date: item.created_at || '', date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index 7fa84711..8494a7a2 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -99,7 +99,7 @@ export default function CategoryScreen() { id: item.id.toString(), title: item.title || "Titlu necunoscut", category: categoryTitle, - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + date: item.created_at || '', date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index a825e612..e32ae38e 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -64,7 +64,7 @@ export default function HomeScreen() { id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: t('home.news'), - date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), + date: item.created_at || '', author: item.author_name || "", image: item.image_url || undefined, content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), @@ -77,7 +77,7 @@ export default function HomeScreen() { id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: t('home.events'), - date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), + date: item.created_at || '', date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 9c8252d7..3107e37b 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -62,7 +62,7 @@ export default function HomeScreen() { id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: t('home.news'), - date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), + date: item.created_at || '', author: item.author_name || "", image: item.image_url || undefined, content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), @@ -75,7 +75,7 @@ export default function HomeScreen() { id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: t('home.events'), - date: isoToRomanianDateStr(item.created_at) || t('common.unknownDate'), + date: item.created_at || '', date_start: isoToRomanianDateStr(item.start_date) || "", date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", @@ -238,7 +238,7 @@ export default function HomeScreen() { }); const latest4 = allAnnouncements.slice(0, 4).map((item) => ({ ...item, - date: isoToRomanianDateStr(item.created_at) || "Dată necunoscută", + date: item.created_at || '', date_start: undefined, })); const featuredItem = latest4[0]; diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index a034a618..3b1421d1 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -131,8 +131,8 @@ function VizualizareScreen() { date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", - posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || "Dată necunoscută", + posted_at: item.created_at || "", + date: item.start_date || "", author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, @@ -193,7 +193,7 @@ function VizualizareScreen() { } else if (isFacility) { mappedItem = { id: match.id.toString(), type: "Facilitate", title: match.name || t('common.unknownTitle'), image: match.image_url || "", content: match.description || "", schedules: match.schedules || [] }; } else { - mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: match.title || t('common.unknownTitle'), category: match.type === "NOUTATE" ? t('home.news') : t('home.events'), content: match.content || t('common.unknownContent'), image: match.image_url || "", location: match.location_name || t('common.unknownLocation'), date_start: isoToRomanianDateStr(match.start_date) || "", date_end: isoToRomanianDateStr(match.end_date) || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: isoToRomanianDateStr(match.created_at) || "", date: isoToRomanianDateStr(match.start_date) || t('common.unknownDate'), author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; + mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: match.title || t('common.unknownTitle'), category: match.type === "NOUTATE" ? t('home.news') : t('home.events'), content: match.content || t('common.unknownContent'), image: match.image_url || "", location: match.location_name || t('common.unknownLocation'), date_start: match.start_date || "", date_end: match.end_date || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: match.created_at || "", date: match.start_date || "", author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; } setItemData(mappedItem); setLoading(false); @@ -271,7 +271,7 @@ function VizualizareScreen() { const createdTime = itemData?.created_at ? new Date(itemData.created_at).getTime() : 0; const updatedTime = itemData?.updated_at ? new Date(itemData.updated_at).getTime() : 0; const isUpdated = createdTime > 0 && updatedTime > 0 && Math.abs(updatedTime - createdTime) > 60000; - const formattedUpdateDate = itemData?.updated_at ? getFormattedDate(isoToRomanianDateStr(itemData.updated_at)) : ""; + const formattedUpdateDate = itemData?.updated_at ? getFormattedDate(itemData.updated_at) : ""; if (loading && !itemData) { return ( diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index d41e5f0b..54777462 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -147,8 +147,8 @@ function VizualizareScreen() { date_end: isoToRomanianDateStr(item.end_date) || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", - posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || t('common.unknownDate'), + posted_at: item.created_at || "", + date: item.start_date || "", author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, @@ -233,12 +233,12 @@ function VizualizareScreen() { content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), image: item.image_url || "", location: item.location_name || t('common.unknownLocation'), - date_start: isoToRomanianDateStr(item.start_date) || "", - date_end: isoToRomanianDateStr(item.end_date) || "", + date_start: item.start_date || "", + date_end: item.end_date || "", time_start: item.start_date ? new Date(item.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", - posted_at: isoToRomanianDateStr(item.created_at) || "", - date: isoToRomanianDateStr(item.start_date) || t('common.unknownDate'), + posted_at: item.created_at || "", + date: item.start_date || "", author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, @@ -289,9 +289,7 @@ function VizualizareScreen() { } }; - const displayDateValue = category === t('home.news') - ? (posted_at && posted_at !== t('common.unknownDate') ? posted_at : "") - : (date && date !== t('common.unknownDate') ? date : posted_at); + const displayDateValue = posted_at && posted_at !== t('common.unknownDate') ? posted_at : ""; const formattedDate = getFormattedDate(displayDateValue as string); const readingTime = getReadingTime(content as string); @@ -300,7 +298,7 @@ function VizualizareScreen() { const createdTime = itemData?.created_at ? new Date(itemData.created_at).getTime() : 0; const updatedTime = itemData?.updated_at ? new Date(itemData.updated_at).getTime() : 0; const isUpdated = createdTime > 0 && updatedTime > 0 && Math.abs(updatedTime - createdTime) > 60000; - const formattedUpdateDate = itemData?.updated_at ? getFormattedDate(isoToRomanianDateStr(itemData.updated_at)) : ""; + const formattedUpdateDate = itemData?.updated_at ? getFormattedDate(itemData.updated_at) : ""; if (loading && !itemData) { return ( diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index ab914452..39487066 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -1,6 +1,6 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import React, { useState, useEffect } from "react"; -import { View, Text, Switch, Pressable, Linking, Platform } from "react-native"; +import { View, Text, Switch, Pressable, Linking, Platform, Alert } from "react-native"; import Animated, { useSharedValue, useAnimatedScrollHandler, useAnimatedStyle, interpolate, Extrapolation } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter, Stack } from "expo-router"; @@ -11,6 +11,8 @@ import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { settingsStore } from "@/utils/settings-store"; import { useTranslation } from 'react-i18next'; +import { LANGUAGES } from '@/constants/languages'; +import { storage } from '@/services/api'; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import GlobeIcon from "@/assets/icons/svg/globe-europe.svg"; @@ -44,14 +46,19 @@ export default function SettingsScreen() { return unsubscribe; }, []); - const languages = [ - { code: "ro", label: "Română" }, - { code: "en", label: "English" }, - { code: "es", label: "Español" }, - { code: "fr", label: "Français" }, - { code: "de", label: "Deutsch" }, - { code: "it", label: "Italiano" } - ]; + const languages = LANGUAGES; + + const handleClearCache = () => { + Alert.alert("Șterge cache", "Se va reseta onboarding-ul. La repornire vei vedea din nou cererea de permisiuni.", [ + { text: "Anulează", style: "cancel" }, + { + text: "Șterge", style: "destructive", onPress: async () => { + await storage.removeItem("has_seen_onboarding"); + Alert.alert("Gata", "Cache șters. Repornește aplicația."); + } + }, + ]); + }; const handleOpenWebsite = async () => { try { @@ -199,7 +206,7 @@ export default function SettingsScreen() { {/* Link Site */} - ({ flexDirection: "row", @@ -212,6 +219,17 @@ export default function SettingsScreen() { {t('settings.visitWebsite')} + + {/* Șterge cache (dev) */} + ({ + paddingVertical: Spacing.sm, + opacity: pressed ? 0.6 : 1 + })} + > + Șterge cache +
diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index 2da14abf..b9abe089 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -11,6 +11,7 @@ import { CategoryHeader } from "@/components/ui/display/category-header"; import { WebContainer } from "@/components/ui/layout/web-container"; import { settingsStore } from "@/utils/settings-store"; import { useTranslation } from 'react-i18next'; +import { LANGUAGES } from '@/constants/languages'; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import GlobeIcon from "@/assets/icons/svg/globe-europe.svg"; @@ -39,14 +40,7 @@ export default function SettingsScreen() { return unsubscribe; }, []); - const languages = [ - { code: "ro", label: "Română" }, - { code: "en", label: "English" }, - { code: "es", label: "Español" }, - { code: "fr", label: "Français" }, - { code: "de", label: "Deutsch" }, - { code: "it", label: "Italiano" } - ]; + const languages = LANGUAGES; const handleOpenWebsite = () => { Linking.openURL("https://www.ugal.ro").catch((err) => diff --git a/Frontend/Mobile/src/constants/languages.ts b/Frontend/Mobile/src/constants/languages.ts new file mode 100644 index 00000000..99093e26 --- /dev/null +++ b/Frontend/Mobile/src/constants/languages.ts @@ -0,0 +1,18 @@ +export const LANGUAGES = [ + { code: "ro", label: "Română" }, + { code: "en", label: "English" }, + { code: "es", label: "Español" }, + { code: "fr", label: "Français" }, + { code: "de", label: "Deutsch" }, + { code: "it", label: "Italiano" }, + { code: "el", label: "Ελληνικά" }, + { code: "tr", label: "Türkçe" }, + { code: "vi", label: "Tiếng Việt" }, + { code: "uk", label: "Українська" }, + { code: "ru", label: "Русский" }, + { code: "ar", label: "العربية" }, + { code: "zh", label: "中文" }, + { code: "ja", label: "日本語" }, + { code: "ko", label: "한국어" }, + { code: "hi", label: "हिन्दी" }, +]; diff --git a/Frontend/Mobile/src/i18n/index.ts b/Frontend/Mobile/src/i18n/index.ts index 3965773e..d40cc7a8 100644 --- a/Frontend/Mobile/src/i18n/index.ts +++ b/Frontend/Mobile/src/i18n/index.ts @@ -4,11 +4,39 @@ import { settingsStore } from '@/utils/settings-store'; import ro from './locales/ro.json'; import en from './locales/en.json'; +import es from './locales/es.json'; +import fr from './locales/fr.json'; +import de from './locales/de.json'; +import it from './locales/it.json'; +import el from './locales/el.json'; +import tr from './locales/tr.json'; +import vi from './locales/vi.json'; +import uk from './locales/uk.json'; +import ru from './locales/ru.json'; +import ar from './locales/ar.json'; +import zh from './locales/zh.json'; +import ja from './locales/ja.json'; +import ko from './locales/ko.json'; +import hi from './locales/hi.json'; i18n.use(initReactI18next).init({ resources: { ro: { translation: ro }, en: { translation: en }, + es: { translation: es }, + fr: { translation: fr }, + de: { translation: de }, + it: { translation: it }, + el: { translation: el }, + tr: { translation: tr }, + vi: { translation: vi }, + uk: { translation: uk }, + ru: { translation: ru }, + ar: { translation: ar }, + zh: { translation: zh }, + ja: { translation: ja }, + ko: { translation: ko }, + hi: { translation: hi }, }, lng: settingsStore.getLang(), fallbackLng: 'en', diff --git a/Frontend/Mobile/src/i18n/locales/ar.json b/Frontend/Mobile/src/i18n/locales/ar.json new file mode 100644 index 00000000..f8bb2c5e --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/ar.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "أخبار", + "events": "فعاليات", + "faculties": "الكليات", + "facilities": "المرافق", + "recent": "الأحدث", + "noNews": "لم يتم العثور على أخبار.", + "noEvents": "لم يتم العثور على فعاليات.", + "noFaculties": "لم يتم العثور على كليات.", + "noFacilities": "لم يتم العثور على مرافق.", + "loadErrorNews": "تعذر تحميل الأخبار.", + "loadErrorEvents": "تعذر تحميل الفعاليات.", + "loadErrorFaculties": "تعذر تحميل الكليات.", + "loadErrorFacilities": "تعذر تحميل المرافق.", + "emptyNews": "لا توجد أخبار متاحة.", + "emptyEvents": "لا توجد فعاليات متاحة.", + "emptyFaculties": "لا توجد كليات متاحة.", + "emptyFacilities": "لا توجد مرافق متاحة.", + "refreshError": "تعذر تحديث البيانات. يرجى التحقق من اتصالك بالإنترنت." + }, + "category": { + "empty": "لا توجد عناصر في هذه الفئة.", + "allFaculties": "جميع الكليات", + "all": "الكل", + "refreshError": "تعذر تحديث بيانات هذه الفئة. يرجى التحقق من اتصالك بالإنترنت.", + "fallback": "الفئة" + }, + "detail": { + "eventInfo": "معلومات الفعالية", + "facilityInfo": "معلومات المرفق", + "schedule": "الجدول الزمني:", + "from": "من", + "until": "إلى", + "aboutEvent": "عن الفعالية", + "aboutFaculty": "عن الكلية", + "aboutFacility": "عن المرفق", + "details": "التفاصيل", + "contact": "التواصل والموقع", + "address": "العنوان", + "phone": "الهاتف", + "website": "الموقع الإلكتروني", + "updated": "تم التحديث:", + "relatedArticles": "مقالات مشابهة", + "more": "المزيد", + "loadError": "تعذر تحميل تفاصيل الصفحة.", + "notFound": "لم يتم العثور على التفاصيل.", + "callFaculty": "التواصل مع الكلية", + "callFacility": "التواصل مع المرفق", + "callConfirm": "هل تريد الاتصال بـ {{phone}}؟", + "cancel": "إلغاء", + "call": "اتصال" + }, + "common": { + "unknownTitle": "عنوان غير معروف", + "unknownContent": "محتوى غير معروف", + "unknownDate": "تاريخ غير معروف", + "unknownLocation": "موقع غير معروف", + "unknownAddress": "عنوان غير معروف", + "unknownStartDate": "تاريخ بداية غير معروف", + "unknownEndDate": "تاريخ انتهاء غير معروف", + "home": "الرئيسية", + "news": "أخبار", + "events": "فعاليات", + "faculty": "كلية", + "facility": "مرفق", + "category": "فئة", + "article": "مقالة", + "updateError": "خطأ في التحديث", + "selected": "(محدد)", + "retry": "إعادة المحاولة", + "connectionError": "تعذر الاتصال بالخادم. يرجى المحاولة مرة أخرى.", + "user": "المستخدم", + "dashboard": "لوحة التحكم", + "attachedFiles": "الملفات المرفقة", + "noItems": "لا توجد عناصر في هذه الفئة.", + "loading": "جارٍ التحميل...", + "errorTitle": "عذراً! حدث خطأ ما...", + "viewAll": "عرض المزيد", + "university": "الجامعة", + "universityPlatform": "منصتك الجامعية" + }, + "days": { + "1": "الاثنين", + "2": "الثلاثاء", + "3": "الأربعاء", + "4": "الخميس", + "5": "الجمعة", + "6": "السبت", + "7": "الأحد" + }, + "language": { + "title": "لغة التطبيق", + "header": "اللغة", + "select": "اختر اللغة", + "selected": "(محدد)", + "current": "اللغة الحالية" + }, + "nav": { + "home": "الرئيسية", + "map": "الخريطة", + "canteen": "المطعم", + "reports": "الشكاوى", + "more": "المزيد" + }, + "more": { + "title": "المزيد", + "visitGalati": "زيارة Galați", + "login": "تسجيل الدخول", + "disconnect": "تسجيل الخروج", + "settings": "الإعدادات", + "profileTitle": "ملفك الشخصي", + "profileLoggedIn": "أنت مسجل الدخول بالفعل. هل تريد تسجيل الخروج؟", + "cancel": "إلغاء", + "logout": "تسجيل الخروج" + }, + "settings": { + "title": "الإعدادات", + "appearanceLang": "المظهر واللغة", + "themeApp": "مظهر التطبيق", + "currentTheme": "المظهر الحالي:", + "currentLang": "اللغة الحالية:", + "supportInfo": "المساعدة والمعلومات", + "visitWebsite": "زيارة موقع UGAL", + "appSlogan": "تم إنشاؤه لطلاب جامعة «Dunărea de Jos» في Galați" + }, + "theme": { + "title": "مظهر التطبيق", + "header": "المظهر", + "select": "اختر المظهر", + "system": "النظام", + "light": "فاتح", + "dark": "داكن", + "current": "المظهر الحالي", + "switchToLight": "التبديل إلى المظهر الفاتح", + "switchToDark": "التبديل إلى المظهر الداكن" + }, + "canteen": { + "title": "المطعم", + "today": "اليوم", + "empty": "لا توجد قائمة طعام متاحة لهذا اليوم.", + "updateError": "تعذر تحديث قائمة المطعم. يرجى التحقق من اتصالك بالإنترنت." + }, + "map": { + "title": "الخريطة", + "allLocations": "جميع المواقع", + "facilities": "المرافق" + }, + "reports": { + "title": "الشكاوى", + "all": "جميع الشكاوى", + "mine": "شكاواي", + "active": "نشطة", + "rejected": "مرفوضة", + "completed": "محلولة", + "newReport": "شكوى جديدة", + "updateError": "تعذر تحديث الشكاوى. يرجى التحقق من اتصالك بالإنترنت.", + "loginRequired": "يجب تسجيل الدخول", + "loginDesc": "سجّل الدخول لإرسال أو عرض شكاواك.", + "login": "تسجيل الدخول", + "empty": "لا توجد شكاوى في هذا القسم", + "emptyDesc": "لا توجد سجلات حالياً.", + "unknownLocation": "موقع غير معروف", + "location": "الموقع", + "titleField": "العنوان", + "descDetailed": "وصف تفصيلي", + "addPhoto": "إضافة صورة", + "addPhotoBtn": "إضافة صورة", + "submit": "إرسال الشكوى", + "photoPermission": "يلزم الوصول إلى المعرض لإضافة صورة.", + "titleRequired": "العنوان إلزامي.", + "descRequired": "الوصف إلزامي.", + "photoRequired": "الصورة إلزامية.", + "submitError": "حدث خطأ أثناء إرسال الشكوى. يرجى المحاولة مرة أخرى.", + "infoTitle": "معلومات الشكوى", + "descSection": "وصف المشكلة", + "progressTitle": "سجل التقدم", + "statusActive": "نشطة", + "statusRejected": "مرفوضة", + "statusCompleted": "محلولة", + "missingTitle": "العنوان مفقود", + "noDescription": "لم يتم إضافة وصف.", + "notFound": "لم يتم العثور على الشكوى.", + "loadError": "حدث خطأ أثناء تحميل الشكوى.", + "loadErrorGeneral": "حدث خطأ أثناء تحميل الشكاوى.", + "step1Title": "تم تسجيل الشكوى", + "step1Desc": "تم حفظ الشكوى في النظام.", + "step2ActiveTitle": "قيد المراجعة", + "step2ActiveDesc": "يقوم مسؤول بتقييم تفاصيل المشكلة.", + "step3ActiveTitle": "اكتمل الحل", + "step3ActiveDesc": "سيتدخل الفريق لإصلاح الوضع.", + "step2RejectedTitle": "مرفوضة", + "step2RejectedDesc": "تم رفض الطلب من قبل الفريق الإداري.", + "step2CompletedTitle": "قيد المراجعة الإدارية", + "step2CompletedDesc": "تمت معالجة المشكلة بنجاح.", + "step3CompletedTitle": "محلولة", + "step3CompletedDesc": "تم حل المشكلة في الموقع من قبل الفريق التقني.", + "general": "عام", + "exterior": "خارجي" + }, + "auth": { + "title": "تسجيل الدخول", + "subtitle": "أدخل بيانات اعتمادك للوصول إلى حسابك", + "email": "البريد الإلكتروني", + "password": "كلمة المرور", + "login": "تسجيل الدخول", + "emailRequired": "البريد الإلكتروني إلزامي.", + "emailInvalid": "تنسيق البريد الإلكتروني غير صالح.", + "passwordRequired": "كلمة المرور إلزامية.", + "passwordTooShort": "يجب أن تتكون كلمة المرور من 6 أحرف على الأقل.", + "invalidCredentials": "بريد إلكتروني أو كلمة مرور غير صحيحة." + }, + "onboarding": { + "exploreTitle": "استكشف الحرم الجامعي", + "exploreDesc": "اكتشف مباني ومرافق الحرم الجامعي مباشرة على الخريطة.", + "continue": "متابعة" + }, + "navbar": { + "unauthenticated": "غير مصادق", + "theme": "المظهر", + "logout": "تسجيل الخروج", + "login": "تسجيل الدخول", + "openMenu": "فتح القائمة", + "closeMenu": "إغلاق القائمة", + "home": "الرئيسية", + "announcements": "الإعلانات", + "news": "أخبار", + "events": "فعاليات" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "المساعد الافتراضي", + "placeholder": "اطرح سؤالاً...", + "noValidResponse": "لم يتم تلقي استجابة صالحة.", + "errorOccurred": "حدث خطأ. يرجى المحاولة مرة أخرى.", + "assistantError": "خطأ في المساعد.", + "howCanIHelp": "كيف يمكنني مساعدتك اليوم؟", + "promptSuggestion": "اسألني عن الفعاليات أو المطعم أو الخريطة أو الشكاوى.", + "noResponse": "⚠️ لم يُرجع المساعد أي استجابة. يرجى المحاولة لاحقاً.", + "newConversation": "محادثة جديدة", + "closeChat": "إغلاق الدردشة", + "sendMessage": "إرسال رسالة", + "openAssistant": "فتح مساعد Ace", + "closeAssistant": "إغلاق مساعد Ace" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/de.json b/Frontend/Mobile/src/i18n/locales/de.json new file mode 100644 index 00000000..0e155032 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/de.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Nachrichten", + "events": "Veranstaltungen", + "faculties": "Fakultäten", + "facilities": "Einrichtungen", + "recent": "Aktuell", + "noNews": "Keine Nachrichten gefunden.", + "noEvents": "Keine Veranstaltungen gefunden.", + "noFaculties": "Keine Fakultäten gefunden.", + "noFacilities": "Keine Einrichtungen gefunden.", + "loadErrorNews": "Nachrichten konnten nicht geladen werden.", + "loadErrorEvents": "Veranstaltungen konnten nicht geladen werden.", + "loadErrorFaculties": "Fakultäten konnten nicht geladen werden.", + "loadErrorFacilities": "Einrichtungen konnten nicht geladen werden.", + "emptyNews": "Keine Nachrichten verfügbar.", + "emptyEvents": "Keine Veranstaltungen verfügbar.", + "emptyFaculties": "Keine Fakultäten verfügbar.", + "emptyFacilities": "Keine Einrichtungen verfügbar.", + "refreshError": "Daten konnten nicht aktualisiert werden. Bitte überprüfe deine Internetverbindung." + }, + "category": { + "empty": "Keine Einträge in dieser Kategorie.", + "allFaculties": "Alle Fakultäten", + "all": "Alle", + "refreshError": "Daten für diese Kategorie konnten nicht aktualisiert werden. Bitte überprüfe deine Internetverbindung.", + "fallback": "Kategorie" + }, + "detail": { + "eventInfo": "Veranstaltungsinformationen", + "facilityInfo": "Einrichtungsinformationen", + "schedule": "Zeitplan:", + "from": "Von", + "until": "Bis", + "aboutEvent": "Über die Veranstaltung", + "aboutFaculty": "Über die Fakultät", + "aboutFacility": "Über die Einrichtung", + "details": "Details", + "contact": "Kontakt und Standort", + "address": "Adresse", + "phone": "Telefon", + "website": "Website", + "updated": "Aktualisiert:", + "relatedArticles": "Ähnliche Artikel", + "more": "Mehr", + "loadError": "Details für diese Seite konnten nicht geladen werden.", + "notFound": "Details nicht gefunden.", + "callFaculty": "Fakultät kontaktieren", + "callFacility": "Einrichtung kontaktieren", + "callConfirm": "Möchten Sie {{phone}} anrufen?", + "cancel": "Abbrechen", + "call": "Anrufen" + }, + "common": { + "unknownTitle": "Unbekannter Titel", + "unknownContent": "Unbekannter Inhalt", + "unknownDate": "Unbekanntes Datum", + "unknownLocation": "Unbekannter Ort", + "unknownAddress": "Unbekannte Adresse", + "unknownStartDate": "Unbekanntes Startdatum", + "unknownEndDate": "Unbekanntes Enddatum", + "home": "Startseite", + "news": "Nachrichten", + "events": "Veranstaltungen", + "faculty": "Fakultät", + "facility": "Einrichtung", + "category": "Kategorie", + "article": "Artikel", + "updateError": "Aktualisierungsfehler", + "selected": "(Ausgewählt)", + "retry": "Erneut versuchen", + "connectionError": "Verbindung zum Server nicht möglich. Bitte versuche es erneut.", + "user": "Benutzer", + "dashboard": "Dashboard", + "attachedFiles": "Angehängte Dateien", + "noItems": "Keine Einträge in dieser Kategorie.", + "loading": "Wird geladen...", + "errorTitle": "Ups! Etwas ist schiefgelaufen...", + "viewAll": "Mehr anzeigen", + "university": "Universität", + "universityPlatform": "Deine Universitätsplattform" + }, + "days": { + "1": "Montag", + "2": "Dienstag", + "3": "Mittwoch", + "4": "Donnerstag", + "5": "Freitag", + "6": "Samstag", + "7": "Sonntag" + }, + "language": { + "title": "App-Sprache", + "header": "Sprache", + "select": "Sprache auswählen", + "selected": "(Ausgewählt)", + "current": "Aktuelle Sprache" + }, + "nav": { + "home": "Startseite", + "map": "Karte", + "canteen": "Mensa", + "reports": "Meldungen", + "more": "Mehr" + }, + "more": { + "title": "Mehr", + "visitGalati": "Galați besuchen", + "login": "Anmelden", + "disconnect": "Abmelden", + "settings": "Einstellungen", + "profileTitle": "Dein Profil", + "profileLoggedIn": "Du bist bereits angemeldet. Möchtest du dich abmelden?", + "cancel": "Abbrechen", + "logout": "Abmelden" + }, + "settings": { + "title": "Einstellungen", + "appearanceLang": "Erscheinungsbild & Sprache", + "themeApp": "App-Design", + "currentTheme": "Aktuelles Design:", + "currentLang": "Aktuelle Sprache:", + "supportInfo": "Hilfe & Info", + "visitWebsite": "UGAL-Website besuchen", + "appSlogan": "Erstellt für die Studierenden der Universität «Dunărea de Jos» in Galați" + }, + "theme": { + "title": "App-Design", + "header": "Design", + "select": "Design auswählen", + "system": "System", + "light": "Hell", + "dark": "Dunkel", + "current": "Aktuelles Design", + "switchToLight": "Zum hellen Design wechseln", + "switchToDark": "Zum dunklen Design wechseln" + }, + "canteen": { + "title": "Mensa", + "today": "Heute", + "empty": "Kein Menü für diesen Tag verfügbar.", + "updateError": "Menü der Mensa konnte nicht aktualisiert werden. Bitte überprüfe deine Internetverbindung." + }, + "map": { + "title": "Karte", + "allLocations": "Alle Standorte", + "facilities": "Einrichtungen" + }, + "reports": { + "title": "Meldungen", + "all": "Alle Meldungen", + "mine": "Meine Meldungen", + "active": "Aktiv", + "rejected": "Abgelehnt", + "completed": "Gelöst", + "newReport": "Neue Meldung", + "updateError": "Meldungen konnten nicht aktualisiert werden. Bitte überprüfe deine Internetverbindung.", + "loginRequired": "Du musst angemeldet sein", + "loginDesc": "Melde dich an, um Meldungen einzureichen oder anzuzeigen.", + "login": "Anmelden", + "empty": "Keine Meldungen in diesem Bereich", + "emptyDesc": "Derzeit keine Einträge vorhanden.", + "unknownLocation": "Unbekannter Ort", + "location": "Ort", + "titleField": "Titel", + "descDetailed": "Detaillierte Beschreibung", + "addPhoto": "Foto hinzufügen", + "addPhotoBtn": "Foto hinzufügen", + "submit": "Meldung einreichen", + "photoPermission": "Zugriff auf die Fotogalerie ist erforderlich, um ein Foto hinzuzufügen.", + "titleRequired": "Titel ist erforderlich.", + "descRequired": "Beschreibung ist erforderlich.", + "photoRequired": "Ein Foto ist erforderlich.", + "submitError": "Beim Einreichen der Meldung ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "infoTitle": "Meldungsinformationen", + "descSection": "Problembeschreibung", + "progressTitle": "Fortschrittsverlauf", + "statusActive": "Aktiv", + "statusRejected": "Abgelehnt", + "statusCompleted": "Gelöst", + "missingTitle": "Fehlender Titel", + "noDescription": "Keine Beschreibung hinzugefügt.", + "notFound": "Meldung nicht gefunden.", + "loadError": "Beim Laden der Meldung ist ein Fehler aufgetreten.", + "loadErrorGeneral": "Beim Laden der Meldungen ist ein Fehler aufgetreten.", + "step1Title": "Meldung registriert", + "step1Desc": "Die Meldung wurde im System gespeichert.", + "step2ActiveTitle": "In Bearbeitung", + "step2ActiveDesc": "Ein Administrator wertet die Problemdetails aus.", + "step3ActiveTitle": "Lösung abgeschlossen", + "step3ActiveDesc": "Das Team wird eingreifen, um die Situation zu beheben.", + "step2RejectedTitle": "Abgelehnt", + "step2RejectedDesc": "Die Anfrage wurde vom Verwaltungsteam abgelehnt.", + "step2CompletedTitle": "In administrativer Prüfung", + "step2CompletedDesc": "Das Problem wurde erfolgreich bearbeitet.", + "step3CompletedTitle": "Gelöst", + "step3CompletedDesc": "Das Problem wurde vor Ort vom technischen Personal behoben.", + "general": "Allgemein", + "exterior": "Außenbereich" + }, + "auth": { + "title": "Anmelden", + "subtitle": "Gib deine Anmeldedaten ein, um auf dein Konto zuzugreifen", + "email": "E-Mail", + "password": "Passwort", + "login": "Anmelden", + "emailRequired": "E-Mail ist erforderlich.", + "emailInvalid": "Ungültiges E-Mail-Format.", + "passwordRequired": "Passwort ist erforderlich.", + "passwordTooShort": "Das Passwort muss mindestens 6 Zeichen lang sein.", + "invalidCredentials": "Falsche E-Mail oder falsches Passwort." + }, + "onboarding": { + "exploreTitle": "Campus erkunden", + "exploreDesc": "Entdecke die Gebäude und Einrichtungen des Universitätscampus direkt auf der Karte.", + "continue": "Weiter" + }, + "navbar": { + "unauthenticated": "Nicht angemeldet", + "theme": "Design", + "logout": "Abmelden", + "login": "Anmelden", + "openMenu": "Menü öffnen", + "closeMenu": "Menü schließen", + "home": "Startseite", + "announcements": "Ankündigungen", + "news": "Nachrichten", + "events": "Veranstaltungen" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Virtueller Assistent", + "placeholder": "Stelle eine Frage...", + "noValidResponse": "Keine gültige Antwort erhalten.", + "errorOccurred": "Ein Fehler ist aufgetreten. Bitte versuche es erneut.", + "assistantError": "Assistentenfehler.", + "howCanIHelp": "Wie kann ich dir heute helfen?", + "promptSuggestion": "Frag mich zu Veranstaltungen, der Mensa, der Karte oder Meldungen.", + "noResponse": "⚠️ Der Assistent hat keine Antwort zurückgegeben. Bitte versuche es später erneut.", + "newConversation": "Neues Gespräch", + "closeChat": "Chat schließen", + "sendMessage": "Nachricht senden", + "openAssistant": "Ace-Assistenten öffnen", + "closeAssistant": "Ace-Assistenten schließen" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/el.json b/Frontend/Mobile/src/i18n/locales/el.json new file mode 100644 index 00000000..1982d518 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/el.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Νέα", + "events": "Εκδηλώσεις", + "faculties": "Σχολές", + "facilities": "Εγκαταστάσεις", + "recent": "Πρόσφατα", + "noNews": "Δεν βρέθηκαν νέα.", + "noEvents": "Δεν βρέθηκαν εκδηλώσεις.", + "noFaculties": "Δεν βρέθηκαν σχολές.", + "noFacilities": "Δεν βρέθηκαν εγκαταστάσεις.", + "loadErrorNews": "Αδυναμία φόρτωσης νέων.", + "loadErrorEvents": "Αδυναμία φόρτωσης εκδηλώσεων.", + "loadErrorFaculties": "Αδυναμία φόρτωσης σχολών.", + "loadErrorFacilities": "Αδυναμία φόρτωσης εγκαταστάσεων.", + "emptyNews": "Δεν υπάρχουν διαθέσιμα νέα.", + "emptyEvents": "Δεν υπάρχουν διαθέσιμες εκδηλώσεις.", + "emptyFaculties": "Δεν υπάρχουν διαθέσιμες σχολές.", + "emptyFacilities": "Δεν υπάρχουν διαθέσιμες εγκαταστάσεις.", + "refreshError": "Αδυναμία ανανέωσης δεδομένων. Ελέγξτε τη σύνδεσή σας στο διαδίκτυο." + }, + "category": { + "empty": "Δεν υπάρχουν στοιχεία σε αυτή την κατηγορία.", + "allFaculties": "Όλες οι Σχολές", + "all": "Όλες", + "refreshError": "Αδυναμία ανανέωσης δεδομένων για αυτή την κατηγορία. Ελέγξτε τη σύνδεσή σας.", + "fallback": "Κατηγορία" + }, + "detail": { + "eventInfo": "Πληροφορίες εκδήλωσης", + "facilityInfo": "Πληροφορίες εγκατάστασης", + "schedule": "Πρόγραμμα:", + "from": "Από", + "until": "Έως", + "aboutEvent": "Σχετικά με την εκδήλωση", + "aboutFaculty": "Σχετικά με τη σχολή", + "aboutFacility": "Σχετικά με την εγκατάσταση", + "details": "Λεπτομέρειες", + "contact": "Επικοινωνία και Τοποθεσία", + "address": "Διεύθυνση", + "phone": "Τηλέφωνο", + "website": "Ιστότοπος", + "updated": "Ενημερώθηκε:", + "relatedArticles": "Παρόμοια άρθρα", + "more": "Περισσότερα", + "loadError": "Αδυναμία φόρτωσης λεπτομερειών.", + "notFound": "Δεν βρέθηκαν λεπτομέρειες.", + "callFaculty": "Επικοινωνία Σχολής", + "callFacility": "Επικοινωνία Εγκατάστασης", + "callConfirm": "Θέλετε να καλέσετε το {{phone}};", + "cancel": "Ακύρωση", + "call": "Κλήση" + }, + "common": { + "unknownTitle": "Άγνωστος τίτλος", + "unknownContent": "Άγνωστο περιεχόμενο", + "unknownDate": "Άγνωστη ημερομηνία", + "unknownLocation": "Άγνωστη τοποθεσία", + "unknownAddress": "Άγνωστη διεύθυνση", + "unknownStartDate": "Άγνωστη ημερομηνία έναρξης", + "unknownEndDate": "Άγνωστη ημερομηνία λήξης", + "home": "Αρχική", + "news": "Νέα", + "events": "Εκδηλώσεις", + "faculty": "Σχολή", + "facility": "Εγκατάσταση", + "category": "Κατηγορία", + "article": "Άρθρο", + "updateError": "Σφάλμα ενημέρωσης", + "selected": "(Επιλεγμένο)", + "retry": "Επανάληψη", + "connectionError": "Αδυναμία σύνδεσης στον διακομιστή. Δοκιμάστε ξανά.", + "user": "Χρήστης", + "dashboard": "Πίνακας", + "attachedFiles": "Συνημμένα αρχεία", + "noItems": "Δεν υπάρχουν στοιχεία σε αυτή την κατηγορία.", + "loading": "Φόρτωση...", + "errorTitle": "Ωχ! Κάτι πήγε στραβά...", + "viewAll": "Προβολή περισσότερων", + "university": "Πανεπιστήμιο", + "universityPlatform": "Η πανεπιστημιακή σας πλατφόρμα" + }, + "days": { + "1": "Δευτέρα", + "2": "Τρίτη", + "3": "Τετάρτη", + "4": "Πέμπτη", + "5": "Παρασκευή", + "6": "Σάββατο", + "7": "Κυριακή" + }, + "language": { + "title": "Γλώσσα εφαρμογής", + "header": "Γλώσσα", + "select": "Επιλογή γλώσσας", + "selected": "(Επιλεγμένο)", + "current": "Τρέχουσα γλώσσα" + }, + "nav": { + "home": "Αρχική", + "map": "Χάρτης", + "canteen": "Κυλικείο", + "reports": "Αναφορές", + "more": "Περισσότερα" + }, + "more": { + "title": "Περισσότερα", + "visitGalati": "Επίσκεψη στο Galați", + "login": "Σύνδεση", + "disconnect": "Αποσύνδεση", + "settings": "Ρυθμίσεις", + "profileTitle": "Το προφίλ σας", + "profileLoggedIn": "Είστε ήδη συνδεδεμένοι. Θέλετε να αποσυνδεθείτε;", + "cancel": "Ακύρωση", + "logout": "Αποσύνδεση" + }, + "settings": { + "title": "Ρυθμίσεις", + "appearanceLang": "Εμφάνιση & Γλώσσα", + "themeApp": "Θέμα εφαρμογής", + "currentTheme": "Τρέχον θέμα:", + "currentLang": "Τρέχουσα γλώσσα:", + "supportInfo": "Βοήθεια & Πληροφορίες", + "visitWebsite": "Επίσκεψη ιστότοπου UGAL", + "appSlogan": "Δημιουργήθηκε για τους φοιτητές του Πανεπιστημίου «Dunărea de Jos» του Galați" + }, + "theme": { + "title": "Θέμα εφαρμογής", + "header": "Θέμα", + "select": "Επιλογή θέματος", + "system": "Σύστημα", + "light": "Ανοιχτό", + "dark": "Σκοτεινό", + "current": "Τρέχον θέμα", + "switchToLight": "Εναλλαγή σε ανοιχτό θέμα", + "switchToDark": "Εναλλαγή σε σκοτεινό θέμα" + }, + "canteen": { + "title": "Κυλικείο", + "today": "Σήμερα", + "empty": "Δεν υπάρχει διαθέσιμο μενού για αυτή την ημέρα.", + "updateError": "Αδυναμία ανανέωσης μενού κυλικείου. Ελέγξτε τη σύνδεσή σας." + }, + "map": { + "title": "Χάρτης", + "allLocations": "Όλες οι τοποθεσίες", + "facilities": "Εγκαταστάσεις" + }, + "reports": { + "title": "Αναφορές", + "all": "Όλες οι αναφορές", + "mine": "Οι αναφορές μου", + "active": "Ενεργές", + "rejected": "Απορριφθείσες", + "completed": "Επιλυμένες", + "newReport": "Νέα αναφορά", + "updateError": "Αδυναμία ανανέωσης αναφορών. Ελέγξτε τη σύνδεσή σας.", + "loginRequired": "Απαιτείται σύνδεση", + "loginDesc": "Συνδεθείτε για να υποβάλετε ή να δείτε τις αναφορές σας.", + "login": "Σύνδεση", + "empty": "Δεν υπάρχουν αναφορές σε αυτή την ενότητα", + "emptyDesc": "Δεν υπάρχουν εγγραφές αυτή τη στιγμή.", + "unknownLocation": "Άγνωστη τοποθεσία", + "location": "Τοποθεσία", + "titleField": "Τίτλος", + "descDetailed": "Λεπτομερής περιγραφή", + "addPhoto": "Προσθήκη φωτογραφίας", + "addPhotoBtn": "Προσθήκη φωτογραφίας", + "submit": "Υποβολή αναφοράς", + "photoPermission": "Απαιτείται πρόσβαση στη γκαλερί για την προσθήκη φωτογραφίας.", + "titleRequired": "Ο τίτλος είναι υποχρεωτικός.", + "descRequired": "Η περιγραφή είναι υποχρεωτική.", + "photoRequired": "Η φωτογραφία είναι υποχρεωτική.", + "submitError": "Παρουσιάστηκε σφάλμα κατά την υποβολή. Δοκιμάστε ξανά.", + "infoTitle": "Πληροφορίες αναφοράς", + "descSection": "Περιγραφή προβλήματος", + "progressTitle": "Ιστορικό προόδου", + "statusActive": "Ενεργή", + "statusRejected": "Απορριφθείσα", + "statusCompleted": "Επιλυμένη", + "missingTitle": "Λείπει τίτλος", + "noDescription": "Δεν προστέθηκε περιγραφή.", + "notFound": "Η αναφορά δεν βρέθηκε.", + "loadError": "Σφάλμα κατά τη φόρτωση της αναφοράς.", + "loadErrorGeneral": "Σφάλμα κατά τη φόρτωση των αναφορών.", + "step1Title": "Αναφορά καταχωρήθηκε", + "step1Desc": "Η αναφορά αποθηκεύτηκε στο σύστημα.", + "step2ActiveTitle": "Υπό εξέταση", + "step2ActiveDesc": "Ένας διαχειριστής αξιολογεί τις λεπτομέρειες.", + "step3ActiveTitle": "Επίλυση ολοκληρώθηκε", + "step3ActiveDesc": "Η ομάδα θα παρέμβει για την αντιμετώπιση.", + "step2RejectedTitle": "Απορρίφθηκε", + "step2RejectedDesc": "Το αίτημα απορρίφθηκε από τη διοίκηση.", + "step2CompletedTitle": "Υπό διοικητική εξέταση", + "step2CompletedDesc": "Το πρόβλημα επεξεργάστηκε επιτυχώς.", + "step3CompletedTitle": "Επιλύθηκε", + "step3CompletedDesc": "Το πρόβλημα επιλύθηκε επί τόπου από το τεχνικό προσωπικό.", + "general": "Γενικά", + "exterior": "Εξωτερικό" + }, + "auth": { + "title": "Σύνδεση", + "subtitle": "Εισαγάγετε τα διαπιστευτήριά σας για πρόσβαση στον λογαριασμό σας", + "email": "Email", + "password": "Κωδικός πρόσβασης", + "login": "Σύνδεση", + "emailRequired": "Το email είναι υποχρεωτικό.", + "emailInvalid": "Μη έγκυρη μορφή email.", + "passwordRequired": "Ο κωδικός πρόσβασης είναι υποχρεωτικός.", + "passwordTooShort": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.", + "invalidCredentials": "Λανθασμένο email ή κωδικός πρόσβασης." + }, + "onboarding": { + "exploreTitle": "Εξερευνήστε την Πανεπιστημιούπολη", + "exploreDesc": "Ανακαλύψτε τα κτίρια και τις εγκαταστάσεις απευθείας στον χάρτη.", + "continue": "Συνέχεια" + }, + "navbar": { + "unauthenticated": "Μη συνδεδεμένος", + "theme": "Θέμα", + "logout": "Αποσύνδεση", + "login": "Σύνδεση", + "openMenu": "Άνοιγμα μενού", + "closeMenu": "Κλείσιμο μενού", + "home": "Αρχική", + "announcements": "Ανακοινώσεις", + "news": "Νέα", + "events": "Εκδηλώσεις" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Εικονικός βοηθός", + "placeholder": "Κάντε μια ερώτηση...", + "noValidResponse": "Δεν ελήφθη έγκυρη απάντηση.", + "errorOccurred": "Παρουσιάστηκε σφάλμα. Δοκιμάστε ξανά.", + "assistantError": "Σφάλμα βοηθού.", + "howCanIHelp": "Πώς μπορώ να σας βοηθήσω σήμερα;", + "promptSuggestion": "Ρωτήστε με για εκδηλώσεις, κυλικείο, χάρτη ή αναφορές.", + "noResponse": "⚠️ Ο βοηθός δεν επέστρεψε απάντηση. Δοκιμάστε αργότερα.", + "newConversation": "Νέα συνομιλία", + "closeChat": "Κλείσιμο συνομιλίας", + "sendMessage": "Αποστολή μηνύματος", + "openAssistant": "Άνοιγμα βοηθού Ace", + "closeAssistant": "Κλείσιμο βοηθού Ace" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/es.json b/Frontend/Mobile/src/i18n/locales/es.json new file mode 100644 index 00000000..1ccd1b46 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/es.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Noticias", + "events": "Eventos", + "faculties": "Facultades", + "facilities": "Instalaciones", + "recent": "Recientes", + "noNews": "No se encontraron noticias.", + "noEvents": "No se encontraron eventos.", + "noFaculties": "No se encontraron facultades.", + "noFacilities": "No se encontraron instalaciones.", + "loadErrorNews": "No se pudieron cargar las noticias.", + "loadErrorEvents": "No se pudieron cargar los eventos.", + "loadErrorFaculties": "No se pudieron cargar las facultades.", + "loadErrorFacilities": "No se pudieron cargar las instalaciones.", + "emptyNews": "No hay noticias disponibles.", + "emptyEvents": "No hay eventos disponibles.", + "emptyFaculties": "No hay facultades disponibles.", + "emptyFacilities": "No hay instalaciones disponibles.", + "refreshError": "No se pudieron actualizar los datos. Por favor, comprueba tu conexión a internet." + }, + "category": { + "empty": "No hay elementos en esta categoría.", + "allFaculties": "Todas las Facultades", + "all": "Todas", + "refreshError": "No se pudieron actualizar los datos de esta categoría. Por favor, comprueba tu conexión a internet.", + "fallback": "Categoría" + }, + "detail": { + "eventInfo": "Información del evento", + "facilityInfo": "Información de la instalación", + "schedule": "Horario:", + "from": "Desde", + "until": "Hasta", + "aboutEvent": "Sobre el evento", + "aboutFaculty": "Sobre la facultad", + "aboutFacility": "Sobre la instalación", + "details": "Detalles", + "contact": "Contacto y Ubicación", + "address": "Dirección", + "phone": "Teléfono", + "website": "Sitio web", + "updated": "Actualizado:", + "relatedArticles": "Artículos similares", + "more": "Más", + "loadError": "No se pudieron cargar los detalles de esta página.", + "notFound": "Detalles no encontrados.", + "callFaculty": "Contacto Facultad", + "callFacility": "Contacto Instalación", + "callConfirm": "¿Deseas llamar al {{phone}}?", + "cancel": "Cancelar", + "call": "Llamar" + }, + "common": { + "unknownTitle": "Título desconocido", + "unknownContent": "Contenido desconocido", + "unknownDate": "Fecha desconocida", + "unknownLocation": "Ubicación desconocida", + "unknownAddress": "Dirección desconocida", + "unknownStartDate": "Fecha de inicio desconocida", + "unknownEndDate": "Fecha de fin desconocida", + "home": "Inicio", + "news": "Noticias", + "events": "Eventos", + "faculty": "Facultad", + "facility": "Instalación", + "category": "Categoría", + "article": "Artículo", + "updateError": "Error de actualización", + "selected": "(Seleccionado)", + "retry": "Reintentar", + "connectionError": "No se pudo conectar al servidor. Por favor, inténtalo de nuevo.", + "user": "Usuario", + "dashboard": "Panel", + "attachedFiles": "Archivos adjuntos", + "noItems": "No hay elementos en esta categoría.", + "loading": "Cargando...", + "errorTitle": "¡Ups! Algo salió mal...", + "viewAll": "Ver más", + "university": "Universidad", + "universityPlatform": "Tu plataforma universitaria" + }, + "days": { + "1": "Lunes", + "2": "Martes", + "3": "Miércoles", + "4": "Jueves", + "5": "Viernes", + "6": "Sábado", + "7": "Domingo" + }, + "language": { + "title": "Idioma de la app", + "header": "Idioma", + "select": "Seleccionar idioma", + "selected": "(Seleccionado)", + "current": "Idioma actual" + }, + "nav": { + "home": "Inicio", + "map": "Mapa", + "canteen": "Comedor", + "reports": "Quejas", + "more": "Más" + }, + "more": { + "title": "Más", + "visitGalati": "Visitar Galați", + "login": "Iniciar sesión", + "disconnect": "Cerrar sesión", + "settings": "Ajustes", + "profileTitle": "Tu perfil", + "profileLoggedIn": "Ya has iniciado sesión. ¿Deseas cerrar sesión?", + "cancel": "Cancelar", + "logout": "Cerrar sesión" + }, + "settings": { + "title": "Ajustes", + "appearanceLang": "Apariencia e Idioma", + "themeApp": "Tema de la app", + "currentTheme": "Tema actual:", + "currentLang": "Idioma actual:", + "supportInfo": "Ayuda e Info", + "visitWebsite": "Visitar sitio web UGAL", + "appSlogan": "Creado para los estudiantes de la Universidad \"Dunărea de Jos\" de Galați" + }, + "theme": { + "title": "Tema de la app", + "header": "Tema", + "select": "Seleccionar tema", + "system": "Sistema", + "light": "Claro", + "dark": "Oscuro", + "current": "Tema actual", + "switchToLight": "Cambiar a tema claro", + "switchToDark": "Cambiar a tema oscuro" + }, + "canteen": { + "title": "Comedor", + "today": "Hoy", + "empty": "No hay menú disponible para este día.", + "updateError": "No se pudo actualizar el menú del comedor. Por favor, comprueba tu conexión a internet." + }, + "map": { + "title": "Mapa", + "allLocations": "Todas las ubicaciones", + "facilities": "Instalaciones" + }, + "reports": { + "title": "Quejas", + "all": "Todas las quejas", + "mine": "Mis quejas", + "active": "Activas", + "rejected": "Rechazadas", + "completed": "Resueltas", + "newReport": "Nueva queja", + "updateError": "No se pudieron actualizar las quejas. Por favor, comprueba tu conexión a internet.", + "loginRequired": "Debes iniciar sesión", + "loginDesc": "Inicia sesión para enviar o ver tus quejas.", + "login": "Iniciar sesión", + "empty": "No hay quejas en esta sección", + "emptyDesc": "No hay registros en este momento.", + "unknownLocation": "Ubicación desconocida", + "location": "Ubicación", + "titleField": "Título", + "descDetailed": "Descripción detallada", + "addPhoto": "Añadir una foto", + "addPhotoBtn": "Añadir foto", + "submit": "Enviar queja", + "photoPermission": "Se requiere acceso a la galería para añadir una foto.", + "titleRequired": "El título es obligatorio.", + "descRequired": "La descripción es obligatoria.", + "photoRequired": "Es obligatorio añadir una foto.", + "submitError": "Se produjo un error al enviar la queja. Por favor, inténtalo de nuevo.", + "infoTitle": "Información de la queja", + "descSection": "Descripción del problema", + "progressTitle": "Historial de progreso", + "statusActive": "Activa", + "statusRejected": "Rechazada", + "statusCompleted": "Resuelta", + "missingTitle": "Título faltante", + "noDescription": "Sin descripción añadida.", + "notFound": "No se encontró la queja.", + "loadError": "Se produjo un error al cargar la queja.", + "loadErrorGeneral": "Se produjo un error al cargar las quejas.", + "step1Title": "Queja registrada", + "step1Desc": "La queja ha sido guardada en el sistema.", + "step2ActiveTitle": "En revisión", + "step2ActiveDesc": "Un administrador está evaluando los detalles del problema.", + "step3ActiveTitle": "Resolución completada", + "step3ActiveDesc": "El equipo intervendrá para solucionar la situación.", + "step2RejectedTitle": "Rechazada", + "step2RejectedDesc": "La solicitud fue rechazada por el equipo administrativo.", + "step2CompletedTitle": "En revisión administrativa", + "step2CompletedDesc": "El problema fue procesado con éxito.", + "step3CompletedTitle": "Resuelta", + "step3CompletedDesc": "El problema fue resuelto en el lugar por el personal técnico.", + "general": "General", + "exterior": "Exterior" + }, + "auth": { + "title": "Iniciar sesión", + "subtitle": "Introduce tus credenciales para acceder a tu cuenta", + "email": "Correo electrónico", + "password": "Contraseña", + "login": "Iniciar sesión", + "emailRequired": "El correo electrónico es obligatorio.", + "emailInvalid": "Formato de correo electrónico inválido.", + "passwordRequired": "La contraseña es obligatoria.", + "passwordTooShort": "La contraseña debe tener al menos 6 caracteres.", + "invalidCredentials": "Correo electrónico o contraseña incorrectos." + }, + "onboarding": { + "exploreTitle": "Explora el Campus", + "exploreDesc": "Descubre los edificios y las instalaciones del campus universitario directamente en el mapa.", + "continue": "Continuar" + }, + "navbar": { + "unauthenticated": "No autenticado", + "theme": "Tema", + "logout": "Cerrar sesión", + "login": "Iniciar sesión", + "openMenu": "Abrir menú", + "closeMenu": "Cerrar menú", + "home": "Inicio", + "announcements": "Anuncios", + "news": "Noticias", + "events": "Eventos" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Asistente virtual", + "placeholder": "Haz una pregunta...", + "noValidResponse": "No se recibió una respuesta válida.", + "errorOccurred": "Se produjo un error. Por favor, inténtalo de nuevo.", + "assistantError": "Error del asistente.", + "howCanIHelp": "¿En qué puedo ayudarte hoy?", + "promptSuggestion": "Pregúntame sobre eventos, el comedor, el mapa o quejas.", + "noResponse": "⚠️ El asistente no devolvió ninguna respuesta. Por favor, inténtalo más tarde.", + "newConversation": "Nueva conversación", + "closeChat": "Cerrar chat", + "sendMessage": "Enviar mensaje", + "openAssistant": "Abrir asistente Ace", + "closeAssistant": "Cerrar asistente Ace" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/fr.json b/Frontend/Mobile/src/i18n/locales/fr.json new file mode 100644 index 00000000..4495c616 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/fr.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Actualités", + "events": "Événements", + "faculties": "Facultés", + "facilities": "Installations", + "recent": "Récents", + "noNews": "Aucune actualité trouvée.", + "noEvents": "Aucun événement trouvé.", + "noFaculties": "Aucune faculté trouvée.", + "noFacilities": "Aucune installation trouvée.", + "loadErrorNews": "Impossible de charger les actualités.", + "loadErrorEvents": "Impossible de charger les événements.", + "loadErrorFaculties": "Impossible de charger les facultés.", + "loadErrorFacilities": "Impossible de charger les installations.", + "emptyNews": "Aucune actualité disponible.", + "emptyEvents": "Aucun événement disponible.", + "emptyFaculties": "Aucune faculté disponible.", + "emptyFacilities": "Aucune installation disponible.", + "refreshError": "Impossible d'actualiser les données. Veuillez vérifier votre connexion internet." + }, + "category": { + "empty": "Aucun élément dans cette catégorie.", + "allFaculties": "Toutes les Facultés", + "all": "Toutes", + "refreshError": "Impossible d'actualiser les données de cette catégorie. Veuillez vérifier votre connexion internet.", + "fallback": "Catégorie" + }, + "detail": { + "eventInfo": "Informations sur l'événement", + "facilityInfo": "Informations sur l'installation", + "schedule": "Horaire :", + "from": "Du", + "until": "Au", + "aboutEvent": "À propos de l'événement", + "aboutFaculty": "À propos de la faculté", + "aboutFacility": "À propos de l'installation", + "details": "Détails", + "contact": "Contact et Localisation", + "address": "Adresse", + "phone": "Téléphone", + "website": "Site web", + "updated": "Mis à jour :", + "relatedArticles": "Articles similaires", + "more": "Plus", + "loadError": "Impossible de charger les détails de cette page.", + "notFound": "Détails introuvables.", + "callFaculty": "Contact Faculté", + "callFacility": "Contact Installation", + "callConfirm": "Voulez-vous appeler le {{phone}} ?", + "cancel": "Annuler", + "call": "Appeler" + }, + "common": { + "unknownTitle": "Titre inconnu", + "unknownContent": "Contenu inconnu", + "unknownDate": "Date inconnue", + "unknownLocation": "Lieu inconnu", + "unknownAddress": "Adresse inconnue", + "unknownStartDate": "Date de début inconnue", + "unknownEndDate": "Date de fin inconnue", + "home": "Accueil", + "news": "Actualités", + "events": "Événements", + "faculty": "Faculté", + "facility": "Installation", + "category": "Catégorie", + "article": "Article", + "updateError": "Erreur de mise à jour", + "selected": "(Sélectionné)", + "retry": "Réessayer", + "connectionError": "Impossible de se connecter au serveur. Veuillez réessayer.", + "user": "Utilisateur", + "dashboard": "Tableau de bord", + "attachedFiles": "Fichiers joints", + "noItems": "Aucun élément dans cette catégorie.", + "loading": "Chargement...", + "errorTitle": "Oups ! Quelque chose s'est mal passé...", + "viewAll": "Voir plus", + "university": "Université", + "universityPlatform": "Votre plateforme universitaire" + }, + "days": { + "1": "Lundi", + "2": "Mardi", + "3": "Mercredi", + "4": "Jeudi", + "5": "Vendredi", + "6": "Samedi", + "7": "Dimanche" + }, + "language": { + "title": "Langue de l'app", + "header": "Langue", + "select": "Sélectionner la langue", + "selected": "(Sélectionné)", + "current": "Langue actuelle" + }, + "nav": { + "home": "Accueil", + "map": "Carte", + "canteen": "Cantine", + "reports": "Signalements", + "more": "Plus" + }, + "more": { + "title": "Plus", + "visitGalati": "Visiter Galați", + "login": "Connexion", + "disconnect": "Se déconnecter", + "settings": "Paramètres", + "profileTitle": "Votre profil", + "profileLoggedIn": "Vous êtes déjà connecté. Voulez-vous vous déconnecter ?", + "cancel": "Annuler", + "logout": "Déconnexion" + }, + "settings": { + "title": "Paramètres", + "appearanceLang": "Apparence et Langue", + "themeApp": "Thème de l'app", + "currentTheme": "Thème actuel :", + "currentLang": "Langue actuelle :", + "supportInfo": "Aide et Info", + "visitWebsite": "Visiter le site UGAL", + "appSlogan": "Créé pour les étudiants de l'Université « Dunărea de Jos » de Galați" + }, + "theme": { + "title": "Thème de l'app", + "header": "Thème", + "select": "Sélectionner le thème", + "system": "Système", + "light": "Clair", + "dark": "Sombre", + "current": "Thème actuel", + "switchToLight": "Passer au thème clair", + "switchToDark": "Passer au thème sombre" + }, + "canteen": { + "title": "Cantine", + "today": "Aujourd'hui", + "empty": "Aucun menu disponible pour ce jour.", + "updateError": "Impossible d'actualiser le menu de la cantine. Veuillez vérifier votre connexion internet." + }, + "map": { + "title": "Carte", + "allLocations": "Tous les lieux", + "facilities": "Installations" + }, + "reports": { + "title": "Signalements", + "all": "Tous les signalements", + "mine": "Mes signalements", + "active": "Actifs", + "rejected": "Rejetés", + "completed": "Résolus", + "newReport": "Nouveau signalement", + "updateError": "Impossible d'actualiser les signalements. Veuillez vérifier votre connexion internet.", + "loginRequired": "Vous devez être connecté", + "loginDesc": "Connectez-vous pour soumettre ou consulter vos signalements.", + "login": "Connexion", + "empty": "Aucun signalement dans cette section", + "emptyDesc": "Aucun enregistrement pour le moment.", + "unknownLocation": "Lieu inconnu", + "location": "Lieu", + "titleField": "Titre", + "descDetailed": "Description détaillée", + "addPhoto": "Ajouter une photo", + "addPhotoBtn": "Ajouter une photo", + "submit": "Envoyer le signalement", + "photoPermission": "L'accès à la galerie photo est requis pour ajouter une photo.", + "titleRequired": "Le titre est obligatoire.", + "descRequired": "La description est obligatoire.", + "photoRequired": "Une photo est obligatoire.", + "submitError": "Une erreur s'est produite lors de l'envoi du signalement. Veuillez réessayer.", + "infoTitle": "Informations sur le signalement", + "descSection": "Description du problème", + "progressTitle": "Historique de progression", + "statusActive": "Actif", + "statusRejected": "Rejeté", + "statusCompleted": "Résolu", + "missingTitle": "Titre manquant", + "noDescription": "Aucune description ajoutée.", + "notFound": "Signalement introuvable.", + "loadError": "Une erreur s'est produite lors du chargement du signalement.", + "loadErrorGeneral": "Une erreur s'est produite lors du chargement des signalements.", + "step1Title": "Signalement enregistré", + "step1Desc": "Le signalement a été sauvegardé dans le système.", + "step2ActiveTitle": "En cours d'analyse", + "step2ActiveDesc": "Un administrateur évalue les détails du problème.", + "step3ActiveTitle": "Résolution terminée", + "step3ActiveDesc": "L'équipe interviendra pour résoudre la situation.", + "step2RejectedTitle": "Rejeté", + "step2RejectedDesc": "La demande a été rejetée par l'équipe administrative.", + "step2CompletedTitle": "En révision administrative", + "step2CompletedDesc": "Le problème a été traité avec succès.", + "step3CompletedTitle": "Résolu", + "step3CompletedDesc": "Le problème a été résolu sur place par le personnel technique.", + "general": "Général", + "exterior": "Extérieur" + }, + "auth": { + "title": "Connexion", + "subtitle": "Entrez vos identifiants pour accéder à votre compte", + "email": "E-mail", + "password": "Mot de passe", + "login": "Connexion", + "emailRequired": "L'e-mail est obligatoire.", + "emailInvalid": "Format d'e-mail invalide.", + "passwordRequired": "Le mot de passe est obligatoire.", + "passwordTooShort": "Le mot de passe doit contenir au moins 6 caractères.", + "invalidCredentials": "E-mail ou mot de passe incorrect." + }, + "onboarding": { + "exploreTitle": "Explorer le Campus", + "exploreDesc": "Découvrez les bâtiments et les installations du campus universitaire directement sur la carte.", + "continue": "Continuer" + }, + "navbar": { + "unauthenticated": "Non connecté", + "theme": "Thème", + "logout": "Déconnexion", + "login": "Connexion", + "openMenu": "Ouvrir le menu", + "closeMenu": "Fermer le menu", + "home": "Accueil", + "announcements": "Annonces", + "news": "Actualités", + "events": "Événements" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Assistant virtuel", + "placeholder": "Posez une question...", + "noValidResponse": "Aucune réponse valide reçue.", + "errorOccurred": "Une erreur s'est produite. Veuillez réessayer.", + "assistantError": "Erreur de l'assistant.", + "howCanIHelp": "Comment puis-je vous aider aujourd'hui ?", + "promptSuggestion": "Posez-moi des questions sur les événements, la cantine, la carte ou les signalements.", + "noResponse": "⚠️ L'assistant n'a retourné aucune réponse. Veuillez réessayer plus tard.", + "newConversation": "Nouvelle conversation", + "closeChat": "Fermer le chat", + "sendMessage": "Envoyer le message", + "openAssistant": "Ouvrir l'assistant Ace", + "closeAssistant": "Fermer l'assistant Ace" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/hi.json b/Frontend/Mobile/src/i18n/locales/hi.json new file mode 100644 index 00000000..430fa647 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/hi.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "समाचार", + "events": "कार्यक्रम", + "faculties": "विभाग", + "facilities": "सुविधाएँ", + "recent": "हाल का", + "noNews": "कोई समाचार नहीं मिला।", + "noEvents": "कोई कार्यक्रम नहीं मिला।", + "noFaculties": "कोई विभाग नहीं मिला।", + "noFacilities": "कोई सुविधा नहीं मिली।", + "loadErrorNews": "समाचार लोड नहीं हो सका।", + "loadErrorEvents": "कार्यक्रम लोड नहीं हो सके।", + "loadErrorFaculties": "विभाग लोड नहीं हो सके।", + "loadErrorFacilities": "सुविधाएँ लोड नहीं हो सकीं।", + "emptyNews": "कोई समाचार उपलब्ध नहीं है।", + "emptyEvents": "कोई कार्यक्रम उपलब्ध नहीं है।", + "emptyFaculties": "कोई विभाग उपलब्ध नहीं है।", + "emptyFacilities": "कोई सुविधा उपलब्ध नहीं है।", + "refreshError": "डेटा रिफ्रेश नहीं हो सका। अपना इंटरनेट कनेक्शन जाँचें।" + }, + "category": { + "empty": "इस श्रेणी में कोई आइटम नहीं है।", + "allFaculties": "सभी विभाग", + "all": "सभी", + "refreshError": "इस श्रेणी का डेटा रिफ्रेश नहीं हो सका। अपना इंटरनेट कनेक्शन जाँचें।", + "fallback": "श्रेणी" + }, + "detail": { + "eventInfo": "कार्यक्रम की जानकारी", + "facilityInfo": "सुविधा की जानकारी", + "schedule": "समय-सारणी:", + "from": "से", + "until": "तक", + "aboutEvent": "कार्यक्रम के बारे में", + "aboutFaculty": "विभाग के बारे में", + "aboutFacility": "सुविधा के बारे में", + "details": "विवरण", + "contact": "संपर्क और स्थान", + "address": "पता", + "phone": "फ़ोन", + "website": "वेबसाइट", + "updated": "अपडेट किया:", + "relatedArticles": "संबंधित लेख", + "more": "और देखें", + "loadError": "पृष्ठ विवरण लोड नहीं हो सका।", + "notFound": "विवरण नहीं मिला।", + "callFaculty": "विभाग से संपर्क करें", + "callFacility": "सुविधा से संपर्क करें", + "callConfirm": "क्या आप {{phone}} पर कॉल करना चाहते हैं?", + "cancel": "रद्द करें", + "call": "कॉल करें" + }, + "common": { + "unknownTitle": "अज्ञात शीर्षक", + "unknownContent": "अज्ञात सामग्री", + "unknownDate": "अज्ञात तिथि", + "unknownLocation": "अज्ञात स्थान", + "unknownAddress": "अज्ञात पता", + "unknownStartDate": "अज्ञात प्रारंभ तिथि", + "unknownEndDate": "अज्ञात समाप्ति तिथि", + "home": "होम", + "news": "समाचार", + "events": "कार्यक्रम", + "faculty": "विभाग", + "facility": "सुविधा", + "category": "श्रेणी", + "article": "लेख", + "updateError": "अपडेट त्रुटि", + "selected": "(चयनित)", + "retry": "पुनः प्रयास करें", + "connectionError": "सर्वर से कनेक्ट नहीं हो सका। कृपया पुनः प्रयास करें।", + "user": "उपयोगकर्ता", + "dashboard": "डैशबोर्ड", + "attachedFiles": "संलग्न फ़ाइलें", + "noItems": "इस श्रेणी में कोई आइटम नहीं है।", + "loading": "लोड हो रहा है...", + "errorTitle": "अरे! कुछ गलत हो गया...", + "viewAll": "और देखें", + "university": "विश्वविद्यालय", + "universityPlatform": "आपका विश्वविद्यालय प्लेटफ़ॉर्म" + }, + "days": { + "1": "सोमवार", + "2": "मंगलवार", + "3": "बुधवार", + "4": "गुरुवार", + "5": "शुक्रवार", + "6": "शनिवार", + "7": "रविवार" + }, + "language": { + "title": "ऐप की भाषा", + "header": "भाषा", + "select": "भाषा चुनें", + "selected": "(चयनित)", + "current": "वर्तमान भाषा" + }, + "nav": { + "home": "होम", + "map": "नक्शा", + "canteen": "कैंटीन", + "reports": "शिकायतें", + "more": "और" + }, + "more": { + "title": "और", + "visitGalati": "Galați देखें", + "login": "लॉग इन", + "disconnect": "लॉग आउट", + "settings": "सेटिंग्स", + "profileTitle": "आपकी प्रोफ़ाइल", + "profileLoggedIn": "आप पहले से लॉग इन हैं। क्या आप लॉग आउट करना चाहते हैं?", + "cancel": "रद्द करें", + "logout": "लॉग आउट" + }, + "settings": { + "title": "सेटिंग्स", + "appearanceLang": "रूप और भाषा", + "themeApp": "ऐप थीम", + "currentTheme": "वर्तमान थीम:", + "currentLang": "वर्तमान भाषा:", + "supportInfo": "सहायता और जानकारी", + "visitWebsite": "UGAL वेबसाइट देखें", + "appSlogan": "Galați के «Dunărea de Jos» विश्वविद्यालय के छात्रों के लिए बनाया गया" + }, + "theme": { + "title": "ऐप थीम", + "header": "थीम", + "select": "थीम चुनें", + "system": "सिस्टम", + "light": "लाइट", + "dark": "डार्क", + "current": "वर्तमान थीम", + "switchToLight": "लाइट थीम पर स्विच करें", + "switchToDark": "डार्क थीम पर स्विच करें" + }, + "canteen": { + "title": "कैंटीन", + "today": "आज", + "empty": "आज के लिए कोई मेनू उपलब्ध नहीं है।", + "updateError": "कैंटीन मेनू रिफ्रेश नहीं हो सका। अपना इंटरनेट कनेक्शन जाँचें।" + }, + "map": { + "title": "नक्शा", + "allLocations": "सभी स्थान", + "facilities": "सुविधाएँ" + }, + "reports": { + "title": "शिकायतें", + "all": "सभी शिकायतें", + "mine": "मेरी शिकायतें", + "active": "सक्रिय", + "rejected": "अस्वीकृत", + "completed": "हल किया गया", + "newReport": "नई शिकायत", + "updateError": "शिकायतें रिफ्रेश नहीं हो सकीं। अपना इंटरनेट कनेक्शन जाँचें।", + "loginRequired": "लॉग इन आवश्यक है", + "loginDesc": "शिकायत दर्ज करने या देखने के लिए लॉग इन करें।", + "login": "लॉग इन", + "empty": "इस अनुभाग में कोई शिकायत नहीं है", + "emptyDesc": "अभी कोई रिकॉर्ड नहीं है।", + "unknownLocation": "अज्ञात स्थान", + "location": "स्थान", + "titleField": "शीर्षक", + "descDetailed": "विस्तृत विवरण", + "addPhoto": "फ़ोटो जोड़ें", + "addPhotoBtn": "फ़ोटो जोड़ें", + "submit": "शिकायत दर्ज करें", + "photoPermission": "फ़ोटो जोड़ने के लिए फ़ोटो लाइब्रेरी की अनुमति आवश्यक है।", + "titleRequired": "शीर्षक आवश्यक है।", + "descRequired": "विवरण आवश्यक है।", + "photoRequired": "फ़ोटो आवश्यक है।", + "submitError": "शिकायत दर्ज करने में त्रुटि हुई। कृपया पुनः प्रयास करें।", + "infoTitle": "शिकायत की जानकारी", + "descSection": "समस्या का विवरण", + "progressTitle": "प्रगति इतिहास", + "statusActive": "सक्रिय", + "statusRejected": "अस्वीकृत", + "statusCompleted": "हल किया गया", + "missingTitle": "शीर्षक नहीं है", + "noDescription": "कोई विवरण नहीं जोड़ा गया।", + "notFound": "शिकायत नहीं मिली।", + "loadError": "शिकायत लोड करने में त्रुटि हुई।", + "loadErrorGeneral": "शिकायतें लोड करने में त्रुटि हुई।", + "step1Title": "शिकायत दर्ज की गई", + "step1Desc": "शिकायत सिस्टम में सहेजी गई।", + "step2ActiveTitle": "समीक्षाधीन", + "step2ActiveDesc": "एक प्रबंधक समस्या के विवरण का मूल्यांकन कर रहा है।", + "step3ActiveTitle": "समाधान पूर्ण", + "step3ActiveDesc": "टीम स्थिति सुधारने के लिए हस्तक्षेप करेगी।", + "step2RejectedTitle": "अस्वीकृत", + "step2RejectedDesc": "अनुरोध प्रबंधन टीम द्वारा अस्वीकृत किया गया।", + "step2CompletedTitle": "प्रशासनिक समीक्षाधीन", + "step2CompletedDesc": "समस्या सफलतापूर्वक संसाधित की गई।", + "step3CompletedTitle": "हल किया गया", + "step3CompletedDesc": "तकनीकी टीम ने मौके पर समस्या हल की।", + "general": "सामान्य", + "exterior": "बाहरी" + }, + "auth": { + "title": "लॉग इन", + "subtitle": "अपने खाते तक पहुँचने के लिए अपनी जानकारी दर्ज करें", + "email": "ईमेल", + "password": "पासवर्ड", + "login": "लॉग इन", + "emailRequired": "ईमेल आवश्यक है।", + "emailInvalid": "अमान्य ईमेल प्रारूप।", + "passwordRequired": "पासवर्ड आवश्यक है।", + "passwordTooShort": "पासवर्ड कम से कम 6 अक्षरों का होना चाहिए।", + "invalidCredentials": "ईमेल या पासवर्ड गलत है।" + }, + "onboarding": { + "exploreTitle": "कैंपस खोजें", + "exploreDesc": "नक्शे पर विश्वविद्यालय परिसर की इमारतें और सुविधाएँ देखें।", + "continue": "जारी रखें" + }, + "navbar": { + "unauthenticated": "अप्रमाणित", + "theme": "थीम", + "logout": "लॉग आउट", + "login": "लॉग इन", + "openMenu": "मेनू खोलें", + "closeMenu": "मेनू बंद करें", + "home": "होम", + "announcements": "घोषणाएँ", + "news": "समाचार", + "events": "कार्यक्रम" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "वर्चुअल सहायक", + "placeholder": "प्रश्न पूछें...", + "noValidResponse": "कोई मान्य उत्तर नहीं मिला।", + "errorOccurred": "त्रुटि हुई। कृपया पुनः प्रयास करें।", + "assistantError": "सहायक त्रुटि।", + "howCanIHelp": "आज मैं आपकी कैसे मदद कर सकता हूँ?", + "promptSuggestion": "कार्यक्रम, कैंटीन, नक्शा या शिकायतों के बारे में पूछें।", + "noResponse": "⚠️ सहायक ने कोई उत्तर नहीं दिया। कृपया बाद में पुनः प्रयास करें।", + "newConversation": "नई बातचीत", + "closeChat": "चैट बंद करें", + "sendMessage": "संदेश भेजें", + "openAssistant": "Ace सहायक खोलें", + "closeAssistant": "Ace सहायक बंद करें" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/it.json b/Frontend/Mobile/src/i18n/locales/it.json new file mode 100644 index 00000000..6071b5e4 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/it.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Notizie", + "events": "Eventi", + "faculties": "Facoltà", + "facilities": "Strutture", + "recent": "Recenti", + "noNews": "Nessuna notizia trovata.", + "noEvents": "Nessun evento trovato.", + "noFaculties": "Nessuna facoltà trovata.", + "noFacilities": "Nessuna struttura trovata.", + "loadErrorNews": "Impossibile caricare le notizie.", + "loadErrorEvents": "Impossibile caricare gli eventi.", + "loadErrorFaculties": "Impossibile caricare le facoltà.", + "loadErrorFacilities": "Impossibile caricare le strutture.", + "emptyNews": "Nessuna notizia disponibile.", + "emptyEvents": "Nessun evento disponibile.", + "emptyFaculties": "Nessuna facoltà disponibile.", + "emptyFacilities": "Nessuna struttura disponibile.", + "refreshError": "Impossibile aggiornare i dati. Controlla la tua connessione internet." + }, + "category": { + "empty": "Nessun elemento in questa categoria.", + "allFaculties": "Tutte le Facoltà", + "all": "Tutte", + "refreshError": "Impossibile aggiornare i dati di questa categoria. Controlla la tua connessione internet.", + "fallback": "Categoria" + }, + "detail": { + "eventInfo": "Informazioni sull'evento", + "facilityInfo": "Informazioni sulla struttura", + "schedule": "Orario:", + "from": "Dal", + "until": "Al", + "aboutEvent": "Sull'evento", + "aboutFaculty": "Sulla facoltà", + "aboutFacility": "Sulla struttura", + "details": "Dettagli", + "contact": "Contatto e Posizione", + "address": "Indirizzo", + "phone": "Telefono", + "website": "Sito web", + "updated": "Aggiornato:", + "relatedArticles": "Articoli simili", + "more": "Altro", + "loadError": "Impossibile caricare i dettagli di questa pagina.", + "notFound": "Dettagli non trovati.", + "callFaculty": "Contatto Facoltà", + "callFacility": "Contatto Struttura", + "callConfirm": "Vuoi chiamare il {{phone}}?", + "cancel": "Annulla", + "call": "Chiama" + }, + "common": { + "unknownTitle": "Titolo sconosciuto", + "unknownContent": "Contenuto sconosciuto", + "unknownDate": "Data sconosciuta", + "unknownLocation": "Posizione sconosciuta", + "unknownAddress": "Indirizzo sconosciuto", + "unknownStartDate": "Data di inizio sconosciuta", + "unknownEndDate": "Data di fine sconosciuta", + "home": "Home", + "news": "Notizie", + "events": "Eventi", + "faculty": "Facoltà", + "facility": "Struttura", + "category": "Categoria", + "article": "Articolo", + "updateError": "Errore di aggiornamento", + "selected": "(Selezionato)", + "retry": "Riprova", + "connectionError": "Impossibile connettersi al server. Riprova.", + "user": "Utente", + "dashboard": "Dashboard", + "attachedFiles": "File allegati", + "noItems": "Nessun elemento in questa categoria.", + "loading": "Caricamento...", + "errorTitle": "Ops! Qualcosa è andato storto...", + "viewAll": "Vedi altro", + "university": "Università", + "universityPlatform": "La tua piattaforma universitaria" + }, + "days": { + "1": "Lunedì", + "2": "Martedì", + "3": "Mercoledì", + "4": "Giovedì", + "5": "Venerdì", + "6": "Sabato", + "7": "Domenica" + }, + "language": { + "title": "Lingua dell'app", + "header": "Lingua", + "select": "Seleziona lingua", + "selected": "(Selezionato)", + "current": "Lingua attuale" + }, + "nav": { + "home": "Home", + "map": "Mappa", + "canteen": "Mensa", + "reports": "Segnalazioni", + "more": "Altro" + }, + "more": { + "title": "Altro", + "visitGalati": "Visita Galați", + "login": "Accedi", + "disconnect": "Disconnetti", + "settings": "Impostazioni", + "profileTitle": "Il tuo profilo", + "profileLoggedIn": "Hai già effettuato l'accesso. Vuoi disconnetterti?", + "cancel": "Annulla", + "logout": "Disconnetti" + }, + "settings": { + "title": "Impostazioni", + "appearanceLang": "Aspetto e Lingua", + "themeApp": "Tema dell'app", + "currentTheme": "Tema attuale:", + "currentLang": "Lingua attuale:", + "supportInfo": "Aiuto e Info", + "visitWebsite": "Visita il sito UGAL", + "appSlogan": "Creato per gli studenti dell'Università «Dunărea de Jos» di Galați" + }, + "theme": { + "title": "Tema dell'app", + "header": "Tema", + "select": "Seleziona tema", + "system": "Sistema", + "light": "Chiaro", + "dark": "Scuro", + "current": "Tema attuale", + "switchToLight": "Passa al tema chiaro", + "switchToDark": "Passa al tema scuro" + }, + "canteen": { + "title": "Mensa", + "today": "Oggi", + "empty": "Nessun menu disponibile per questo giorno.", + "updateError": "Impossibile aggiornare il menu della mensa. Controlla la tua connessione internet." + }, + "map": { + "title": "Mappa", + "allLocations": "Tutte le posizioni", + "facilities": "Strutture" + }, + "reports": { + "title": "Segnalazioni", + "all": "Tutte le segnalazioni", + "mine": "Le mie segnalazioni", + "active": "Attive", + "rejected": "Rifiutate", + "completed": "Risolte", + "newReport": "Nuova segnalazione", + "updateError": "Impossibile aggiornare le segnalazioni. Controlla la tua connessione internet.", + "loginRequired": "Devi essere connesso", + "loginDesc": "Accedi per inviare o visualizzare le tue segnalazioni.", + "login": "Accedi", + "empty": "Nessuna segnalazione in questa sezione", + "emptyDesc": "Nessun record al momento.", + "unknownLocation": "Posizione sconosciuta", + "location": "Posizione", + "titleField": "Titolo", + "descDetailed": "Descrizione dettagliata", + "addPhoto": "Aggiungi una foto", + "addPhotoBtn": "Aggiungi foto", + "submit": "Invia segnalazione", + "photoPermission": "L'accesso alla galleria fotografica è necessario per aggiungere una foto.", + "titleRequired": "Il titolo è obbligatorio.", + "descRequired": "La descrizione è obbligatoria.", + "photoRequired": "Una foto è obbligatoria.", + "submitError": "Si è verificato un errore durante l'invio della segnalazione. Riprova.", + "infoTitle": "Informazioni sulla segnalazione", + "descSection": "Descrizione del problema", + "progressTitle": "Cronologia progressi", + "statusActive": "Attiva", + "statusRejected": "Rifiutata", + "statusCompleted": "Risolta", + "missingTitle": "Titolo mancante", + "noDescription": "Nessuna descrizione aggiunta.", + "notFound": "Segnalazione non trovata.", + "loadError": "Si è verificato un errore durante il caricamento della segnalazione.", + "loadErrorGeneral": "Si è verificato un errore durante il caricamento delle segnalazioni.", + "step1Title": "Segnalazione registrata", + "step1Desc": "La segnalazione è stata salvata nel sistema.", + "step2ActiveTitle": "In fase di analisi", + "step2ActiveDesc": "Un amministratore sta valutando i dettagli del problema.", + "step3ActiveTitle": "Risoluzione completata", + "step3ActiveDesc": "Il team interverrà per risolvere la situazione.", + "step2RejectedTitle": "Rifiutata", + "step2RejectedDesc": "La richiesta è stata rifiutata dal team amministrativo.", + "step2CompletedTitle": "In revisione amministrativa", + "step2CompletedDesc": "Il problema è stato elaborato con successo.", + "step3CompletedTitle": "Risolta", + "step3CompletedDesc": "Il problema è stato risolto in loco dal personale tecnico.", + "general": "Generale", + "exterior": "Esterno" + }, + "auth": { + "title": "Accedi", + "subtitle": "Inserisci le tue credenziali per accedere al tuo account", + "email": "Email", + "password": "Password", + "login": "Accedi", + "emailRequired": "L'email è obbligatoria.", + "emailInvalid": "Formato email non valido.", + "passwordRequired": "La password è obbligatoria.", + "passwordTooShort": "La password deve contenere almeno 6 caratteri.", + "invalidCredentials": "Email o password errati." + }, + "onboarding": { + "exploreTitle": "Esplora il Campus", + "exploreDesc": "Scopri gli edifici e le strutture del campus universitario direttamente sulla mappa.", + "continue": "Continua" + }, + "navbar": { + "unauthenticated": "Non autenticato", + "theme": "Tema", + "logout": "Disconnetti", + "login": "Accedi", + "openMenu": "Apri menu", + "closeMenu": "Chiudi menu", + "home": "Home", + "announcements": "Annunci", + "news": "Notizie", + "events": "Eventi" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Assistente virtuale", + "placeholder": "Fai una domanda...", + "noValidResponse": "Nessuna risposta valida ricevuta.", + "errorOccurred": "Si è verificato un errore. Riprova.", + "assistantError": "Errore dell'assistente.", + "howCanIHelp": "Come posso aiutarti oggi?", + "promptSuggestion": "Chiedimi degli eventi, della mensa, della mappa o delle segnalazioni.", + "noResponse": "⚠️ L'assistente non ha restituito alcuna risposta. Riprova più tardi.", + "newConversation": "Nuova conversazione", + "closeChat": "Chiudi chat", + "sendMessage": "Invia messaggio", + "openAssistant": "Apri assistente Ace", + "closeAssistant": "Chiudi assistente Ace" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/ja.json b/Frontend/Mobile/src/i18n/locales/ja.json new file mode 100644 index 00000000..274488ec --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/ja.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "ニュース", + "events": "イベント", + "faculties": "学部", + "facilities": "施設", + "recent": "最新", + "noNews": "ニュースが見つかりません。", + "noEvents": "イベントが見つかりません。", + "noFaculties": "学部が見つかりません。", + "noFacilities": "施設が見つかりません。", + "loadErrorNews": "ニュースを読み込めませんでした。", + "loadErrorEvents": "イベントを読み込めませんでした。", + "loadErrorFaculties": "学部を読み込めませんでした。", + "loadErrorFacilities": "施設を読み込めませんでした。", + "emptyNews": "利用可能なニュースはありません。", + "emptyEvents": "利用可能なイベントはありません。", + "emptyFaculties": "利用可能な学部はありません。", + "emptyFacilities": "利用可能な施設はありません。", + "refreshError": "データを更新できませんでした。インターネット接続を確認してください。" + }, + "category": { + "empty": "このカテゴリにはアイテムがありません。", + "allFaculties": "全学部", + "all": "すべて", + "refreshError": "このカテゴリのデータを更新できませんでした。インターネット接続を確認してください。", + "fallback": "カテゴリ" + }, + "detail": { + "eventInfo": "イベント情報", + "facilityInfo": "施設情報", + "schedule": "スケジュール:", + "from": "から", + "until": "まで", + "aboutEvent": "イベントについて", + "aboutFaculty": "学部について", + "aboutFacility": "施設について", + "details": "詳細", + "contact": "連絡先と場所", + "address": "住所", + "phone": "電話", + "website": "ウェブサイト", + "updated": "更新済み:", + "relatedArticles": "関連記事", + "more": "もっと見る", + "loadError": "ページの詳細を読み込めませんでした。", + "notFound": "詳細が見つかりません。", + "callFaculty": "学部に連絡", + "callFacility": "施設に連絡", + "callConfirm": "{{phone}} に電話しますか?", + "cancel": "キャンセル", + "call": "電話する" + }, + "common": { + "unknownTitle": "不明なタイトル", + "unknownContent": "不明なコンテンツ", + "unknownDate": "不明な日付", + "unknownLocation": "不明な場所", + "unknownAddress": "不明な住所", + "unknownStartDate": "不明な開始日", + "unknownEndDate": "不明な終了日", + "home": "ホーム", + "news": "ニュース", + "events": "イベント", + "faculty": "学部", + "facility": "施設", + "category": "カテゴリ", + "article": "記事", + "updateError": "更新エラー", + "selected": "(選択済み)", + "retry": "再試行", + "connectionError": "サーバーに接続できませんでした。もう一度お試しください。", + "user": "ユーザー", + "dashboard": "ダッシュボード", + "attachedFiles": "添付ファイル", + "noItems": "このカテゴリにはアイテムがありません。", + "loading": "読み込み中...", + "errorTitle": "おっと!問題が発生しました...", + "viewAll": "もっと見る", + "university": "大学", + "universityPlatform": "あなたの大学プラットフォーム" + }, + "days": { + "1": "月曜日", + "2": "火曜日", + "3": "水曜日", + "4": "木曜日", + "5": "金曜日", + "6": "土曜日", + "7": "日曜日" + }, + "language": { + "title": "アプリの言語", + "header": "言語", + "select": "言語を選択", + "selected": "(選択済み)", + "current": "現在の言語" + }, + "nav": { + "home": "ホーム", + "map": "マップ", + "canteen": "食堂", + "reports": "苦情", + "more": "その他" + }, + "more": { + "title": "その他", + "visitGalati": "Galați を訪問", + "login": "ログイン", + "disconnect": "ログアウト", + "settings": "設定", + "profileTitle": "あなたのプロフィール", + "profileLoggedIn": "すでにログインしています。ログアウトしますか?", + "cancel": "キャンセル", + "logout": "ログアウト" + }, + "settings": { + "title": "設定", + "appearanceLang": "外観と言語", + "themeApp": "アプリのテーマ", + "currentTheme": "現在のテーマ:", + "currentLang": "現在の言語:", + "supportInfo": "ヘルプと情報", + "visitWebsite": "UGAL ウェブサイトを訪問", + "appSlogan": "Galați の「Dunărea de Jos」大学の学生のために作られました" + }, + "theme": { + "title": "アプリのテーマ", + "header": "テーマ", + "select": "テーマを選択", + "system": "システム", + "light": "ライト", + "dark": "ダーク", + "current": "現在のテーマ", + "switchToLight": "ライトテーマに切り替え", + "switchToDark": "ダークテーマに切り替え" + }, + "canteen": { + "title": "食堂", + "today": "今日", + "empty": "この日のメニューはありません。", + "updateError": "食堂のメニューを更新できませんでした。インターネット接続を確認してください。" + }, + "map": { + "title": "マップ", + "allLocations": "すべての場所", + "facilities": "施設" + }, + "reports": { + "title": "苦情", + "all": "すべての苦情", + "mine": "自分の苦情", + "active": "アクティブ", + "rejected": "却下済み", + "completed": "解決済み", + "newReport": "新しい苦情", + "updateError": "苦情を更新できませんでした。インターネット接続を確認してください。", + "loginRequired": "ログインが必要です", + "loginDesc": "苦情を送信または確認するにはログインしてください。", + "login": "ログイン", + "empty": "このセクションに苦情はありません", + "emptyDesc": "現在、記録はありません。", + "unknownLocation": "不明な場所", + "location": "場所", + "titleField": "タイトル", + "descDetailed": "詳細な説明", + "addPhoto": "写真を追加", + "addPhotoBtn": "写真を追加", + "submit": "苦情を送信", + "photoPermission": "写真を追加するにはフォトライブラリへのアクセスが必要です。", + "titleRequired": "タイトルは必須です。", + "descRequired": "説明は必須です。", + "photoRequired": "写真は必須です。", + "submitError": "苦情の送信中にエラーが発生しました。もう一度お試しください。", + "infoTitle": "苦情情報", + "descSection": "問題の説明", + "progressTitle": "進捗履歴", + "statusActive": "アクティブ", + "statusRejected": "却下済み", + "statusCompleted": "解決済み", + "missingTitle": "タイトルがありません", + "noDescription": "説明が追加されていません。", + "notFound": "苦情が見つかりません。", + "loadError": "苦情の読み込み中にエラーが発生しました。", + "loadErrorGeneral": "苦情リストの読み込み中にエラーが発生しました。", + "step1Title": "苦情が登録されました", + "step1Desc": "苦情がシステムに保存されました。", + "step2ActiveTitle": "審査中", + "step2ActiveDesc": "管理者が問題の詳細を評価しています。", + "step3ActiveTitle": "解決完了", + "step3ActiveDesc": "チームが状況を改善するために介入します。", + "step2RejectedTitle": "却下されました", + "step2RejectedDesc": "リクエストは管理チームによって却下されました。", + "step2CompletedTitle": "行政審査中", + "step2CompletedDesc": "問題は正常に処理されました。", + "step3CompletedTitle": "解決済み", + "step3CompletedDesc": "技術スタッフが現場で問題を解決しました。", + "general": "一般", + "exterior": "外部" + }, + "auth": { + "title": "ログイン", + "subtitle": "アカウントにアクセスするための認証情報を入力してください", + "email": "メールアドレス", + "password": "パスワード", + "login": "ログイン", + "emailRequired": "メールアドレスは必須です。", + "emailInvalid": "無効なメールアドレス形式です。", + "passwordRequired": "パスワードは必須です。", + "passwordTooShort": "パスワードは6文字以上である必要があります。", + "invalidCredentials": "メールアドレスまたはパスワードが間違っています。" + }, + "onboarding": { + "exploreTitle": "キャンパスを探索", + "exploreDesc": "マップで大学キャンパスの建物と施設を直接発見してください。", + "continue": "続ける" + }, + "navbar": { + "unauthenticated": "未認証", + "theme": "テーマ", + "logout": "ログアウト", + "login": "ログイン", + "openMenu": "メニューを開く", + "closeMenu": "メニューを閉じる", + "home": "ホーム", + "announcements": "お知らせ", + "news": "ニュース", + "events": "イベント" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "バーチャルアシスタント", + "placeholder": "質問を入力...", + "noValidResponse": "有効な応答を受信できませんでした。", + "errorOccurred": "エラーが発生しました。もう一度お試しください。", + "assistantError": "アシスタントエラー。", + "howCanIHelp": "今日はどのようなお手伝いができますか?", + "promptSuggestion": "イベント、食堂、マップ、または苦情についてお聞きください。", + "noResponse": "⚠️ アシスタントが応答を返しませんでした。後でもう一度お試しください。", + "newConversation": "新しい会話", + "closeChat": "チャットを閉じる", + "sendMessage": "メッセージを送信", + "openAssistant": "Ace アシスタントを開く", + "closeAssistant": "Ace アシスタントを閉じる" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/ko.json b/Frontend/Mobile/src/i18n/locales/ko.json new file mode 100644 index 00000000..7099e444 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/ko.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "뉴스", + "events": "행사", + "faculties": "학부", + "facilities": "시설", + "recent": "최신", + "noNews": "뉴스를 찾을 수 없습니다.", + "noEvents": "행사를 찾을 수 없습니다.", + "noFaculties": "학부를 찾을 수 없습니다.", + "noFacilities": "시설을 찾을 수 없습니다.", + "loadErrorNews": "뉴스를 불러올 수 없습니다.", + "loadErrorEvents": "행사를 불러올 수 없습니다.", + "loadErrorFaculties": "학부를 불러올 수 없습니다.", + "loadErrorFacilities": "시설을 불러올 수 없습니다.", + "emptyNews": "이용 가능한 뉴스가 없습니다.", + "emptyEvents": "이용 가능한 행사가 없습니다.", + "emptyFaculties": "이용 가능한 학부가 없습니다.", + "emptyFacilities": "이용 가능한 시설이 없습니다.", + "refreshError": "데이터를 새로고침할 수 없습니다. 인터넷 연결을 확인해 주세요." + }, + "category": { + "empty": "이 카테고리에 항목이 없습니다.", + "allFaculties": "모든 학부", + "all": "전체", + "refreshError": "이 카테고리의 데이터를 새로고침할 수 없습니다. 인터넷 연결을 확인해 주세요.", + "fallback": "카테고리" + }, + "detail": { + "eventInfo": "행사 정보", + "facilityInfo": "시설 정보", + "schedule": "일정:", + "from": "시작", + "until": "종료", + "aboutEvent": "행사 소개", + "aboutFaculty": "학부 소개", + "aboutFacility": "시설 소개", + "details": "세부 정보", + "contact": "연락처 및 위치", + "address": "주소", + "phone": "전화", + "website": "웹사이트", + "updated": "업데이트됨:", + "relatedArticles": "관련 기사", + "more": "더 보기", + "loadError": "페이지 세부 정보를 불러올 수 없습니다.", + "notFound": "세부 정보를 찾을 수 없습니다.", + "callFaculty": "학부 연락처", + "callFacility": "시설 연락처", + "callConfirm": "{{phone}}에 전화하시겠습니까?", + "cancel": "취소", + "call": "전화" + }, + "common": { + "unknownTitle": "알 수 없는 제목", + "unknownContent": "알 수 없는 내용", + "unknownDate": "알 수 없는 날짜", + "unknownLocation": "알 수 없는 위치", + "unknownAddress": "알 수 없는 주소", + "unknownStartDate": "알 수 없는 시작 날짜", + "unknownEndDate": "알 수 없는 종료 날짜", + "home": "홈", + "news": "뉴스", + "events": "행사", + "faculty": "학부", + "facility": "시설", + "category": "카테고리", + "article": "기사", + "updateError": "업데이트 오류", + "selected": "(선택됨)", + "retry": "다시 시도", + "connectionError": "서버에 연결할 수 없습니다. 다시 시도해 주세요.", + "user": "사용자", + "dashboard": "대시보드", + "attachedFiles": "첨부 파일", + "noItems": "이 카테고리에 항목이 없습니다.", + "loading": "로딩 중...", + "errorTitle": "이런! 문제가 발생했습니다...", + "viewAll": "더 보기", + "university": "대학교", + "universityPlatform": "당신의 대학 플랫폼" + }, + "days": { + "1": "월요일", + "2": "화요일", + "3": "수요일", + "4": "목요일", + "5": "금요일", + "6": "토요일", + "7": "일요일" + }, + "language": { + "title": "앱 언어", + "header": "언어", + "select": "언어 선택", + "selected": "(선택됨)", + "current": "현재 언어" + }, + "nav": { + "home": "홈", + "map": "지도", + "canteen": "식당", + "reports": "민원", + "more": "더 보기" + }, + "more": { + "title": "더 보기", + "visitGalati": "Galați 방문", + "login": "로그인", + "disconnect": "로그아웃", + "settings": "설정", + "profileTitle": "내 프로필", + "profileLoggedIn": "이미 로그인되어 있습니다. 로그아웃하시겠습니까?", + "cancel": "취소", + "logout": "로그아웃" + }, + "settings": { + "title": "설정", + "appearanceLang": "외관 및 언어", + "themeApp": "앱 테마", + "currentTheme": "현재 테마:", + "currentLang": "현재 언어:", + "supportInfo": "도움말 및 정보", + "visitWebsite": "UGAL 웹사이트 방문", + "appSlogan": "Galați «Dunărea de Jos» 대학교 학생들을 위해 만들어졌습니다" + }, + "theme": { + "title": "앱 테마", + "header": "테마", + "select": "테마 선택", + "system": "시스템", + "light": "라이트", + "dark": "다크", + "current": "현재 테마", + "switchToLight": "라이트 테마로 전환", + "switchToDark": "다크 테마로 전환" + }, + "canteen": { + "title": "식당", + "today": "오늘", + "empty": "오늘 사용 가능한 메뉴가 없습니다.", + "updateError": "식당 메뉴를 새로고침할 수 없습니다. 인터넷 연결을 확인해 주세요." + }, + "map": { + "title": "지도", + "allLocations": "모든 위치", + "facilities": "시설" + }, + "reports": { + "title": "민원", + "all": "모든 민원", + "mine": "내 민원", + "active": "진행 중", + "rejected": "거부됨", + "completed": "해결됨", + "newReport": "새 민원", + "updateError": "민원을 새로고침할 수 없습니다. 인터넷 연결을 확인해 주세요.", + "loginRequired": "로그인이 필요합니다", + "loginDesc": "민원을 제출하거나 확인하려면 로그인하세요.", + "login": "로그인", + "empty": "이 섹션에 민원이 없습니다", + "emptyDesc": "현재 기록이 없습니다.", + "unknownLocation": "알 수 없는 위치", + "location": "위치", + "titleField": "제목", + "descDetailed": "상세 설명", + "addPhoto": "사진 추가", + "addPhotoBtn": "사진 추가", + "submit": "민원 제출", + "photoPermission": "사진을 추가하려면 사진 라이브러리 접근 권한이 필요합니다.", + "titleRequired": "제목은 필수입니다.", + "descRequired": "설명은 필수입니다.", + "photoRequired": "사진은 필수입니다.", + "submitError": "민원 제출 중 오류가 발생했습니다. 다시 시도해 주세요.", + "infoTitle": "민원 정보", + "descSection": "문제 설명", + "progressTitle": "진행 내역", + "statusActive": "진행 중", + "statusRejected": "거부됨", + "statusCompleted": "해결됨", + "missingTitle": "제목 없음", + "noDescription": "설명이 추가되지 않았습니다.", + "notFound": "민원을 찾을 수 없습니다.", + "loadError": "민원을 불러오는 중 오류가 발생했습니다.", + "loadErrorGeneral": "민원 목록을 불러오는 중 오류가 발생했습니다.", + "step1Title": "민원이 등록되었습니다", + "step1Desc": "민원이 시스템에 저장되었습니다.", + "step2ActiveTitle": "검토 중", + "step2ActiveDesc": "관리자가 문제 세부 정보를 평가하고 있습니다.", + "step3ActiveTitle": "해결 완료", + "step3ActiveDesc": "팀이 상황을 해결하기 위해 개입할 것입니다.", + "step2RejectedTitle": "거부됨", + "step2RejectedDesc": "요청이 관리팀에 의해 거부되었습니다.", + "step2CompletedTitle": "행정 검토 중", + "step2CompletedDesc": "문제가 성공적으로 처리되었습니다.", + "step3CompletedTitle": "해결됨", + "step3CompletedDesc": "기술 직원이 현장에서 문제를 해결했습니다.", + "general": "일반", + "exterior": "외부" + }, + "auth": { + "title": "로그인", + "subtitle": "계정에 접근하려면 자격 증명을 입력하세요", + "email": "이메일", + "password": "비밀번호", + "login": "로그인", + "emailRequired": "이메일은 필수입니다.", + "emailInvalid": "유효하지 않은 이메일 형식입니다.", + "passwordRequired": "비밀번호는 필수입니다.", + "passwordTooShort": "비밀번호는 최소 6자 이상이어야 합니다.", + "invalidCredentials": "이메일 또는 비밀번호가 올바르지 않습니다." + }, + "onboarding": { + "exploreTitle": "캠퍼스 탐색", + "exploreDesc": "지도에서 대학 캠퍼스의 건물과 시설을 직접 발견하세요.", + "continue": "계속" + }, + "navbar": { + "unauthenticated": "인증되지 않음", + "theme": "테마", + "logout": "로그아웃", + "login": "로그인", + "openMenu": "메뉴 열기", + "closeMenu": "메뉴 닫기", + "home": "홈", + "announcements": "공지사항", + "news": "뉴스", + "events": "행사" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "가상 도우미", + "placeholder": "질문하기...", + "noValidResponse": "유효한 응답을 받지 못했습니다.", + "errorOccurred": "오류가 발생했습니다. 다시 시도해 주세요.", + "assistantError": "도우미 오류.", + "howCanIHelp": "오늘 어떻게 도와드릴까요?", + "promptSuggestion": "행사, 식당, 지도 또는 민원에 대해 물어보세요.", + "noResponse": "⚠️ 도우미가 응답을 반환하지 않았습니다. 나중에 다시 시도해 주세요.", + "newConversation": "새 대화", + "closeChat": "채팅 닫기", + "sendMessage": "메시지 보내기", + "openAssistant": "Ace 도우미 열기", + "closeAssistant": "Ace 도우미 닫기" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/ru.json b/Frontend/Mobile/src/i18n/locales/ru.json new file mode 100644 index 00000000..214329c1 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/ru.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Новости", + "events": "События", + "faculties": "Факультеты", + "facilities": "Объекты", + "recent": "Последние", + "noNews": "Новости не найдены.", + "noEvents": "События не найдены.", + "noFaculties": "Факультеты не найдены.", + "noFacilities": "Объекты не найдены.", + "loadErrorNews": "Не удалось загрузить новости.", + "loadErrorEvents": "Не удалось загрузить события.", + "loadErrorFaculties": "Не удалось загрузить факультеты.", + "loadErrorFacilities": "Не удалось загрузить объекты.", + "emptyNews": "Нет доступных новостей.", + "emptyEvents": "Нет доступных событий.", + "emptyFaculties": "Нет доступных факультетов.", + "emptyFacilities": "Нет доступных объектов.", + "refreshError": "Не удалось обновить данные. Проверьте подключение к интернету." + }, + "category": { + "empty": "В этой категории нет элементов.", + "allFaculties": "Все Факультеты", + "all": "Все", + "refreshError": "Не удалось обновить данные для этой категории. Проверьте подключение к интернету.", + "fallback": "Категория" + }, + "detail": { + "eventInfo": "Информация о событии", + "facilityInfo": "Информация об объекте", + "schedule": "Расписание:", + "from": "С", + "until": "До", + "aboutEvent": "О событии", + "aboutFaculty": "О факультете", + "aboutFacility": "Об объекте", + "details": "Детали", + "contact": "Контакт и Местоположение", + "address": "Адрес", + "phone": "Телефон", + "website": "Сайт", + "updated": "Обновлено:", + "relatedArticles": "Похожие статьи", + "more": "Больше", + "loadError": "Не удалось загрузить детали страницы.", + "notFound": "Детали не найдены.", + "callFaculty": "Контакт Факультета", + "callFacility": "Контакт Объекта", + "callConfirm": "Хотите позвонить на {{phone}}?", + "cancel": "Отмена", + "call": "Позвонить" + }, + "common": { + "unknownTitle": "Неизвестное название", + "unknownContent": "Неизвестное содержимое", + "unknownDate": "Неизвестная дата", + "unknownLocation": "Неизвестное место", + "unknownAddress": "Неизвестный адрес", + "unknownStartDate": "Неизвестная дата начала", + "unknownEndDate": "Неизвестная дата окончания", + "home": "Главная", + "news": "Новости", + "events": "События", + "faculty": "Факультет", + "facility": "Объект", + "category": "Категория", + "article": "Статья", + "updateError": "Ошибка обновления", + "selected": "(Выбрано)", + "retry": "Повторить", + "connectionError": "Не удалось подключиться к серверу. Попробуйте ещё раз.", + "user": "Пользователь", + "dashboard": "Панель", + "attachedFiles": "Прикреплённые файлы", + "noItems": "В этой категории нет элементов.", + "loading": "Загрузка...", + "errorTitle": "Упс! Что-то пошло не так...", + "viewAll": "Смотреть больше", + "university": "Университет", + "universityPlatform": "Ваша университетская платформа" + }, + "days": { + "1": "Понедельник", + "2": "Вторник", + "3": "Среда", + "4": "Четверг", + "5": "Пятница", + "6": "Суббота", + "7": "Воскресенье" + }, + "language": { + "title": "Язык приложения", + "header": "Язык", + "select": "Выбрать язык", + "selected": "(Выбрано)", + "current": "Текущий язык" + }, + "nav": { + "home": "Главная", + "map": "Карта", + "canteen": "Столовая", + "reports": "Жалобы", + "more": "Больше" + }, + "more": { + "title": "Больше", + "visitGalati": "Посетить Galați", + "login": "Войти", + "disconnect": "Выйти", + "settings": "Настройки", + "profileTitle": "Ваш профиль", + "profileLoggedIn": "Вы уже вошли. Хотите выйти?", + "cancel": "Отмена", + "logout": "Выйти" + }, + "settings": { + "title": "Настройки", + "appearanceLang": "Внешний вид и Язык", + "themeApp": "Тема приложения", + "currentTheme": "Текущая тема:", + "currentLang": "Текущий язык:", + "supportInfo": "Помощь и Информация", + "visitWebsite": "Посетить сайт UGAL", + "appSlogan": "Создано для студентов Университета «Dunărea de Jos» в Galați" + }, + "theme": { + "title": "Тема приложения", + "header": "Тема", + "select": "Выбрать тему", + "system": "Системная", + "light": "Светлая", + "dark": "Тёмная", + "current": "Текущая тема", + "switchToLight": "Переключить на светлую тему", + "switchToDark": "Переключить на тёмную тему" + }, + "canteen": { + "title": "Столовая", + "today": "Сегодня", + "empty": "Меню на этот день недоступно.", + "updateError": "Не удалось обновить меню столовой. Проверьте подключение к интернету." + }, + "map": { + "title": "Карта", + "allLocations": "Все места", + "facilities": "Объекты" + }, + "reports": { + "title": "Жалобы", + "all": "Все жалобы", + "mine": "Мои жалобы", + "active": "Активные", + "rejected": "Отклонённые", + "completed": "Решённые", + "newReport": "Новая жалоба", + "updateError": "Не удалось обновить жалобы. Проверьте подключение к интернету.", + "loginRequired": "Необходимо войти", + "loginDesc": "Войдите, чтобы отправить или просмотреть свои жалобы.", + "login": "Войти", + "empty": "В этом разделе нет жалоб", + "emptyDesc": "Записей пока нет.", + "unknownLocation": "Неизвестное место", + "location": "Место", + "titleField": "Название", + "descDetailed": "Подробное описание", + "addPhoto": "Добавить фото", + "addPhotoBtn": "Добавить фото", + "submit": "Отправить жалобу", + "photoPermission": "Для добавления фото необходим доступ к галерее.", + "titleRequired": "Название обязательно.", + "descRequired": "Описание обязательно.", + "photoRequired": "Фото обязательно.", + "submitError": "При отправке жалобы произошла ошибка. Попробуйте ещё раз.", + "infoTitle": "Информация о жалобе", + "descSection": "Описание проблемы", + "progressTitle": "История прогресса", + "statusActive": "Активная", + "statusRejected": "Отклонённая", + "statusCompleted": "Решённая", + "missingTitle": "Название отсутствует", + "noDescription": "Описание не добавлено.", + "notFound": "Жалоба не найдена.", + "loadError": "При загрузке жалобы произошла ошибка.", + "loadErrorGeneral": "При загрузке жалоб произошла ошибка.", + "step1Title": "Жалоба зарегистрирована", + "step1Desc": "Жалоба сохранена в системе.", + "step2ActiveTitle": "На рассмотрении", + "step2ActiveDesc": "Администратор оценивает детали проблемы.", + "step3ActiveTitle": "Решение завершено", + "step3ActiveDesc": "Команда вмешается для устранения ситуации.", + "step2RejectedTitle": "Отклонена", + "step2RejectedDesc": "Запрос отклонён административной командой.", + "step2CompletedTitle": "На административном рассмотрении", + "step2CompletedDesc": "Проблема успешно обработана.", + "step3CompletedTitle": "Решена", + "step3CompletedDesc": "Проблема решена на месте техническим персоналом.", + "general": "Общее", + "exterior": "Внешнее" + }, + "auth": { + "title": "Вход", + "subtitle": "Введите данные для доступа к аккаунту", + "email": "Электронная почта", + "password": "Пароль", + "login": "Войти", + "emailRequired": "Электронная почта обязательна.", + "emailInvalid": "Неверный формат электронной почты.", + "passwordRequired": "Пароль обязателен.", + "passwordTooShort": "Пароль должен содержать не менее 6 символов.", + "invalidCredentials": "Неверная электронная почта или пароль." + }, + "onboarding": { + "exploreTitle": "Исследуйте Кампус", + "exploreDesc": "Откройте для себя здания и объекты университетского кампуса прямо на карте.", + "continue": "Продолжить" + }, + "navbar": { + "unauthenticated": "Не аутентифицирован", + "theme": "Тема", + "logout": "Выйти", + "login": "Войти", + "openMenu": "Открыть меню", + "closeMenu": "Закрыть меню", + "home": "Главная", + "announcements": "Объявления", + "news": "Новости", + "events": "События" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Виртуальный помощник", + "placeholder": "Задайте вопрос...", + "noValidResponse": "Не получен действительный ответ.", + "errorOccurred": "Произошла ошибка. Попробуйте ещё раз.", + "assistantError": "Ошибка помощника.", + "howCanIHelp": "Чем я могу вам помочь сегодня?", + "promptSuggestion": "Спросите меня о событиях, столовой, карте или жалобах.", + "noResponse": "⚠️ Помощник не вернул ответа. Попробуйте позже.", + "newConversation": "Новый разговор", + "closeChat": "Закрыть чат", + "sendMessage": "Отправить сообщение", + "openAssistant": "Открыть помощника Ace", + "closeAssistant": "Закрыть помощника Ace" + } +} diff --git a/Frontend/Mobile/src/i18n/locales/tr.json b/Frontend/Mobile/src/i18n/locales/tr.json new file mode 100644 index 00000000..ddee384e --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/tr.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Haberler", + "events": "Etkinlikler", + "faculties": "Fakülteler", + "facilities": "Tesisler", + "recent": "Son", + "noNews": "Haber bulunamadı.", + "noEvents": "Etkinlik bulunamadı.", + "noFaculties": "Fakülte bulunamadı.", + "noFacilities": "Tesis bulunamadı.", + "loadErrorNews": "Haberler yüklenemedi.", + "loadErrorEvents": "Etkinlikler yüklenemedi.", + "loadErrorFaculties": "Fakülteler yüklenemedi.", + "loadErrorFacilities": "Tesisler yüklenemedi.", + "emptyNews": "Haber mevcut değil.", + "emptyEvents": "Etkinlik mevcut değil.", + "emptyFaculties": "Fakülte mevcut değil.", + "emptyFacilities": "Tesis mevcut değil.", + "refreshError": "Veriler yenilenemedi. Lütfen internet bağlantınızı kontrol edin." + }, + "category": { + "empty": "Bu kategoride öğe yok.", + "allFaculties": "Tüm Fakülteler", + "all": "Tümü", + "refreshError": "Bu kategori için veriler yenilenemedi. Lütfen internet bağlantınızı kontrol edin.", + "fallback": "Kategori" + }, + "detail": { + "eventInfo": "Etkinlik bilgileri", + "facilityInfo": "Tesis bilgileri", + "schedule": "Program:", + "from": "Başlangıç", + "until": "Bitiş", + "aboutEvent": "Etkinlik hakkında", + "aboutFaculty": "Fakülte hakkında", + "aboutFacility": "Tesis hakkında", + "details": "Detaylar", + "contact": "İletişim ve Konum", + "address": "Adres", + "phone": "Telefon", + "website": "Web sitesi", + "updated": "Güncellendi:", + "relatedArticles": "Benzer makaleler", + "more": "Daha fazla", + "loadError": "Sayfa detayları yüklenemedi.", + "notFound": "Detaylar bulunamadı.", + "callFaculty": "Fakülte İletişim", + "callFacility": "Tesis İletişim", + "callConfirm": "{{phone}} numarasını aramak istiyor musunuz?", + "cancel": "İptal", + "call": "Ara" + }, + "common": { + "unknownTitle": "Bilinmeyen başlık", + "unknownContent": "Bilinmeyen içerik", + "unknownDate": "Bilinmeyen tarih", + "unknownLocation": "Bilinmeyen konum", + "unknownAddress": "Bilinmeyen adres", + "unknownStartDate": "Bilinmeyen başlangıç tarihi", + "unknownEndDate": "Bilinmeyen bitiş tarihi", + "home": "Ana Sayfa", + "news": "Haberler", + "events": "Etkinlikler", + "faculty": "Fakülte", + "facility": "Tesis", + "category": "Kategori", + "article": "Makale", + "updateError": "Güncelleme hatası", + "selected": "(Seçildi)", + "retry": "Tekrar dene", + "connectionError": "Sunucuya bağlanılamadı. Lütfen tekrar deneyin.", + "user": "Kullanıcı", + "dashboard": "Pano", + "attachedFiles": "Ekli dosyalar", + "noItems": "Bu kategoride öğe yok.", + "loading": "Yükleniyor...", + "errorTitle": "Hata! Bir şeyler ters gitti...", + "viewAll": "Daha fazla gör", + "university": "Üniversite", + "universityPlatform": "Üniversite platformunuz" + }, + "days": { + "1": "Pazartesi", + "2": "Salı", + "3": "Çarşamba", + "4": "Perşembe", + "5": "Cuma", + "6": "Cumartesi", + "7": "Pazar" + }, + "language": { + "title": "Uygulama dili", + "header": "Dil", + "select": "Dil seçin", + "selected": "(Seçildi)", + "current": "Mevcut dil" + }, + "nav": { + "home": "Ana Sayfa", + "map": "Harita", + "canteen": "Yemekhane", + "reports": "Şikayetler", + "more": "Daha fazla" + }, + "more": { + "title": "Daha fazla", + "visitGalati": "Galați'yi ziyaret et", + "login": "Giriş yap", + "disconnect": "Çıkış yap", + "settings": "Ayarlar", + "profileTitle": "Profiliniz", + "profileLoggedIn": "Zaten giriş yaptınız. Çıkış yapmak istiyor musunuz?", + "cancel": "İptal", + "logout": "Çıkış yap" + }, + "settings": { + "title": "Ayarlar", + "appearanceLang": "Görünüm ve Dil", + "themeApp": "Uygulama teması", + "currentTheme": "Mevcut tema:", + "currentLang": "Mevcut dil:", + "supportInfo": "Yardım ve Bilgi", + "visitWebsite": "UGAL web sitesini ziyaret et", + "appSlogan": "Galați «Dunărea de Jos» Üniversitesi öğrencileri için oluşturuldu" + }, + "theme": { + "title": "Uygulama teması", + "header": "Tema", + "select": "Tema seçin", + "system": "Sistem", + "light": "Açık", + "dark": "Koyu", + "current": "Mevcut tema", + "switchToLight": "Açık temaya geç", + "switchToDark": "Koyu temaya geç" + }, + "canteen": { + "title": "Yemekhane", + "today": "Bugün", + "empty": "Bu gün için menü mevcut değil.", + "updateError": "Yemekhane menüsü yenilenemedi. Lütfen internet bağlantınızı kontrol edin." + }, + "map": { + "title": "Harita", + "allLocations": "Tüm konumlar", + "facilities": "Tesisler" + }, + "reports": { + "title": "Şikayetler", + "all": "Tüm şikayetler", + "mine": "Şikayetlerim", + "active": "Aktif", + "rejected": "Reddedildi", + "completed": "Çözüldü", + "newReport": "Yeni şikayet", + "updateError": "Şikayetler yenilenemedi. Lütfen internet bağlantınızı kontrol edin.", + "loginRequired": "Giriş yapmanız gerekiyor", + "loginDesc": "Şikayetlerinizi göndermek veya görüntülemek için giriş yapın.", + "login": "Giriş yap", + "empty": "Bu bölümde şikayet yok", + "emptyDesc": "Şu an kayıt bulunmuyor.", + "unknownLocation": "Bilinmeyen konum", + "location": "Konum", + "titleField": "Başlık", + "descDetailed": "Ayrıntılı açıklama", + "addPhoto": "Fotoğraf ekle", + "addPhotoBtn": "Fotoğraf ekle", + "submit": "Şikayeti gönder", + "photoPermission": "Fotoğraf eklemek için galeri erişimi gereklidir.", + "titleRequired": "Başlık zorunludur.", + "descRequired": "Açıklama zorunludur.", + "photoRequired": "Fotoğraf zorunludur.", + "submitError": "Şikayet gönderilirken hata oluştu. Lütfen tekrar deneyin.", + "infoTitle": "Şikayet bilgileri", + "descSection": "Sorun açıklaması", + "progressTitle": "İlerleme geçmişi", + "statusActive": "Aktif", + "statusRejected": "Reddedildi", + "statusCompleted": "Çözüldü", + "missingTitle": "Başlık eksik", + "noDescription": "Açıklama eklenmedi.", + "notFound": "Şikayet bulunamadı.", + "loadError": "Şikayet yüklenirken hata oluştu.", + "loadErrorGeneral": "Şikayetler yüklenirken hata oluştu.", + "step1Title": "Şikayet kaydedildi", + "step1Desc": "Şikayet sisteme kaydedildi.", + "step2ActiveTitle": "İnceleniyor", + "step2ActiveDesc": "Bir yönetici sorunu değerlendiriyor.", + "step3ActiveTitle": "Çözüm tamamlandı", + "step3ActiveDesc": "Ekip durumu düzeltmek için müdahale edecek.", + "step2RejectedTitle": "Reddedildi", + "step2RejectedDesc": "Talep yönetim ekibi tarafından reddedildi.", + "step2CompletedTitle": "İdari incelemede", + "step2CompletedDesc": "Sorun başarıyla işlendi.", + "step3CompletedTitle": "Çözüldü", + "step3CompletedDesc": "Sorun teknik personel tarafından yerinde çözüldü.", + "general": "Genel", + "exterior": "Dış alan" + }, + "auth": { + "title": "Giriş yap", + "subtitle": "Hesabınıza erişmek için kimlik bilgilerinizi girin", + "email": "E-posta", + "password": "Şifre", + "login": "Giriş yap", + "emailRequired": "E-posta zorunludur.", + "emailInvalid": "Geçersiz e-posta formatı.", + "passwordRequired": "Şifre zorunludur.", + "passwordTooShort": "Şifre en az 6 karakter olmalıdır.", + "invalidCredentials": "Yanlış e-posta veya şifre." + }, + "onboarding": { + "exploreTitle": "Kampüsü Keşfet", + "exploreDesc": "Üniversite kampüsünün binalarını ve tesislerini doğrudan haritada keşfedin.", + "continue": "Devam et" + }, + "navbar": { + "unauthenticated": "Giriş yapılmadı", + "theme": "Tema", + "logout": "Çıkış yap", + "login": "Giriş yap", + "openMenu": "Menüyü aç", + "closeMenu": "Menüyü kapat", + "home": "Ana Sayfa", + "announcements": "Duyurular", + "news": "Haberler", + "events": "Etkinlikler" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Sanal asistan", + "placeholder": "Soru sorun...", + "noValidResponse": "Geçerli yanıt alınamadı.", + "errorOccurred": "Hata oluştu. Lütfen tekrar deneyin.", + "assistantError": "Asistan hatası.", + "howCanIHelp": "Bugün size nasıl yardımcı olabilirim?", + "promptSuggestion": "Etkinlikler, yemekhane, harita veya şikayetler hakkında sorun.", + "noResponse": "⚠️ Asistan yanıt vermedi. Lütfen daha sonra tekrar deneyin.", + "newConversation": "Yeni sohbet", + "closeChat": "Sohbeti kapat", + "sendMessage": "Mesaj gönder", + "openAssistant": "Ace asistanını aç", + "closeAssistant": "Ace asistanını kapat" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/uk.json b/Frontend/Mobile/src/i18n/locales/uk.json new file mode 100644 index 00000000..a0e1f75f --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/uk.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Новини", + "events": "Події", + "faculties": "Факультети", + "facilities": "Об'єкти", + "recent": "Останні", + "noNews": "Новини не знайдено.", + "noEvents": "Події не знайдено.", + "noFaculties": "Факультети не знайдено.", + "noFacilities": "Об'єкти не знайдено.", + "loadErrorNews": "Не вдалося завантажити новини.", + "loadErrorEvents": "Не вдалося завантажити події.", + "loadErrorFaculties": "Не вдалося завантажити факультети.", + "loadErrorFacilities": "Не вдалося завантажити об'єкти.", + "emptyNews": "Немає доступних новин.", + "emptyEvents": "Немає доступних подій.", + "emptyFaculties": "Немає доступних факультетів.", + "emptyFacilities": "Немає доступних об'єктів.", + "refreshError": "Не вдалося оновити дані. Перевірте підключення до інтернету." + }, + "category": { + "empty": "У цій категорії немає елементів.", + "allFaculties": "Усі Факультети", + "all": "Усі", + "refreshError": "Не вдалося оновити дані для цієї категорії. Перевірте підключення до інтернету.", + "fallback": "Категорія" + }, + "detail": { + "eventInfo": "Інформація про подію", + "facilityInfo": "Інформація про об'єкт", + "schedule": "Розклад:", + "from": "Від", + "until": "До", + "aboutEvent": "Про подію", + "aboutFaculty": "Про факультет", + "aboutFacility": "Про об'єкт", + "details": "Деталі", + "contact": "Контакт та Розташування", + "address": "Адреса", + "phone": "Телефон", + "website": "Сайт", + "updated": "Оновлено:", + "relatedArticles": "Схожі статті", + "more": "Більше", + "loadError": "Не вдалося завантажити деталі сторінки.", + "notFound": "Деталі не знайдено.", + "callFaculty": "Контакт Факультету", + "callFacility": "Контакт Об'єкту", + "callConfirm": "Бажаєте зателефонувати на {{phone}}?", + "cancel": "Скасувати", + "call": "Зателефонувати" + }, + "common": { + "unknownTitle": "Невідома назва", + "unknownContent": "Невідомий вміст", + "unknownDate": "Невідома дата", + "unknownLocation": "Невідоме місце", + "unknownAddress": "Невідома адреса", + "unknownStartDate": "Невідома дата початку", + "unknownEndDate": "Невідома дата завершення", + "home": "Головна", + "news": "Новини", + "events": "Події", + "faculty": "Факультет", + "facility": "Об'єкт", + "category": "Категорія", + "article": "Стаття", + "updateError": "Помилка оновлення", + "selected": "(Вибрано)", + "retry": "Спробувати ще", + "connectionError": "Не вдалося підключитися до сервера. Спробуйте ще раз.", + "user": "Користувач", + "dashboard": "Панель", + "attachedFiles": "Прикріплені файли", + "noItems": "У цій категорії немає елементів.", + "loading": "Завантаження...", + "errorTitle": "Ой! Щось пішло не так...", + "viewAll": "Переглянути більше", + "university": "Університет", + "universityPlatform": "Ваша університетська платформа" + }, + "days": { + "1": "Понеділок", + "2": "Вівторок", + "3": "Середа", + "4": "Четвер", + "5": "П'ятниця", + "6": "Субота", + "7": "Неділя" + }, + "language": { + "title": "Мова додатку", + "header": "Мова", + "select": "Вибрати мову", + "selected": "(Вибрано)", + "current": "Поточна мова" + }, + "nav": { + "home": "Головна", + "map": "Карта", + "canteen": "Їдальня", + "reports": "Скарги", + "more": "Більше" + }, + "more": { + "title": "Більше", + "visitGalati": "Відвідати Galați", + "login": "Увійти", + "disconnect": "Вийти", + "settings": "Налаштування", + "profileTitle": "Ваш профіль", + "profileLoggedIn": "Ви вже увійшли. Бажаєте вийти?", + "cancel": "Скасувати", + "logout": "Вийти" + }, + "settings": { + "title": "Налаштування", + "appearanceLang": "Зовнішній вигляд і Мова", + "themeApp": "Тема додатку", + "currentTheme": "Поточна тема:", + "currentLang": "Поточна мова:", + "supportInfo": "Допомога та Інформація", + "visitWebsite": "Відвідати сайт UGAL", + "appSlogan": "Створено для студентів Університету «Dunărea de Jos» у Galați" + }, + "theme": { + "title": "Тема додатку", + "header": "Тема", + "select": "Вибрати тему", + "system": "Системна", + "light": "Світла", + "dark": "Темна", + "current": "Поточна тема", + "switchToLight": "Перейти на світлу тему", + "switchToDark": "Перейти на темну тему" + }, + "canteen": { + "title": "Їдальня", + "today": "Сьогодні", + "empty": "Меню на цей день недоступне.", + "updateError": "Не вдалося оновити меню їдальні. Перевірте підключення до інтернету." + }, + "map": { + "title": "Карта", + "allLocations": "Усі місця", + "facilities": "Об'єкти" + }, + "reports": { + "title": "Скарги", + "all": "Усі скарги", + "mine": "Мої скарги", + "active": "Активні", + "rejected": "Відхилені", + "completed": "Вирішені", + "newReport": "Нова скарга", + "updateError": "Не вдалося оновити скарги. Перевірте підключення до інтернету.", + "loginRequired": "Необхідно увійти", + "loginDesc": "Увійдіть, щоб надіслати або переглянути свої скарги.", + "login": "Увійти", + "empty": "У цьому розділі немає скарг", + "emptyDesc": "Наразі записів немає.", + "unknownLocation": "Невідоме місце", + "location": "Місце", + "titleField": "Назва", + "descDetailed": "Детальний опис", + "addPhoto": "Додати фото", + "addPhotoBtn": "Додати фото", + "submit": "Надіслати скаргу", + "photoPermission": "Для додавання фото необхідний доступ до галереї.", + "titleRequired": "Назва є обов'язковою.", + "descRequired": "Опис є обов'язковим.", + "photoRequired": "Фото є обов'язковим.", + "submitError": "Під час надсилання скарги сталася помилка. Спробуйте ще раз.", + "infoTitle": "Інформація про скаргу", + "descSection": "Опис проблеми", + "progressTitle": "Історія прогресу", + "statusActive": "Активна", + "statusRejected": "Відхилена", + "statusCompleted": "Вирішена", + "missingTitle": "Відсутня назва", + "noDescription": "Опис не додано.", + "notFound": "Скаргу не знайдено.", + "loadError": "Під час завантаження скарги сталася помилка.", + "loadErrorGeneral": "Під час завантаження скарг сталася помилка.", + "step1Title": "Скаргу зареєстровано", + "step1Desc": "Скаргу збережено в системі.", + "step2ActiveTitle": "На розгляді", + "step2ActiveDesc": "Адміністратор оцінює деталі проблеми.", + "step3ActiveTitle": "Вирішення завершено", + "step3ActiveDesc": "Команда втрутиться для виправлення ситуації.", + "step2RejectedTitle": "Відхилено", + "step2RejectedDesc": "Запит відхилено адміністративною командою.", + "step2CompletedTitle": "На адміністративному розгляді", + "step2CompletedDesc": "Проблему успішно опрацьовано.", + "step3CompletedTitle": "Вирішено", + "step3CompletedDesc": "Проблему вирішено на місці технічним персоналом.", + "general": "Загальне", + "exterior": "Зовнішнє" + }, + "auth": { + "title": "Вхід", + "subtitle": "Введіть свої дані для доступу до акаунту", + "email": "Електронна пошта", + "password": "Пароль", + "login": "Увійти", + "emailRequired": "Електронна пошта є обов'язковою.", + "emailInvalid": "Невірний формат електронної пошти.", + "passwordRequired": "Пароль є обов'язковим.", + "passwordTooShort": "Пароль має містити щонайменше 6 символів.", + "invalidCredentials": "Невірна електронна пошта або пароль." + }, + "onboarding": { + "exploreTitle": "Дослідіть Кампус", + "exploreDesc": "Відкрийте для себе будівлі та об'єкти університетського кампусу безпосередньо на карті.", + "continue": "Продовжити" + }, + "navbar": { + "unauthenticated": "Не автентифіковано", + "theme": "Тема", + "logout": "Вийти", + "login": "Увійти", + "openMenu": "Відкрити меню", + "closeMenu": "Закрити меню", + "home": "Головна", + "announcements": "Оголошення", + "news": "Новини", + "events": "Події" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Віртуальний помічник", + "placeholder": "Задайте питання...", + "noValidResponse": "Не отримано дійсної відповіді.", + "errorOccurred": "Сталася помилка. Спробуйте ще раз.", + "assistantError": "Помилка помічника.", + "howCanIHelp": "Чим я можу вам допомогти сьогодні?", + "promptSuggestion": "Запитайте мене про події, їдальню, карту або скарги.", + "noResponse": "⚠️ Помічник не повернув відповіді. Спробуйте пізніше.", + "newConversation": "Нова розмова", + "closeChat": "Закрити чат", + "sendMessage": "Надіслати повідомлення", + "openAssistant": "Відкрити помічника Ace", + "closeAssistant": "Закрити помічника Ace" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/vi.json b/Frontend/Mobile/src/i18n/locales/vi.json new file mode 100644 index 00000000..e6695c5b --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/vi.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "Tin tức", + "events": "Sự kiện", + "faculties": "Khoa", + "facilities": "Cơ sở vật chất", + "recent": "Gần đây", + "noNews": "Không tìm thấy tin tức.", + "noEvents": "Không tìm thấy sự kiện.", + "noFaculties": "Không tìm thấy khoa.", + "noFacilities": "Không tìm thấy cơ sở vật chất.", + "loadErrorNews": "Không thể tải tin tức.", + "loadErrorEvents": "Không thể tải sự kiện.", + "loadErrorFaculties": "Không thể tải danh sách khoa.", + "loadErrorFacilities": "Không thể tải cơ sở vật chất.", + "emptyNews": "Không có tin tức nào.", + "emptyEvents": "Không có sự kiện nào.", + "emptyFaculties": "Không có khoa nào.", + "emptyFacilities": "Không có cơ sở vật chất nào.", + "refreshError": "Không thể làm mới dữ liệu. Vui lòng kiểm tra kết nối internet." + }, + "category": { + "empty": "Không có mục nào trong danh mục này.", + "allFaculties": "Tất cả các Khoa", + "all": "Tất cả", + "refreshError": "Không thể làm mới dữ liệu danh mục này. Vui lòng kiểm tra kết nối internet.", + "fallback": "Danh mục" + }, + "detail": { + "eventInfo": "Thông tin sự kiện", + "facilityInfo": "Thông tin cơ sở vật chất", + "schedule": "Lịch trình:", + "from": "Từ", + "until": "Đến", + "aboutEvent": "Về sự kiện", + "aboutFaculty": "Về khoa", + "aboutFacility": "Về cơ sở vật chất", + "details": "Chi tiết", + "contact": "Liên hệ và Vị trí", + "address": "Địa chỉ", + "phone": "Điện thoại", + "website": "Trang web", + "updated": "Đã cập nhật:", + "relatedArticles": "Bài viết liên quan", + "more": "Xem thêm", + "loadError": "Không thể tải chi tiết trang.", + "notFound": "Không tìm thấy chi tiết.", + "callFaculty": "Liên hệ Khoa", + "callFacility": "Liên hệ Cơ sở", + "callConfirm": "Bạn có muốn gọi {{phone}} không?", + "cancel": "Hủy", + "call": "Gọi" + }, + "common": { + "unknownTitle": "Tiêu đề không xác định", + "unknownContent": "Nội dung không xác định", + "unknownDate": "Ngày không xác định", + "unknownLocation": "Vị trí không xác định", + "unknownAddress": "Địa chỉ không xác định", + "unknownStartDate": "Ngày bắt đầu không xác định", + "unknownEndDate": "Ngày kết thúc không xác định", + "home": "Trang chủ", + "news": "Tin tức", + "events": "Sự kiện", + "faculty": "Khoa", + "facility": "Cơ sở", + "category": "Danh mục", + "article": "Bài viết", + "updateError": "Lỗi cập nhật", + "selected": "(Đã chọn)", + "retry": "Thử lại", + "connectionError": "Không thể kết nối máy chủ. Vui lòng thử lại.", + "user": "Người dùng", + "dashboard": "Bảng điều khiển", + "attachedFiles": "Tệp đính kèm", + "noItems": "Không có mục nào trong danh mục này.", + "loading": "Đang tải...", + "errorTitle": "Ôi! Đã xảy ra sự cố...", + "viewAll": "Xem thêm", + "university": "Trường đại học", + "universityPlatform": "Nền tảng đại học của bạn" + }, + "days": { + "1": "Thứ Hai", + "2": "Thứ Ba", + "3": "Thứ Tư", + "4": "Thứ Năm", + "5": "Thứ Sáu", + "6": "Thứ Bảy", + "7": "Chủ Nhật" + }, + "language": { + "title": "Ngôn ngữ ứng dụng", + "header": "Ngôn ngữ", + "select": "Chọn ngôn ngữ", + "selected": "(Đã chọn)", + "current": "Ngôn ngữ hiện tại" + }, + "nav": { + "home": "Trang chủ", + "map": "Bản đồ", + "canteen": "Căng tin", + "reports": "Báo cáo", + "more": "Thêm" + }, + "more": { + "title": "Thêm", + "visitGalati": "Thăm Galați", + "login": "Đăng nhập", + "disconnect": "Đăng xuất", + "settings": "Cài đặt", + "profileTitle": "Hồ sơ của bạn", + "profileLoggedIn": "Bạn đã đăng nhập. Bạn có muốn đăng xuất không?", + "cancel": "Hủy", + "logout": "Đăng xuất" + }, + "settings": { + "title": "Cài đặt", + "appearanceLang": "Giao diện & Ngôn ngữ", + "themeApp": "Giao diện ứng dụng", + "currentTheme": "Giao diện hiện tại:", + "currentLang": "Ngôn ngữ hiện tại:", + "supportInfo": "Trợ giúp & Thông tin", + "visitWebsite": "Truy cập trang web UGAL", + "appSlogan": "Được tạo cho sinh viên Trường Đại học «Dunărea de Jos» tại Galați" + }, + "theme": { + "title": "Giao diện ứng dụng", + "header": "Giao diện", + "select": "Chọn giao diện", + "system": "Hệ thống", + "light": "Sáng", + "dark": "Tối", + "current": "Giao diện hiện tại", + "switchToLight": "Chuyển sang giao diện sáng", + "switchToDark": "Chuyển sang giao diện tối" + }, + "canteen": { + "title": "Căng tin", + "today": "Hôm nay", + "empty": "Không có thực đơn cho ngày hôm nay.", + "updateError": "Không thể làm mới thực đơn căng tin. Vui lòng kiểm tra kết nối internet." + }, + "map": { + "title": "Bản đồ", + "allLocations": "Tất cả các vị trí", + "facilities": "Cơ sở vật chất" + }, + "reports": { + "title": "Báo cáo", + "all": "Tất cả báo cáo", + "mine": "Báo cáo của tôi", + "active": "Đang hoạt động", + "rejected": "Bị từ chối", + "completed": "Đã giải quyết", + "newReport": "Báo cáo mới", + "updateError": "Không thể làm mới báo cáo. Vui lòng kiểm tra kết nối internet.", + "loginRequired": "Bạn cần đăng nhập", + "loginDesc": "Đăng nhập để gửi hoặc xem báo cáo của bạn.", + "login": "Đăng nhập", + "empty": "Không có báo cáo trong mục này", + "emptyDesc": "Hiện không có hồ sơ nào.", + "unknownLocation": "Vị trí không xác định", + "location": "Vị trí", + "titleField": "Tiêu đề", + "descDetailed": "Mô tả chi tiết", + "addPhoto": "Thêm ảnh", + "addPhotoBtn": "Thêm ảnh", + "submit": "Gửi báo cáo", + "photoPermission": "Cần quyền truy cập thư viện ảnh để thêm ảnh.", + "titleRequired": "Tiêu đề là bắt buộc.", + "descRequired": "Mô tả là bắt buộc.", + "photoRequired": "Ảnh là bắt buộc.", + "submitError": "Đã xảy ra lỗi khi gửi báo cáo. Vui lòng thử lại.", + "infoTitle": "Thông tin báo cáo", + "descSection": "Mô tả vấn đề", + "progressTitle": "Lịch sử tiến trình", + "statusActive": "Đang hoạt động", + "statusRejected": "Bị từ chối", + "statusCompleted": "Đã giải quyết", + "missingTitle": "Thiếu tiêu đề", + "noDescription": "Chưa thêm mô tả.", + "notFound": "Không tìm thấy báo cáo.", + "loadError": "Đã xảy ra lỗi khi tải báo cáo.", + "loadErrorGeneral": "Đã xảy ra lỗi khi tải danh sách báo cáo.", + "step1Title": "Báo cáo đã được đăng ký", + "step1Desc": "Báo cáo đã được lưu vào hệ thống.", + "step2ActiveTitle": "Đang xem xét", + "step2ActiveDesc": "Quản trị viên đang đánh giá chi tiết vấn đề.", + "step3ActiveTitle": "Giải quyết hoàn tất", + "step3ActiveDesc": "Đội ngũ sẽ can thiệp để giải quyết tình huống.", + "step2RejectedTitle": "Bị từ chối", + "step2RejectedDesc": "Yêu cầu bị từ chối bởi nhóm quản trị.", + "step2CompletedTitle": "Đang xem xét hành chính", + "step2CompletedDesc": "Vấn đề đã được xử lý thành công.", + "step3CompletedTitle": "Đã giải quyết", + "step3CompletedDesc": "Vấn đề đã được giải quyết tại chỗ bởi nhân viên kỹ thuật.", + "general": "Chung", + "exterior": "Bên ngoài" + }, + "auth": { + "title": "Đăng nhập", + "subtitle": "Nhập thông tin đăng nhập để truy cập tài khoản của bạn", + "email": "Email", + "password": "Mật khẩu", + "login": "Đăng nhập", + "emailRequired": "Email là bắt buộc.", + "emailInvalid": "Định dạng email không hợp lệ.", + "passwordRequired": "Mật khẩu là bắt buộc.", + "passwordTooShort": "Mật khẩu phải có ít nhất 6 ký tự.", + "invalidCredentials": "Email hoặc mật khẩu không đúng." + }, + "onboarding": { + "exploreTitle": "Khám phá Khuôn viên", + "exploreDesc": "Khám phá các tòa nhà và cơ sở vật chất của khuôn viên đại học trực tiếp trên bản đồ.", + "continue": "Tiếp tục" + }, + "navbar": { + "unauthenticated": "Chưa đăng nhập", + "theme": "Giao diện", + "logout": "Đăng xuất", + "login": "Đăng nhập", + "openMenu": "Mở menu", + "closeMenu": "Đóng menu", + "home": "Trang chủ", + "announcements": "Thông báo", + "news": "Tin tức", + "events": "Sự kiện" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "Trợ lý ảo", + "placeholder": "Đặt câu hỏi...", + "noValidResponse": "Không nhận được phản hồi hợp lệ.", + "errorOccurred": "Đã xảy ra lỗi. Vui lòng thử lại.", + "assistantError": "Lỗi trợ lý.", + "howCanIHelp": "Tôi có thể giúp gì cho bạn hôm nay?", + "promptSuggestion": "Hỏi tôi về sự kiện, căng tin, bản đồ hoặc báo cáo.", + "noResponse": "⚠️ Trợ lý không trả về phản hồi. Vui lòng thử lại sau.", + "newConversation": "Cuộc trò chuyện mới", + "closeChat": "Đóng chat", + "sendMessage": "Gửi tin nhắn", + "openAssistant": "Mở trợ lý Ace", + "closeAssistant": "Đóng trợ lý Ace" + } +} \ No newline at end of file diff --git a/Frontend/Mobile/src/i18n/locales/zh.json b/Frontend/Mobile/src/i18n/locales/zh.json new file mode 100644 index 00000000..49c25f64 --- /dev/null +++ b/Frontend/Mobile/src/i18n/locales/zh.json @@ -0,0 +1,246 @@ +{ + "home": { + "news": "新闻", + "events": "活动", + "faculties": "院系", + "facilities": "设施", + "recent": "最新", + "noNews": "未找到新闻。", + "noEvents": "未找到活动。", + "noFaculties": "未找到院系。", + "noFacilities": "未找到设施。", + "loadErrorNews": "无法加载新闻。", + "loadErrorEvents": "无法加载活动。", + "loadErrorFaculties": "无法加载院系。", + "loadErrorFacilities": "无法加载设施。", + "emptyNews": "暂无新闻。", + "emptyEvents": "暂无活动。", + "emptyFaculties": "暂无院系。", + "emptyFacilities": "暂无设施。", + "refreshError": "无法刷新数据,请检查您的网络连接。" + }, + "category": { + "empty": "此分类中没有内容。", + "allFaculties": "所有院系", + "all": "全部", + "refreshError": "无法刷新此分类的数据,请检查您的网络连接。", + "fallback": "分类" + }, + "detail": { + "eventInfo": "活动信息", + "facilityInfo": "设施信息", + "schedule": "时间安排:", + "from": "从", + "until": "至", + "aboutEvent": "关于活动", + "aboutFaculty": "关于院系", + "aboutFacility": "关于设施", + "details": "详情", + "contact": "联系方式与位置", + "address": "地址", + "phone": "电话", + "website": "网站", + "updated": "已更新:", + "relatedArticles": "相关文章", + "more": "更多", + "loadError": "无法加载页面详情。", + "notFound": "未找到详情。", + "callFaculty": "联系院系", + "callFacility": "联系设施", + "callConfirm": "是否拨打 {{phone}}?", + "cancel": "取消", + "call": "拨打" + }, + "common": { + "unknownTitle": "未知标题", + "unknownContent": "未知内容", + "unknownDate": "未知日期", + "unknownLocation": "未知位置", + "unknownAddress": "未知地址", + "unknownStartDate": "未知开始日期", + "unknownEndDate": "未知结束日期", + "home": "首页", + "news": "新闻", + "events": "活动", + "faculty": "院系", + "facility": "设施", + "category": "分类", + "article": "文章", + "updateError": "更新错误", + "selected": "(已选)", + "retry": "重试", + "connectionError": "无法连接到服务器,请重试。", + "user": "用户", + "dashboard": "仪表板", + "attachedFiles": "附件", + "noItems": "此分类中没有内容。", + "loading": "加载中...", + "errorTitle": "哎呀!出了点问题...", + "viewAll": "查看更多", + "university": "大学", + "universityPlatform": "您的大学平台" + }, + "days": { + "1": "星期一", + "2": "星期二", + "3": "星期三", + "4": "星期四", + "5": "星期五", + "6": "星期六", + "7": "星期日" + }, + "language": { + "title": "应用语言", + "header": "语言", + "select": "选择语言", + "selected": "(已选)", + "current": "当前语言" + }, + "nav": { + "home": "首页", + "map": "地图", + "canteen": "食堂", + "reports": "投诉", + "more": "更多" + }, + "more": { + "title": "更多", + "visitGalati": "参观 Galați", + "login": "登录", + "disconnect": "登出", + "settings": "设置", + "profileTitle": "您的个人资料", + "profileLoggedIn": "您已登录。是否要退出?", + "cancel": "取消", + "logout": "登出" + }, + "settings": { + "title": "设置", + "appearanceLang": "外观与语言", + "themeApp": "应用主题", + "currentTheme": "当前主题:", + "currentLang": "当前语言:", + "supportInfo": "帮助与信息", + "visitWebsite": "访问 UGAL 官网", + "appSlogan": "专为加拉茨「Dunărea de Jos」大学学生创建" + }, + "theme": { + "title": "应用主题", + "header": "主题", + "select": "选择主题", + "system": "系统", + "light": "浅色", + "dark": "深色", + "current": "当前主题", + "switchToLight": "切换到浅色主题", + "switchToDark": "切换到深色主题" + }, + "canteen": { + "title": "食堂", + "today": "今天", + "empty": "今天没有可用菜单。", + "updateError": "无法刷新食堂菜单,请检查您的网络连接。" + }, + "map": { + "title": "地图", + "allLocations": "所有地点", + "facilities": "设施" + }, + "reports": { + "title": "投诉", + "all": "所有投诉", + "mine": "我的投诉", + "active": "进行中", + "rejected": "已拒绝", + "completed": "已解决", + "newReport": "新投诉", + "updateError": "无法刷新投诉,请检查您的网络连接。", + "loginRequired": "需要登录", + "loginDesc": "请登录后提交或查看您的投诉。", + "login": "登录", + "empty": "此部分没有投诉", + "emptyDesc": "目前没有记录。", + "unknownLocation": "未知位置", + "location": "位置", + "titleField": "标题", + "descDetailed": "详细描述", + "addPhoto": "添加照片", + "addPhotoBtn": "添加照片", + "submit": "提交投诉", + "photoPermission": "需要相册访问权限才能添加照片。", + "titleRequired": "标题为必填项。", + "descRequired": "描述为必填项。", + "photoRequired": "照片为必填项。", + "submitError": "提交投诉时发生错误,请重试。", + "infoTitle": "投诉信息", + "descSection": "问题描述", + "progressTitle": "进度历史", + "statusActive": "进行中", + "statusRejected": "已拒绝", + "statusCompleted": "已解决", + "missingTitle": "缺少标题", + "noDescription": "未添加描述。", + "notFound": "未找到投诉。", + "loadError": "加载投诉时发生错误。", + "loadErrorGeneral": "加载投诉列表时发生错误。", + "step1Title": "投诉已登记", + "step1Desc": "投诉已保存至系统。", + "step2ActiveTitle": "审核中", + "step2ActiveDesc": "管理员正在评估问题详情。", + "step3ActiveTitle": "解决完成", + "step3ActiveDesc": "团队将介入处理。", + "step2RejectedTitle": "已拒绝", + "step2RejectedDesc": "请求已被管理团队拒绝。", + "step2CompletedTitle": "行政审核中", + "step2CompletedDesc": "问题已成功处理。", + "step3CompletedTitle": "已解决", + "step3CompletedDesc": "技术人员已现场解决问题。", + "general": "一般", + "exterior": "外部" + }, + "auth": { + "title": "登录", + "subtitle": "输入您的凭据以访问您的账户", + "email": "电子邮件", + "password": "密码", + "login": "登录", + "emailRequired": "电子邮件为必填项。", + "emailInvalid": "电子邮件格式无效。", + "passwordRequired": "密码为必填项。", + "passwordTooShort": "密码至少需要 6 个字符。", + "invalidCredentials": "电子邮件或密码错误。" + }, + "onboarding": { + "exploreTitle": "探索校园", + "exploreDesc": "直接在地图上发现大学校园的建筑和设施。", + "continue": "继续" + }, + "navbar": { + "unauthenticated": "未认证", + "theme": "主题", + "logout": "登出", + "login": "登录", + "openMenu": "打开菜单", + "closeMenu": "关闭菜单", + "home": "首页", + "announcements": "公告", + "news": "新闻", + "events": "活动" + }, + "ace": { + "title": "Ace", + "virtualAssistant": "虚拟助手", + "placeholder": "提问...", + "noValidResponse": "未收到有效回复。", + "errorOccurred": "发生错误,请重试。", + "assistantError": "助手错误。", + "howCanIHelp": "今天我能帮您做什么?", + "promptSuggestion": "询问我关于活动、食堂、地图或投诉的问题。", + "noResponse": "⚠️ 助手未返回任何回复,请稍后再试。", + "newConversation": "新对话", + "closeChat": "关闭聊天", + "sendMessage": "发送消息", + "openAssistant": "打开 Ace 助手", + "closeAssistant": "关闭 Ace 助手" + } +} From f31a5314bcfe9fe5fb1f9608225542139a5f951c Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 17:38:27 +0300 Subject: [PATCH 11/15] fix: limbi pe multiple pagini --- .../src/app/(public)/acasa/categorie.tsx | 11 +- .../src/app/(public)/acasa/categorie.web.tsx | 13 +- .../Mobile/src/app/(public)/acasa/index.tsx | 2 +- .../src/app/(public)/acasa/index.web.tsx | 4 +- .../src/app/(public)/acasa/vizualizare.tsx | 7 +- .../src/app/(public)/anunt/[id].web.tsx | 10 +- .../Mobile/src/app/(public)/cantina/index.tsx | 12 +- .../src/app/(public)/cantina/index.web.tsx | 8 +- .../src/app/(public)/eveniment/[id].web.tsx | 10 +- .../Mobile/src/app/(public)/harta.web.tsx | 2 +- .../Mobile/src/app/(public)/more/limba.tsx | 11 +- .../src/app/(public)/more/limba.web.tsx | 11 +- .../src/app/(public)/sesizari/detalii.tsx | 8 +- .../src/app/(public)/sesizari/detalii.web.tsx | 8 +- .../src/app/(public)/sesizari/index.tsx | 20 +-- .../src/app/(public)/sesizari/index.web.tsx | 30 ++-- .../components/ui/navigation/profile-menu.tsx | 29 +++- .../components/ui/navigation/theme-menu.tsx | 132 ++++++++++++------ Frontend/Mobile/src/utils/settings-store.ts | 2 +- 19 files changed, 193 insertions(+), 137 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index bf2c6f74..1050cabd 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -93,15 +93,18 @@ export default function CategoryScreen() { response = await api.get("/announcements/", { params: { page: pageToFetch, - size: 20, + size: selectedFacultyId ? 200 : 20, announcement_type: type, - faculty_id: selectedFacultyId || undefined, lang: i18n.language, } }); if (response.data && response.data.items) { - newItems = response.data.items.map((item: any) => ({ + const rawItems = selectedFacultyId + ? response.data.items.filter((item: any) => + (item.faculties ?? []).some((f: any) => f.id.toString() === selectedFacultyId)) + : response.data.items; + newItems = rawItems.map((item: any) => ({ id: item.id.toString(), title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: displayTitle, @@ -153,7 +156,7 @@ export default function CategoryScreen() { } } - if (newItems.length < 20) { + if (selectedFacultyId || newItems.length < 20) { setHasMore(false); } diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index 8494a7a2..c3edcd52 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -88,14 +88,17 @@ export default function CategoryScreen() { response = await api.get("/announcements/", { params: { page: pageToFetch, - size: 20, + size: selectedFacultyId ? 200 : 20, announcement_type: type, - faculty_id: selectedFacultyId || undefined } }); - + if (response.data && response.data.items) { - newItems = response.data.items.map((item: any) => ({ + const rawItems = selectedFacultyId + ? response.data.items.filter((item: any) => + (item.faculties ?? []).some((f: any) => f.id.toString() === selectedFacultyId)) + : response.data.items; + newItems = rawItems.map((item: any) => ({ id: item.id.toString(), title: item.title || "Titlu necunoscut", category: categoryTitle, @@ -147,7 +150,7 @@ export default function CategoryScreen() { } } - if (newItems.length < 20) { + if (selectedFacultyId || newItems.length < 20) { setHasMore(false); } diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index e32ae38e..364c2d21 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -225,7 +225,7 @@ export default function HomeScreen() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [i18n.language]); - const announcementsForHero = [...noutati] + const announcementsForHero = [...noutati, ...evenimente] .sort((a, b) => parseRomanianDate(b.date).getTime() - parseRomanianDate(a.date).getTime()) .slice(0, 3) .map(item => ({ diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 3107e37b..ed5e7083 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -168,8 +168,8 @@ export default function HomeScreen() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [i18n.language]); - // Ultimele 3 anunturi (Noutăți), cele mai recente primele, pentru hero. - const announcementsForHero = [...noutati] + // Ultimele 3 postari (Noutăți + Evenimente), cele mai recente primele, pentru hero. + const announcementsForHero = [...noutati, ...evenimente] .sort((a, b) => parseRomanianDate(b.date).getTime() - parseRomanianDate(a.date).getTime()) .slice(0, 3); diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index 3b1421d1..8259bd74 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -19,7 +19,6 @@ import PhoneIcon from "@/assets/icons/svg/phone.svg"; import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg"; import { CategoryTag } from "@/components/ui/display/news-card"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; -import { FileAttachments } from "@/components/ui/display/file-attachment"; function formatSchedules(schedules: any[], t: (key: string) => string): string[] { if (!schedules || schedules.length === 0) return []; @@ -136,7 +135,6 @@ function VizualizareScreen() { author: item.author_name || "", created_at: item.created_at, updated_at: item.updated_at, - files: item.files || [], }; } } else if (initialTipPagina === "Facultate") { @@ -193,7 +191,7 @@ function VizualizareScreen() { } else if (isFacility) { mappedItem = { id: match.id.toString(), type: "Facilitate", title: match.name || t('common.unknownTitle'), image: match.image_url || "", content: match.description || "", schedules: match.schedules || [] }; } else { - mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: match.title || t('common.unknownTitle'), category: match.type === "NOUTATE" ? t('home.news') : t('home.events'), content: match.content || t('common.unknownContent'), image: match.image_url || "", location: match.location_name || t('common.unknownLocation'), date_start: match.start_date || "", date_end: match.end_date || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: match.created_at || "", date: match.start_date || "", author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; + mappedItem = { id: match.id.toString(), type: match.type === "NOUTATE" ? "Anunț" : "Eveniment", title: (i18n.language !== 'ro' && match.is_translated ? match.translated_title : null) || match.title || t('common.unknownTitle'), category: match.type === "NOUTATE" ? t('home.news') : t('home.events'), content: (i18n.language !== 'ro' && match.is_translated ? match.translated_content : null) || match.content || t('common.unknownContent'), image: match.image_url || "", location: match.location_name || t('common.unknownLocation'), date_start: match.start_date || "", date_end: match.end_date || "", time_start: match.start_date ? new Date(match.start_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", time_end: match.end_date ? new Date(match.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", posted_at: match.created_at || "", date: match.start_date || "", author: match.author_name || "", created_at: match.created_at, updated_at: match.updated_at }; } setItemData(mappedItem); setLoading(false); @@ -491,9 +489,6 @@ function VizualizareScreen() {
- {(tipPagina === "Anunț" || tipPagina === "Eveniment") && ( - - )}
diff --git a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx index 1dc5bcf7..a473818a 100644 --- a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx @@ -17,7 +17,7 @@ export function generateStaticParams() { } export default function AnuntScreen() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const params = useLocalSearchParams(); const id = parseEventId(params.id); const [item, setItem] = useState(null); @@ -38,7 +38,7 @@ export default function AnuntScreen() { setHasError(false); setLoading(true); try { - const res = await api.get(`/announcements/${id}`); + const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language } }); setItem(res.data); } catch (err) { console.warn("[AnuntScreen] Error loading announcement:", err); @@ -48,7 +48,7 @@ export default function AnuntScreen() { } }; run(); - }, [id, retryKey]); + }, [id, retryKey, i18n.language]); if (loading) { return ( @@ -69,8 +69,8 @@ export default function AnuntScreen() { ); } - const title = item.title || "Anunț"; - const content = item.content || ""; + const title = (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || "Anunț"; + const content = (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || ""; const image = item.image_url || ""; const date = isoToRomanianDateStr(item.created_at) || ""; const author = item.author_name || ""; diff --git a/Frontend/Mobile/src/app/(public)/cantina/index.tsx b/Frontend/Mobile/src/app/(public)/cantina/index.tsx index c8bdc26c..d6571984 100644 --- a/Frontend/Mobile/src/app/(public)/cantina/index.tsx +++ b/Frontend/Mobile/src/app/(public)/cantina/index.tsx @@ -33,7 +33,7 @@ export default function CantinaScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [menuData, setMenuData] = useState([]); const [categoriesList, setCategoriesList] = useState([]); @@ -73,8 +73,8 @@ export default function CantinaScreen() { setHasError(false); try { const [menusRes, catsRes] = await Promise.all([ - api.get('/daily-menus/', { params: { page: 1, size: 50 } }), - api.get('/product_categories/', { params: { page: 1, size: 50 } }), + api.get('/daily-menus/', { params: { page: 1, size: 50, lang: i18n.language } }), + api.get('/product_categories/', { params: { page: 1, size: 50, lang: i18n.language } }), ]); if (menusRes.data?.items) { @@ -101,15 +101,15 @@ export default function CantinaScreen() { } setLoading(false); } - }, []); + }, [i18n.language]); const onRefresh = async () => { setRefreshing(true); setHasError(false); try { const [menusRes, catsRes] = await Promise.all([ - api.get('/daily-menus/', { params: { page: 1, size: 50 } }), - api.get('/product_categories/', { params: { page: 1, size: 50 } }), + api.get('/daily-menus/', { params: { page: 1, size: 50, lang: i18n.language } }), + api.get('/product_categories/', { params: { page: 1, size: 50, lang: i18n.language } }), ]); if (menusRes.data?.items) { diff --git a/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx b/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx index 8c84efa7..d9599361 100644 --- a/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/cantina/index.web.tsx @@ -37,7 +37,7 @@ export default function CantinaScreen() { const theme = Colors[themeName]; const insets = useSafeAreaInsets(); const contentTop = useWebContentTop(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [menuData, setMenuData] = useState([]); const [categoriesList, setCategoriesList] = useState([]); @@ -83,8 +83,8 @@ export default function CantinaScreen() { if (active) setHasError(false); const [menusRes, catsRes] = await Promise.all([ - api.get('/daily-menus/', { params: { page: 1, size: 50 } }), - api.get('/product_categories/', { params: { page: 1, size: 50 } }), + api.get('/daily-menus/', { params: { page: 1, size: 50, lang: i18n.language } }), + api.get('/product_categories/', { params: { page: 1, size: 50, lang: i18n.language } }), ]); if (active) { @@ -106,7 +106,7 @@ export default function CantinaScreen() { } loadData(); return () => { active = false; }; - }, [retryKey]); + }, [retryKey, i18n.language]); const currentMenu = useMemo(() => { const dayNum = getDayNumber(selectedDay); diff --git a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx index b4564dea..430d9459 100644 --- a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx @@ -17,7 +17,7 @@ export function generateStaticParams() { } export default function EvenimentScreen() { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const params = useLocalSearchParams(); const id = parseEventId(params.id); const [ev, setEv] = useState(null); @@ -38,7 +38,7 @@ export default function EvenimentScreen() { setHasError(false); setLoading(true); try { - const res = await api.get(`/announcements/${id}`); + const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language } }); setEv(res.data); } catch (err) { console.warn("[EvenimentScreen] Error loading event:", err); @@ -48,7 +48,7 @@ export default function EvenimentScreen() { } }; run(); - }, [id, retryKey]); + }, [id, retryKey, i18n.language]); if (loading) { return ( @@ -69,8 +69,8 @@ export default function EvenimentScreen() { ); } - const title = ev.title || "Eveniment"; - const content = ev.content || ""; + const title = (i18n.language !== 'ro' && ev.is_translated ? ev.translated_title : null) || ev.title || "Eveniment"; + const content = (i18n.language !== 'ro' && ev.is_translated ? ev.translated_content : null) || ev.content || ""; const image = ev.image_url || ""; const location = ev.location_name || ""; const date_start = isoToRomanianDateStr(ev.start_date) || ""; diff --git a/Frontend/Mobile/src/app/(public)/harta.web.tsx b/Frontend/Mobile/src/app/(public)/harta.web.tsx index 42a7dea7..0b04a451 100644 --- a/Frontend/Mobile/src/app/(public)/harta.web.tsx +++ b/Frontend/Mobile/src/app/(public)/harta.web.tsx @@ -194,7 +194,7 @@ export default function HartaScreen() { "Hartă" are aceeasi marime ca "Cantina"/"Sesizări". Harta ramane in afara. */} { settingsStore.setLang(code); diff --git a/Frontend/Mobile/src/app/(public)/more/limba.web.tsx b/Frontend/Mobile/src/app/(public)/more/limba.web.tsx index fe2ff957..97b22c8c 100644 --- a/Frontend/Mobile/src/app/(public)/more/limba.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/limba.web.tsx @@ -12,6 +12,8 @@ import { WebContainer } from "@/components/ui/layout/web-container"; import { settingsStore } from "@/utils/settings-store"; import { useTranslation } from 'react-i18next'; +import { LANGUAGES } from '@/constants/languages'; + import CloseIcon from "@/assets/icons/svg/x.svg"; export default function LanguageScreen() { @@ -27,14 +29,7 @@ export default function LanguageScreen() { const { t, i18n } = useTranslation(); const currentLang = i18n.language; - const languages = [ - { code: "ro", label: "Română" }, - { code: "en", label: "English" }, - { code: "es", label: "Español" }, - { code: "fr", label: "Français" }, - { code: "de", label: "Deutsch" }, - { code: "it", label: "Italiano" } - ]; + const languages = LANGUAGES; const handleSelectLanguage = (code: string) => { settingsStore.setLang(code); diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx index 2734022c..2a97f2af 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx @@ -50,7 +50,7 @@ export default function SesizareDetaliiScreen() { const insets = useSafeAreaInsets(); const id = params.id as string; - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -72,7 +72,7 @@ export default function SesizareDetaliiScreen() { locationsData = JSON.parse(cachedLocs); } try { - const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const locsRes = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; await storage.setItem('cached_facilities', JSON.stringify(locsRes.data.items)); @@ -86,7 +86,7 @@ export default function SesizareDetaliiScreen() { }); // 2. Fetch the specific complaint - const res = await api.get(`/complaints/${id}`); + const res = await api.get(`/complaints/${id}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; setReport({ @@ -108,7 +108,7 @@ export default function SesizareDetaliiScreen() { console.error("[API] Error fetching complaint detail:", err); setError(err.message || t('reports.loadError')); } - }, [id]); + }, [id, i18n.language]); useEffect(() => { const timer = setTimeout(() => { diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx index 500956fc..d426d87a 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx @@ -50,7 +50,7 @@ export default function SesizareDetaliiScreen() { const insets = useSafeAreaInsets(); const id = params.id as string; - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -70,7 +70,7 @@ export default function SesizareDetaliiScreen() { // 1. Fetch locations for mapping let locationsData: any[] = []; try { - const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const locsRes = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; } @@ -83,7 +83,7 @@ export default function SesizareDetaliiScreen() { }); // 2. Fetch the specific complaint - const res = await api.get(`/complaints/${id}`); + const res = await api.get(`/complaints/${id}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; setReport({ @@ -107,7 +107,7 @@ export default function SesizareDetaliiScreen() { } } loadComplaint(); - }, [id, retryKey]); + }, [id, retryKey, i18n.language]); const title = report?.title || ""; const description = report?.description || ""; diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.tsx index 17074eb8..2a0fc8b5 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.tsx @@ -35,7 +35,7 @@ export default function SesizariScreen() { const params = useLocalSearchParams(); const { isAuthenticated, user, isLoading: authLoading } = useAuth(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [reports, setReports] = useState([]); const [loading, setLoading] = useState(true); @@ -66,7 +66,7 @@ export default function SesizariScreen() { locationsData = JSON.parse(cachedLocs); } try { - const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const locsRes = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; await storage.setItem('cached_facilities', JSON.stringify(locsRes.data.items)); @@ -85,30 +85,30 @@ export default function SesizariScreen() { // 3. Fetch complaints based on activeFilter let apiItems: any[] = []; if (activeFilter === "toate") { - const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50 } }); + const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50, lang: i18n.language } }); console.log('[API] Fetched all complaints:', complaintsRes.data); apiItems = complaintsRes.data?.items || []; } else if (activeFilter === "mele") { - const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50 } }); + const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50, lang: i18n.language } }); console.log('[API] Fetched my complaints:', complaintsRes.data); const allItems = complaintsRes.data?.items || []; apiItems = myProfileId ? allItems.filter((item: any) => item.user_id === myProfileId) : []; } else if (activeFilter === "active") { const [resPending, resWorking] = await Promise.all([ - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_asteptare' } }), - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_lucru' } }) + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_asteptare', lang: i18n.language } }), + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_lucru', lang: i18n.language } }) ]); console.log('[API] Fetched active complaints:', { pending: resPending.data, working: resWorking.data }); apiItems = [...(resPending.data?.items || []), ...(resWorking.data?.items || [])]; apiItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); } else if (activeFilter === "respinse") { - const res = await api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'respins' } }); + const res = await api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'respins', lang: i18n.language } }); console.log('[API] Fetched rejected complaints:', res.data); apiItems = res.data?.items || []; } else if (activeFilter === "finalizate") { const [resFinalized, resSolved] = await Promise.all([ - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'finalizat' } }), - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'solutionat' } }) + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'finalizat', lang: i18n.language } }), + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'solutionat', lang: i18n.language } }) ]); console.log('[API] Fetched finalized complaints:', { finalized: resFinalized.data, solved: resSolved.data }); apiItems = [...(resFinalized.data?.items || []), ...(resSolved.data?.items || [])]; @@ -144,7 +144,7 @@ export default function SesizariScreen() { }); return unsubscribe; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [navigation, activeFilter]); + }, [navigation, activeFilter, i18n.language]); const filteredData = reports.filter(item => { if (activeFilter === "toate") return true; diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx index 3293a7ea..0b426f50 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx @@ -39,14 +39,14 @@ export default function SesizariScreen() { const router = useRouter(); const { isAuthenticated, user, isLoading: authLoading } = useAuth(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const filters: FilterItem[] = [ - { id: "toate", title: t('reports.all') }, - { id: "mele", title: t('reports.mine') }, - { id: "active", title: t('reports.active') }, - { id: "respinse", title: t('reports.rejected') }, - { id: "finalizate", title: t('reports.completed') }, + { id: "toate" as const, title: t('reports.all') }, + { id: "mele" as const, title: t('reports.mine') }, + { id: "active" as const, title: t('reports.active') }, + { id: "respinse" as const, title: t('reports.rejected') }, + { id: "finalizate" as const, title: t('reports.completed') }, ]; const [reports, setReports] = useState([]); @@ -65,7 +65,7 @@ export default function SesizariScreen() { // 1. Fetch/load locations to build a map of id -> name let locationsData: any[] = []; try { - const locsRes = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const locsRes = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (locsRes.data?.items) { locationsData = locsRes.data.items; } @@ -84,30 +84,30 @@ export default function SesizariScreen() { // 3. Fetch complaints based on activeFilter let apiItems: any[] = []; if (activeFilter === "toate") { - const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50 } }); + const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50, lang: i18n.language } }); console.log('[API] Fetched all complaints (web):', complaintsRes.data); apiItems = complaintsRes.data?.items || []; } else if (activeFilter === "mele") { - const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50 } }); + const complaintsRes = await api.get('/complaints/', { params: { page: 1, size: 50, lang: i18n.language } }); console.log('[API] Fetched my complaints (web):', complaintsRes.data); const allItems = complaintsRes.data?.items || []; apiItems = myProfileId ? allItems.filter((item: any) => item.user_id === myProfileId) : []; } else if (activeFilter === "active") { const [resPending, resWorking] = await Promise.all([ - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_asteptare' } }), - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_lucru' } }) + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_asteptare', lang: i18n.language } }), + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'in_lucru', lang: i18n.language } }) ]); console.log('[API] Fetched active complaints (web):', { pending: resPending.data, working: resWorking.data }); apiItems = [...(resPending.data?.items || []), ...(resWorking.data?.items || [])]; apiItems.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); } else if (activeFilter === "respinse") { - const res = await api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'respins' } }); + const res = await api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'respins', lang: i18n.language } }); console.log('[API] Fetched rejected complaints (web):', res.data); apiItems = res.data?.items || []; } else if (activeFilter === "finalizate") { const [resFinalized, resSolved] = await Promise.all([ - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'finalizat' } }), - api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'solutionat' } }) + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'finalizat', lang: i18n.language } }), + api.get('/complaints/', { params: { page: 1, size: 50, complaint_status: 'solutionat', lang: i18n.language } }) ]); console.log('[API] Fetched finalized complaints (web):', { finalized: resFinalized.data, solved: resSolved.data }); apiItems = [...(resFinalized.data?.items || []), ...(resSolved.data?.items || [])]; @@ -140,7 +140,7 @@ export default function SesizariScreen() { useCallback(() => { loadData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeFilter]) + }, [activeFilter, i18n.language]) ); useEffect(() => { diff --git a/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx index 92ce244f..2aeb98d5 100644 --- a/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; -import { Linking, Pressable, Text, View } from "react-native"; +import { useEffect, useState, useRef } from "react"; +import { Linking, Pressable, Text, View, Platform } from "react-native"; import Animated, { useSharedValue, withTiming, useAnimatedStyle, interpolate, Extrapolation, Easing } from "react-native-reanimated"; import { useRouter } from "expo-router"; import { Colors, ColorScheme, Spacing } from "@/constants/theme"; @@ -21,6 +21,7 @@ export function ProfileMenu({ onToggle?: () => void; onClose?: () => void; }) { + const containerRef = useRef(null); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const router = useRouter(); @@ -54,12 +55,30 @@ export function ProfileMenu({ })); }, [open, anim]); - // Inchide meniul la scroll (ca meniul de tema). + // Inchide meniul la scroll si la click in afara (pe web). useEffect(() => { if (!open) return; const onScroll = () => close(); document.addEventListener("scroll", onScroll, true); - return () => document.removeEventListener("scroll", onScroll, true); + + let onClickOutside: any; + if (Platform.OS === 'web') { + onClickOutside = (e: MouseEvent) => { + if (containerRef.current && typeof containerRef.current.contains === 'function') { + if (!containerRef.current.contains(e.target as Node)) { + close(); + } + } + }; + document.addEventListener("click", onClickOutside, true); + } + + return () => { + document.removeEventListener("scroll", onScroll, true); + if (Platform.OS === 'web' && onClickOutside) { + document.removeEventListener("click", onClickOutside, true); + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -98,7 +117,7 @@ export function ProfileMenu({ ]; return ( - + {/* Trigger: iconita user, fara border, cu fundal plin cand e deschis (ca rotita). */} void; onClose?: () => void; }) { + const containerRef = useRef(null); const [localOpen, setLocalOpen] = useState(false); const open = controlledOpen !== undefined ? controlledOpen : localOpen; const [subMenu, setSubMenu] = useState<"tema" | "limba" | null>(null); @@ -75,9 +68,34 @@ export function ThemeMenu({ useEffect(() => { if (!open) return; - const closeOnScroll = () => close(); + const closeOnScroll = (e: Event) => { + if (containerRef.current && typeof containerRef.current.contains === 'function') { + if (containerRef.current.contains(e.target as Node)) { + return; + } + } + close(); + }; document.addEventListener("scroll", closeOnScroll, true); - return () => document.removeEventListener("scroll", closeOnScroll, true); + + let onClickOutside: any; + if (Platform.OS === 'web') { + onClickOutside = (e: MouseEvent) => { + if (containerRef.current && typeof containerRef.current.contains === 'function') { + if (!containerRef.current.contains(e.target as Node)) { + close(); + } + } + }; + document.addEventListener("click", onClickOutside, true); + } + + return () => { + document.removeEventListener("scroll", closeOnScroll, true); + if (Platform.OS === 'web' && onClickOutside) { + document.removeEventListener("click", onClickOutside, true); + } + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); @@ -97,7 +115,7 @@ export function ThemeMenu({ const langLabel = LANGUAGES.find((l) => l.code === selectedLang)?.label ?? selectedLang; return ( - + - {(subMenu === "tema" ? THEMES : LANGUAGES).map((opt: any) => { - const isSelected = subMenu === "tema" ? themeMode === opt.id : selectedLang === opt.code; - return ( - { - if (subMenu === "tema") setThemeMode(opt.id); - else settingsStore.setLang(opt.code); - setSubMenu(null); - }} - accessibilityRole="button" - style={({ pressed, hovered }: any) => [ - { paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md }, - (pressed || hovered || isSelected) && { backgroundColor: "rgba(0,0,0,0.05)" }, - ]} - > - {({ pressed, hovered }: any) => ( - - {subMenu === "tema" - ? (opt.id === "system" ? t('theme.system') : opt.id === "light" ? t('theme.light') : opt.id === "dark" ? t('theme.dark') : opt.label) - : opt.label} - - )} - - ); - })} + {subMenu === "tema" ? ( + THEMES.map((opt: any) => { + const isSelected = themeMode === opt.id; + return ( + { + setThemeMode(opt.id); + setSubMenu(null); + }} + accessibilityRole="button" + style={({ pressed, hovered }: any) => [ + { paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md }, + (pressed || hovered || isSelected) && { backgroundColor: "rgba(0,0,0,0.05)" }, + ]} + > + {({ pressed, hovered }: any) => ( + + {opt.id === "system" ? t('theme.system') : opt.id === "light" ? t('theme.light') : opt.id === "dark" ? t('theme.dark') : opt.label} + + )} + + ); + }) + ) : ( + + {LANGUAGES.map((opt: any) => { + const isSelected = selectedLang === opt.code; + return ( + { + settingsStore.setLang(opt.code); + setSubMenu(null); + }} + accessibilityRole="button" + style={({ pressed, hovered }: any) => [ + { paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md }, + (pressed || hovered || isSelected) && { backgroundColor: "rgba(0,0,0,0.05)" }, + ]} + > + {({ pressed, hovered }: any) => ( + + {opt.label} + + )} + + ); + })} + + )} diff --git a/Frontend/Mobile/src/utils/settings-store.ts b/Frontend/Mobile/src/utils/settings-store.ts index 38c91329..f10cbfef 100644 --- a/Frontend/Mobile/src/utils/settings-store.ts +++ b/Frontend/Mobile/src/utils/settings-store.ts @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; -const SUPPORTED_LANGS = ['ro', 'en']; +const SUPPORTED_LANGS = ['ro', 'en', 'es', 'fr', 'de', 'it', 'el', 'tr', 'vi', 'uk', 'ru', 'ar', 'zh', 'ja', 'ko', 'hi']; const KEY_LANG = 'settings_lang'; const KEY_THEME = 'settings_theme'; From f911e3d5b98c930aee507b2c2528b19e58a59e84 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 19:15:37 +0300 Subject: [PATCH 12/15] fix: language - more --- .../src/app/(public)/acasa/categorie.tsx | 8 +- .../src/app/(public)/acasa/categorie.web.tsx | 11 ++- .../Mobile/src/app/(public)/acasa/index.tsx | 6 +- .../src/app/(public)/acasa/index.web.tsx | 8 +- .../src/app/(public)/acasa/vizualizare.tsx | 8 +- .../app/(public)/acasa/vizualizare.web.tsx | 10 ++- Frontend/Mobile/src/app/(public)/harta.tsx | 8 +- .../Mobile/src/app/(public)/harta.web.tsx | 8 +- .../Mobile/src/app/(public)/more/_layout.tsx | 4 +- .../src/app/(public)/more/categorie.tsx | 10 ++- .../src/app/(public)/more/categorie.web.tsx | 10 ++- .../Mobile/src/app/(public)/more/index.tsx | 4 +- .../src/app/(public)/more/index.web.tsx | 4 +- .../Mobile/src/app/(public)/more/setari.tsx | 12 +-- .../src/app/(public)/more/setari.web.tsx | 2 +- .../src/app/(public)/sesizari/adauga.tsx | 6 +- .../src/app/(public)/sesizari/adauga.web.tsx | 6 +- Frontend/Mobile/src/app/index.tsx | 7 +- .../components/ui/display/article-detail.tsx | 10 +-- .../components/ui/display/hero-slideshow.tsx | 81 ++++++++++--------- .../ui/display/hero-slideshow.web.tsx | 4 +- .../components/ui/display/home-highlights.tsx | 24 +++--- .../src/components/ui/display/news-card.tsx | 6 +- .../components/ui/display/news-card.web.tsx | 6 +- .../components/ui/display/sesizare-card.tsx | 4 +- Frontend/Mobile/src/i18n/locales/ar.json | 11 ++- Frontend/Mobile/src/i18n/locales/de.json | 13 ++- Frontend/Mobile/src/i18n/locales/el.json | 13 ++- Frontend/Mobile/src/i18n/locales/en.json | 11 ++- Frontend/Mobile/src/i18n/locales/es.json | 13 ++- Frontend/Mobile/src/i18n/locales/fr.json | 11 ++- Frontend/Mobile/src/i18n/locales/hi.json | 11 ++- Frontend/Mobile/src/i18n/locales/it.json | 13 ++- Frontend/Mobile/src/i18n/locales/ja.json | 11 ++- Frontend/Mobile/src/i18n/locales/ko.json | 11 ++- Frontend/Mobile/src/i18n/locales/ro.json | 11 ++- Frontend/Mobile/src/i18n/locales/ru.json | 11 ++- Frontend/Mobile/src/i18n/locales/tr.json | 13 ++- Frontend/Mobile/src/i18n/locales/uk.json | 13 ++- Frontend/Mobile/src/i18n/locales/vi.json | 13 ++- Frontend/Mobile/src/i18n/locales/zh.json | 11 ++- 41 files changed, 304 insertions(+), 153 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index 1050cabd..dcc5a055 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -60,7 +60,7 @@ export default function CategoryScreen() { const fetchFaculties = async () => { try { const response = await api.get("/faculties/", { - params: { page: 1, size: 50 } + params: { page: 1, size: 50, lang: i18n.language } }); if (response.data && response.data.items) { setFaculties(response.data.items); @@ -125,7 +125,8 @@ export default function CategoryScreen() { response = await api.get("/faculties/", { params: { page: pageToFetch, - size: 20 + size: 20, + lang: i18n.language, } }); if (response.data && response.data.items) { @@ -143,7 +144,8 @@ export default function CategoryScreen() { response = await api.get("/facilities/", { params: { page: pageToFetch, - size: 20 + size: 20, + lang: i18n.language, } }); if (response.data && response.data.items) { diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index c3edcd52..71e864b2 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next"; export default function CategoryScreen() { const { title: categoryTitle } = useLocalSearchParams(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); @@ -54,7 +54,7 @@ export default function CategoryScreen() { const fetchFaculties = async () => { try { const response = await api.get("/faculties/", { - params: { page: 1, size: 50 } + params: { page: 1, size: 50, lang: i18n.language } }); if (response.data && response.data.items) { setFaculties(response.data.items); @@ -90,6 +90,7 @@ export default function CategoryScreen() { page: pageToFetch, size: selectedFacultyId ? 200 : 20, announcement_type: type, + lang: i18n.language, } }); @@ -119,7 +120,8 @@ export default function CategoryScreen() { response = await api.get("/faculties/", { params: { page: pageToFetch, - size: 20 + size: 20, + lang: i18n.language, } }); if (response.data && response.data.items) { @@ -137,7 +139,8 @@ export default function CategoryScreen() { response = await api.get("/facilities/", { params: { page: pageToFetch, - size: 20 + size: 20, + lang: i18n.language, } }); if (response.data && response.data.items) { diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index 364c2d21..5257e9d8 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -123,7 +123,8 @@ export default function HomeScreen() { const response = await api.get("/faculties/", { params: { page: 1, - size: 50 + size: 50, + lang: i18n.language, } }); if (response.data && response.data.items) { @@ -163,7 +164,8 @@ export default function HomeScreen() { const response = await api.get("/facilities/", { params: { page: 1, - size: 50 + size: 50, + lang: i18n.language, } }); if (response.data && response.data.items) { diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index ed5e7083..63db550f 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -101,7 +101,8 @@ export default function HomeScreen() { const response = await api.get("/faculties/", { params: { page: 1, - size: 50 + size: 50, + lang: i18n.language, } }); if (response.data && response.data.items) { @@ -128,7 +129,8 @@ export default function HomeScreen() { const response = await api.get("/facilities/", { params: { page: 1, - size: 50 + size: 50, + lang: i18n.language, } }); if (response.data && response.data.items) { @@ -279,7 +281,7 @@ export default function HomeScreen() { {/* Sectiune intre hero si carusele: 3 carduri compacte + 1 card mare. */} setImgErr(true)} style={{ width: "100%", height: "100%", position: "absolute" }} contentFit="cover" /> diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index 54777462..c144783e 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -60,6 +60,7 @@ function VizualizareScreen() { const [loading, setLoading] = useState(true); const [hasError, setHasError] = useState(false); const [retryKey, setRetryKey] = useState(0); + const [imgErr, setImgErr] = useState(false); const initialItem = { title: (params.title as string) || "", @@ -100,7 +101,7 @@ function VizualizareScreen() { const loadRelated = async () => { if (initialTipPagina === "Facilitate") { try { - const res = await api.get('/facilities/', { params: { page: 1, size: 50 } }); + const res = await api.get('/facilities/', { params: { page: 1, size: 50, lang: i18n.language } }); if (res.data?.items && isMounted) setFacilityPool(res.data.items); } catch (err) { console.warn('[API] Error loading related facilities:', err); @@ -155,7 +156,7 @@ function VizualizareScreen() { }; } } else if (initialTipPagina === "Facultate") { - const res = await api.get(`/faculties/${numericId}`); + const res = await api.get(`/faculties/${numericId}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; fetchedItem = { @@ -170,7 +171,7 @@ function VizualizareScreen() { }; } } else if (initialTipPagina === "Facilitate") { - const res = await api.get(`/facilities/${numericId}`); + const res = await api.get(`/facilities/${numericId}`, { params: { lang: i18n.language } }); if (res.data) { const item = res.data; fetchedItem = { @@ -370,7 +371,8 @@ function VizualizareScreen() { {/* Banner full-bleed: ramane pe toata latimea, in afara canvas-ului scalat */} setImgErr(true)} accessibilityLabel={(title as string) || "Imagine articol"} style={{ width: "100%", height: "100%", position: "absolute" }} contentFit="cover" diff --git a/Frontend/Mobile/src/app/(public)/harta.tsx b/Frontend/Mobile/src/app/(public)/harta.tsx index 35d8869d..0622e8ed 100644 --- a/Frontend/Mobile/src/app/(public)/harta.tsx +++ b/Frontend/Mobile/src/app/(public)/harta.tsx @@ -18,7 +18,7 @@ export default function HartaScreen() { const insets = useSafeAreaInsets(); const themeName = (useColorScheme() ?? 'light') as keyof typeof Colors; const theme = Colors[themeName]; - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [hasError, setHasError] = useState(false); const loadData = useCallback(async () => { @@ -35,8 +35,8 @@ export default function HartaScreen() { // Fetch fresh data from API const [facsRes, locsRes] = await Promise.all([ - api.get('/faculties/', { params: { page: 1, size: 50 } }), - api.get('/locations/', { params: { page: 1, size: 50 } }) + api.get('/faculties/', { params: { page: 1, size: 50, lang: i18n.language } }), + api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }) ]); if (facsRes.data?.items) { @@ -51,7 +51,7 @@ export default function HartaScreen() { console.warn('[API] Error loading map screen data:', err); setHasError(true); } - }, []); + }, [i18n.language]); useEffect(() => { const timer = setTimeout(() => { diff --git a/Frontend/Mobile/src/app/(public)/harta.web.tsx b/Frontend/Mobile/src/app/(public)/harta.web.tsx index 0b04a451..e1aac284 100644 --- a/Frontend/Mobile/src/app/(public)/harta.web.tsx +++ b/Frontend/Mobile/src/app/(public)/harta.web.tsx @@ -39,7 +39,7 @@ export default function HartaScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const contentTop = useWebContentTop(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [hasError, setHasError] = useState(false); const [retryKey, setRetryKey] = useState(0); const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(lastKnownUserLocation); @@ -103,8 +103,8 @@ export default function HartaScreen() { // Fetch fresh data from API const [facsRes, locsRes] = await Promise.all([ - api.get('/faculties/', { params: { page: 1, size: 50 } }), - api.get('/locations/', { params: { page: 1, size: 50 } }) + api.get('/faculties/', { params: { page: 1, size: 50, lang: i18n.language } }), + api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }) ]); if (active) { @@ -122,7 +122,7 @@ export default function HartaScreen() { } loadData(); return () => { active = false; }; - }, [retryKey]); + }, [retryKey, i18n.language]); const facultyFilters = useMemo(() => { return [ diff --git a/Frontend/Mobile/src/app/(public)/more/_layout.tsx b/Frontend/Mobile/src/app/(public)/more/_layout.tsx index e0961c15..1abbf75d 100644 --- a/Frontend/Mobile/src/app/(public)/more/_layout.tsx +++ b/Frontend/Mobile/src/app/(public)/more/_layout.tsx @@ -4,6 +4,7 @@ import { View } from "react-native"; import { Colors, Spacing } from "@/constants/theme"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useTranslation } from 'react-i18next'; export const unstable_settings = { initialRouteName: "index", @@ -13,6 +14,7 @@ export default function MoreLayout() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); + const { t } = useTranslation(); return ( ( - + ), }} diff --git a/Frontend/Mobile/src/app/(public)/more/categorie.tsx b/Frontend/Mobile/src/app/(public)/more/categorie.tsx index d23b98a2..2ba5c206 100644 --- a/Frontend/Mobile/src/app/(public)/more/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/more/categorie.tsx @@ -11,6 +11,7 @@ import { CategoryHeader } from "@/components/ui/display/category-header"; import * as WebBrowser from "expo-web-browser"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; export default function MoreCategoryScreen() { const { categoryId, title: categoryTitle } = useLocalSearchParams(); @@ -19,10 +20,11 @@ export default function MoreCategoryScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const [items, setItems] = useState([]); + const { t, i18n } = useTranslation(); useEffect(() => { if (!categoryId) return; - api.get('/city-guide', { params: { category_id: categoryId } }) + api.get('/city-guide', { params: { category_id: categoryId, lang: i18n.language } }) .then(res => { const data = res.data; if (data && Array.isArray(data)) { @@ -103,7 +105,7 @@ export default function MoreCategoryScreen() { ]} numberOfLines={1} > - {(categoryTitle as string) || "Ghid"} + {(categoryTitle as string) || t('more.guideFallback')} ), @@ -121,7 +123,7 @@ export default function MoreCategoryScreen() { > @@ -140,7 +142,7 @@ export default function MoreCategoryScreen() { {items.length === 0 && ( - Nu există elemente în această categorie. + {t('category.empty')} )} diff --git a/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx index ca41d055..39970b60 100644 --- a/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx @@ -11,6 +11,7 @@ import { WebContainer } from "@/components/ui/layout/web-container"; import { useWebContentTop } from "@/hooks/use-web-content-top"; import BackIcon from "@/assets/icons/svg/chevron-left.svg"; import api from "@/services/api"; +import { useTranslation } from 'react-i18next'; export default function MoreCategoryScreen() { const { categoryId, title: categoryTitle } = useLocalSearchParams(); @@ -19,13 +20,14 @@ export default function MoreCategoryScreen() { const insets = useSafeAreaInsets(); const contentTop = useWebContentTop(); const router = useRouter(); + const { t, i18n } = useTranslation(); const [scrollY] = useState(() => new Animated.Value(0)); const [items, setItems] = useState([]); useEffect(() => { if (!categoryId) return; - api.get('/city-guide', { params: { category_id: categoryId } }) + api.get('/city-guide', { params: { category_id: categoryId, lang: i18n.language } }) .then(res => { const data = res.data; if (data && Array.isArray(data)) { @@ -98,7 +100,7 @@ export default function MoreCategoryScreen() { ]} numberOfLines={1} > - {(categoryTitle as string) || "Ghid"} + {(categoryTitle as string) || t('more.guideFallback')} ), @@ -120,7 +122,7 @@ export default function MoreCategoryScreen() { @@ -139,7 +141,7 @@ export default function MoreCategoryScreen() { {items.length === 0 && ( - Nu există elemente în această categorie. + {t('category.empty')} )} diff --git a/Frontend/Mobile/src/app/(public)/more/index.tsx b/Frontend/Mobile/src/app/(public)/more/index.tsx index 7cb6818f..0473ff93 100644 --- a/Frontend/Mobile/src/app/(public)/more/index.tsx +++ b/Frontend/Mobile/src/app/(public)/more/index.tsx @@ -28,12 +28,12 @@ export default function MoreScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const { isAuthenticated, logout } = useAuth(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [categories, setCategories] = useState([]); useEffect(() => { - api.get('/city-guide/categories') + api.get('/city-guide/categories', { params: { lang: i18n.language } }) .then(res => { const data = res.data; if (data && Array.isArray(data)) { diff --git a/Frontend/Mobile/src/app/(public)/more/index.web.tsx b/Frontend/Mobile/src/app/(public)/more/index.web.tsx index 239b494d..d02ecc2e 100644 --- a/Frontend/Mobile/src/app/(public)/more/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/index.web.tsx @@ -25,11 +25,11 @@ export default function MoreScreen() { const insets = useSafeAreaInsets(); const contentTop = useWebContentTop(); const router = useRouter(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [categories, setCategories] = useState([]); useEffect(() => { - api.get('/city-guide/categories') + api.get('/city-guide/categories', { params: { lang: i18n.language } }) .then(res => { const data = res.data; if (data && Array.isArray(data)) { diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index 39487066..d523eae1 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -49,12 +49,12 @@ export default function SettingsScreen() { const languages = LANGUAGES; const handleClearCache = () => { - Alert.alert("Șterge cache", "Se va reseta onboarding-ul. La repornire vei vedea din nou cererea de permisiuni.", [ - { text: "Anulează", style: "cancel" }, + Alert.alert(t('settings.clearCacheTitle'), t('settings.clearCacheMessage'), [ + { text: t('more.cancel'), style: "cancel" }, { - text: "Șterge", style: "destructive", onPress: async () => { + text: t('settings.clearCacheConfirm'), style: "destructive", onPress: async () => { await storage.removeItem("has_seen_onboarding"); - Alert.alert("Gata", "Cache șters. Repornește aplicația."); + Alert.alert(t('settings.clearCacheDoneTitle'), t('settings.clearCacheDoneMessage')); } }, ]); @@ -185,7 +185,7 @@ export default function SettingsScreen() { })} > - {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || "Română"} + {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || i18n.language} - Șterge cache + {t('settings.clearCacheButton')} diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index b9abe089..5036575d 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -173,7 +173,7 @@ export default function SettingsScreen() { })} > - {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || "Română"} + {t('settings.currentLang')} {languages.find((l) => l.code === i18n.language)?.label || i18n.language} ([]); const [location, setLocation] = useState(t('reports.exterior')); @@ -92,7 +92,7 @@ export default function AdaugaSesizareScreen() { setLocation(parsed[0].name); } } - const res = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const res = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (res.data?.items) { setLocations(res.data.items); await storage.setItem('cached_facilities', JSON.stringify(res.data.items)); @@ -105,7 +105,7 @@ export default function AdaugaSesizareScreen() { } } loadLocations(); - }, []); + }, [i18n.language]); const buildingsList = useMemo(() => { const list = locations.map(loc => loc.name); diff --git a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx index 49d9549a..7c1bdb5f 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx @@ -54,7 +54,7 @@ export default function AdaugaSesizareScreen() { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const insets = useSafeAreaInsets(); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const [locations, setLocations] = useState([]); const [title, setTitle] = useState(""); @@ -67,7 +67,7 @@ export default function AdaugaSesizareScreen() { useEffect(() => { async function loadLocations() { try { - const res = await api.get('/locations/', { params: { page: 1, size: 50 } }); + const res = await api.get('/locations/', { params: { page: 1, size: 50, lang: i18n.language } }); if (res.data?.items) { setLocations(res.data.items); if (res.data.items.length > 0) { @@ -79,7 +79,7 @@ export default function AdaugaSesizareScreen() { } } loadLocations(); - }, []); + }, [i18n.language]); const buildingsList = useMemo(() => { const list = locations.map(loc => loc.name); diff --git a/Frontend/Mobile/src/app/index.tsx b/Frontend/Mobile/src/app/index.tsx index a282a216..9080744e 100644 --- a/Frontend/Mobile/src/app/index.tsx +++ b/Frontend/Mobile/src/app/index.tsx @@ -4,6 +4,7 @@ import { StyleSheet } from "react-native"; import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; import api, { storage } from "@/services/api"; +import i18n from "@/i18n"; export default function SplashScreen() { useEffect(() => { @@ -28,21 +29,21 @@ export default function SplashScreen() { try { await Promise.all([ api.get("/announcements/", { - params: { page: 1, size: 50 } + params: { page: 1, size: 50, lang: i18n.language } }).then(res => { if (res.data && res.data.items) { return storage.setItem('cached_announcements', JSON.stringify(res.data.items)); } }), api.get("/faculties/", { - params: { page: 1, size: 50 } + params: { page: 1, size: 50, lang: i18n.language } }).then(res => { if (res.data && res.data.items) { return storage.setItem('cached_faculties', JSON.stringify(res.data.items)); } }), api.get("/locations/", { - params: { page: 1, size: 50 } + params: { page: 1, size: 50, lang: i18n.language } }).then(res => { if (res.data && res.data.items) { return storage.setItem('cached_facilities', JSON.stringify(res.data.items)); diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index dd8fe5be..13b037da 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -28,7 +28,7 @@ import CalendarIcon from "@/assets/icons/svg/calendar.svg"; import LocationIcon from "@/assets/icons/svg/location.svg"; import PhoneIcon from "@/assets/icons/svg/phone.svg"; import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg"; -import { FileAttachments, type FileItem } from "@/components/ui/display/file-attachment"; +import { type FileItem } from "@/components/ui/display/file-attachment"; // Latimea coloanei din dreapta (sidebar) cand layout-ul e pe doua coloane. const SIDEBAR_WIDTH = 340; @@ -85,6 +85,7 @@ export function ArticleDetail({ const tipPagina = type || "Eveniment"; const [relatedPool, setRelatedPool] = useState([]); + const [imgErr, setImgErr] = useState(false); useEffect(() => { let isMounted = true; @@ -201,7 +202,8 @@ export function ArticleDetail({ {/* Banner full-bleed: ramane pe toata latimea, in afara canvas-ului scalat */} setImgErr(true)} accessibilityLabel={title || t('common.article')} style={{ width: "100%", height: "100%", position: "absolute" }} contentFit="cover" @@ -329,9 +331,7 @@ export function ArticleDetail({ - {(tipPagina === "Anunț" || tipPagina === "Eveniment") && ( - - )} + {/* Dreapta: 3 carduri Noutăți, una sub alta. */} diff --git a/Frontend/Mobile/src/components/ui/display/hero-slideshow.tsx b/Frontend/Mobile/src/components/ui/display/hero-slideshow.tsx index a5d54c3e..1d43893a 100644 --- a/Frontend/Mobile/src/components/ui/display/hero-slideshow.tsx +++ b/Frontend/Mobile/src/components/ui/display/hero-slideshow.tsx @@ -30,6 +30,7 @@ interface HeroSlideshowProps { export function HeroSlideshow({ slides, onPressItem, scrollY }: HeroSlideshowProps) { const { width: windowWidth } = useWindowDimensions(); const [active, setActive] = useState(0); + const [failedImages, setFailedImages] = useState>({}); const scrollRef = useRef(null); const autoRotateTimer = useRef | null>(null); @@ -84,48 +85,52 @@ export function HeroSlideshow({ slides, onPressItem, scrollY }: HeroSlideshowPro onScrollBeginDrag={stopAutoRotate} onScrollEndDrag={startAutoRotate} > - {slides.map((slide) => ( - onPressItem(slide)} - > - - { + const hasError = failedImages[slide.id]; + return ( + onPressItem(slide)} + > + + setFailedImages(prev => ({ ...prev, [slide.id]: true }))} + style={StyleSheet.absoluteFill} + contentFit="cover" + /> + + + - - - - - - {!!slide.category && } - - - {slide.title} - - - - {[getFormattedDate(slide.date), slide.author].filter(Boolean).join(" · ")} - + + + {!!slide.category && } + + + {slide.title} + + + + {[getFormattedDate(slide.date), slide.author].filter(Boolean).join(" · ")} + + - - - ))} + + ); + })} diff --git a/Frontend/Mobile/src/components/ui/display/hero-slideshow.web.tsx b/Frontend/Mobile/src/components/ui/display/hero-slideshow.web.tsx index a9cb8658..b9f595cf 100644 --- a/Frontend/Mobile/src/components/ui/display/hero-slideshow.web.tsx +++ b/Frontend/Mobile/src/components/ui/display/hero-slideshow.web.tsx @@ -44,6 +44,7 @@ export function HeroSlideshow({ slides, onPressItem }: HeroSlideshowProps) { const { width: windowWidth } = useWindowDimensions(); const isMobile = windowWidth < 768; const [active, setActive] = useState(0); + const [failedImages, setFailedImages] = useState>({}); // Cate o valoare de opacitate per slide (primul vizibil, restul ascunse). const [opacities] = useState(() => slides.map((_, i) => new Animated.Value(i === 0 ? 1 : 0))); @@ -86,7 +87,8 @@ export function HeroSlideshow({ slides, onPressItem }: HeroSlideshowProps) { pointerEvents="none" > setFailedImages(prev => ({ ...prev, [slide.id]: true }))} accessibilityLabel={slide.title} style={StyleSheet.absoluteFill} contentFit="cover" diff --git a/Frontend/Mobile/src/components/ui/display/home-highlights.tsx b/Frontend/Mobile/src/components/ui/display/home-highlights.tsx index 66b462bb..da326a58 100644 --- a/Frontend/Mobile/src/components/ui/display/home-highlights.tsx +++ b/Frontend/Mobile/src/components/ui/display/home-highlights.tsx @@ -5,6 +5,7 @@ // - stanga: 3 carduri compacte (poza mica + titlu/data), stil Sesizari, dar mai mic; // - dreapta: un card mare "featured" (imagine full + gradient + titlu). // Pe ecrane inguste (<768px) coloanele se stivuiesc vertical. +import { useState } from "react"; import { View, Text, Pressable, StyleSheet, useWindowDimensions } from "react-native"; import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; @@ -56,14 +57,16 @@ export function CompactCard({ item, onPress }: { item: HighlightItem; onPress: ( /** Card mare "featured": imagine full-bleed + gradient + text jos. */ function FeaturedCard({ item, onPress }: { item: HighlightItem; onPress: () => void }) { + const [imgErr, setImgErr] = useState(false); return ( - [styles.featured, { opacity: pressed ? 0.92 : 1 }]} {...({ dataSet: { card: "true" } } as any)} > setImgErr(true)} accessibilityLabel={item.title} style={[StyleSheet.absoluteFill, { zIndex: 1, overflow: "hidden" }]} contentFit="cover" @@ -95,7 +98,8 @@ export function HomeHighlights({ featured, items, onPressItem, title = "Recomand const { width } = useWindowDimensions(); const stacked = width < 768; - if (!featured && items.length === 0) return null; + // Afisam sectiunea doar cand toate cele 4 sloturi sunt ocupate (1 featured + 3 compacte). + if (!featured || items.length < 3) return null; return ( @@ -112,21 +116,19 @@ export function HomeHighlights({ featured, items, onPressItem, title = "Recomand {items.map((item) => ( onPressItem(item)} /> ))} - {featured ? onPressItem(featured)} /> : null} + onPressItem(featured)} /> ) : ( - // Ecran lat: doua coloane (3 carduri compacte stanga + card mare dreapta). + // Doua coloane (3 carduri compacte stanga + card mare dreapta) {items.map((item) => ( onPressItem(item)} /> ))} - {featured ? ( - - onPressItem(featured)} /> - - ) : null} + + onPressItem(featured)} /> + )} diff --git a/Frontend/Mobile/src/components/ui/display/news-card.tsx b/Frontend/Mobile/src/components/ui/display/news-card.tsx index 88133432..98ccea73 100644 --- a/Frontend/Mobile/src/components/ui/display/news-card.tsx +++ b/Frontend/Mobile/src/components/ui/display/news-card.tsx @@ -62,7 +62,8 @@ export function NewsCard({ const defaultWidth = variant === "list" ? SCREEN_WIDTH - Spacing.xl3 : (variant === "square" ? 180 : (width || SCREEN_WIDTH * 0.85)); const defaultHeight = variant === "list" ? 100 : (variant === "square" ? 180 : (height || (defaultWidth as number) / (16 / 10))); - const cardImage = image || DEFAULT_IMAGE; + const [imgErr, setImgErr] = React.useState(false); + const cardImage = (image && !imgErr) ? image : DEFAULT_IMAGE; if (variant === "list") { return ( @@ -82,6 +83,7 @@ export function NewsCard({ > setImgErr(true)} style={{ width: (height || defaultHeight) as any, height: (height || defaultHeight) as any, borderRadius: Spacing.lg, overflow: "hidden" }} contentFit="cover" /> @@ -117,6 +119,7 @@ export function NewsCard({ > setImgErr(true)} style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0, zIndex: 1 }} contentFit="cover" /> @@ -160,6 +163,7 @@ export function NewsCard({ > setImgErr(true)} style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0, zIndex: 1 }} contentFit="cover" /> diff --git a/Frontend/Mobile/src/components/ui/display/news-card.web.tsx b/Frontend/Mobile/src/components/ui/display/news-card.web.tsx index 02934158..9bb5fbbd 100644 --- a/Frontend/Mobile/src/components/ui/display/news-card.web.tsx +++ b/Frontend/Mobile/src/components/ui/display/news-card.web.tsx @@ -61,7 +61,8 @@ export function NewsCard({ const defaultWidth = variant === "list" ? SCREEN_WIDTH - Spacing.xl3 : (variant === "square" ? 180 : (width || SCREEN_WIDTH * 0.85)); const defaultHeight = variant === "list" ? 100 : (variant === "square" ? 180 : (height || (defaultWidth as number) / (16 / 10))); - const cardImage = image || DEFAULT_IMAGE; + const [imgErr, setImgErr] = React.useState(false); + const cardImage = (image && !imgErr) ? image : DEFAULT_IMAGE; if (variant === "list") { return ( @@ -81,6 +82,7 @@ export function NewsCard({ > setImgErr(true)} accessibilityLabel={title} style={{ width: (height || defaultHeight) as any, height: (height || defaultHeight) as any, borderRadius: Spacing.lg, overflow: "hidden" }} contentFit="cover" @@ -118,6 +120,7 @@ export function NewsCard({ setImgErr(true)} accessibilityLabel={title} style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }} contentFit="cover" @@ -164,6 +167,7 @@ export function NewsCard({ setImgErr(true)} style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }} contentFit="cover" /> diff --git a/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx b/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx index 02e34b2b..3f6026f0 100644 --- a/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx +++ b/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx @@ -27,12 +27,14 @@ export function SesizareCard({ item }: SesizareCardProps) { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; const statusLabel = item.status === "active" ? "Activă" : item.status === "respinse" ? "Respinsă" : "Soluționată"; + const [imgErr, setImgErr] = React.useState(false); return ( setImgErr(true)} style={{ width: 100, height: 100, borderRadius: 10 }} contentFit="cover" /> diff --git a/Frontend/Mobile/src/i18n/locales/ar.json b/Frontend/Mobile/src/i18n/locales/ar.json index f8bb2c5e..7984292b 100644 --- a/Frontend/Mobile/src/i18n/locales/ar.json +++ b/Frontend/Mobile/src/i18n/locales/ar.json @@ -112,7 +112,8 @@ "profileTitle": "ملفك الشخصي", "profileLoggedIn": "أنت مسجل الدخول بالفعل. هل تريد تسجيل الخروج؟", "cancel": "إلغاء", - "logout": "تسجيل الخروج" + "logout": "تسجيل الخروج", + "guideFallback": "دليل" }, "settings": { "title": "الإعدادات", @@ -122,7 +123,13 @@ "currentLang": "اللغة الحالية:", "supportInfo": "المساعدة والمعلومات", "visitWebsite": "زيارة موقع UGAL", - "appSlogan": "تم إنشاؤه لطلاب جامعة «Dunărea de Jos» في Galați" + "appSlogan": "تم إنشاؤه لطلاب جامعة «Dunărea de Jos» في Galați", + "clearCacheButton": "مسح ذاكرة التخزين المؤقت", + "clearCacheTitle": "مسح ذاكرة التخزين المؤقت", + "clearCacheMessage": "سيتم إعادة ضبط عملية الإعداد. عند إعادة التشغيل سترى طلب الأذونات مرة أخرى.", + "clearCacheConfirm": "مسح", + "clearCacheDoneTitle": "تم", + "clearCacheDoneMessage": "تم مسح ذاكرة التخزين المؤقت. أعد تشغيل التطبيق." }, "theme": { "title": "مظهر التطبيق", diff --git a/Frontend/Mobile/src/i18n/locales/de.json b/Frontend/Mobile/src/i18n/locales/de.json index 0e155032..15978b93 100644 --- a/Frontend/Mobile/src/i18n/locales/de.json +++ b/Frontend/Mobile/src/i18n/locales/de.json @@ -112,7 +112,8 @@ "profileTitle": "Dein Profil", "profileLoggedIn": "Du bist bereits angemeldet. Möchtest du dich abmelden?", "cancel": "Abbrechen", - "logout": "Abmelden" + "logout": "Abmelden", + "guideFallback": "Leitfaden" }, "settings": { "title": "Einstellungen", @@ -122,7 +123,13 @@ "currentLang": "Aktuelle Sprache:", "supportInfo": "Hilfe & Info", "visitWebsite": "UGAL-Website besuchen", - "appSlogan": "Erstellt für die Studierenden der Universität «Dunărea de Jos» in Galați" + "appSlogan": "Erstellt für die Studierenden der Universität «Dunărea de Jos» in Galați", + "clearCacheButton": "Cache leeren", + "clearCacheTitle": "Cache leeren", + "clearCacheMessage": "Das Onboarding wird zurückgesetzt. Beim nächsten Start siehst du erneut die Berechtigungsanfrage.", + "clearCacheConfirm": "Leeren", + "clearCacheDoneTitle": "Fertig", + "clearCacheDoneMessage": "Cache gelöscht. Starte die App neu." }, "theme": { "title": "App-Design", @@ -243,4 +250,4 @@ "openAssistant": "Ace-Assistenten öffnen", "closeAssistant": "Ace-Assistenten schließen" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/el.json b/Frontend/Mobile/src/i18n/locales/el.json index 1982d518..8ae3423f 100644 --- a/Frontend/Mobile/src/i18n/locales/el.json +++ b/Frontend/Mobile/src/i18n/locales/el.json @@ -112,7 +112,8 @@ "profileTitle": "Το προφίλ σας", "profileLoggedIn": "Είστε ήδη συνδεδεμένοι. Θέλετε να αποσυνδεθείτε;", "cancel": "Ακύρωση", - "logout": "Αποσύνδεση" + "logout": "Αποσύνδεση", + "guideFallback": "Οδηγός" }, "settings": { "title": "Ρυθμίσεις", @@ -122,7 +123,13 @@ "currentLang": "Τρέχουσα γλώσσα:", "supportInfo": "Βοήθεια & Πληροφορίες", "visitWebsite": "Επίσκεψη ιστότοπου UGAL", - "appSlogan": "Δημιουργήθηκε για τους φοιτητές του Πανεπιστημίου «Dunărea de Jos» του Galați" + "appSlogan": "Δημιουργήθηκε για τους φοιτητές του Πανεπιστημίου «Dunărea de Jos» του Galați", + "clearCacheButton": "Εκκαθάριση cache", + "clearCacheTitle": "Εκκαθάριση cache", + "clearCacheMessage": "Η εισαγωγή θα επαναφερθεί. Στην επόμενη εκκίνηση θα δεις ξανά το αίτημα δικαιωμάτων.", + "clearCacheConfirm": "Εκκαθάριση", + "clearCacheDoneTitle": "Έτοιμο", + "clearCacheDoneMessage": "Η cache διαγράφηκε. Επανεκκίνησε την εφαρμογή." }, "theme": { "title": "Θέμα εφαρμογής", @@ -243,4 +250,4 @@ "openAssistant": "Άνοιγμα βοηθού Ace", "closeAssistant": "Κλείσιμο βοηθού Ace" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/en.json b/Frontend/Mobile/src/i18n/locales/en.json index ac928d7f..0bac3f37 100644 --- a/Frontend/Mobile/src/i18n/locales/en.json +++ b/Frontend/Mobile/src/i18n/locales/en.json @@ -112,7 +112,8 @@ "profileTitle": "Your Profile", "profileLoggedIn": "You're already signed in. Do you want to sign out?", "cancel": "Cancel", - "logout": "Sign Out" + "logout": "Sign Out", + "guideFallback": "Guide" }, "settings": { "title": "Settings", @@ -122,7 +123,13 @@ "currentLang": "Current language:", "supportInfo": "Help & Info", "visitWebsite": "Visit UGAL Website", - "appSlogan": "Created for students of the \"Dunărea de Jos\" University of Galați" + "appSlogan": "Created for students of the \"Dunărea de Jos\" University of Galați", + "clearCacheButton": "Clear cache", + "clearCacheTitle": "Clear cache", + "clearCacheMessage": "This will reset onboarding. You'll see the permissions prompt again on next launch.", + "clearCacheConfirm": "Clear", + "clearCacheDoneTitle": "Done", + "clearCacheDoneMessage": "Cache cleared. Restart the app." }, "theme": { "title": "App Theme", diff --git a/Frontend/Mobile/src/i18n/locales/es.json b/Frontend/Mobile/src/i18n/locales/es.json index 1ccd1b46..ae590640 100644 --- a/Frontend/Mobile/src/i18n/locales/es.json +++ b/Frontend/Mobile/src/i18n/locales/es.json @@ -112,7 +112,8 @@ "profileTitle": "Tu perfil", "profileLoggedIn": "Ya has iniciado sesión. ¿Deseas cerrar sesión?", "cancel": "Cancelar", - "logout": "Cerrar sesión" + "logout": "Cerrar sesión", + "guideFallback": "Guía" }, "settings": { "title": "Ajustes", @@ -122,7 +123,13 @@ "currentLang": "Idioma actual:", "supportInfo": "Ayuda e Info", "visitWebsite": "Visitar sitio web UGAL", - "appSlogan": "Creado para los estudiantes de la Universidad \"Dunărea de Jos\" de Galați" + "appSlogan": "Creado para los estudiantes de la Universidad \"Dunărea de Jos\" de Galați", + "clearCacheButton": "Borrar caché", + "clearCacheTitle": "Borrar caché", + "clearCacheMessage": "Se restablecerá la incorporación. Al reiniciar verás de nuevo la solicitud de permisos.", + "clearCacheConfirm": "Borrar", + "clearCacheDoneTitle": "Listo", + "clearCacheDoneMessage": "Caché borrada. Reinicia la aplicación." }, "theme": { "title": "Tema de la app", @@ -243,4 +250,4 @@ "openAssistant": "Abrir asistente Ace", "closeAssistant": "Cerrar asistente Ace" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/fr.json b/Frontend/Mobile/src/i18n/locales/fr.json index 4495c616..a5568662 100644 --- a/Frontend/Mobile/src/i18n/locales/fr.json +++ b/Frontend/Mobile/src/i18n/locales/fr.json @@ -112,7 +112,8 @@ "profileTitle": "Votre profil", "profileLoggedIn": "Vous êtes déjà connecté. Voulez-vous vous déconnecter ?", "cancel": "Annuler", - "logout": "Déconnexion" + "logout": "Déconnexion", + "guideFallback": "Guide" }, "settings": { "title": "Paramètres", @@ -122,7 +123,13 @@ "currentLang": "Langue actuelle :", "supportInfo": "Aide et Info", "visitWebsite": "Visiter le site UGAL", - "appSlogan": "Créé pour les étudiants de l'Université « Dunărea de Jos » de Galați" + "appSlogan": "Créé pour les étudiants de l'Université « Dunărea de Jos » de Galați", + "clearCacheButton": "Vider le cache", + "clearCacheTitle": "Vider le cache", + "clearCacheMessage": "L'intégration sera réinitialisée. Au prochain démarrage, la demande d'autorisations réapparaîtra.", + "clearCacheConfirm": "Vider", + "clearCacheDoneTitle": "Terminé", + "clearCacheDoneMessage": "Cache vidé. Redémarre l'application." }, "theme": { "title": "Thème de l'app", diff --git a/Frontend/Mobile/src/i18n/locales/hi.json b/Frontend/Mobile/src/i18n/locales/hi.json index 430fa647..f62ec6d7 100644 --- a/Frontend/Mobile/src/i18n/locales/hi.json +++ b/Frontend/Mobile/src/i18n/locales/hi.json @@ -112,7 +112,8 @@ "profileTitle": "आपकी प्रोफ़ाइल", "profileLoggedIn": "आप पहले से लॉग इन हैं। क्या आप लॉग आउट करना चाहते हैं?", "cancel": "रद्द करें", - "logout": "लॉग आउट" + "logout": "लॉग आउट", + "guideFallback": "गाइड" }, "settings": { "title": "सेटिंग्स", @@ -122,7 +123,13 @@ "currentLang": "वर्तमान भाषा:", "supportInfo": "सहायता और जानकारी", "visitWebsite": "UGAL वेबसाइट देखें", - "appSlogan": "Galați के «Dunărea de Jos» विश्वविद्यालय के छात्रों के लिए बनाया गया" + "appSlogan": "Galați के «Dunărea de Jos» विश्वविद्यालय के छात्रों के लिए बनाया गया", + "clearCacheButton": "कैश साफ़ करें", + "clearCacheTitle": "कैश साफ़ करें", + "clearCacheMessage": "ऑनबोर्डिंग रीसेट हो जाएगी। अगली बार खोलने पर आपको फिर से अनुमति अनुरोध दिखेगा।", + "clearCacheConfirm": "साफ़ करें", + "clearCacheDoneTitle": "हो गया", + "clearCacheDoneMessage": "कैश साफ़ हो गया। ऐप को पुनरारंभ करें।" }, "theme": { "title": "ऐप थीम", diff --git a/Frontend/Mobile/src/i18n/locales/it.json b/Frontend/Mobile/src/i18n/locales/it.json index 6071b5e4..3526730b 100644 --- a/Frontend/Mobile/src/i18n/locales/it.json +++ b/Frontend/Mobile/src/i18n/locales/it.json @@ -112,7 +112,8 @@ "profileTitle": "Il tuo profilo", "profileLoggedIn": "Hai già effettuato l'accesso. Vuoi disconnetterti?", "cancel": "Annulla", - "logout": "Disconnetti" + "logout": "Disconnetti", + "guideFallback": "Guida" }, "settings": { "title": "Impostazioni", @@ -122,7 +123,13 @@ "currentLang": "Lingua attuale:", "supportInfo": "Aiuto e Info", "visitWebsite": "Visita il sito UGAL", - "appSlogan": "Creato per gli studenti dell'Università «Dunărea de Jos» di Galați" + "appSlogan": "Creato per gli studenti dell'Università «Dunărea de Jos» di Galați", + "clearCacheButton": "Cancella cache", + "clearCacheTitle": "Cancella cache", + "clearCacheMessage": "L'onboarding verrà reimpostato. Al riavvio vedrai di nuovo la richiesta di autorizzazioni.", + "clearCacheConfirm": "Cancella", + "clearCacheDoneTitle": "Fatto", + "clearCacheDoneMessage": "Cache cancellata. Riavvia l'app." }, "theme": { "title": "Tema dell'app", @@ -243,4 +250,4 @@ "openAssistant": "Apri assistente Ace", "closeAssistant": "Chiudi assistente Ace" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/ja.json b/Frontend/Mobile/src/i18n/locales/ja.json index 274488ec..9509efcb 100644 --- a/Frontend/Mobile/src/i18n/locales/ja.json +++ b/Frontend/Mobile/src/i18n/locales/ja.json @@ -112,7 +112,8 @@ "profileTitle": "あなたのプロフィール", "profileLoggedIn": "すでにログインしています。ログアウトしますか?", "cancel": "キャンセル", - "logout": "ログアウト" + "logout": "ログアウト", + "guideFallback": "ガイド" }, "settings": { "title": "設定", @@ -122,7 +123,13 @@ "currentLang": "現在の言語:", "supportInfo": "ヘルプと情報", "visitWebsite": "UGAL ウェブサイトを訪問", - "appSlogan": "Galați の「Dunărea de Jos」大学の学生のために作られました" + "appSlogan": "Galați の「Dunărea de Jos」大学の学生のために作られました", + "clearCacheButton": "キャッシュを削除", + "clearCacheTitle": "キャッシュを削除", + "clearCacheMessage": "オンボーディングがリセットされます。次回起動時に権限のリクエストが再度表示されます。", + "clearCacheConfirm": "削除", + "clearCacheDoneTitle": "完了", + "clearCacheDoneMessage": "キャッシュを削除しました。アプリを再起動してください。" }, "theme": { "title": "アプリのテーマ", diff --git a/Frontend/Mobile/src/i18n/locales/ko.json b/Frontend/Mobile/src/i18n/locales/ko.json index 7099e444..976de332 100644 --- a/Frontend/Mobile/src/i18n/locales/ko.json +++ b/Frontend/Mobile/src/i18n/locales/ko.json @@ -112,7 +112,8 @@ "profileTitle": "내 프로필", "profileLoggedIn": "이미 로그인되어 있습니다. 로그아웃하시겠습니까?", "cancel": "취소", - "logout": "로그아웃" + "logout": "로그아웃", + "guideFallback": "가이드" }, "settings": { "title": "설정", @@ -122,7 +123,13 @@ "currentLang": "현재 언어:", "supportInfo": "도움말 및 정보", "visitWebsite": "UGAL 웹사이트 방문", - "appSlogan": "Galați «Dunărea de Jos» 대학교 학생들을 위해 만들어졌습니다" + "appSlogan": "Galați «Dunărea de Jos» 대학교 학생들을 위해 만들어졌습니다", + "clearCacheButton": "캐시 지우기", + "clearCacheTitle": "캐시 지우기", + "clearCacheMessage": "온보딩이 초기화됩니다. 다음 실행 시 권한 요청이 다시 표시됩니다.", + "clearCacheConfirm": "지우기", + "clearCacheDoneTitle": "완료", + "clearCacheDoneMessage": "캐시가 삭제되었습니다. 앱을 재시작하세요." }, "theme": { "title": "앱 테마", diff --git a/Frontend/Mobile/src/i18n/locales/ro.json b/Frontend/Mobile/src/i18n/locales/ro.json index 58b4c1bb..08169734 100644 --- a/Frontend/Mobile/src/i18n/locales/ro.json +++ b/Frontend/Mobile/src/i18n/locales/ro.json @@ -112,7 +112,8 @@ "profileTitle": "Profilul tău", "profileLoggedIn": "Ești deja conectat în cont. Vrei să te deconectezi?", "cancel": "Anulează", - "logout": "Deconectare" + "logout": "Deconectare", + "guideFallback": "Ghid" }, "settings": { "title": "Setări", @@ -122,7 +123,13 @@ "currentLang": "Limbă curentă:", "supportInfo": "Asistență & Info", "visitWebsite": "Vizitează Website UGAL", - "appSlogan": "Creat pentru studenții Universității „Dunărea de Jos” din Galați" + "appSlogan": "Creat pentru studenții Universității „Dunărea de Jos” din Galați", + "clearCacheButton": "Șterge cache", + "clearCacheTitle": "Șterge cache", + "clearCacheMessage": "Se va reseta onboarding-ul. La repornire vei vedea din nou cererea de permisiuni.", + "clearCacheConfirm": "Șterge", + "clearCacheDoneTitle": "Gata", + "clearCacheDoneMessage": "Cache șters. Repornește aplicația." }, "theme": { "title": "Temă aplicație", diff --git a/Frontend/Mobile/src/i18n/locales/ru.json b/Frontend/Mobile/src/i18n/locales/ru.json index 214329c1..b362e90e 100644 --- a/Frontend/Mobile/src/i18n/locales/ru.json +++ b/Frontend/Mobile/src/i18n/locales/ru.json @@ -112,7 +112,8 @@ "profileTitle": "Ваш профиль", "profileLoggedIn": "Вы уже вошли. Хотите выйти?", "cancel": "Отмена", - "logout": "Выйти" + "logout": "Выйти", + "guideFallback": "Гид" }, "settings": { "title": "Настройки", @@ -122,7 +123,13 @@ "currentLang": "Текущий язык:", "supportInfo": "Помощь и Информация", "visitWebsite": "Посетить сайт UGAL", - "appSlogan": "Создано для студентов Университета «Dunărea de Jos» в Galați" + "appSlogan": "Создано для студентов Университета «Dunărea de Jos» в Galați", + "clearCacheButton": "Очистить кэш", + "clearCacheTitle": "Очистить кэш", + "clearCacheMessage": "Онбординг будет сброшен. При следующем запуске вы снова увидите запрос разрешений.", + "clearCacheConfirm": "Очистить", + "clearCacheDoneTitle": "Готово", + "clearCacheDoneMessage": "Кэш очищен. Перезапустите приложение." }, "theme": { "title": "Тема приложения", diff --git a/Frontend/Mobile/src/i18n/locales/tr.json b/Frontend/Mobile/src/i18n/locales/tr.json index ddee384e..a7e5b80a 100644 --- a/Frontend/Mobile/src/i18n/locales/tr.json +++ b/Frontend/Mobile/src/i18n/locales/tr.json @@ -112,7 +112,8 @@ "profileTitle": "Profiliniz", "profileLoggedIn": "Zaten giriş yaptınız. Çıkış yapmak istiyor musunuz?", "cancel": "İptal", - "logout": "Çıkış yap" + "logout": "Çıkış yap", + "guideFallback": "Rehber" }, "settings": { "title": "Ayarlar", @@ -122,7 +123,13 @@ "currentLang": "Mevcut dil:", "supportInfo": "Yardım ve Bilgi", "visitWebsite": "UGAL web sitesini ziyaret et", - "appSlogan": "Galați «Dunărea de Jos» Üniversitesi öğrencileri için oluşturuldu" + "appSlogan": "Galați «Dunărea de Jos» Üniversitesi öğrencileri için oluşturuldu", + "clearCacheButton": "Önbelleği temizle", + "clearCacheTitle": "Önbelleği temizle", + "clearCacheMessage": "Katılım süreci sıfırlanacak. Yeniden başlattığında izin isteğini tekrar göreceksin.", + "clearCacheConfirm": "Temizle", + "clearCacheDoneTitle": "Tamam", + "clearCacheDoneMessage": "Önbellek temizlendi. Uygulamayı yeniden başlat." }, "theme": { "title": "Uygulama teması", @@ -243,4 +250,4 @@ "openAssistant": "Ace asistanını aç", "closeAssistant": "Ace asistanını kapat" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/uk.json b/Frontend/Mobile/src/i18n/locales/uk.json index a0e1f75f..7d46d2f1 100644 --- a/Frontend/Mobile/src/i18n/locales/uk.json +++ b/Frontend/Mobile/src/i18n/locales/uk.json @@ -112,7 +112,8 @@ "profileTitle": "Ваш профіль", "profileLoggedIn": "Ви вже увійшли. Бажаєте вийти?", "cancel": "Скасувати", - "logout": "Вийти" + "logout": "Вийти", + "guideFallback": "Гід" }, "settings": { "title": "Налаштування", @@ -122,7 +123,13 @@ "currentLang": "Поточна мова:", "supportInfo": "Допомога та Інформація", "visitWebsite": "Відвідати сайт UGAL", - "appSlogan": "Створено для студентів Університету «Dunărea de Jos» у Galați" + "appSlogan": "Створено для студентів Університету «Dunărea de Jos» у Galați", + "clearCacheButton": "Очистити кеш", + "clearCacheTitle": "Очистити кеш", + "clearCacheMessage": "Онбординг буде скинуто. Під час наступного запуску ти знову побачиш запит дозволів.", + "clearCacheConfirm": "Очистити", + "clearCacheDoneTitle": "Готово", + "clearCacheDoneMessage": "Кеш очищено. Перезапусти застосунок." }, "theme": { "title": "Тема додатку", @@ -243,4 +250,4 @@ "openAssistant": "Відкрити помічника Ace", "closeAssistant": "Закрити помічника Ace" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/vi.json b/Frontend/Mobile/src/i18n/locales/vi.json index e6695c5b..ced91915 100644 --- a/Frontend/Mobile/src/i18n/locales/vi.json +++ b/Frontend/Mobile/src/i18n/locales/vi.json @@ -112,7 +112,8 @@ "profileTitle": "Hồ sơ của bạn", "profileLoggedIn": "Bạn đã đăng nhập. Bạn có muốn đăng xuất không?", "cancel": "Hủy", - "logout": "Đăng xuất" + "logout": "Đăng xuất", + "guideFallback": "Hướng dẫn" }, "settings": { "title": "Cài đặt", @@ -122,7 +123,13 @@ "currentLang": "Ngôn ngữ hiện tại:", "supportInfo": "Trợ giúp & Thông tin", "visitWebsite": "Truy cập trang web UGAL", - "appSlogan": "Được tạo cho sinh viên Trường Đại học «Dunărea de Jos» tại Galați" + "appSlogan": "Được tạo cho sinh viên Trường Đại học «Dunărea de Jos» tại Galați", + "clearCacheButton": "Xóa bộ nhớ đệm", + "clearCacheTitle": "Xóa bộ nhớ đệm", + "clearCacheMessage": "Quá trình giới thiệu sẽ được đặt lại. Khi khởi động lại, bạn sẽ thấy lại yêu cầu quyền.", + "clearCacheConfirm": "Xóa", + "clearCacheDoneTitle": "Xong", + "clearCacheDoneMessage": "Đã xóa bộ nhớ đệm. Hãy khởi động lại ứng dụng." }, "theme": { "title": "Giao diện ứng dụng", @@ -243,4 +250,4 @@ "openAssistant": "Mở trợ lý Ace", "closeAssistant": "Đóng trợ lý Ace" } -} \ No newline at end of file +} diff --git a/Frontend/Mobile/src/i18n/locales/zh.json b/Frontend/Mobile/src/i18n/locales/zh.json index 49c25f64..ab5f1462 100644 --- a/Frontend/Mobile/src/i18n/locales/zh.json +++ b/Frontend/Mobile/src/i18n/locales/zh.json @@ -112,7 +112,8 @@ "profileTitle": "您的个人资料", "profileLoggedIn": "您已登录。是否要退出?", "cancel": "取消", - "logout": "登出" + "logout": "登出", + "guideFallback": "指南" }, "settings": { "title": "设置", @@ -122,7 +123,13 @@ "currentLang": "当前语言:", "supportInfo": "帮助与信息", "visitWebsite": "访问 UGAL 官网", - "appSlogan": "专为加拉茨「Dunărea de Jos」大学学生创建" + "appSlogan": "专为加拉茨「Dunărea de Jos」大学学生创建", + "clearCacheButton": "清除缓存", + "clearCacheTitle": "清除缓存", + "clearCacheMessage": "引导流程将被重置。下次启动时你将再次看到权限请求。", + "clearCacheConfirm": "清除", + "clearCacheDoneTitle": "完成", + "clearCacheDoneMessage": "缓存已清除。请重启应用。" }, "theme": { "title": "应用主题", From d72100f01dbf2b71358cbf8158bba9fdfaf47418 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 19:51:12 +0300 Subject: [PATCH 13/15] fix: languages pentru pagina de onboarding --- Frontend/Mobile/app.json | 3 +- Frontend/Mobile/package-lock.json | 20 ++++++ Frontend/Mobile/package.json | 1 + .../Mobile/src/app/(public)/more/setari.tsx | 2 +- .../src/app/(public)/sesizari/adauga.tsx | 4 +- .../src/app/(public)/sesizari/adauga.web.tsx | 4 +- .../src/app/(public)/sesizari/detalii.web.tsx | 62 +++++++++---------- .../src/app/(public)/sesizari/index.web.tsx | 4 +- Frontend/Mobile/src/app/ace.tsx | 6 +- .../components/ui/display/sesizare-card.tsx | 4 +- .../src/components/ui/layout/expandable.tsx | 2 +- .../components/ui/layout/expandable.web.tsx | 2 +- Frontend/Mobile/src/i18n/locales/ar.json | 9 ++- Frontend/Mobile/src/i18n/locales/de.json | 9 ++- Frontend/Mobile/src/i18n/locales/el.json | 9 ++- Frontend/Mobile/src/i18n/locales/en.json | 9 ++- Frontend/Mobile/src/i18n/locales/es.json | 9 ++- Frontend/Mobile/src/i18n/locales/fr.json | 9 ++- Frontend/Mobile/src/i18n/locales/hi.json | 9 ++- Frontend/Mobile/src/i18n/locales/it.json | 9 ++- Frontend/Mobile/src/i18n/locales/ja.json | 9 ++- Frontend/Mobile/src/i18n/locales/ko.json | 9 ++- Frontend/Mobile/src/i18n/locales/ro.json | 9 ++- Frontend/Mobile/src/i18n/locales/ru.json | 9 ++- Frontend/Mobile/src/i18n/locales/tr.json | 9 ++- Frontend/Mobile/src/i18n/locales/uk.json | 9 ++- Frontend/Mobile/src/i18n/locales/vi.json | 9 ++- Frontend/Mobile/src/i18n/locales/zh.json | 9 ++- Frontend/Mobile/src/utils/settings-store.ts | 6 +- 29 files changed, 184 insertions(+), 80 deletions(-) diff --git a/Frontend/Mobile/app.json b/Frontend/Mobile/app.json index f83940c6..5eb70cb5 100644 --- a/Frontend/Mobile/app.json +++ b/Frontend/Mobile/app.json @@ -62,7 +62,8 @@ } } ], - "expo-image" + "expo-image", + "expo-localization" ], "experiments": { "typedRoutes": true, diff --git a/Frontend/Mobile/package-lock.json b/Frontend/Mobile/package-lock.json index cc8b1074..ecfe597e 100644 --- a/Frontend/Mobile/package-lock.json +++ b/Frontend/Mobile/package-lock.json @@ -23,6 +23,7 @@ "expo-image-picker": "~56.0.18", "expo-linear-gradient": "^56.0.4", "expo-linking": "~56.0.14", + "expo-localization": "~56.0.6", "expo-location": "~56.0.18", "expo-notifications": "~56.0.18", "expo-router": "~56.2.11", @@ -6847,6 +6848,19 @@ "react-native": "*" } }, + "node_modules/expo-localization": { + "version": "56.0.6", + "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-56.0.6.tgz", + "integrity": "sha512-zzBVoUFHCVNBywcxGsspoZeIXebihOo/AnmQYE4jMv8gHCSKlLNFT+ft+0+mWcZCMs9necvUs8S8TDonAu/xBA==", + "license": "MIT", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, "node_modules/expo-location": { "version": "56.0.18", "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.18.tgz", @@ -11509,6 +11523,12 @@ "node": ">=4" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-array-concat": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", diff --git a/Frontend/Mobile/package.json b/Frontend/Mobile/package.json index 196dc65e..bc8544dc 100644 --- a/Frontend/Mobile/package.json +++ b/Frontend/Mobile/package.json @@ -18,6 +18,7 @@ "expo-image-picker": "~56.0.18", "expo-linear-gradient": "^56.0.4", "expo-linking": "~56.0.14", + "expo-localization": "~56.0.6", "expo-location": "~56.0.18", "expo-notifications": "~56.0.18", "expo-router": "~56.2.11", diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index d523eae1..fc7d67c2 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -228,7 +228,7 @@ export default function SettingsScreen() { opacity: pressed ? 0.6 : 1 })} > - {t('settings.clearCacheButton')} + {t('settings.clearCacheButton')} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx b/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx index c614f311..dff1d875 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.tsx @@ -258,7 +258,7 @@ export default function AdaugaSesizareScreen() { setTitle(text); if (errors.title) setErrors({ ...errors, title: undefined }); }} - placeholder="Ex: Încălzire defectă în amfiteatru" + placeholder={t('reports.titlePlaceholder')} placeholderTextColor={theme.textSecondary} style={{ height: 56, @@ -288,7 +288,7 @@ export default function AdaugaSesizareScreen() { setDescription(text); if (errors.description) setErrors({ ...errors, description: undefined }); }} - placeholder="Descrie în detaliu problema întâmpinată..." + placeholder={t('reports.descPlaceholder')} placeholderTextColor={theme.textSecondary} multiline numberOfLines={4} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx index 7c1bdb5f..1c1c7d43 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx @@ -245,7 +245,7 @@ export default function AdaugaSesizareScreen() { setTitle(text); if (errors.title) setErrors({ ...errors, title: undefined }); }} - placeholder="Ex: Încălzire defectă în amfiteatru" + placeholder={t('reports.titlePlaceholder')} placeholderTextColor={theme.textSecondary} style={{ height: 56, @@ -275,7 +275,7 @@ export default function AdaugaSesizareScreen() { setDescription(text); if (errors.description) setErrors({ ...errors, description: undefined }); }} - placeholder="Descrie în detaliu problema întâmpinată..." + placeholder={t('reports.descPlaceholder')} placeholderTextColor={theme.textSecondary} multiline numberOfLines={4} diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx index d426d87a..88c4f531 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx @@ -88,22 +88,22 @@ export default function SesizareDetaliiScreen() { const item = res.data; setReport({ id: item.id.toString(), - title: item.title || "Titlu lipsă", - description: item.description || "Nicio descriere adăugată.", - category: "General", - location: locationMap.get(item.location_id) || "Locație nespecificată", + title: item.title || t('reports.missingTitle'), + description: item.description || t('reports.noDescription'), + category: t('reports.general'), + location: locationMap.get(item.location_id) || t('reports.unknownLocation'), status: mapApiStatus(item.status), - date: item.created_at || "Dată nespecificată", + date: item.created_at || t('common.unknownDate'), image: resolveImageUrl(item.image_url) || "", }); } else { - setError("Sesizarea nu a putut fi găsită."); + setError(t('reports.notFound')); } setLoading(false); } catch (err: any) { setLoading(false); console.error("[API] Error fetching complaint detail web:", err); - setError(err.message || "A apărut o eroare la încărcarea sesizării."); + setError(err.message || t('reports.loadError')); } } loadComplaint(); @@ -111,11 +111,11 @@ export default function SesizareDetaliiScreen() { const title = report?.title || ""; const description = report?.description || ""; - const location = report?.location || "Locație nespecificată"; + const location = report?.location || t('reports.unknownLocation'); const status = report?.status || "active"; - const date = report?.date || "Dată nespecificată"; + const date = report?.date || t('common.unknownDate'); - const statusLabel = status === "active" ? "Activă" : status === "respinse" ? "Respinsă" : "Soluționată"; + const statusLabel = status === "active" ? t('reports.statusActive') : status === "respinse" ? t('reports.statusRejected') : t('reports.statusCompleted'); const [modalVisible, setModalVisible] = useState(false); const [selectedImage, setSelectedImage] = useState(null); @@ -135,25 +135,25 @@ export default function SesizareDetaliiScreen() { switch (status) { case "active": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "În curs de analiză", desc: "Un administrator evaluează detaliile problemei.", active: true, completed: true }, - { title: "Soluționare finalizată", desc: "Echipa va interveni pentru a remedia situația.", completed: false }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2ActiveTitle'), desc: t('reports.step2ActiveDesc'), active: true, completed: true }, + { title: t('reports.step3ActiveTitle'), desc: t('reports.step3ActiveDesc'), completed: false }, ]; case "respinse": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "Respinsă", desc: "Solicitarea a fost respinsă de către echipa administrativă.", completed: true, isError: true }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2RejectedTitle'), desc: t('reports.step2RejectedDesc'), completed: true, isError: true }, ]; case "finalizate": return [ - { title: "Sesizare înregistrată", desc: "Sesizarea a fost salvată în sistem.", completed: true, date }, - { title: "În analiză administrativă", desc: "Problema a fost procesată cu succes.", completed: true }, - { title: "Soluționată", desc: "Problema a fost rezolvată în teren de personalul tehnic.", completed: true, isSuccess: true }, + { title: t('reports.step1Title'), desc: t('reports.step1Desc'), completed: true, date }, + { title: t('reports.step2CompletedTitle'), desc: t('reports.step2CompletedDesc'), completed: true }, + { title: t('reports.step3CompletedTitle'), desc: t('reports.step3CompletedDesc'), completed: true, isSuccess: true }, ]; default: return []; } - }, [status, date]); + }, [status, date, t]); if (loading) { return ( @@ -171,8 +171,8 @@ export default function SesizareDetaliiScreen() { @@ -201,13 +201,13 @@ export default function SesizareDetaliiScreen() { - setRetryKey(prev => prev + 1)} style={{ minHeight: 500, paddingVertical: Spacing.xl4 }} /> @@ -233,8 +233,8 @@ export default function SesizareDetaliiScreen() {
@@ -243,7 +243,7 @@ export default function SesizareDetaliiScreen() { - Informații sesizare + {t('reports.infoTitle')} @@ -264,7 +264,7 @@ export default function SesizareDetaliiScreen() { - Descriere problemă + {t('reports.descSection')} {description} @@ -290,7 +290,7 @@ export default function SesizareDetaliiScreen() { /> - Istoric progres + {t('reports.progressTitle')} {steps.map((step, index) => { diff --git a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx index 0b426f50..307d9b66 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx @@ -171,8 +171,8 @@ export default function SesizariScreen() { return ( - - {renderFormattedText(text, { color: theme.text }, { fontWeight: '700', color: theme.text })} + + {renderFormattedText(text, { color: '#FFFFFF' }, { fontWeight: '700', color: '#FFFFFF' })} diff --git a/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx b/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx index 3f6026f0..3399a300 100644 --- a/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx +++ b/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx @@ -6,6 +6,7 @@ import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import AlertOctagonIcon from "@/assets/icons/svg/alert-octagon.svg"; import { getFormattedDate } from "@/utils/date"; +import { useTranslation } from 'react-i18next'; export interface Sesizare { id: string; @@ -26,7 +27,8 @@ interface SesizareCardProps { export function SesizareCard({ item }: SesizareCardProps) { const themeName = (useColorScheme() ?? "light") as keyof typeof Colors; const theme = Colors[themeName]; - const statusLabel = item.status === "active" ? "Activă" : item.status === "respinse" ? "Respinsă" : "Soluționată"; + const { t } = useTranslation(); + const statusLabel = item.status === "active" ? t('reports.statusActive') : item.status === "respinse" ? t('reports.statusRejected') : t('reports.statusCompleted'); const [imgErr, setImgErr] = React.useState(false); return ( diff --git a/Frontend/Mobile/src/components/ui/layout/expandable.tsx b/Frontend/Mobile/src/components/ui/layout/expandable.tsx index abfc8970..171212ec 100644 --- a/Frontend/Mobile/src/components/ui/layout/expandable.tsx +++ b/Frontend/Mobile/src/components/ui/layout/expandable.tsx @@ -37,7 +37,7 @@ export function Expandable({ title, children, initialExpanded = false, expanded, flexDirection: "row", justifyContent: "space-between", alignItems: "center", - paddingTop: Spacing.md, + paddingTop: Spacing.lg, paddingBottom: 0, paddingHorizontal: 0, }, diff --git a/Frontend/Mobile/src/components/ui/layout/expandable.web.tsx b/Frontend/Mobile/src/components/ui/layout/expandable.web.tsx index d0c0a150..69ec43cf 100644 --- a/Frontend/Mobile/src/components/ui/layout/expandable.web.tsx +++ b/Frontend/Mobile/src/components/ui/layout/expandable.web.tsx @@ -41,7 +41,7 @@ export function Expandable({ title, children, initialExpanded = false, expanded, flexDirection: "row", justifyContent: "space-between", alignItems: "center", - paddingVertical: Spacing.md, + paddingVertical: Spacing.lg, paddingHorizontal: 0, }, { opacity: pressed ? 0.7 : 1 }, diff --git a/Frontend/Mobile/src/i18n/locales/ar.json b/Frontend/Mobile/src/i18n/locales/ar.json index 7984292b..18ff831c 100644 --- a/Frontend/Mobile/src/i18n/locales/ar.json +++ b/Frontend/Mobile/src/i18n/locales/ar.json @@ -78,7 +78,8 @@ "errorTitle": "عذراً! حدث خطأ ما...", "viewAll": "عرض المزيد", "university": "الجامعة", - "universityPlatform": "منصتك الجامعية" + "universityPlatform": "منصتك الجامعية", + "error": "خطأ" }, "days": { "1": "الاثنين", @@ -203,7 +204,11 @@ "step3CompletedTitle": "محلولة", "step3CompletedDesc": "تم حل المشكلة في الموقع من قبل الفريق التقني.", "general": "عام", - "exterior": "خارجي" + "exterior": "خارجي", + "detailsFallbackTitle": "تفاصيل البلاغ", + "titlePlaceholder": "مثال: تدفئة معطلة في المدرج", + "descPlaceholder": "صف المشكلة التي واجهتها بالتفصيل...", + "seoDescription": "أبلغ عن مشاكل الحرم الجامعي والسكن الطلابي وتابع حالة بلاغاتك — InsideUGAL." }, "auth": { "title": "تسجيل الدخول", diff --git a/Frontend/Mobile/src/i18n/locales/de.json b/Frontend/Mobile/src/i18n/locales/de.json index 15978b93..cf1e0321 100644 --- a/Frontend/Mobile/src/i18n/locales/de.json +++ b/Frontend/Mobile/src/i18n/locales/de.json @@ -78,7 +78,8 @@ "errorTitle": "Ups! Etwas ist schiefgelaufen...", "viewAll": "Mehr anzeigen", "university": "Universität", - "universityPlatform": "Deine Universitätsplattform" + "universityPlatform": "Deine Universitätsplattform", + "error": "Fehler" }, "days": { "1": "Montag", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Gelöst", "step3CompletedDesc": "Das Problem wurde vor Ort vom technischen Personal behoben.", "general": "Allgemein", - "exterior": "Außenbereich" + "exterior": "Außenbereich", + "detailsFallbackTitle": "Meldungsdetails", + "titlePlaceholder": "Z. B.: Heizung im Hörsaal defekt", + "descPlaceholder": "Beschreibe das Problem im Detail...", + "seoDescription": "Melde Probleme auf dem Campus und in den Wohnheimen und verfolge den Status deiner Meldungen — InsideUGAL." }, "auth": { "title": "Anmelden", diff --git a/Frontend/Mobile/src/i18n/locales/el.json b/Frontend/Mobile/src/i18n/locales/el.json index 8ae3423f..2b11a423 100644 --- a/Frontend/Mobile/src/i18n/locales/el.json +++ b/Frontend/Mobile/src/i18n/locales/el.json @@ -78,7 +78,8 @@ "errorTitle": "Ωχ! Κάτι πήγε στραβά...", "viewAll": "Προβολή περισσότερων", "university": "Πανεπιστήμιο", - "universityPlatform": "Η πανεπιστημιακή σας πλατφόρμα" + "universityPlatform": "Η πανεπιστημιακή σας πλατφόρμα", + "error": "Σφάλμα" }, "days": { "1": "Δευτέρα", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Επιλύθηκε", "step3CompletedDesc": "Το πρόβλημα επιλύθηκε επί τόπου από το τεχνικό προσωπικό.", "general": "Γενικά", - "exterior": "Εξωτερικό" + "exterior": "Εξωτερικό", + "detailsFallbackTitle": "Λεπτομέρειες αναφοράς", + "titlePlaceholder": "Π.χ.: Χαλασμένη θέρμανση στο αμφιθέατρο", + "descPlaceholder": "Περιέγραψε λεπτομερώς το πρόβλημα που αντιμετώπισες...", + "seoDescription": "Ανέφερε προβλήματα στο πανεπιστήμιο και στις εστίες και παρακολούθησε την κατάσταση των αναφορών σου — InsideUGAL." }, "auth": { "title": "Σύνδεση", diff --git a/Frontend/Mobile/src/i18n/locales/en.json b/Frontend/Mobile/src/i18n/locales/en.json index 0bac3f37..6f5ea5dd 100644 --- a/Frontend/Mobile/src/i18n/locales/en.json +++ b/Frontend/Mobile/src/i18n/locales/en.json @@ -78,7 +78,8 @@ "errorTitle": "Oops! Something went wrong...", "viewAll": "View all", "university": "University", - "universityPlatform": "Your university platform" + "universityPlatform": "Your university platform", + "error": "Error" }, "days": { "1": "Monday", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Resolved", "step3CompletedDesc": "The issue was resolved on-site by the technical staff.", "general": "General", - "exterior": "Exterior" + "exterior": "Exterior", + "detailsFallbackTitle": "Report details", + "titlePlaceholder": "E.g.: Broken heating in the amphitheater", + "descPlaceholder": "Describe the issue you encountered in detail...", + "seoDescription": "Report campus and dorm issues and track the status of your reports — InsideUGAL." }, "auth": { "title": "Sign In", diff --git a/Frontend/Mobile/src/i18n/locales/es.json b/Frontend/Mobile/src/i18n/locales/es.json index ae590640..c3842016 100644 --- a/Frontend/Mobile/src/i18n/locales/es.json +++ b/Frontend/Mobile/src/i18n/locales/es.json @@ -78,7 +78,8 @@ "errorTitle": "¡Ups! Algo salió mal...", "viewAll": "Ver más", "university": "Universidad", - "universityPlatform": "Tu plataforma universitaria" + "universityPlatform": "Tu plataforma universitaria", + "error": "Error" }, "days": { "1": "Lunes", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Resuelta", "step3CompletedDesc": "El problema fue resuelto en el lugar por el personal técnico.", "general": "General", - "exterior": "Exterior" + "exterior": "Exterior", + "detailsFallbackTitle": "Detalles del reporte", + "titlePlaceholder": "Ej.: Calefacción averiada en el anfiteatro", + "descPlaceholder": "Describe en detalle el problema que encontraste...", + "seoDescription": "Reporta problemas del campus y las residencias y sigue el estado de tus reportes — InsideUGAL." }, "auth": { "title": "Iniciar sesión", diff --git a/Frontend/Mobile/src/i18n/locales/fr.json b/Frontend/Mobile/src/i18n/locales/fr.json index a5568662..3ccb1ad2 100644 --- a/Frontend/Mobile/src/i18n/locales/fr.json +++ b/Frontend/Mobile/src/i18n/locales/fr.json @@ -78,7 +78,8 @@ "errorTitle": "Oups ! Quelque chose s'est mal passé...", "viewAll": "Voir plus", "university": "Université", - "universityPlatform": "Votre plateforme universitaire" + "universityPlatform": "Votre plateforme universitaire", + "error": "Erreur" }, "days": { "1": "Lundi", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Résolu", "step3CompletedDesc": "Le problème a été résolu sur place par le personnel technique.", "general": "Général", - "exterior": "Extérieur" + "exterior": "Extérieur", + "detailsFallbackTitle": "Détails du signalement", + "titlePlaceholder": "Ex : Chauffage en panne dans l'amphithéâtre", + "descPlaceholder": "Décris en détail le problème rencontré...", + "seoDescription": "Signale les problèmes du campus et des résidences et suis le statut de tes signalements — InsideUGAL." }, "auth": { "title": "Connexion", diff --git a/Frontend/Mobile/src/i18n/locales/hi.json b/Frontend/Mobile/src/i18n/locales/hi.json index f62ec6d7..e011d45f 100644 --- a/Frontend/Mobile/src/i18n/locales/hi.json +++ b/Frontend/Mobile/src/i18n/locales/hi.json @@ -78,7 +78,8 @@ "errorTitle": "अरे! कुछ गलत हो गया...", "viewAll": "और देखें", "university": "विश्वविद्यालय", - "universityPlatform": "आपका विश्वविद्यालय प्लेटफ़ॉर्म" + "universityPlatform": "आपका विश्वविद्यालय प्लेटफ़ॉर्म", + "error": "त्रुटि" }, "days": { "1": "सोमवार", @@ -203,7 +204,11 @@ "step3CompletedTitle": "हल किया गया", "step3CompletedDesc": "तकनीकी टीम ने मौके पर समस्या हल की।", "general": "सामान्य", - "exterior": "बाहरी" + "exterior": "बाहरी", + "detailsFallbackTitle": "रिपोर्ट विवरण", + "titlePlaceholder": "उदा: एम्फीथिएटर में हीटिंग खराब है", + "descPlaceholder": "सामने आई समस्या का विस्तार से वर्णन करें...", + "seoDescription": "कैंपस और छात्रावास की समस्याओं की रिपोर्ट करें और अपनी रिपोर्ट की स्थिति ट्रैक करें — InsideUGAL." }, "auth": { "title": "लॉग इन", diff --git a/Frontend/Mobile/src/i18n/locales/it.json b/Frontend/Mobile/src/i18n/locales/it.json index 3526730b..f2794d30 100644 --- a/Frontend/Mobile/src/i18n/locales/it.json +++ b/Frontend/Mobile/src/i18n/locales/it.json @@ -78,7 +78,8 @@ "errorTitle": "Ops! Qualcosa è andato storto...", "viewAll": "Vedi altro", "university": "Università", - "universityPlatform": "La tua piattaforma universitaria" + "universityPlatform": "La tua piattaforma universitaria", + "error": "Errore" }, "days": { "1": "Lunedì", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Risolta", "step3CompletedDesc": "Il problema è stato risolto in loco dal personale tecnico.", "general": "Generale", - "exterior": "Esterno" + "exterior": "Esterno", + "detailsFallbackTitle": "Dettagli segnalazione", + "titlePlaceholder": "Es.: Riscaldamento guasto nell'anfiteatro", + "descPlaceholder": "Descrivi in dettaglio il problema riscontrato...", + "seoDescription": "Segnala problemi del campus e dei collegi e monitora lo stato delle tue segnalazioni — InsideUGAL." }, "auth": { "title": "Accedi", diff --git a/Frontend/Mobile/src/i18n/locales/ja.json b/Frontend/Mobile/src/i18n/locales/ja.json index 9509efcb..a047cccb 100644 --- a/Frontend/Mobile/src/i18n/locales/ja.json +++ b/Frontend/Mobile/src/i18n/locales/ja.json @@ -78,7 +78,8 @@ "errorTitle": "おっと!問題が発生しました...", "viewAll": "もっと見る", "university": "大学", - "universityPlatform": "あなたの大学プラットフォーム" + "universityPlatform": "あなたの大学プラットフォーム", + "error": "エラー" }, "days": { "1": "月曜日", @@ -203,7 +204,11 @@ "step3CompletedTitle": "解決済み", "step3CompletedDesc": "技術スタッフが現場で問題を解決しました。", "general": "一般", - "exterior": "外部" + "exterior": "外部", + "detailsFallbackTitle": "報告の詳細", + "titlePlaceholder": "例:講堂の暖房が故障している", + "descPlaceholder": "発生した問題を詳しく説明してください...", + "seoDescription": "キャンパスや寮の問題を報告し、報告の状況を確認できます — InsideUGAL。" }, "auth": { "title": "ログイン", diff --git a/Frontend/Mobile/src/i18n/locales/ko.json b/Frontend/Mobile/src/i18n/locales/ko.json index 976de332..ebf07019 100644 --- a/Frontend/Mobile/src/i18n/locales/ko.json +++ b/Frontend/Mobile/src/i18n/locales/ko.json @@ -78,7 +78,8 @@ "errorTitle": "이런! 문제가 발생했습니다...", "viewAll": "더 보기", "university": "대학교", - "universityPlatform": "당신의 대학 플랫폼" + "universityPlatform": "당신의 대학 플랫폼", + "error": "오류" }, "days": { "1": "월요일", @@ -203,7 +204,11 @@ "step3CompletedTitle": "해결됨", "step3CompletedDesc": "기술 직원이 현장에서 문제를 해결했습니다.", "general": "일반", - "exterior": "외부" + "exterior": "외부", + "detailsFallbackTitle": "신고 세부정보", + "titlePlaceholder": "예: 강당 난방 고장", + "descPlaceholder": "발생한 문제를 자세히 설명해 주세요...", + "seoDescription": "캠퍼스 및 기숙사 문제를 신고하고 신고 상태를 확인하세요 — InsideUGAL." }, "auth": { "title": "로그인", diff --git a/Frontend/Mobile/src/i18n/locales/ro.json b/Frontend/Mobile/src/i18n/locales/ro.json index 08169734..690d14f7 100644 --- a/Frontend/Mobile/src/i18n/locales/ro.json +++ b/Frontend/Mobile/src/i18n/locales/ro.json @@ -78,7 +78,8 @@ "errorTitle": "Ups! A intervenit o eroare...", "viewAll": "Vezi mai multe", "university": "Universitate", - "universityPlatform": "Platforma ta universitară" + "universityPlatform": "Platforma ta universitară", + "error": "Eroare" }, "days": { "1": "Luni", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Soluționată", "step3CompletedDesc": "Problema a fost rezolvată în teren de personalul tehnic.", "general": "General", - "exterior": "Exterior" + "exterior": "Exterior", + "detailsFallbackTitle": "Detalii sesizare", + "titlePlaceholder": "Ex: Încălzire defectă în amfiteatru", + "descPlaceholder": "Descrie în detaliu problema întâmpinată...", + "seoDescription": "Raportează probleme din campus și cămine și urmărește statusul sesizărilor — InsideUGAL." }, "auth": { "title": "Autentificare", diff --git a/Frontend/Mobile/src/i18n/locales/ru.json b/Frontend/Mobile/src/i18n/locales/ru.json index b362e90e..0f9000ba 100644 --- a/Frontend/Mobile/src/i18n/locales/ru.json +++ b/Frontend/Mobile/src/i18n/locales/ru.json @@ -78,7 +78,8 @@ "errorTitle": "Упс! Что-то пошло не так...", "viewAll": "Смотреть больше", "university": "Университет", - "universityPlatform": "Ваша университетская платформа" + "universityPlatform": "Ваша университетская платформа", + "error": "Ошибка" }, "days": { "1": "Понедельник", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Решена", "step3CompletedDesc": "Проблема решена на месте техническим персоналом.", "general": "Общее", - "exterior": "Внешнее" + "exterior": "Внешнее", + "detailsFallbackTitle": "Детали обращения", + "titlePlaceholder": "Напр.: Не работает отопление в амфитеатре", + "descPlaceholder": "Подробно опишите возникшую проблему...", + "seoDescription": "Сообщайте о проблемах в кампусе и общежитиях и отслеживайте статус своих обращений — InsideUGAL." }, "auth": { "title": "Вход", diff --git a/Frontend/Mobile/src/i18n/locales/tr.json b/Frontend/Mobile/src/i18n/locales/tr.json index a7e5b80a..0031122d 100644 --- a/Frontend/Mobile/src/i18n/locales/tr.json +++ b/Frontend/Mobile/src/i18n/locales/tr.json @@ -78,7 +78,8 @@ "errorTitle": "Hata! Bir şeyler ters gitti...", "viewAll": "Daha fazla gör", "university": "Üniversite", - "universityPlatform": "Üniversite platformunuz" + "universityPlatform": "Üniversite platformunuz", + "error": "Hata" }, "days": { "1": "Pazartesi", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Çözüldü", "step3CompletedDesc": "Sorun teknik personel tarafından yerinde çözüldü.", "general": "Genel", - "exterior": "Dış alan" + "exterior": "Dış alan", + "detailsFallbackTitle": "Bildirim ayrıntıları", + "titlePlaceholder": "Örn: Amfitiyatroda kalorifer arızalı", + "descPlaceholder": "Karşılaştığın sorunu ayrıntılı olarak anlat...", + "seoDescription": "Kampüs ve yurt sorunlarını bildir ve bildirimlerinin durumunu takip et — InsideUGAL." }, "auth": { "title": "Giriş yap", diff --git a/Frontend/Mobile/src/i18n/locales/uk.json b/Frontend/Mobile/src/i18n/locales/uk.json index 7d46d2f1..b8d61b1e 100644 --- a/Frontend/Mobile/src/i18n/locales/uk.json +++ b/Frontend/Mobile/src/i18n/locales/uk.json @@ -78,7 +78,8 @@ "errorTitle": "Ой! Щось пішло не так...", "viewAll": "Переглянути більше", "university": "Університет", - "universityPlatform": "Ваша університетська платформа" + "universityPlatform": "Ваша університетська платформа", + "error": "Помилка" }, "days": { "1": "Понеділок", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Вирішено", "step3CompletedDesc": "Проблему вирішено на місці технічним персоналом.", "general": "Загальне", - "exterior": "Зовнішнє" + "exterior": "Зовнішнє", + "detailsFallbackTitle": "Деталі звернення", + "titlePlaceholder": "Напр.: Не працює опалення в амфітеатрі", + "descPlaceholder": "Детально опиши проблему, з якою ти зіткнувся...", + "seoDescription": "Повідомляй про проблеми в кампусі та гуртожитках і відстежуй статус своїх звернень — InsideUGAL." }, "auth": { "title": "Вхід", diff --git a/Frontend/Mobile/src/i18n/locales/vi.json b/Frontend/Mobile/src/i18n/locales/vi.json index ced91915..219f227a 100644 --- a/Frontend/Mobile/src/i18n/locales/vi.json +++ b/Frontend/Mobile/src/i18n/locales/vi.json @@ -78,7 +78,8 @@ "errorTitle": "Ôi! Đã xảy ra sự cố...", "viewAll": "Xem thêm", "university": "Trường đại học", - "universityPlatform": "Nền tảng đại học của bạn" + "universityPlatform": "Nền tảng đại học của bạn", + "error": "Lỗi" }, "days": { "1": "Thứ Hai", @@ -203,7 +204,11 @@ "step3CompletedTitle": "Đã giải quyết", "step3CompletedDesc": "Vấn đề đã được giải quyết tại chỗ bởi nhân viên kỹ thuật.", "general": "Chung", - "exterior": "Bên ngoài" + "exterior": "Bên ngoài", + "detailsFallbackTitle": "Chi tiết báo cáo", + "titlePlaceholder": "VD: Hệ thống sưởi bị hỏng ở giảng đường", + "descPlaceholder": "Mô tả chi tiết vấn đề bạn gặp phải...", + "seoDescription": "Báo cáo các vấn đề ở khuôn viên trường và ký túc xá, đồng thời theo dõi trạng thái báo cáo của bạn — InsideUGAL." }, "auth": { "title": "Đăng nhập", diff --git a/Frontend/Mobile/src/i18n/locales/zh.json b/Frontend/Mobile/src/i18n/locales/zh.json index ab5f1462..6bd1597a 100644 --- a/Frontend/Mobile/src/i18n/locales/zh.json +++ b/Frontend/Mobile/src/i18n/locales/zh.json @@ -78,7 +78,8 @@ "errorTitle": "哎呀!出了点问题...", "viewAll": "查看更多", "university": "大学", - "universityPlatform": "您的大学平台" + "universityPlatform": "您的大学平台", + "error": "错误" }, "days": { "1": "星期一", @@ -203,7 +204,11 @@ "step3CompletedTitle": "已解决", "step3CompletedDesc": "技术人员已现场解决问题。", "general": "一般", - "exterior": "外部" + "exterior": "外部", + "detailsFallbackTitle": "报告详情", + "titlePlaceholder": "例如:阶梯教室暖气故障", + "descPlaceholder": "详细描述你遇到的问题...", + "seoDescription": "报告校园和宿舍问题,并跟踪你的报告状态 — InsideUGAL。" }, "auth": { "title": "登录", diff --git a/Frontend/Mobile/src/utils/settings-store.ts b/Frontend/Mobile/src/utils/settings-store.ts index f10cbfef..c48af822 100644 --- a/Frontend/Mobile/src/utils/settings-store.ts +++ b/Frontend/Mobile/src/utils/settings-store.ts @@ -1,14 +1,14 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; +import { getLocales } from 'expo-localization'; const SUPPORTED_LANGS = ['ro', 'en', 'es', 'fr', 'de', 'it', 'el', 'tr', 'vi', 'uk', 'ru', 'ar', 'zh', 'ja', 'ko', 'hi']; const KEY_LANG = 'settings_lang'; const KEY_THEME = 'settings_theme'; function getDeviceLang(): string { - const locale = Intl.DateTimeFormat().resolvedOptions().locale; - const lang = locale.split('-')[0]; - return SUPPORTED_LANGS.includes(lang) ? lang : 'en'; + const lang = getLocales()[0]?.languageCode; + return lang && SUPPORTED_LANGS.includes(lang) ? lang : 'en'; } function lsRead(key: string): string | null { From 3ba3a4524dc554e12c00c36df5f9361690e7319b Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 20:21:05 +0300 Subject: [PATCH 14/15] fix: lint, translations --- Frontend/Mobile/src/app/(auth)/index.web.tsx | 2 +- .../src/app/(onboarding)/notificari.tsx | 2 +- .../src/app/(public)/acasa/categorie.tsx | 3 +- .../src/app/(public)/acasa/categorie.web.tsx | 29 ++++++++++--------- .../Mobile/src/app/(public)/acasa/index.tsx | 5 ++-- .../src/app/(public)/acasa/index.web.tsx | 1 + .../src/app/(public)/acasa/vizualizare.tsx | 4 +-- .../app/(public)/acasa/vizualizare.web.tsx | 9 +++--- .../src/app/(public)/anunt/[id].web.tsx | 2 +- .../src/app/(public)/eveniment/[id].web.tsx | 2 +- .../src/app/(public)/more/categorie.tsx | 2 +- .../src/app/(public)/more/categorie.web.tsx | 2 +- .../Mobile/src/app/(public)/more/index.tsx | 2 +- .../src/app/(public)/more/index.web.tsx | 2 +- .../Mobile/src/app/(public)/more/setari.tsx | 6 ++-- .../src/app/(public)/more/setari.web.tsx | 6 ++-- .../src/app/(public)/sesizari/detalii.tsx | 2 +- .../src/app/(public)/sesizari/detalii.web.tsx | 2 +- Frontend/Mobile/src/app/index.tsx | 2 +- .../Mobile/src/components/map/map-pin.tsx | 2 +- .../Mobile/src/components/map/map.web.tsx | 4 ++- .../components/ui/display/article-detail.tsx | 2 +- .../components/ui/display/file-attachment.tsx | 2 +- .../components/ui/navigation/theme-menu.tsx | 11 +++++-- .../components/ui/navigation/web-navbar.tsx | 1 - Frontend/Mobile/src/constants/theme.ts | 1 - Frontend/Mobile/src/i18n/index.ts | 1 + Frontend/Mobile/src/services/api.ts | 1 + 28 files changed, 59 insertions(+), 51 deletions(-) diff --git a/Frontend/Mobile/src/app/(auth)/index.web.tsx b/Frontend/Mobile/src/app/(auth)/index.web.tsx index 6322c0dc..10e7237a 100644 --- a/Frontend/Mobile/src/app/(auth)/index.web.tsx +++ b/Frontend/Mobile/src/app/(auth)/index.web.tsx @@ -3,7 +3,7 @@ import { View, Text, Pressable, TextInput, KeyboardAvoidingView, ScrollView, Act import { Image } from "expo-image"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { useRouter } from "expo-router"; -import { Colors, Spacing, WebSidePadding } from "@/constants/theme"; +import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { KeyboardProvider } from 'react-native-keyboard-controller'; import { useAuth } from "@/contexts/auth-context"; diff --git a/Frontend/Mobile/src/app/(onboarding)/notificari.tsx b/Frontend/Mobile/src/app/(onboarding)/notificari.tsx index a09f8fd1..57b8ba80 100644 --- a/Frontend/Mobile/src/app/(onboarding)/notificari.tsx +++ b/Frontend/Mobile/src/app/(onboarding)/notificari.tsx @@ -5,6 +5,6 @@ import { View } from "react-native"; export default function NotificariScreen() { const router = useRouter(); - useEffect(() => { router.replace("/(onboarding)/locatie" as any); }, []); + useEffect(() => { router.replace("/(onboarding)/locatie" as any); }, [router]); return ; } diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx index dcc5a055..20b4aa67 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx @@ -70,7 +70,7 @@ export default function CategoryScreen() { } }; fetchFaculties(); - }, []); + }, [i18n.language]); const facultyFilters: FilterItem[] = [ { id: null, title: t('category.allFaculties'), abbreviation: t('category.all') }, @@ -96,6 +96,7 @@ export default function CategoryScreen() { size: selectedFacultyId ? 200 : 20, announcement_type: type, lang: i18n.language, + include_untranslated: true, } }); diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx index 71e864b2..177dc57a 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx @@ -64,10 +64,10 @@ export default function CategoryScreen() { } }; fetchFaculties(); - }, []); + }, [i18n.language]); const facultyFilters: FilterItem[] = [ - { id: null, title: "Toate Facultățile", abbreviation: "Toate" }, + { id: null, title: t('category.allFaculties'), abbreviation: t('category.all') }, ...faculties.map(f => ({ id: f.id.toString(), title: f.name, @@ -91,6 +91,7 @@ export default function CategoryScreen() { size: selectedFacultyId ? 200 : 20, announcement_type: type, lang: i18n.language, + include_untranslated: true, } }); @@ -101,7 +102,7 @@ export default function CategoryScreen() { : response.data.items; newItems = rawItems.map((item: any) => ({ id: item.id.toString(), - title: item.title || "Titlu necunoscut", + title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'), category: categoryTitle, date: item.created_at || '', date_start: isoToRomanianDateStr(item.start_date) || "", @@ -110,8 +111,8 @@ export default function CategoryScreen() { time_end: item.end_date ? new Date(item.end_date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "", author: item.author_name || "", image: item.image_url || undefined, - content: item.content || "Conținut necunoscut", - location: item.location_name || "Locație necunoscută", + content: (i18n.language !== 'ro' && item.is_translated ? item.translated_content : null) || item.content || t('common.unknownContent'), + location: item.location_name || t('common.unknownLocation'), created_at: item.created_at, updated_at: item.updated_at, })); @@ -127,12 +128,12 @@ export default function CategoryScreen() { if (response.data && response.data.items) { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.logo_url || undefined, - address: item.address || "Adresă necunoscută", + address: item.address || t('common.unknownAddress'), phone: item.phone || "", website: item.website_url || "", - content: item.description || "Conținut necunoscut", + content: item.description || t('common.unknownContent'), })); } } else if (categoryTitle === "Facilități") { @@ -146,7 +147,7 @@ export default function CategoryScreen() { if (response.data && response.data.items) { newItems = response.data.items.map((item: any) => ({ id: item.id.toString(), - title: item.name || "Titlu necunoscut", + title: item.name || t('common.unknownTitle'), image: item.image_url || undefined, content: item.description || "", })); @@ -219,8 +220,8 @@ export default function CategoryScreen() { return ( - {(categoryTitle as string) || "Categorie"} + {categoryLabel} ), @@ -283,7 +284,7 @@ export default function CategoryScreen() { - Nu există elemente în această categorie. + {t('category.empty')} )} diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.tsx index 5257e9d8..08a4239c 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx @@ -5,12 +5,12 @@ import Animated, { useSharedValue } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; -import { Colors, ColorScheme, Spacing } from "@/constants/theme"; +import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { Carousel } from "@/components/ui/display/carousel/carousel"; import { CAROUSEL_CARD_MARGIN } from "@/components/ui/display/carousel/carousel.shared"; import { NewsCard } from "@/components/ui/display/news-card"; -import { HeroSlideshow, HERO_HEIGHT } from "@/components/ui/display/hero-slideshow"; +import { HeroSlideshow } from "@/components/ui/display/hero-slideshow"; import { getFormattedDate, parseRomanianDate, isoToRomanianDateStr, getTodayRomanianDate } from "@/utils/date"; import api, { storage } from "@/services/api"; import { useTranslation } from 'react-i18next'; @@ -51,6 +51,7 @@ export default function HomeScreen() { announcement_type: undefined, faculty_id: undefined, lang: i18n.language, + include_untranslated: true, } }); if (response.data && response.data.items) { diff --git a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx index 63db550f..e8746182 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/index.web.tsx @@ -51,6 +51,7 @@ export default function HomeScreen() { announcement_type: undefined, faculty_id: undefined, lang: i18n.language, + include_untranslated: true, } }); if (response.data && response.data.items) { diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx index 8542741e..a734cd5f 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.tsx @@ -116,7 +116,7 @@ function VizualizareScreen() { if (isNumeric) { if (initialTipPagina === "Eveniment" || initialTipPagina === "Anunț") { - const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language } }); + const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language, include_untranslated: true } }); if (res.data) { const item = res.data; fetchedItem = { @@ -212,7 +212,7 @@ function VizualizareScreen() { interactionTask.cancel(); } }; - }, [id, initialTipPagina, retryKey, i18n.language]); + }, [id, initialTipPagina, retryKey, i18n.language, t]); const onRefresh = () => { setRefreshing(true); diff --git a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx index c144783e..48352743 100644 --- a/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx +++ b/Frontend/Mobile/src/app/(public)/acasa/vizualizare.web.tsx @@ -108,7 +108,7 @@ function VizualizareScreen() { } } else { try { - const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language } }); + const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language, include_untranslated: true } }); if (res.data?.items && isMounted) setRelatedPool(res.data.items); } catch (err) { console.warn('[API] Error loading related announcements:', err); @@ -133,7 +133,7 @@ function VizualizareScreen() { if (isNumeric) { try { if (initialTipPagina === "Eveniment" || initialTipPagina === "Anunț") { - const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language } }); + const res = await api.get(`/announcements/${numericId}`, { params: { lang: i18n.language, include_untranslated: true } }); if (res.data) { const item = res.data; fetchedItem = { @@ -204,7 +204,7 @@ function VizualizareScreen() { return () => { isMounted = false; }; - }, [id, initialTipPagina, retryKey, i18n.language]); + }, [id, initialTipPagina, retryKey, i18n.language, t]); const title = itemData?.title || ""; const category = itemData?.category || ""; @@ -219,7 +219,6 @@ function VizualizareScreen() { const address = itemData?.address || ""; const phone = itemData?.phone || ""; const website = itemData?.website || ""; - const date = itemData?.date || ""; // Anunturi inrudite: prioritizam aceeasi categorie ca articolul curent, apoi // completam cu restul. Excludem articolul curent (dupa titlu). Sidebar-ul ia @@ -540,7 +539,7 @@ function VizualizareScreen() { {/* Jos, sub tot: 3 carduri pe un rand. */} {bottomItems.length > 0 && ( - + {t('detail.more')} {bottomCardWidth > 0 && bottomItems.map((item: any) => ( diff --git a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx index a473818a..86a4d906 100644 --- a/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx @@ -38,7 +38,7 @@ export default function AnuntScreen() { setHasError(false); setLoading(true); try { - const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language } }); + const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language, include_untranslated: true } }); setItem(res.data); } catch (err) { console.warn("[AnuntScreen] Error loading announcement:", err); diff --git a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx index 430d9459..a270d427 100644 --- a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx +++ b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx @@ -38,7 +38,7 @@ export default function EvenimentScreen() { setHasError(false); setLoading(true); try { - const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language } }); + const res = await api.get(`/announcements/${id}`, { params: { lang: i18n.language, include_untranslated: true } }); setEv(res.data); } catch (err) { console.warn("[EvenimentScreen] Error loading event:", err); diff --git a/Frontend/Mobile/src/app/(public)/more/categorie.tsx b/Frontend/Mobile/src/app/(public)/more/categorie.tsx index 2ba5c206..cc096e44 100644 --- a/Frontend/Mobile/src/app/(public)/more/categorie.tsx +++ b/Frontend/Mobile/src/app/(public)/more/categorie.tsx @@ -38,7 +38,7 @@ export default function MoreCategoryScreen() { .catch(() => { setItems([]); }); - }, [categoryId]); + }, [categoryId, i18n.language]); const scrollY = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler((event) => { diff --git a/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx index 39970b60..ba6b7ef3 100644 --- a/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/categorie.web.tsx @@ -41,7 +41,7 @@ export default function MoreCategoryScreen() { .catch(() => { setItems([]); }); - }, [categoryId]); + }, [categoryId, i18n.language]); const handlePress = (item: any) => { if (item.website) { diff --git a/Frontend/Mobile/src/app/(public)/more/index.tsx b/Frontend/Mobile/src/app/(public)/more/index.tsx index 0473ff93..9773145f 100644 --- a/Frontend/Mobile/src/app/(public)/more/index.tsx +++ b/Frontend/Mobile/src/app/(public)/more/index.tsx @@ -47,7 +47,7 @@ export default function MoreScreen() { .catch(() => { setCategories([]); }); - }, []); + }, [i18n.language]); const renderIcon = (iconName: string, color: string) => { switch (iconName) { diff --git a/Frontend/Mobile/src/app/(public)/more/index.web.tsx b/Frontend/Mobile/src/app/(public)/more/index.web.tsx index d02ecc2e..ff9683d7 100644 --- a/Frontend/Mobile/src/app/(public)/more/index.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/index.web.tsx @@ -43,7 +43,7 @@ export default function MoreScreen() { .catch(() => { setCategories([]); }); - }, []); + }, [i18n.language]); const renderIcon = (iconName: string, color: string) => { switch (iconName) { diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index fc7d67c2..9783d551 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -1,6 +1,6 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import React, { useState, useEffect } from "react"; -import { View, Text, Switch, Pressable, Linking, Platform, Alert } from "react-native"; +import { View, Text, Pressable, Linking, Alert } from "react-native"; import Animated, { useSharedValue, useAnimatedScrollHandler, useAnimatedStyle, interpolate, Extrapolation } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter, Stack } from "expo-router"; @@ -34,14 +34,12 @@ export default function SettingsScreen() { const { t, i18n } = useTranslation(); - // Local settings states driven by settingsStore + // Local settings state driven by settingsStore const [selectedTheme, setSelectedTheme] = useState(() => settingsStore.getTheme()); - const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); useEffect(() => { const unsubscribe = settingsStore.subscribe(() => { setSelectedTheme(settingsStore.getTheme()); - setSelectedLang(settingsStore.getLang()); }); return unsubscribe; }, []); diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index 5036575d..fe252230 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -1,6 +1,6 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import React, { useState, useEffect } from "react"; -import { View, Text, Switch, Pressable, Animated, Linking } from "react-native"; +import { View, Text, Pressable, Animated, Linking } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter, Stack } from "expo-router"; @@ -28,14 +28,12 @@ export default function SettingsScreen() { const { t, i18n } = useTranslation(); - // Local settings states driven by settingsStore + // Local settings state driven by settingsStore const [selectedTheme, setSelectedTheme] = useState(() => settingsStore.getTheme()); - const [selectedLang, setSelectedLang] = useState(() => settingsStore.getLang()); useEffect(() => { const unsubscribe = settingsStore.subscribe(() => { setSelectedTheme(settingsStore.getTheme()); - setSelectedLang(settingsStore.getLang()); }); return unsubscribe; }, []); diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx index 2a97f2af..6ab5c902 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.tsx @@ -108,7 +108,7 @@ export default function SesizareDetaliiScreen() { console.error("[API] Error fetching complaint detail:", err); setError(err.message || t('reports.loadError')); } - }, [id, i18n.language]); + }, [id, i18n.language, t]); useEffect(() => { const timer = setTimeout(() => { diff --git a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx index 88c4f531..f604a383 100644 --- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx +++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx @@ -107,7 +107,7 @@ export default function SesizareDetaliiScreen() { } } loadComplaint(); - }, [id, retryKey, i18n.language]); + }, [id, retryKey, i18n.language, t]); const title = report?.title || ""; const description = report?.description || ""; diff --git a/Frontend/Mobile/src/app/index.tsx b/Frontend/Mobile/src/app/index.tsx index 9080744e..004abd6e 100644 --- a/Frontend/Mobile/src/app/index.tsx +++ b/Frontend/Mobile/src/app/index.tsx @@ -29,7 +29,7 @@ export default function SplashScreen() { try { await Promise.all([ api.get("/announcements/", { - params: { page: 1, size: 50, lang: i18n.language } + params: { page: 1, size: 50, lang: i18n.language, include_untranslated: true } }).then(res => { if (res.data && res.data.items) { return storage.setItem('cached_announcements', JSON.stringify(res.data.items)); diff --git a/Frontend/Mobile/src/components/map/map-pin.tsx b/Frontend/Mobile/src/components/map/map-pin.tsx index f87ea250..75ac22f0 100644 --- a/Frontend/Mobile/src/components/map/map-pin.tsx +++ b/Frontend/Mobile/src/components/map/map-pin.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { View, Text, Platform } from 'react-native'; import { useTheme } from '@/hooks/use-theme'; import { ColorScheme } from '@/constants/theme'; -import { getBuildingLetter, getFacilityStatus } from '@/utils/map-helper'; +import { getFacilityStatus } from '@/utils/map-helper'; import ForkKnifeIcon from '@/assets/icons/svg/fork-knife.svg'; import BookIcon from '@/assets/icons/svg/book.svg'; diff --git a/Frontend/Mobile/src/components/map/map.web.tsx b/Frontend/Mobile/src/components/map/map.web.tsx index b148d7fa..faef51c7 100644 --- a/Frontend/Mobile/src/components/map/map.web.tsx +++ b/Frontend/Mobile/src/components/map/map.web.tsx @@ -81,7 +81,7 @@ export default function Map({ themeName, selectedFacultyId, onFacultySelect, bui map.current = null; setMapLoaded(false); }; - }, [mapStyle]); + }, [mapStyle, onMapClick]); useEffect(() => { if (!map.current || !defaultCenter || cameraInitialized.current) return; @@ -205,6 +205,8 @@ export default function Map({ themeName, selectedFacultyId, onFacultySelect, bui userMarkerRef.current = marker; } } + // userLocation e citit doar la focus (nu vrem sa rulam la fiecare update GPS - de asta e deja tratat de efectul de mai sus). + // eslint-disable-next-line react-hooks/exhaustive-deps }, [focusKey, mapLoaded]); useEffect(() => { diff --git a/Frontend/Mobile/src/components/ui/display/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx index 13b037da..acbaba1b 100644 --- a/Frontend/Mobile/src/components/ui/display/article-detail.tsx +++ b/Frontend/Mobile/src/components/ui/display/article-detail.tsx @@ -91,7 +91,7 @@ export function ArticleDetail({ let isMounted = true; const loadRelated = async () => { try { - const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language } }); + const res = await api.get('/announcements/', { params: { page: 1, size: 20, lang: i18n.language, include_untranslated: true } }); if (res.data?.items && isMounted) { setRelatedPool(res.data.items); } diff --git a/Frontend/Mobile/src/components/ui/display/file-attachment.tsx b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx index d42e71c4..3edff7ce 100644 --- a/Frontend/Mobile/src/components/ui/display/file-attachment.tsx +++ b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx @@ -1,5 +1,5 @@ import { Platform, Linking, TouchableOpacity, View, Text } from "react-native"; -import { Colors, ColorScheme, Spacing } from "@/constants/theme"; +import { Colors, Spacing } from "@/constants/theme"; import { Typography } from "@/constants/typography"; import { useColorScheme } from "@/hooks/use-color-scheme"; import FileIcon from "@/assets/icons/svg/file.svg"; diff --git a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx index a1a2b642..a5b19a7c 100644 --- a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx @@ -53,10 +53,17 @@ export function ThemeMenu({ const toggle = () => onToggle ? onToggle() : setLocalOpen((v) => !v); const close = () => { setSubMenu(null); - onClose ? onClose() : setLocalOpen(false); + if (onClose) onClose(); else setLocalOpen(false); }; - useEffect(() => { if (!open) setSubMenu(null); }, [open]); + // Reseteaza submeniul cand "open" trece la false (inclusiv cand parintele + // il inchide direct, fara sa treaca prin close()) - ajustare de stare in + // timpul randarii, nu intr-un efect, ca sa evitam randari in cascada. + const [prevOpen, setPrevOpen] = useState(open); + if (open !== prevOpen) { + setPrevOpen(open); + if (!open) setSubMenu(null); + } useEffect(() => { anim.set(withTiming(open ? 1 : 0, { duration: open ? 280 : 200, easing: Easing.out(Easing.cubic) })); diff --git a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx index fe4cc6e4..c900d186 100644 --- a/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/web-navbar.tsx @@ -24,7 +24,6 @@ import { useColorScheme } from "@/hooks/use-color-scheme"; import { useNavbarScrolled } from "@/contexts/web-scroll-context"; import { WebContainer, WEB_COMPACT_BREAKPOINT } from "@/components/ui/layout/web-container"; import { ThemeMenu } from "@/components/ui/navigation/theme-menu"; -import { ThemeToggle } from "@/components/ui/navigation/theme-toggle"; import { ProfileMenu, DASHBOARD_URL } from "@/components/ui/navigation/profile-menu"; import ChevronIcon from "@/assets/icons/svg/chevron-left.svg"; import api, { getAuthToken, logout } from "@/services/api"; diff --git a/Frontend/Mobile/src/constants/theme.ts b/Frontend/Mobile/src/constants/theme.ts index d1f3b553..b2c1ee6b 100644 --- a/Frontend/Mobile/src/constants/theme.ts +++ b/Frontend/Mobile/src/constants/theme.ts @@ -7,7 +7,6 @@ import '@/global.css'; import { Platform } from 'react-native'; import { Spacing } from './spacing'; -import { ThemeProvider } from '@/contexts/theme-context'; export { Spacing }; diff --git a/Frontend/Mobile/src/i18n/index.ts b/Frontend/Mobile/src/i18n/index.ts index d40cc7a8..acef8a13 100644 --- a/Frontend/Mobile/src/i18n/index.ts +++ b/Frontend/Mobile/src/i18n/index.ts @@ -19,6 +19,7 @@ import ja from './locales/ja.json'; import ko from './locales/ko.json'; import hi from './locales/hi.json'; +// eslint-disable-next-line import/no-named-as-default-member i18n.use(initReactI18next).init({ resources: { ro: { translation: ro }, diff --git a/Frontend/Mobile/src/services/api.ts b/Frontend/Mobile/src/services/api.ts index 75eaac8a..c1eeb8fa 100644 --- a/Frontend/Mobile/src/services/api.ts +++ b/Frontend/Mobile/src/services/api.ts @@ -140,6 +140,7 @@ const api = axios.create({ }, }); +// eslint-disable-next-line import/no-named-as-default-member export const ace = axios.create({ baseURL: `${Config.LLM_BASE_URL}/api/v1/campus-chat/stream`, timeout: 0, From 5853f1ceb63a7a59c960404b214bc63ed43a9902 Mon Sep 17 00:00:00 2001 From: alexurrc18 Date: Thu, 2 Jul 2026 20:49:48 +0300 Subject: [PATCH 15/15] added: version number --- Frontend/Mobile/src/app/(public)/more/setari.tsx | 10 +++------- Frontend/Mobile/src/app/(public)/more/setari.web.tsx | 3 ++- .../Mobile/src/components/ui/navigation/theme-menu.tsx | 10 +++++++--- Frontend/Mobile/src/constants/config.ts | 4 ++++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Frontend/Mobile/src/app/(public)/more/setari.tsx b/Frontend/Mobile/src/app/(public)/more/setari.tsx index 9783d551..752888d5 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx @@ -7,6 +7,7 @@ import { useRouter, Stack } from "expo-router"; import * as WebBrowser from "expo-web-browser"; import { Colors, Spacing } from "@/constants/theme"; +import { Config } from "@/constants/config"; import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; import { settingsStore } from "@/utils/settings-store"; @@ -125,10 +126,6 @@ export default function SettingsScreen() { {/* SECȚIUNEA 1: ASPECT & LIMBĂ */} - - {t('settings.appearanceLang')} - - {/* Opțiune Temă */} @@ -165,7 +162,6 @@ export default function SettingsScreen() { {/* Opțiune Limbă */} - {t('language.title')} {/* Buton navigare limbă - navighează la limba.tsx */} @@ -234,7 +230,7 @@ export default function SettingsScreen() { {/* Subsol (App Version) */} - InsideUGAL v0.1.0 + InsideUGAL v{Config.APP_VERSION} {t('settings.appSlogan')} diff --git a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx index fe252230..77f6d0ae 100644 --- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx +++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx @@ -5,6 +5,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter, Stack } from "expo-router"; import { Colors, Spacing, WebSidePadding } from "@/constants/theme"; +import { Config } from "@/constants/config"; import { useWebContentTop } from "@/hooks/use-web-content-top"; import { Typography } from "@/constants/typography"; import { CategoryHeader } from "@/components/ui/display/category-header"; @@ -211,7 +212,7 @@ export default function SettingsScreen() { {/* Subsol (App Version) */} - InsideUGAL v0.1.0 + InsideUGAL v{Config.APP_VERSION} {t('settings.appSlogan')} diff --git a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx index a5b19a7c..a1ecdadc 100644 --- a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx +++ b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef } from "react"; import { Pressable, View, Text, Platform, ScrollView } from "react-native"; import Animated, { useSharedValue, withTiming, useAnimatedStyle, interpolate, Extrapolation, Easing } from "react-native-reanimated"; import { ColorScheme, Spacing, Colors } from "@/constants/theme"; +import { Config } from "@/constants/config"; import { Typography } from "@/constants/typography"; import { useThemeContext } from "@/contexts/theme-context"; import { useColorScheme } from "@/hooks/use-color-scheme"; @@ -222,7 +223,7 @@ export function ThemeMenu({ {[ { label: t('theme.current'), value: themeLabel, sub: "tema" as const }, { label: t('language.current'), value: langLabel, sub: "limba" as const }, - ].map((item, i) => { + ].map((item) => { const isActive = subMenu === item.sub; return ( 0 ? 1 : 0, - borderTopColor: "rgba(0,0,0,0.08)", }, (pressed || hovered || isActive) && { backgroundColor: "rgba(0,0,0,0.05)" }, ]} @@ -261,6 +260,11 @@ export function ThemeMenu({ ); })} + + + InsideUGAL v{Config.APP_VERSION} + + diff --git a/Frontend/Mobile/src/constants/config.ts b/Frontend/Mobile/src/constants/config.ts index 4ff45ae7..39405b8a 100644 --- a/Frontend/Mobile/src/constants/config.ts +++ b/Frontend/Mobile/src/constants/config.ts @@ -1,6 +1,10 @@ +import Constants from 'expo-constants'; + export const Config = { MAPTILER_STYLE_URL: process.env.EXPO_PUBLIC_MAPTILER_STYLE_URL as string, API_BASE_URL: process.env.EXPO_PUBLIC_API_BASE_URL as string, LLM_BASE_URL: process.env.EXPO_PUBLIC_LLM_BASE_URL as string, DASHBOARD_URL: process.env.EXPO_PUBLIC_DASHBOARD_URL as string, + // Sursa unica pentru versiunea afisata in UI (Setari, footer web) — citita din app.json, nu duplicata manual. + APP_VERSION: Constants.expoConfig?.version ?? '1.0.0', }; \ No newline at end of file