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/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/package-lock.json b/Frontend/Mobile/package-lock.json
index ba6b9f64..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",
@@ -31,9 +32,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",
@@ -6845,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",
@@ -8017,6 +8033,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 +8090,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 +10932,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",
@@ -11443,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",
@@ -12759,6 +12845,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 +12910,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..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",
@@ -26,9 +27,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)/_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() {
({
...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 c0a2409e..10e7237a 100644
--- a/Frontend/Mobile/src/app/(auth)/index.web.tsx
+++ b/Frontend/Mobile/src/app/(auth)/index.web.tsx
@@ -1,17 +1,24 @@
-import { useState } from "react";
-import { View, Text, Pressable, TextInput, KeyboardAvoidingView, ScrollView, ActivityIndicator } from "react-native";
+import { useState, useRef } from "react";
+import { View, Text, Pressable, TextInput, KeyboardAvoidingView, ScrollView, ActivityIndicator, useWindowDimensions } from "react-native";
+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";
+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");
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 +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);
@@ -47,28 +54,61 @@ 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);
}
};
+ const { width } = useWindowDimensions();
+ const isDesktop = width >= 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
+ {t('auth.title')}
- Introdu datele pentru a intra în cont
+ {t('auth.subtitle')}
@@ -81,7 +121,7 @@ export default function LoginScreen() {
{/* Email Input */}
- Email
+ {t('auth.email')}
{
@@ -112,7 +152,7 @@ export default function LoginScreen() {
{/* Password Input */}
- Parolă
+ {t('auth.password')}
{
@@ -157,12 +197,37 @@ export default function LoginScreen() {
{submitting ? (
) : (
- Autentificare
+ {t('auth.login')}
)}
+
+ {/* Dreapta: logo mare (doar desktop) */}
+ {isDesktop && (
+
+
+
+ )}
);
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/(onboarding)/notificari.tsx b/Frontend/Mobile/src/app/(onboarding)/notificari.tsx
index 6116ff0c..57b8ba80 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); }, [router]);
+ return ;
+}
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/_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/categorie.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.tsx
index e8a9d3b2..20b4aa67 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);
@@ -52,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);
@@ -62,10 +70,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,
@@ -85,26 +93,31 @@ 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,
+ include_untranslated: true,
}
});
-
+
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,
- 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: 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" }) : "",
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,
}));
@@ -113,38 +126,40 @@ 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) {
newItems = response.data.items.map((item: any) => ({
id: item.id.toString(),
- title: item.name || "Titlu necunoscut",
- image: item.image_url || undefined,
- address: item.address || "Adresă necunoscută",
+ title: item.name || t('common.unknownTitle'),
+ image: item.logo_url || undefined,
+ 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") {
response = await api.get("/facilities/", {
params: {
page: pageToFetch,
- size: 20
+ size: 20,
+ lang: i18n.language,
}
});
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 || "",
}));
}
}
- if (newItems.length < 20) {
+ if (selectedFacultyId || newItems.length < 20) {
setHasMore(false);
}
@@ -171,7 +186,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 +252,7 @@ export default function CategoryScreen() {
]}
numberOfLines={1}
>
- {(categoryTitle as string) || "Categorie"}
+ {displayTitle}
),
@@ -258,7 +273,7 @@ export default function CategoryScreen() {
>
- Nu există elemente în această categorie.
+ {t('category.empty')}
)}
>
diff --git a/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx b/Frontend/Mobile/src/app/(public)/acasa/categorie.web.tsx
index 4c61d794..177dc57a 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, i18n } = 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
@@ -46,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);
@@ -56,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,
@@ -80,26 +88,31 @@ 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,
+ include_untranslated: true,
}
});
-
+
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",
+ title: (i18n.language !== 'ro' && item.is_translated ? item.translated_title : null) || item.title || t('common.unknownTitle'),
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" }) : "",
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,
}));
@@ -108,38 +121,40 @@ 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) {
newItems = response.data.items.map((item: any) => ({
id: item.id.toString(),
- title: item.name || "Titlu necunoscut",
- image: item.image_url || undefined,
- address: item.address || "Adresă necunoscută",
+ title: item.name || t('common.unknownTitle'),
+ image: item.logo_url || undefined,
+ 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") {
response = await api.get("/facilities/", {
params: {
page: pageToFetch,
- size: 20
+ size: 20,
+ lang: i18n.language,
}
});
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 || "",
}));
}
}
- if (newItems.length < 20) {
+ if (selectedFacultyId || newItems.length < 20) {
setHasMore(false);
}
@@ -205,8 +220,8 @@ export default function CategoryScreen() {
return (
- {(categoryTitle as string) || "Categorie"}
+ {categoryLabel}
),
@@ -269,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 e3e34061..08a4239c 100644
--- a/Frontend/Mobile/src/app/(public)/acasa/index.tsx
+++ b/Frontend/Mobile/src/app/(public)/acasa/index.tsx
@@ -1,73 +1,30 @@
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 { 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';
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 { t, i18n } = useTranslation();
const [noutati, setNoutati] = useState([]);
const [evenimente, setEvenimente] = useState([]);
const [facultati, setFacultati] = useState([]);
@@ -75,28 +32,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 () => {
@@ -109,24 +49,26 @@ export default function HomeScreen() {
page: 1,
size: 50,
announcement_type: undefined,
- faculty_id: undefined
+ faculty_id: undefined,
+ lang: i18n.language,
+ include_untranslated: true,
}
});
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: item.created_at || '',
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,
}));
@@ -134,17 +76,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: 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" }) : "",
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,
}));
@@ -159,12 +101,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" }) : "",
@@ -182,7 +124,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) {
@@ -222,7 +165,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) {
@@ -268,7 +212,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;
@@ -282,23 +226,23 @@ 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]
+ const announcementsForHero = [...noutati, ...evenimente]
.sort((a, b) => parseRomanianDate(b.date).getTime() - parseRomanianDate(a.date).getTime())
.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
}
];
@@ -333,12 +277,8 @@ export default function HomeScreen() {
});
};
- const handleNotificationsPress = () => {
- router.push("/(public)/acasa/notificari");
- };
-
- const activeNoutati = noutati;
- const activeEvenimente = evenimente;
+ const activeNoutati = noutati.slice(0, 3);
+ const activeEvenimente = evenimente.slice(0, 3);
const activeFacultati = facultati;
const activeFacilitati = facilitati;
@@ -360,110 +300,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}
-
-
- )}
-
- )}
-
-
{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"
@@ -544,12 +380,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"
@@ -567,12 +403,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..e8746182 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,24 @@ export default function HomeScreen() {
page: 1,
size: 50,
announcement_type: undefined,
- faculty_id: undefined
+ faculty_id: undefined,
+ lang: i18n.language,
+ include_untranslated: true,
}
});
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: item.created_at || '',
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 +74,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: 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" }) : "",
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,
}));
@@ -98,7 +102,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) {
@@ -125,7 +130,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) {
@@ -163,10 +169,10 @@ 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]
+ // 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);
@@ -174,9 +180,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
}
];
@@ -194,11 +200,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;
}
@@ -235,7 +241,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];
@@ -276,7 +282,7 @@ export default function HomeScreen() {
{/* Sectiune intre hero si carusele: 3 carduri compacte + 1 card mare. */}
- 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"
renderItem={({ item, index }) => (
@@ -305,7 +311,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)}
/>
)}
@@ -315,16 +321,16 @@ 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"
renderItem={({ item, index }) => (
@@ -334,7 +340,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)}
/>
)}
@@ -344,15 +350,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 +377,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/notificari.tsx b/Frontend/Mobile/src/app/(public)/acasa/notificari.tsx
index ff975005..3215338a 100644
--- a/Frontend/Mobile/src/app/(public)/acasa/notificari.tsx
+++ b/Frontend/Mobile/src/app/(public)/acasa/notificari.tsx
@@ -1,152 +1,4 @@
-import { useColorScheme } from "@/hooks/use-color-scheme";
-import React, { useState, useEffect } from "react";
-import { View, Text, ScrollView, Pressable, Linking } from "react-native";
-import { Stack, useRouter } from "expo-router";
-import { storage } from "@/services/api";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { Colors, Spacing } from "@/constants/theme";
-import { Typography } from "@/constants/typography";
-import { NotificareCard, Notificare } from "@/components/ui/display/notificare-card";
-import BackIcon from "@/assets/icons/svg/chevron-left.svg";
-import TaskIcon from "@/assets/icons/svg/task.svg";
-
-export const MOCK_NOTIFICARI: Notificare[] = [
- {
- id: "1",
- data: "27 iunie 2026",
- titlu: "Rezultate sesiune de vară",
- continut: "Rezultatele pentru sesiunea de examene de vară au fost publicate în Registrul Matricol. Vă rugăm să verificați situația academică.",
- actiune: "/(public)/acasa/vizualizare?id=1",
- },
- {
- id: "2",
- data: "25 iunie 2026",
- titlu: "Eveniment: Ziua Porților Deschise",
- continut: "Universitatea Dunărea de Jos organizează Ziua Porților Deschise pe 30 iunie 2026, începând cu ora 10:00, în Campusul Științei.",
- actiune: "/(public)/acasa/vizualizare?id=2",
- },
- {
- id: "3",
- data: "20 iunie 2026",
- titlu: "Întrerupere servicii IT",
- continut: "Pe data de 22 iunie 2026, între orele 22:00 și 02:00, platforma academică va fi indisponibilă pentru lucrări de mentenanță.",
- },
- {
- id: "4",
- data: "15 iunie 2026",
- titlu: "Bursele pentru semestrul II",
- continut: "Au fost aprobate listele de burse pentru semestrul al II-lea al anului universitar 2025-2026. Verificați lista afișată la secretariat.",
- actiune: "/(public)/acasa/vizualizare?id=4",
- },
- {
- id: "5",
- data: "12 iunie 2026",
- titlu: "Înscrieri la cazare",
- continut: "Perioada de înscriere pentru cazarea în căminele studențești a început. Depuneți cererea online până pe 30 iunie 2026.",
- actiune: "/(public)/acasa/vizualizare?id=5",
- },
- {
- id: "6",
- data: "8 iunie 2026",
- titlu: "Concurs de proiecte studențești",
- continut: "Te invităm să participi la concursul anual de proiecte studențești. Premii și mentorat pentru cele mai bune idei.",
- actiune: "/(public)/acasa/vizualizare?id=6",
- },
- {
- id: "7",
- data: "3 iunie 2026",
- titlu: "Program bibliotecă în sesiune",
- continut: "Pe perioada sesiunii, biblioteca universitară are program prelungit, de luni până sâmbătă, între orele 8:00 și 22:00.",
- actiune: "/(public)/acasa/vizualizare?id=7",
- },
-];
-
-export default function NotificariScreen() {
- const router = useRouter();
- const insets = useSafeAreaInsets();
- const themeName = (useColorScheme() ?? "light") as keyof typeof Colors;
- const theme = Colors[themeName];
- const [readIds, setReadIds] = useState>(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 5f278a43..a734cd5f 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";
@@ -19,9 +20,7 @@ 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";
-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[] = [];
@@ -39,8 +38,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;
}
@@ -52,12 +51,14 @@ 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);
const [retryKey, setRetryKey] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refreshStartRef = useRef(0);
+ const [imgErr, setImgErr] = useState(false);
const initialItem = {
title: (params.title as string) || "",
@@ -115,51 +116,51 @@ 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, include_untranslated: true } });
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ă",
+ posted_at: item.created_at || "",
+ date: item.start_date || "",
author: item.author_name || "",
created_at: item.created_at,
updated_at: item.updated_at,
};
}
} 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 = {
id: item.id.toString(),
type: "Facultate",
- title: item.name || "Titlu necunoscut",
- image: item.image_url || "",
- address: item.address || "Adresă necunoscută",
+ title: item.name || t('common.unknownTitle'),
+ image: item.logo_url || "",
+ 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") {
- 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 = {
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 || [],
@@ -187,11 +188,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.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 || 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: (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);
@@ -211,7 +212,7 @@ function VizualizareScreen() {
interactionTask.cancel();
}
};
- }, [id, initialTipPagina, retryKey]);
+ }, [id, initialTipPagina, retryKey, i18n.language, t]);
const onRefresh = () => {
setRefreshing(true);
@@ -249,11 +250,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}`) }
]
);
};
@@ -269,7 +270,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 (
@@ -297,7 +298,7 @@ function VizualizareScreen() {
}}
/>
setRetryKey(prev => prev + 1)}
/>
@@ -320,7 +321,7 @@ function VizualizareScreen() {
),
headerShadowVisible: false,
headerTintColor: scrolledPast ? theme.text : ColorScheme.white,
- headerTitle: scrolledPast ? (title || "Detalii") : "",
+ headerTitle: scrolledPast ? (title || t('detail.details')) : "",
headerLeft: () => (
router.back()}
@@ -345,7 +346,8 @@ function VizualizareScreen() {
>
setImgErr(true)}
style={{ width: "100%", height: "100%", position: "absolute" }}
contentFit="cover"
/>
@@ -360,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'))}
)}
@@ -377,7 +379,7 @@ function VizualizareScreen() {
{isUpdated && formattedUpdateDate ? (
- Actualizat: {formattedUpdateDate}
+ {t('detail.updated')} {formattedUpdateDate}
) : null}
@@ -386,7 +388,7 @@ function VizualizareScreen() {
{tipPagina === "Eveniment" && (
- Informații eveniment
+ {t('detail.eventInfo')}
@@ -394,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 || ""}
@@ -416,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')}
@@ -434,7 +436,7 @@ function VizualizareScreen() {
- Telefon
+ {t('detail.phone')}
{phone}
@@ -448,7 +450,7 @@ function VizualizareScreen() {
- Website
+ {t('detail.website')}
Linking.openURL(website as string)}>
{website}
@@ -461,16 +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')}
- {formatSchedules(itemData.schedules).map((line: string, i: number) => (
+ {t('detail.schedule')}
+ {formatSchedules(itemData.schedules, t).map((line: string, i: number) => (
{line}
))}
@@ -481,12 +484,13 @@ 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 a6c4187c..48352743 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";
@@ -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,9 +56,11 @@ 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);
+ const [imgErr, setImgErr] = useState(false);
const initialItem = {
title: (params.title as string) || "",
@@ -100,14 +101,14 @@ 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);
}
} 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, include_untranslated: true } });
if (res.data?.items && isMounted) setRelatedPool(res.data.items);
} catch (err) {
console.warn('[API] Error loading related announcements:', err);
@@ -132,51 +133,51 @@ 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, include_untranslated: true } });
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ă",
+ posted_at: item.created_at || "",
+ date: item.start_date || "",
author: item.author_name || "",
created_at: item.created_at,
updated_at: item.updated_at,
};
}
} 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 = {
id: item.id.toString(),
type: "Facultate",
- title: item.name || "Titlu necunoscut",
- image: item.image_url || "",
- address: item.address || "Adresă necunoscută",
+ title: item.name || t('common.unknownTitle'),
+ image: item.logo_url || "",
+ 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") {
- 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 = {
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 +204,7 @@ function VizualizareScreen() {
return () => {
isMounted = false;
};
- }, [id, initialTipPagina, retryKey]);
+ }, [id, initialTipPagina, retryKey, i18n.language, t]);
const title = itemData?.title || "";
const category = itemData?.category || "";
@@ -218,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
@@ -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ă",
- date_start: isoToRomanianDateStr(item.start_date) || "",
- date_end: isoToRomanianDateStr(item.end_date) || "",
+ location: item.location_name || t('common.unknownLocation'),
+ 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) || "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,
@@ -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 || "",
}));
@@ -278,34 +278,27 @@ function VizualizareScreen() {
pathname: "/(public)/acasa/vizualizare",
params: {
id: item.id,
- type: item.category === "Evenimente" ? "Eveniment" : "Anunț",
+ type: item.category === t('home.events') ? "Eveniment" : "Anunț",
},
});
};
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(t('detail.callConfirm', { phone }))) {
+ Linking.openURL(`tel:${phone}`);
+ }
};
- const displayDateValue = category === "Noutăți"
- ? (posted_at && posted_at !== "Dată necunoscută" ? posted_at : "")
- : (date && date !== "Dată necunoscută" ? 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);
- const dateDisplay = category === "Noutăți" ? (formattedDate ? `${formattedDate} | ${readingTime}` : "Dată necunoscută") : (formattedDate || "Dată necunoscută");
+ 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;
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 (
@@ -319,7 +312,7 @@ function VizualizareScreen() {
return (
setRetryKey(prev => prev + 1)}
/>
@@ -329,12 +322,17 @@ 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: "Acasă", href: "/(public)/acasa" },
+ { 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) || "Titlu necunoscut" },
+ { label: (title as string) || t('common.unknownTitle') },
];
// SEO: descriere din continut + date structurate (Event / NewsArticle).
@@ -363,7 +361,7 @@ function VizualizareScreen() {
return (
-
+
{jsonLd ? (
) : null}
@@ -372,7 +370,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"
@@ -394,7 +393,7 @@ function VizualizareScreen() {
) : (
- {category || (tipPagina === "Facultate" ? "Facultate" : "Categorie")}
+ {category || TIP_LABELS[tipPagina] || t('common.category')}
)}
- {title || "Titlu necunoscut"}
+ {title || t('common.unknownTitle')}
@@ -424,7 +423,7 @@ function VizualizareScreen() {
{isUpdated && formattedUpdateDate ? (
- Actualizat: {formattedUpdateDate}
+ {t('detail.updated')} {formattedUpdateDate}
) : null}
@@ -432,16 +431,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 || ""}
@@ -449,7 +448,7 @@ function VizualizareScreen() {
- {location || "Locație necunoscută"}
+ {location || t('common.unknownLocation')}
@@ -459,14 +458,14 @@ function VizualizareScreen() {
{tipPagina === "Facultate" && (
- Contact și Locație
+ {t('detail.contact')}
- Adresă
+ {t('detail.address')}
- {address || "Adresă necunoscută"}
+ {address || t('common.unknownAddress')}
@@ -475,7 +474,7 @@ function VizualizareScreen() {
- Telefon
+ {t('detail.phone')}
{phone}
@@ -487,7 +486,7 @@ function VizualizareScreen() {
- Website
+ {t('detail.website')}
Linking.openURL(website as string)}>
{website}
@@ -498,16 +497,17 @@ function VizualizareScreen() {
)}
- {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || []).length > 0 && (
+ {tipPagina === "Facilitate" && formatSchedules(itemData?.schedules || [], t).length > 0 && (
- Informații facilitate
+ {t('detail.facilityInfo')}
- {formatSchedules(itemData.schedules).map((line: string, i: number) => (
+ {t('detail.schedule')}
+ {formatSchedules(itemData.schedules, t).map((line: string, i: number) => (
{line}
))}
@@ -518,10 +518,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')}
@@ -529,7 +529,7 @@ function VizualizareScreen() {
{/* Dreapta: 3 carduri Noutăți, una sub alta. */}
{sidebarItems.length > 0 && (
- Articole similare
+ {t('detail.relatedArticles')}
{sidebarItems.map((item) => (
openItem(item)} />
))}
@@ -539,8 +539,8 @@ 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)/anunt/[id].web.tsx b/Frontend/Mobile/src/app/(public)/anunt/[id].web.tsx
index 7d26f82a..86a4d906 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, i18n } = useTranslation();
const params = useLocalSearchParams();
const id = parseEventId(params.id);
const [item, setItem] = useState(null);
@@ -36,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, include_untranslated: true } });
setItem(res.data);
} catch (err) {
console.warn("[AnuntScreen] Error loading announcement:", err);
@@ -46,7 +48,7 @@ export default function AnuntScreen() {
}
};
run();
- }, [id, retryKey]);
+ }, [id, retryKey, i18n.language]);
if (loading) {
return (
@@ -67,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 || "";
@@ -90,12 +92,13 @@ export default function AnuntScreen() {
>
);
diff --git a/Frontend/Mobile/src/app/(public)/cantina/index.tsx b/Frontend/Mobile/src/app/(public)/cantina/index.tsx
index 054ec07f..d6571984 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, i18n } = 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);
@@ -71,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) {
@@ -99,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) {
@@ -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..d9599361 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, i18n } = 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>({
@@ -81,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) {
@@ -104,7 +106,7 @@ export default function CantinaScreen() {
}
loadData();
return () => { active = false; };
- }, [retryKey]);
+ }, [retryKey, i18n.language]);
const currentMenu = useMemo(() => {
const dayNum = getDayNumber(selectedDay);
@@ -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)/eveniment/[id].web.tsx b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx
index cd02e3cf..a270d427 100644
--- a/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx
+++ b/Frontend/Mobile/src/app/(public)/eveniment/[id].web.tsx
@@ -9,6 +9,7 @@ import { parseEventId, allEventParams } 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 EvenimentScreen() {
+ const { t, i18n } = useTranslation();
const params = useLocalSearchParams();
const id = parseEventId(params.id);
const [ev, setEv] = useState(null);
@@ -36,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, include_untranslated: true } });
setEv(res.data);
} catch (err) {
console.warn("[EvenimentScreen] Error loading event:", err);
@@ -46,7 +48,7 @@ export default function EvenimentScreen() {
}
};
run();
- }, [id, retryKey]);
+ }, [id, retryKey, i18n.language]);
if (loading) {
return (
@@ -67,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) || "";
@@ -98,7 +100,7 @@ export default function EvenimentScreen() {
>
);
diff --git a/Frontend/Mobile/src/app/(public)/harta.tsx b/Frontend/Mobile/src/app/(public)/harta.tsx
index eec7a9e3..0622e8ed 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, i18n } = useTranslation();
const [hasError, setHasError] = useState(false);
const loadData = useCallback(async () => {
@@ -33,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) {
@@ -49,7 +51,7 @@ export default function HartaScreen() {
console.warn('[API] Error loading map screen data:', err);
setHasError(true);
}
- }, []);
+ }, [i18n.language]);
useEffect(() => {
const timer = setTimeout(() => {
@@ -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);
@@ -98,31 +100,19 @@ export default function HartaScreen() {
async function loadData() {
try {
if (active) setHasError(false);
- // Load cached data first for immediate render
- const [cachedFacs, cachedLocs] = await Promise.all([
- storage.getItem('cached_faculties'),
- storage.getItem('cached_facilities'),
- ]);
-
- if (active) {
- if (cachedFacs) setFaculties(JSON.parse(cachedFacs));
- if (cachedLocs) setLocations(JSON.parse(cachedLocs));
- }
// 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) {
if (facsRes.data?.items) {
setFaculties(facsRes.data.items);
- await storage.setItem('cached_faculties', JSON.stringify(facsRes.data.items));
}
if (locsRes.data?.items) {
setLocations(locsRes.data.items);
- await storage.setItem('cached_facilities', JSON.stringify(locsRes.data.items));
}
}
} catch (err) {
@@ -132,18 +122,18 @@ export default function HartaScreen() {
}
loadData();
return () => { active = false; };
- }, [retryKey]);
+ }, [retryKey, i18n.language]);
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 +166,7 @@ export default function HartaScreen() {
/>
(
-
+
),
}}
diff --git a/Frontend/Mobile/src/app/(public)/more/categorie.tsx b/Frontend/Mobile/src/app/(public)/more/categorie.tsx
index d23b98a2..cc096e44 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)) {
@@ -36,7 +38,7 @@ export default function MoreCategoryScreen() {
.catch(() => {
setItems([]);
});
- }, [categoryId]);
+ }, [categoryId, i18n.language]);
const scrollY = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler((event) => {
@@ -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..ba6b7ef3 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)) {
@@ -39,7 +41,7 @@ export default function MoreCategoryScreen() {
.catch(() => {
setItems([]);
});
- }, [categoryId]);
+ }, [categoryId, i18n.language]);
const handlePress = (item: any) => {
if (item.website) {
@@ -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 1271a841..9773145f 100644
--- a/Frontend/Mobile/src/app/(public)/more/index.tsx
+++ b/Frontend/Mobile/src/app/(public)/more/index.tsx
@@ -20,6 +20,7 @@ import GlobeIcon from "@/assets/icons/svg/globe-europe.svg";
import UserIcon from "@/assets/icons/svg/user.svg";
import SettingsIcon from "@/assets/icons/svg/cog.svg";
import DoorOpenAltIcon from "@/assets/icons/svg/door-open-alt.svg";
+import { useTranslation } from 'react-i18next';
export default function MoreScreen() {
const themeName = (useColorScheme() ?? "light") as keyof typeof Colors;
@@ -27,11 +28,12 @@ export default function MoreScreen() {
const insets = useSafeAreaInsets();
const router = useRouter();
const { isAuthenticated, logout } = useAuth();
+ 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)) {
@@ -45,7 +47,7 @@ export default function MoreScreen() {
.catch(() => {
setCategories([]);
});
- }, []);
+ }, [i18n.language]);
const renderIcon = (iconName: string, color: string) => {
switch (iconName) {
@@ -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..ff9683d7 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,10 +25,11 @@ export default function MoreScreen() {
const insets = useSafeAreaInsets();
const contentTop = useWebContentTop();
const router = useRouter();
+ 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)) {
@@ -41,7 +43,7 @@ export default function MoreScreen() {
.catch(() => {
setCategories([]);
});
- }, []);
+ }, [i18n.language]);
const renderIcon = (iconName: string, color: string) => {
switch (iconName) {
@@ -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..b0137c1c 100644
--- a/Frontend/Mobile/src/app/(public)/more/limba.tsx
+++ b/Frontend/Mobile/src/app/(public)/more/limba.tsx
@@ -9,6 +9,9 @@ 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 { LANGUAGES } from '@/constants/languages';
import CloseIcon from "@/assets/icons/svg/x.svg";
@@ -27,17 +30,10 @@ 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ă" },
- { 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);
@@ -78,7 +74,7 @@ export default function LanguageScreen() {
]}
numberOfLines={1}
>
- Limbă aplicație
+ {t('language.title')}
),
@@ -94,11 +90,11 @@ export default function LanguageScreen() {
onScroll={scrollHandler}
scrollEventThrottle={16}
>
-
+
- Selectează limba
+ {t('language.select')}
@@ -133,7 +129,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..97b22c8c 100644
--- a/Frontend/Mobile/src/app/(public)/more/limba.web.tsx
+++ b/Frontend/Mobile/src/app/(public)/more/limba.web.tsx
@@ -10,6 +10,9 @@ 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 { LANGUAGES } from '@/constants/languages';
import CloseIcon from "@/assets/icons/svg/x.svg";
@@ -23,17 +26,10 @@ 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ă" },
- { 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);
@@ -80,7 +76,7 @@ export default function LanguageScreen() {
]}
numberOfLines={1}
>
- Limbă aplicație
+ {t('language.title')}
),
@@ -101,11 +97,11 @@ export default function LanguageScreen() {
>
-
+
- Selectează limba
+ {t('language.select')}
@@ -140,7 +136,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 0a443de2..752888d5 100644
--- a/Frontend/Mobile/src/app/(public)/more/setari.tsx
+++ b/Frontend/Mobile/src/app/(public)/more/setari.tsx
@@ -1,15 +1,19 @@
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, 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";
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";
+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";
@@ -29,29 +33,31 @@ export default function SettingsScreen() {
opacity: interpolate(scrollY.value, [50, 90], [0, 1], Extrapolation.CLAMP),
}));
- // Local settings states driven by settingsStore
+ const { t, i18n } = useTranslation();
+
+ // 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;
}, []);
- const [notifyStaff, setNotifyStaff] = useState(true);
- const [notifyStiri, setNotifyStiri] = useState(true);
-
- 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(t('settings.clearCacheTitle'), t('settings.clearCacheMessage'), [
+ { text: t('more.cancel'), style: "cancel" },
+ {
+ text: t('settings.clearCacheConfirm'), style: "destructive", onPress: async () => {
+ await storage.removeItem("has_seen_onboarding");
+ Alert.alert(t('settings.clearCacheDoneTitle'), t('settings.clearCacheDoneMessage'));
+ }
+ },
+ ]);
+ };
const handleOpenWebsite = async () => {
try {
@@ -98,7 +104,7 @@ export default function SettingsScreen() {
]}
numberOfLines={1}
>
- Setări
+ {t('settings.title')}
),
@@ -114,20 +120,16 @@ export default function SettingsScreen() {
onScroll={scrollHandler}
scrollEventThrottle={16}
>
-
+
{/* SECȚIUNEA 1: ASPECT & LIMBĂ */}
-
- Aspect & Limbă
-
-
{/* 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
{/* 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 || i18n.language}
- {/* SECȚIUNEA 2: NOTIFICĂRI */}
+ {/* SECȚIUNEA 2: DESPRE & ACȚIUNI */}
- Notificări
-
-
-
- {/* Notificare 1 (Alerte Campus) */}
-
-
- Alerte Campus
-
-
-
-
- {/* Notificare 2 (Știri & Anunțuri) */}
-
-
- Știri & Anunțuri
-
-
-
-
-
-
- {/* SECȚIUNEA 3: DESPRE & ACȚIUNI */}
-
-
- Asistență & Info
+ {t('settings.supportInfo')}
{/* Link Site */}
- ({
flexDirection: "row",
@@ -244,19 +210,30 @@ export default function SettingsScreen() {
opacity: pressed ? 0.6 : 1
})}
>
- Vizitează Website UGAL
+ {t('settings.visitWebsite')}
+
+ {/* Șterge cache (dev) */}
+ ({
+ paddingVertical: Spacing.sm,
+ opacity: pressed ? 0.6 : 1
+ })}
+ >
+ {t('settings.clearCacheButton')}
+
{/* Subsol (App Version) */}
- InsideUGAL v0.1.0
+ InsideUGAL v{Config.APP_VERSION}
- 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 bd3c1d69..77f6d0ae 100644
--- a/Frontend/Mobile/src/app/(public)/more/setari.web.tsx
+++ b/Frontend/Mobile/src/app/(public)/more/setari.web.tsx
@@ -1,15 +1,18 @@
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";
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";
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";
@@ -24,29 +27,19 @@ export default function SettingsScreen() {
const [scrollY] = useState(() => new Animated.Value(0));
- // Local settings states driven by settingsStore
+ const { t, i18n } = useTranslation();
+
+ // 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;
}, []);
- const [notifyStaff, setNotifyStaff] = useState(true);
- const [notifyStiri, setNotifyStiri] = useState(true);
-
- 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) =>
@@ -94,7 +87,7 @@ export default function SettingsScreen() {
]}
numberOfLines={1}
>
- Setări
+ {t('settings.title')}
),
@@ -115,20 +108,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 || i18n.language}
- {/* 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
+ {t('settings.supportInfo')}
@@ -243,7 +203,7 @@ export default function SettingsScreen() {
opacity: pressed ? 0.6 : 1
})}
>
- Vizitează Website UGAL
+ {t('settings.visitWebsite')}
@@ -252,10 +212,10 @@ export default function SettingsScreen() {
{/* Subsol (App Version) */}
- InsideUGAL v0.1.0
+ InsideUGAL v{Config.APP_VERSION}
- 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..dff1d875 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, i18n } = 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([]);
@@ -90,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));
@@ -103,23 +105,24 @@ export default function AdaugaSesizareScreen() {
}
}
loadLocations();
- }, []);
+ }, [i18n.language]);
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')}
{
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,
@@ -278,14 +281,14 @@ export default function AdaugaSesizareScreen() {
- Descriere detaliată
+ {t('reports.descDetailed')}
{
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}
@@ -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..1c1c7d43 100644
--- a/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx
+++ b/Frontend/Mobile/src/app/(public)/sesizari/adauga.web.tsx
@@ -8,10 +8,11 @@ import * as ImagePicker from "expo-image-picker";
import { Colors, Spacing } from "@/constants/theme";
import { Typography } from "@/constants/typography";
import { WebContainer } from "@/components/ui/layout/web-container";
-import api, { storage } from "@/services/api";
+import api 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, i18n } = 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 }>({});
@@ -65,19 +67,10 @@ export default function AdaugaSesizareScreen() {
useEffect(() => {
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 } });
+ 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));
- if (res.data.items.length > 0 && !cached) {
+ if (res.data.items.length > 0) {
setLocation(res.data.items[0].name);
}
}
@@ -86,23 +79,24 @@ export default function AdaugaSesizareScreen() {
}
}
loadLocations();
- }, []);
+ }, [i18n.language]);
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 +106,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 +127,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 +177,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 +212,7 @@ export default function AdaugaSesizareScreen() {
>
- Sesizare nouă
+ {t('reports.newReport')}
@@ -229,7 +223,7 @@ export default function AdaugaSesizareScreen() {
)}
- Locație
+ {t('reports.location')}
{buildingsList.map((bldg) => (
- Titlu
+ {t('reports.titleField')}
{
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,
@@ -274,14 +268,14 @@ export default function AdaugaSesizareScreen() {
- Descriere detaliată
+ {t('reports.descDetailed')}
{
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}
@@ -307,7 +301,7 @@ export default function AdaugaSesizareScreen() {
)}
- Adaugă o fotografie
+ {t('reports.addPhoto')}
{photos.length < 1 && (
- Adaugă poză
+ {t('reports.addPhotoBtn')}
)}
@@ -400,7 +394,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..6ab5c902 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, i18n } = useTranslation();
+
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [report, setReport] = useState(null);
@@ -70,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));
@@ -84,29 +86,29 @@ 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({
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]);
+ }, [id, i18n.language, t]);
useEffect(() => {
const timer = setTimeout(() => {
@@ -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..f604a383 100644
--- a/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx
+++ b/Frontend/Mobile/src/app/(public)/sesizari/detalii.web.tsx
@@ -12,8 +12,9 @@ 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';
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, i18n } = useTranslation();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -65,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 } });
+ 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));
}
} catch (locError) {
console.warn('[API] Could not fetch fresh locations for complaint detail web:', locError);
@@ -86,39 +83,39 @@ 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({
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();
- }, [id, retryKey]);
+ }, [id, retryKey, i18n.language, t]);
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);
@@ -138,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 (
@@ -173,9 +170,9 @@ export default function SesizareDetaliiScreen() {
@@ -203,14 +200,14 @@ export default function SesizareDetaliiScreen() {
- setRetryKey(prev => prev + 1)}
style={{ minHeight: 500, paddingVertical: Spacing.xl4 }}
/>
@@ -235,9 +232,9 @@ export default function SesizareDetaliiScreen() {
@@ -246,7 +243,7 @@ export default function SesizareDetaliiScreen() {
- Informații sesizare
+ {t('reports.infoTitle')}
@@ -267,7 +264,7 @@ export default function SesizareDetaliiScreen() {
- Descriere problemă
+ {t('reports.descSection')}
{description}
@@ -293,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.tsx b/Frontend/Mobile/src/app/(public)/sesizari/index.tsx
index e974a559..2a0fc8b5 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, i18n } = 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);
};
@@ -64,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));
@@ -83,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 || [])];
@@ -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;
}
};
@@ -142,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;
@@ -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..307d9b66 100644
--- a/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx
+++ b/Frontend/Mobile/src/app/(public)/sesizari/index.web.tsx
@@ -12,20 +12,13 @@ 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';
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, i18n } = useTranslation();
+
+ const filters: FilterItem[] = [
+ { 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([]);
const [loading, setLoading] = useState(true);
@@ -62,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 } });
+ 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));
}
} catch (locError) {
console.warn('[API] Could not fetch fresh locations for complaints:', locError);
@@ -87,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 || [])];
@@ -121,10 +118,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 +130,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'));
}
};
@@ -143,7 +140,7 @@ export default function SesizariScreen() {
useCallback(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [activeFilter])
+ }, [activeFilter, i18n.language])
);
useEffect(() => {
@@ -174,8 +171,8 @@ export default function SesizariScreen() {
return (
setActiveFilter((id as FilterType) || "mele")}
@@ -214,16 +211,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 +242,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..88a29cdd 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';
@@ -74,12 +75,12 @@ function UserMessageBubble({ text, timestamp, theme }: UserMessageBubbleProps) {
borderRadius: Spacing.md,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
- backgroundColor: theme.surface,
+ backgroundColor: theme.primary,
borderBottomRightRadius: 4,
}}
>
-
- {renderFormattedText(text, { color: theme.text }, { fontWeight: '700', color: theme.text })}
+
+ {renderFormattedText(text, { color: '#FFFFFF' }, { fontWeight: '700', color: '#FFFFFF' })}
@@ -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/app/index.tsx b/Frontend/Mobile/src/app/index.tsx
index dece2fe2..004abd6e 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(() => {
@@ -16,7 +17,7 @@ export default function SplashScreen() {
if (hasSeenOnboarding === "true") {
router.replace("/(public)/acasa");
} else {
- router.replace("/(onboarding)/notificari");
+ router.replace("/(onboarding)/locatie");
}
}
};
@@ -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, include_untranslated: true }
}).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/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 a3814b64..faef51c7 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';
@@ -80,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;
@@ -204,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(() => {
@@ -221,11 +224,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/article-detail.tsx b/Frontend/Mobile/src/components/ui/display/article-detail.tsx
index b505e672..acbaba1b 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";
@@ -18,15 +18,17 @@ 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";
-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";
import PhoneIcon from "@/assets/icons/svg/phone.svg";
import WebsiteIcon from "@/assets/icons/svg/globe-europe.svg";
+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;
@@ -50,6 +52,7 @@ export interface ArticleDetailProps {
website?: string;
date?: string;
author?: string;
+ files?: FileItem[];
}
export function ArticleDetail({
@@ -69,34 +72,28 @@ export function ArticleDetail({
website = "",
date = "",
author = "",
+ files,
}: ArticleDetailProps) {
const themeName = (useColorScheme() ?? "light") as keyof typeof Colors;
const theme = Colors[themeName];
const insets = useSafeAreaInsets();
const router = useRouter();
const { width } = useWindowDimensions();
+ const { t, i18n } = useTranslation();
const twoCol = width >= TWO_COL_BREAKPOINT;
const tipPagina = type || "Eveniment";
const [relatedPool, setRelatedPool] = useState([]);
+ const [imgErr, setImgErr] = useState(false);
useEffect(() => {
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, include_untranslated: true } });
+ if (res.data?.items && isMounted) {
+ setRelatedPool(res.data.items);
}
} catch (err) {
console.warn('[ArticleDetail] Error loading related announcements:', err);
@@ -106,7 +103,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
@@ -116,17 +113,17 @@ export function ArticleDetail({
.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,
@@ -152,18 +149,18 @@ export function ArticleDetail({
// Navigare catre alt articol. Evenimentele si anunturile au URL curat.
const openItem = (item: any) => {
- 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,
@@ -179,29 +176,24 @@ 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(t('detail.callConfirm', { phone }))) {
+ Linking.openURL(`tel:${phone}`);
+ }
};
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.
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 (
@@ -210,8 +202,9 @@ 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"
/>
@@ -232,7 +225,7 @@ export function ArticleDetail({
) : (
- {category || (tipPagina === "Facultate" ? "Facultate" : "Categorie")}
+ {category || (tipPagina === "Facultate" ? t('common.faculty') : t('common.category'))}
)}
- {title || "Titlu"}
+ {title || t('common.unknownTitle')}
@@ -257,30 +250,30 @@ 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 || ""}
-
- {location || "Locație nespecificată"}
+
+ {location || t('common.unknownLocation')}
@@ -290,14 +283,14 @@ export function ArticleDetail({
{tipPagina === "Facultate" && (
- Contact și Locație
+ {t('detail.contact')}
- Adresă
-
- {address || "Nespecificată"}
+ {t('detail.address')}
+
+ {address || t('common.unknownAddress')}
@@ -306,9 +299,9 @@ export function ArticleDetail({
- Telefon
+ {t('detail.phone')}
- {phone}
+ {phone}
@@ -318,9 +311,9 @@ export function ArticleDetail({
- Website
+ {t('detail.website')}
Linking.openURL(website)}>
- {website}
+ {website}
@@ -331,18 +324,20 @@ 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')}
+
+
{/* Dreapta: 3 carduri Noutăți, una sub alta. */}
{sidebarItems.length > 0 && (
- Articole similare
+ {t('detail.relatedArticles')}
{sidebarItems.map((item) => (
openItem(item)} />
))}
@@ -353,7 +348,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/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/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
new file mode 100644
index 00000000..3edff7ce
--- /dev/null
+++ b/Frontend/Mobile/src/components/ui/display/file-attachment.tsx
@@ -0,0 +1,75 @@
+import { Platform, Linking, TouchableOpacity, View, Text } from "react-native";
+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";
+import DownloadIcon from "@/assets/icons/svg/arrow-to-bottom-stroke.svg";
+import { useTranslation } from "react-i18next";
+
+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];
+ const { t } = useTranslation();
+
+ if (!files?.length) return null;
+
+ return (
+
+ {t('common.attachedFiles')}
+
+ {files.map((f, i) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
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 b55a58c9..98ccea73 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 (
setImgErr(true)}
style={{ width: (height || defaultHeight) as any, height: (height || defaultHeight) as any, borderRadius: Spacing.lg, overflow: "hidden" }}
contentFit="cover"
/>
@@ -115,6 +119,7 @@ export function NewsCard({
>
setImgErr(true)}
style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0, zIndex: 1 }}
contentFit="cover"
/>
@@ -158,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 83a0eb56..9bb5fbbd 100644
--- a/Frontend/Mobile/src/components/ui/display/news-card.web.tsx
+++ b/Frontend/Mobile/src/components/ui/display/news-card.web.tsx
@@ -5,6 +5,7 @@ import { LinearGradient } from "expo-linear-gradient";
import { Typography } from "@/constants/typography";
import { Colors, Spacing, ColorScheme } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
+import { useTranslation } from "react-i18next";
const DEFAULT_IMAGE = require("@/assets/images/campus-stiintei.png");
@@ -22,7 +23,8 @@ export interface NewsCardProps {
}
export function CategoryTag({ category }: { category: string }) {
- const isEvent = category === "Evenimente";
+ const { t } = useTranslation();
+ const isEvent = category === t('home.events');
return (
setImgErr(true)}
accessibilityLabel={title}
style={{ width: (height || defaultHeight) as any, height: (height || defaultHeight) as any, borderRadius: Spacing.lg, overflow: "hidden" }}
contentFit="cover"
@@ -113,12 +117,15 @@ export function NewsCard({
})}
{...(Platform.OS === "web" ? ({ dataSet: { card: "true" } } as any) : {})}
>
-
+
+ setImgErr(true)}
+ accessibilityLabel={title}
+ style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}
+ contentFit="cover"
+ />
+
-
+
+ setImgErr(true)}
+ style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}
+ contentFit="cover"
+ />
+
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/display/sesizare-card.tsx b/Frontend/Mobile/src/components/ui/display/sesizare-card.tsx
index 02e34b2b..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,13 +27,16 @@ 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 (
setImgErr(true)}
style={{ width: 100, height: 100, borderRadius: 10 }}
contentFit="cover"
/>
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 (
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/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/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/profile-menu.tsx b/Frontend/Mobile/src/components/ui/navigation/profile-menu.tsx
index 8732c678..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";
@@ -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 || "";
@@ -20,10 +21,12 @@ 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();
const { isAuthenticated, user, logout } = useAuth();
+ const { t } = useTranslation();
const [localOpen, setLocalOpen] = useState(false);
const open = controlledOpen !== undefined ? controlledOpen : localOpen;
@@ -52,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]);
@@ -96,13 +117,13 @@ export function ProfileMenu({
];
return (
-
+
{/* Trigger: iconita user, fara border, cu fundal plin cand e deschis (ca rotita). */}
({ opacity: pressed ? 0.6 : 1 })}
>
- {isAuthenticated ? (user?.name || "Utilizator") : "Neautentificat"}
+ {isAuthenticated ? (user?.name || t('common.user')) : t('navbar.unauthenticated')}
{isAuthenticated && user?.email ? (
@@ -170,11 +191,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 8c149631..a1ecdadc 100644
--- a/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx
+++ b/Frontend/Mobile/src/components/ui/navigation/theme-menu.tsx
@@ -1,18 +1,29 @@
-// 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 { 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";
+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";
+import { LANGUAGES } from "@/constants/languages";
+
+const THEMES = [
+ { id: "system" as const, label: "Sistem" },
+ { id: "light" as const, label: "Luminos" },
+ { id: "dark" as const, label: "Întunecat" },
+];
+
+const SHADOW = {
+ shadowColor: ColorScheme.pureBlack,
+ shadowOffset: { width: 0, height: 6 },
+ shadowOpacity: 0.12,
+ shadowRadius: 16,
+};
export function ThemeMenu({
solid = true,
@@ -25,45 +36,74 @@ export function ThemeMenu({
onToggle?: () => void;
onClose?: () => void;
}) {
+ const containerRef = useRef(null);
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 { t } = useTranslation();
- 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);
+ if (onClose) onClose(); else setLocalOpen(false);
};
+ // 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),
- }));
+ 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();
+ 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]);
@@ -74,28 +114,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 = 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 (
-
- {/* 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.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}
+
+ )}
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* Panoul principal: Temă curentă + Limbă curentă */}
+
+ {[
+ { label: t('theme.current'), value: themeLabel, sub: "tema" as const },
+ { label: t('language.current'), value: langLabel, sub: "limba" as const },
+ ].map((item) => {
+ 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,
},
- (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}
+
+
+
+ >
)}
);
})}
+
+
+ InsideUGAL v{Config.APP_VERSION}
+
+
);
-}
+}
\ No newline at end of file
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.
@@ -88,6 +87,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
@@ -95,7 +95,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).
@@ -170,7 +170,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);
@@ -276,23 +276,15 @@ export function WebNavbar() {
router.push("/(public)/acasa")}
accessibilityRole="link"
- accessibilityLabel="InsideUGAL — Acasă"
+ accessibilityLabel={"InsideUGAL — " + t('navbar.home')}
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
>
{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
@@ -300,7 +292,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 })}
>
@@ -316,7 +308,7 @@ export function WebNavbar() {
if (link.dropdown) {
return (
@@ -332,7 +324,7 @@ export function WebNavbar() {
gap: 2,
})}
>
- {link.label}
+ {t(link.labelKey)}
@@ -359,7 +351,7 @@ export function WebNavbar() {
{ color: (pressed || hovered) ? theme.primary : ColorScheme.black },
]}
>
- {sub.label}
+ {t(sub.labelKey)}
)}
@@ -378,17 +370,12 @@ 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)}
);
})}
- setActiveMenu(activeMenu === "notifications" ? null : "notifications")}
- onClose={() => activeMenu === "notifications" && setActiveMenu(null)}
- />
- {/* 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)}
))}
@@ -454,32 +441,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}
@@ -494,7 +490,7 @@ export function WebNavbar() {
}}
style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]}
>
-
+
Dashboard
@@ -510,8 +506,8 @@ export function WebNavbar() {
}}
style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]}
>
-
- Deconectare
+
+ {t('navbar.logout')}
) : (
@@ -522,8 +518,8 @@ export function WebNavbar() {
}}
style={({ pressed }) => [styles.panelLink, { opacity: pressed ? 0.6 : 1 }]}
>
-
- Autentificare
+
+ {t('navbar.login')}
)}
@@ -610,6 +606,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/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
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/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/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..acef8a13
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/index.ts
@@ -0,0 +1,47 @@
+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';
+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';
+
+// eslint-disable-next-line import/no-named-as-default-member
+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',
+ interpolation: { escapeValue: false },
+});
+
+export default i18n;
diff --git a/Frontend/Mobile/src/i18n/locales/ar.json b/Frontend/Mobile/src/i18n/locales/ar.json
new file mode 100644
index 00000000..18ff831c
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/ar.json
@@ -0,0 +1,258 @@
+{
+ "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": "منصتك الجامعية",
+ "error": "خطأ"
+ },
+ "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": "تسجيل الخروج",
+ "guideFallback": "دليل"
+ },
+ "settings": {
+ "title": "الإعدادات",
+ "appearanceLang": "المظهر واللغة",
+ "themeApp": "مظهر التطبيق",
+ "currentTheme": "المظهر الحالي:",
+ "currentLang": "اللغة الحالية:",
+ "supportInfo": "المساعدة والمعلومات",
+ "visitWebsite": "زيارة موقع UGAL",
+ "appSlogan": "تم إنشاؤه لطلاب جامعة «Dunărea de Jos» في Galați",
+ "clearCacheButton": "مسح ذاكرة التخزين المؤقت",
+ "clearCacheTitle": "مسح ذاكرة التخزين المؤقت",
+ "clearCacheMessage": "سيتم إعادة ضبط عملية الإعداد. عند إعادة التشغيل سترى طلب الأذونات مرة أخرى.",
+ "clearCacheConfirm": "مسح",
+ "clearCacheDoneTitle": "تم",
+ "clearCacheDoneMessage": "تم مسح ذاكرة التخزين المؤقت. أعد تشغيل التطبيق."
+ },
+ "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": "خارجي",
+ "detailsFallbackTitle": "تفاصيل البلاغ",
+ "titlePlaceholder": "مثال: تدفئة معطلة في المدرج",
+ "descPlaceholder": "صف المشكلة التي واجهتها بالتفصيل...",
+ "seoDescription": "أبلغ عن مشاكل الحرم الجامعي والسكن الطلابي وتابع حالة بلاغاتك — InsideUGAL."
+ },
+ "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..cf1e0321
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/de.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Fehler"
+ },
+ "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",
+ "guideFallback": "Leitfaden"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/el.json b/Frontend/Mobile/src/i18n/locales/el.json
new file mode 100644
index 00000000..2b11a423
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/el.json
@@ -0,0 +1,258 @@
+{
+ "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": "Η πανεπιστημιακή σας πλατφόρμα",
+ "error": "Σφάλμα"
+ },
+ "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": "Αποσύνδεση",
+ "guideFallback": "Οδηγός"
+ },
+ "settings": {
+ "title": "Ρυθμίσεις",
+ "appearanceLang": "Εμφάνιση & Γλώσσα",
+ "themeApp": "Θέμα εφαρμογής",
+ "currentTheme": "Τρέχον θέμα:",
+ "currentLang": "Τρέχουσα γλώσσα:",
+ "supportInfo": "Βοήθεια & Πληροφορίες",
+ "visitWebsite": "Επίσκεψη ιστότοπου UGAL",
+ "appSlogan": "Δημιουργήθηκε για τους φοιτητές του Πανεπιστημίου «Dunărea de Jos» του Galați",
+ "clearCacheButton": "Εκκαθάριση cache",
+ "clearCacheTitle": "Εκκαθάριση cache",
+ "clearCacheMessage": "Η εισαγωγή θα επαναφερθεί. Στην επόμενη εκκίνηση θα δεις ξανά το αίτημα δικαιωμάτων.",
+ "clearCacheConfirm": "Εκκαθάριση",
+ "clearCacheDoneTitle": "Έτοιμο",
+ "clearCacheDoneMessage": "Η cache διαγράφηκε. Επανεκκίνησε την εφαρμογή."
+ },
+ "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": "Εξωτερικό",
+ "detailsFallbackTitle": "Λεπτομέρειες αναφοράς",
+ "titlePlaceholder": "Π.χ.: Χαλασμένη θέρμανση στο αμφιθέατρο",
+ "descPlaceholder": "Περιέγραψε λεπτομερώς το πρόβλημα που αντιμετώπισες...",
+ "seoDescription": "Ανέφερε προβλήματα στο πανεπιστήμιο και στις εστίες και παρακολούθησε την κατάσταση των αναφορών σου — InsideUGAL."
+ },
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/en.json b/Frontend/Mobile/src/i18n/locales/en.json
new file mode 100644
index 00000000..6f5ea5dd
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/en.json
@@ -0,0 +1,258 @@
+{
+ "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...",
+ "viewAll": "View all",
+ "university": "University",
+ "universityPlatform": "Your university platform",
+ "error": "Error"
+ },
+ "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",
+ "guideFallback": "Guide"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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/es.json b/Frontend/Mobile/src/i18n/locales/es.json
new file mode 100644
index 00000000..c3842016
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/es.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Error"
+ },
+ "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",
+ "guideFallback": "Guía"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/fr.json b/Frontend/Mobile/src/i18n/locales/fr.json
new file mode 100644
index 00000000..3ccb1ad2
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/fr.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Erreur"
+ },
+ "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",
+ "guideFallback": "Guide"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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..e011d45f
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/hi.json
@@ -0,0 +1,258 @@
+{
+ "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": "आपका विश्वविद्यालय प्लेटफ़ॉर्म",
+ "error": "त्रुटि"
+ },
+ "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": "लॉग आउट",
+ "guideFallback": "गाइड"
+ },
+ "settings": {
+ "title": "सेटिंग्स",
+ "appearanceLang": "रूप और भाषा",
+ "themeApp": "ऐप थीम",
+ "currentTheme": "वर्तमान थीम:",
+ "currentLang": "वर्तमान भाषा:",
+ "supportInfo": "सहायता और जानकारी",
+ "visitWebsite": "UGAL वेबसाइट देखें",
+ "appSlogan": "Galați के «Dunărea de Jos» विश्वविद्यालय के छात्रों के लिए बनाया गया",
+ "clearCacheButton": "कैश साफ़ करें",
+ "clearCacheTitle": "कैश साफ़ करें",
+ "clearCacheMessage": "ऑनबोर्डिंग रीसेट हो जाएगी। अगली बार खोलने पर आपको फिर से अनुमति अनुरोध दिखेगा।",
+ "clearCacheConfirm": "साफ़ करें",
+ "clearCacheDoneTitle": "हो गया",
+ "clearCacheDoneMessage": "कैश साफ़ हो गया। ऐप को पुनरारंभ करें।"
+ },
+ "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": "बाहरी",
+ "detailsFallbackTitle": "रिपोर्ट विवरण",
+ "titlePlaceholder": "उदा: एम्फीथिएटर में हीटिंग खराब है",
+ "descPlaceholder": "सामने आई समस्या का विस्तार से वर्णन करें...",
+ "seoDescription": "कैंपस और छात्रावास की समस्याओं की रिपोर्ट करें और अपनी रिपोर्ट की स्थिति ट्रैक करें — InsideUGAL."
+ },
+ "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..f2794d30
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/it.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Errore"
+ },
+ "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",
+ "guideFallback": "Guida"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/ja.json b/Frontend/Mobile/src/i18n/locales/ja.json
new file mode 100644
index 00000000..a047cccb
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/ja.json
@@ -0,0 +1,258 @@
+{
+ "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": "あなたの大学プラットフォーム",
+ "error": "エラー"
+ },
+ "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": "ログアウト",
+ "guideFallback": "ガイド"
+ },
+ "settings": {
+ "title": "設定",
+ "appearanceLang": "外観と言語",
+ "themeApp": "アプリのテーマ",
+ "currentTheme": "現在のテーマ:",
+ "currentLang": "現在の言語:",
+ "supportInfo": "ヘルプと情報",
+ "visitWebsite": "UGAL ウェブサイトを訪問",
+ "appSlogan": "Galați の「Dunărea de Jos」大学の学生のために作られました",
+ "clearCacheButton": "キャッシュを削除",
+ "clearCacheTitle": "キャッシュを削除",
+ "clearCacheMessage": "オンボーディングがリセットされます。次回起動時に権限のリクエストが再度表示されます。",
+ "clearCacheConfirm": "削除",
+ "clearCacheDoneTitle": "完了",
+ "clearCacheDoneMessage": "キャッシュを削除しました。アプリを再起動してください。"
+ },
+ "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": "外部",
+ "detailsFallbackTitle": "報告の詳細",
+ "titlePlaceholder": "例:講堂の暖房が故障している",
+ "descPlaceholder": "発生した問題を詳しく説明してください...",
+ "seoDescription": "キャンパスや寮の問題を報告し、報告の状況を確認できます — InsideUGAL。"
+ },
+ "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..ebf07019
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/ko.json
@@ -0,0 +1,258 @@
+{
+ "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": "당신의 대학 플랫폼",
+ "error": "오류"
+ },
+ "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": "로그아웃",
+ "guideFallback": "가이드"
+ },
+ "settings": {
+ "title": "설정",
+ "appearanceLang": "외관 및 언어",
+ "themeApp": "앱 테마",
+ "currentTheme": "현재 테마:",
+ "currentLang": "현재 언어:",
+ "supportInfo": "도움말 및 정보",
+ "visitWebsite": "UGAL 웹사이트 방문",
+ "appSlogan": "Galați «Dunărea de Jos» 대학교 학생들을 위해 만들어졌습니다",
+ "clearCacheButton": "캐시 지우기",
+ "clearCacheTitle": "캐시 지우기",
+ "clearCacheMessage": "온보딩이 초기화됩니다. 다음 실행 시 권한 요청이 다시 표시됩니다.",
+ "clearCacheConfirm": "지우기",
+ "clearCacheDoneTitle": "완료",
+ "clearCacheDoneMessage": "캐시가 삭제되었습니다. 앱을 재시작하세요."
+ },
+ "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": "외부",
+ "detailsFallbackTitle": "신고 세부정보",
+ "titlePlaceholder": "예: 강당 난방 고장",
+ "descPlaceholder": "발생한 문제를 자세히 설명해 주세요...",
+ "seoDescription": "캠퍼스 및 기숙사 문제를 신고하고 신고 상태를 확인하세요 — InsideUGAL."
+ },
+ "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/ro.json b/Frontend/Mobile/src/i18n/locales/ro.json
new file mode 100644
index 00000000..690d14f7
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/ro.json
@@ -0,0 +1,258 @@
+{
+ "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...",
+ "viewAll": "Vezi mai multe",
+ "university": "Universitate",
+ "universityPlatform": "Platforma ta universitară",
+ "error": "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",
+ "guideFallback": "Ghid"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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/i18n/locales/ru.json b/Frontend/Mobile/src/i18n/locales/ru.json
new file mode 100644
index 00000000..0f9000ba
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/ru.json
@@ -0,0 +1,258 @@
+{
+ "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": "Ваша университетская платформа",
+ "error": "Ошибка"
+ },
+ "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": "Выйти",
+ "guideFallback": "Гид"
+ },
+ "settings": {
+ "title": "Настройки",
+ "appearanceLang": "Внешний вид и Язык",
+ "themeApp": "Тема приложения",
+ "currentTheme": "Текущая тема:",
+ "currentLang": "Текущий язык:",
+ "supportInfo": "Помощь и Информация",
+ "visitWebsite": "Посетить сайт UGAL",
+ "appSlogan": "Создано для студентов Университета «Dunărea de Jos» в Galați",
+ "clearCacheButton": "Очистить кэш",
+ "clearCacheTitle": "Очистить кэш",
+ "clearCacheMessage": "Онбординг будет сброшен. При следующем запуске вы снова увидите запрос разрешений.",
+ "clearCacheConfirm": "Очистить",
+ "clearCacheDoneTitle": "Готово",
+ "clearCacheDoneMessage": "Кэш очищен. Перезапустите приложение."
+ },
+ "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": "Внешнее",
+ "detailsFallbackTitle": "Детали обращения",
+ "titlePlaceholder": "Напр.: Не работает отопление в амфитеатре",
+ "descPlaceholder": "Подробно опишите возникшую проблему...",
+ "seoDescription": "Сообщайте о проблемах в кампусе и общежитиях и отслеживайте статус своих обращений — InsideUGAL."
+ },
+ "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..0031122d
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/tr.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Hata"
+ },
+ "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",
+ "guideFallback": "Rehber"
+ },
+ "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",
+ "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ı",
+ "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",
+ "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",
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/uk.json b/Frontend/Mobile/src/i18n/locales/uk.json
new file mode 100644
index 00000000..b8d61b1e
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/uk.json
@@ -0,0 +1,258 @@
+{
+ "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": "Ваша університетська платформа",
+ "error": "Помилка"
+ },
+ "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": "Вийти",
+ "guideFallback": "Гід"
+ },
+ "settings": {
+ "title": "Налаштування",
+ "appearanceLang": "Зовнішній вигляд і Мова",
+ "themeApp": "Тема додатку",
+ "currentTheme": "Поточна тема:",
+ "currentLang": "Поточна мова:",
+ "supportInfo": "Допомога та Інформація",
+ "visitWebsite": "Відвідати сайт UGAL",
+ "appSlogan": "Створено для студентів Університету «Dunărea de Jos» у Galați",
+ "clearCacheButton": "Очистити кеш",
+ "clearCacheTitle": "Очистити кеш",
+ "clearCacheMessage": "Онбординг буде скинуто. Під час наступного запуску ти знову побачиш запит дозволів.",
+ "clearCacheConfirm": "Очистити",
+ "clearCacheDoneTitle": "Готово",
+ "clearCacheDoneMessage": "Кеш очищено. Перезапусти застосунок."
+ },
+ "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": "Зовнішнє",
+ "detailsFallbackTitle": "Деталі звернення",
+ "titlePlaceholder": "Напр.: Не працює опалення в амфітеатрі",
+ "descPlaceholder": "Детально опиши проблему, з якою ти зіткнувся...",
+ "seoDescription": "Повідомляй про проблеми в кампусі та гуртожитках і відстежуй статус своїх звернень — InsideUGAL."
+ },
+ "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/vi.json b/Frontend/Mobile/src/i18n/locales/vi.json
new file mode 100644
index 00000000..219f227a
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/vi.json
@@ -0,0 +1,258 @@
+{
+ "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",
+ "error": "Lỗi"
+ },
+ "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",
+ "guideFallback": "Hướng dẫn"
+ },
+ "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",
+ "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",
+ "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",
+ "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",
+ "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"
+ }
+}
diff --git a/Frontend/Mobile/src/i18n/locales/zh.json b/Frontend/Mobile/src/i18n/locales/zh.json
new file mode 100644
index 00000000..6bd1597a
--- /dev/null
+++ b/Frontend/Mobile/src/i18n/locales/zh.json
@@ -0,0 +1,258 @@
+{
+ "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": "您的大学平台",
+ "error": "错误"
+ },
+ "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": "登出",
+ "guideFallback": "指南"
+ },
+ "settings": {
+ "title": "设置",
+ "appearanceLang": "外观与语言",
+ "themeApp": "应用主题",
+ "currentTheme": "当前主题:",
+ "currentLang": "当前语言:",
+ "supportInfo": "帮助与信息",
+ "visitWebsite": "访问 UGAL 官网",
+ "appSlogan": "专为加拉茨「Dunărea de Jos」大学学生创建",
+ "clearCacheButton": "清除缓存",
+ "clearCacheTitle": "清除缓存",
+ "clearCacheMessage": "引导流程将被重置。下次启动时你将再次看到权限请求。",
+ "clearCacheConfirm": "清除",
+ "clearCacheDoneTitle": "完成",
+ "clearCacheDoneMessage": "缓存已清除。请重启应用。"
+ },
+ "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": "外部",
+ "detailsFallbackTitle": "报告详情",
+ "titlePlaceholder": "例如:阶梯教室暖气故障",
+ "descPlaceholder": "详细描述你遇到的问题...",
+ "seoDescription": "报告校园和宿舍问题,并跟踪你的报告状态 — InsideUGAL。"
+ },
+ "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/services/api.ts b/Frontend/Mobile/src/services/api.ts
index f80d4e48..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,
@@ -393,6 +394,11 @@ function cleanErrorMessage(detail: string | undefined, defaultMsg: string, statu
}
}
+ // Server-side errors are not auth errors
+ if (status && status >= 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 d7e6bfd0..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) => {
- if (!text) return "1 min citire";
- const words = text.split(/\s+/).length;
- const minutes = Math.max(1, Math.ceil(words / 200));
- 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 17ea5ce7..c48af822 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';
+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 lang = getLocales()[0]?.languageCode;
+ return lang && 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,7 +64,11 @@ 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));
}
subscribe(listener: () => void) {
@@ -33,4 +83,4 @@ class SettingsStore {
}
}
-export const settingsStore = new SettingsStore();
+export const settingsStore = new SettingsStore();
\ No newline at end of file