diff --git a/backend/api/openapi.yaml b/backend/api/openapi.yaml index c3310bf4..891df758 100644 --- a/backend/api/openapi.yaml +++ b/backend/api/openapi.yaml @@ -1895,6 +1895,15 @@ paths: summary: Creates a saved event description: Creates a saved event operationId: create-saved + parameters: + - name: Accept-Language + in: header + schema: + type: string + default: en-US + enum: + - en-US + - th-TH requestBody: content: application/json: @@ -1948,6 +1957,14 @@ paths: default: 10 minimum: 1 maximum: 100 + - name: Accept-Language + in: header + schema: + type: string + default: en-US + enum: + - en-US + - th-TH responses: "200": description: OK @@ -3390,6 +3407,7 @@ components: - name - active - presigned_url + - location_id - created_at - updated_at - stripe_account_id diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 4f2482ac..2243e3fe 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -36,7 +36,6 @@ func main() { } }() - port := cfg.Application.Port // Listen for connections with a goroutine @@ -54,7 +53,6 @@ func main() { slog.Info("Shutting down server") - // Shutdown server with timeout shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() diff --git a/backend/internal/models/saved.go b/backend/internal/models/saved.go index 75df2cdd..f8ef272a 100644 --- a/backend/internal/models/saved.go +++ b/backend/internal/models/saved.go @@ -19,6 +19,7 @@ type CreateSavedInput struct { GuardianID uuid.UUID `json:"guardian_id" db:"guardian_id" doc:"ID of the guardian that saved this."` EventID uuid.UUID `json:"event_id" db:"event_id" doc:"ID of this saved event."` } + AcceptLanguage string `header:"Accept-Language" default:"en-US" enum:"en-US,th-TH"` } type CreateSavedOutput struct { @@ -36,9 +37,10 @@ type DeleteSavedOutput struct { } type GetSavedInput struct { - ID uuid.UUID `path:"id"` - Page int `query:"page" minimum:"1" default:"1" doc:"Page number (starts at 1)"` - PageSize int `query:"page_size" minimum:"1" maximum:"100" default:"10" doc:"Number of items per page"` + ID uuid.UUID `path:"id"` + Page int `query:"page" minimum:"1" default:"1" doc:"Page number (starts at 1)"` + PageSize int `query:"page_size" minimum:"1" maximum:"100" default:"10" doc:"Number of items per page"` + AcceptLanguage string `header:"Accept-Language" default:"en-US" enum:"en-US,th-TH"` } type GetSavedOutput struct { diff --git a/backend/internal/service/handler/saved/getByGuardianID.go b/backend/internal/service/handler/saved/getByGuardianID.go index df52e811..ec4a3bb6 100644 --- a/backend/internal/service/handler/saved/getByGuardianID.go +++ b/backend/internal/service/handler/saved/getByGuardianID.go @@ -9,13 +9,13 @@ import ( "github.com/google/uuid" ) -func (h *Handler) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) { +func (h *Handler) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error) { if _, err := h.GuardianRepository.GetGuardianByID(ctx, id); err != nil { return nil, errs.BadRequest("Invalid guardian_id: guardian does not exist") } - reviews, httpErr := h.SavedRepository.GetByGuardianID(ctx, id, pagination) + reviews, httpErr := h.SavedRepository.GetByGuardianID(ctx, id, pagination, AcceptLanguage) if httpErr != nil { return nil, httpErr } diff --git a/backend/internal/service/handler/saved/handler_test.go b/backend/internal/service/handler/saved/handler_test.go index c9a43a9a..67e184f0 100644 --- a/backend/internal/service/handler/saved/handler_test.go +++ b/backend/internal/service/handler/saved/handler_test.go @@ -37,6 +37,7 @@ func TestHandler_GetByGuardianID(t *testing.T) { mock.Anything, uuid.MustParse("11111111-1111-1111-1111-111111111111"), mock.AnythingOfType("utils.Pagination"), + mock.AnythingOfType("string"), ).Return([]models.Saved{ { ID: uuid.MustParse("20000000-0000-0000-0000-000000000001"), @@ -81,6 +82,7 @@ func TestHandler_GetByGuardianID(t *testing.T) { mock.Anything, uuid.MustParse("22222222-2222-2222-2222-222222222222"), mock.AnythingOfType("utils.Pagination"), + mock.AnythingOfType("string"), ).Return(nil, errs.BadRequest("cannot fetch saved")) }, wantSaved: nil, @@ -105,7 +107,7 @@ func TestHandler_GetByGuardianID(t *testing.T) { pagination := utils.Pagination{Page: 1, Limit: 10} - saved, err := handler.GetByGuardianID(context.Background(), tt.guardianID, pagination) + saved, err := handler.GetByGuardianID(context.Background(), tt.guardianID, pagination, "en-US") if tt.wantErr { assert.Error(t, err) diff --git a/backend/internal/service/routes/saved.go b/backend/internal/service/routes/saved.go index 1d90d39a..0b2986a9 100644 --- a/backend/internal/service/routes/saved.go +++ b/backend/internal/service/routes/saved.go @@ -38,7 +38,7 @@ func SetUpSavedRoutes(api huma.API, repo *storage.Repository) { Limit: limit, } - saveds, err := savedHandler.SavedRepository.GetByGuardianID(ctx, input.ID, pagination) + saveds, err := savedHandler.SavedRepository.GetByGuardianID(ctx, input.ID, pagination, input.AcceptLanguage) if err != nil { return nil, err } diff --git a/backend/internal/service/routes/saved_routes_test.go b/backend/internal/service/routes/saved_routes_test.go index 63b394e7..9882cb90 100644 --- a/backend/internal/service/routes/saved_routes_test.go +++ b/backend/internal/service/routes/saved_routes_test.go @@ -71,6 +71,7 @@ func TestGetSavedByGuardianID_Success(t *testing.T) { mock.Anything, guardianID, utils.Pagination{Page: 1, Limit: 10}, + mock.AnythingOfType("string"), ).Return(expectedSaved, nil) app, _ := setupSavedTestAPI(mockRepo) @@ -141,6 +142,7 @@ func TestGetSavedByGuardianID_WithPagination(t *testing.T) { mock.Anything, guardianID, utils.Pagination{Page: 2, Limit: 10}, + mock.AnythingOfType("string"), ).Return(expectedSaved, nil) app, _ := setupSavedTestAPI(mockRepo) diff --git a/backend/internal/storage/postgres/schema/saved/create.go b/backend/internal/storage/postgres/schema/saved/create.go index fa44a2bd..d11cde26 100644 --- a/backend/internal/storage/postgres/schema/saved/create.go +++ b/backend/internal/storage/postgres/schema/saved/create.go @@ -7,9 +7,10 @@ import ( "skillspark/internal/storage/postgres/schema" ) -const language = "en-US" +var language string func (r *SavedRepository) CreateSaved(ctx context.Context, saved *models.CreateSavedInput) (*models.Saved, error) { + language = saved.AcceptLanguage query, err := schema.ReadSQLBaseScript("create.sql", SqlSavedFiles) if err != nil { diff --git a/backend/internal/storage/postgres/schema/saved/get_all_for_user.go b/backend/internal/storage/postgres/schema/saved/get_all_for_user.go index edb61a85..069b2fd0 100644 --- a/backend/internal/storage/postgres/schema/saved/get_all_for_user.go +++ b/backend/internal/storage/postgres/schema/saved/get_all_for_user.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" ) -func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) { +func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error) { query, err := schema.ReadSQLBaseScript("get_all_for_user.sql", SqlSavedFiles) if err != nil { @@ -64,7 +64,7 @@ func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID return nil, &err } - switch language { + switch AcceptLanguage { case "th-TH": if titleTH != nil { s.Event.Title = *titleTH diff --git a/backend/internal/storage/postgres/schema/saved/get_all_for_user_test.go b/backend/internal/storage/postgres/schema/saved/get_all_for_user_test.go index 623cb991..4093cb5d 100644 --- a/backend/internal/storage/postgres/schema/saved/get_all_for_user_test.go +++ b/backend/internal/storage/postgres/schema/saved/get_all_for_user_test.go @@ -46,7 +46,7 @@ func TestGetReviewsByGuardianID(t *testing.T) { } pagination := utils.Pagination{Limit: 10, Page: 1} - reviews, err := repo.GetByGuardianID(ctx, firstSaved.GuardianID, pagination) + reviews, err := repo.GetByGuardianID(ctx, firstSaved.GuardianID, pagination, "en-US") require.Nil(t, err) require.Len(t, reviews, len(expectedSaved)) } @@ -60,7 +60,7 @@ func TestGetReviewsByGuardianID_NoReviews(t *testing.T) { g := guardian.CreateTestGuardian(t, ctx, testDB) pagination := utils.Pagination{Limit: 10, Page: 1} - reviews, err := repo.GetByGuardianID(ctx, g.ID, pagination) + reviews, err := repo.GetByGuardianID(ctx, g.ID, pagination, "en-US") require.Nil(t, err) require.NotNil(t, reviews) diff --git a/backend/internal/storage/repo-mocks/savedMock.go b/backend/internal/storage/repo-mocks/savedMock.go index f5828b37..b7bbf87b 100644 --- a/backend/internal/storage/repo-mocks/savedMock.go +++ b/backend/internal/storage/repo-mocks/savedMock.go @@ -24,8 +24,8 @@ func (m *MockSavedRepository) CreateSaved(ctx context.Context, input *models.Cre return args.Get(0).(*models.Saved), args.Error(1) } -func (m *MockSavedRepository) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) { - args := m.Called(ctx, id, pagination) +func (m *MockSavedRepository) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination, acceptLanguage string) ([]models.Saved, error) { + args := m.Called(ctx, id, pagination, acceptLanguage) if args.Get(0) == nil { if args.Get(1) == nil { return nil, nil diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index c7bff0e7..06184ded 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -131,7 +131,7 @@ type NotificationRepository interface { type SavedRepository interface { CreateSaved(ctx context.Context, saved *models.CreateSavedInput) (*models.Saved, error) DeleteSaved(ctx context.Context, id uuid.UUID) error - GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) + GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error) } type Repository struct { diff --git a/frontend/apps/mobile/app/(app)/(tabs)/_layout.tsx b/frontend/apps/mobile/app/(app)/(tabs)/_layout.tsx index 1b04bb37..bcbe1840 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/_layout.tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -5,9 +5,11 @@ import { HapticTab } from "@/components/haptic-tab"; import { IconSymbol } from "@/components/ui/icon-symbol"; import { Colors } from "@/constants/theme"; import { useColorScheme } from "@/hooks/use-color-scheme"; +import { useTranslation } from "react-i18next"; export default function TabLayout() { const colorScheme = useColorScheme(); + const { t: translate } = useTranslation(); return ( ( ), @@ -29,7 +31,7 @@ export default function TabLayout() { ( ), @@ -38,7 +40,7 @@ export default function TabLayout() { , }} /> diff --git a/frontend/apps/mobile/app/(app)/(tabs)/event/[id].tsx b/frontend/apps/mobile/app/(app)/(tabs)/event/[id].tsx index 95aa807f..2f755369 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/event/[id].tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/event/[id].tsx @@ -17,6 +17,7 @@ import { AppColors } from "@/constants/theme"; import { StarRating } from "@/components/StarRating"; import { BookmarkButton } from "@/components/BookmarkButton"; import { formatDuration } from "@/utils/format"; +import { useTranslation } from "react-i18next"; function formatAddress(occurrence: EventOccurrence) { const loc = occurrence.location; @@ -29,7 +30,11 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) const insets = useSafeAreaInsets(); const [descriptionExpanded, setDescriptionExpanded] = useState(false); const [descriptionTruncated, setDescriptionTruncated] = useState(false); - const duration = formatDuration(occurrence.start_time, occurrence.end_time); + const { t: translate } = useTranslation(); + const duration = formatDuration(occurrence.start_time, occurrence.end_time, { + hr: translate('event.hr'), + min: translate('event.min'), + }); const address = formatAddress(occurrence); return ( @@ -67,7 +72,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) style={{ shadowColor: "#000", shadowOpacity: 0.15, shadowRadius: 8 }} > - Back + {translate('event.back')} @@ -116,7 +121,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) {descriptionTruncated && ( setDescriptionExpanded((prev) => !prev)} className="mb-3.5"> - {descriptionExpanded ? "See less" : "See more"} + {descriptionExpanded ? translate('event.seeLess') : translate('event.seeMore')} )} @@ -128,13 +133,13 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) className="border-[1.5px] rounded-full px-4 py-[7px]" style={{ borderColor: AppColors.borderLight }} > - {cat} + {translate(`interests.${cat}`, { defaultValue: cat })} ))} {occurrence.price} THB - /Session + {translate('event.perSession')} @@ -161,7 +166,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) className="flex-row items-center gap-[5px] bg-[#F3F4F6] rounded-full px-3 py-[7px]" > - 8 min + 8 {translate('event.minWalk')} @@ -175,7 +180,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) > - Home + {translate('event.home')} @@ -184,7 +189,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) - Location + {translate('event.location')} - Register + {translate('event.register')} @@ -206,6 +211,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence }) export default function EventOccurrenceScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { data: response, isLoading, error } = useGetEventOccurrencesById(id); + const { t: translate } = useTranslation(); if (isLoading) { return ( @@ -219,7 +225,7 @@ export default function EventOccurrenceScreen() { return ( - Event not found + {translate('event.notFound')} ); diff --git a/frontend/apps/mobile/app/(app)/(tabs)/family/index.tsx b/frontend/apps/mobile/app/(app)/(tabs)/family/index.tsx index 87e534e4..39fbfda1 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/family/index.tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/family/index.tsx @@ -5,10 +5,11 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { IconSymbol } from '@/components/ui/icon-symbol'; -import { useGetChildrenByGuardianId, useGetGuardianById } from '@skillspark/api-client'; import { Colors, AppColors } from '@/constants/theme'; import { ChildListItem } from '@/components/ChildListItem'; import { SectionHeader } from '@/components/SectionHeader'; +import { useTranslation } from 'react-i18next'; +import { useGuardian } from '@/hooks/use-guardian'; import { useAuthContext } from '@/hooks/use-auth-context'; import { ErrorScreen } from '@/components/ErrorScreen'; @@ -17,23 +18,11 @@ export default function FamilyListScreen() { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const theme = Colors[colorScheme ?? 'light']; + const { t: translate } = useTranslation(); + const { guardian, children, isLoading } = useGuardian(); const { guardianId } = useAuthContext(); - const { data: guardianResponse, isLoading: guardianLoading } = useGetGuardianById(guardianId!, { - query: { - enabled: !!guardianId, - } - }); - const { data: childrenResponse, isLoading: childrenLoading } = useGetChildrenByGuardianId(guardianId!, { - query: { - enabled: !!guardianId, - } - }); - - const guardian = guardianResponse?.status === 200 ? guardianResponse.data : null; - const children = childrenResponse?.status === 200 ? childrenResponse.data : []; - const handleAddChild = () => { router.push('/family/manage'); }; @@ -52,11 +41,11 @@ export default function FamilyListScreen() { }); }; - if (!guardianId) { - return ; + if (!guardianId) { + return ; } - if (guardianLoading || childrenLoading) { + if (isLoading) { return ( @@ -74,7 +63,7 @@ export default function FamilyListScreen() { > - Family Information + {translate('familyInformation.title')} @@ -91,12 +80,12 @@ export default function FamilyListScreen() { {children.length === 0 && ( - No child profiles added yet. + {translate('common.noChildProfilesAdded')} )} {children.map((child: any, idx: number) => ( @@ -109,8 +98,8 @@ export default function FamilyListScreen() { ))} {}} /> {/* TODO: Replace with real emergency contact data from API */} diff --git a/frontend/apps/mobile/app/(app)/(tabs)/family/manage.tsx b/frontend/apps/mobile/app/(app)/(tabs)/family/manage.tsx index eb13dee9..48cde6a5 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/family/manage.tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/family/manage.tsx @@ -17,6 +17,9 @@ import { IconSymbol } from '@/components/ui/icon-symbol'; import { useQueryClient } from '@tanstack/react-query'; import { useCreateChild, useUpdateChild, useDeleteChild, getGetChildrenByGuardianIdQueryKey } from '@skillspark/api-client'; import { ChildProfileForm, MONTHS } from '@/components/ChildProfileForm'; +import { useTranslation } from 'react-i18next'; +import { useGuardian } from '@/hooks/use-guardian'; + import { useAuthContext } from '@/hooks/use-auth-context'; import { ErrorScreen } from '@/components/ErrorScreen'; @@ -28,6 +31,7 @@ export default function ManageChildScreen() { const theme = Colors[colorScheme ?? 'light']; const { guardianId } = useAuthContext(); + const { t: translate } = useTranslation(); const isEditing = !!params.id; const [firstName, setFirstName] = useState( @@ -57,6 +61,7 @@ export default function ManageChildScreen() { const [showYearDrop, setShowYearDrop] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); + const queryClient = useQueryClient(); const createChildMutation = useCreateChild(); const updateChildMutation = useUpdateChild(); @@ -68,7 +73,7 @@ export default function ManageChildScreen() { const handleSave = async () => { if (!firstName || !birthYear || !birthMonth || !schoolId) { - Alert.alert('Error', 'Please fill in all required fields (Name, Birth Date, School ID)'); + Alert.alert(translate('common.error'), translate('childProfile.requiredFieldsError')); return; } const name = [firstName, lastName].filter(Boolean).join(' '); @@ -91,7 +96,7 @@ export default function ManageChildScreen() { router.back(); } catch (error) { console.error(error); - Alert.alert('Error', 'Failed to save. Please try again.'); + Alert.alert(translate('common.errorOccurred'), translate('childProfile.saveError')); } finally { setIsSubmitting(false); } @@ -99,12 +104,12 @@ export default function ManageChildScreen() { const handleDelete = () => { Alert.alert( - 'Delete Profile', - 'Are you sure you want to remove this child profile?', + translate('childProfile.deleteProfile'), + translate('childProfile.deleteConfirm'), [ - { text: 'Cancel', style: 'cancel' }, + { text: translate('common.cancel'), style: 'cancel' }, { - text: 'Delete', style: 'destructive', + text: translate('payment.delete'), style: 'destructive', onPress: async () => { setIsSubmitting(true); try { @@ -112,7 +117,7 @@ export default function ManageChildScreen() { await queryClient.invalidateQueries({ queryKey: getGetChildrenByGuardianIdQueryKey(guardianId) }); router.back(); } catch { - Alert.alert('Error', 'Failed to delete.'); + Alert.alert(translate('common.errorOccurred'), translate('childProfile.deleteError')); setIsSubmitting(false); } }, @@ -137,17 +142,17 @@ export default function ManageChildScreen() { router.back()} className="w-8 h-8 justify-center items-start"> - Family Information + {translate('familyInformation.title')} {isEditing ? ( - Delete + {translate('payment.delete')} ) : ( )} - {isEditing ? 'Edit Child Profile' : 'Create Child Profile'} + {isEditing ? translate('childProfile.editTitle') : translate('childProfile.createTitle')} - {isSubmitting ? 'Saving...' : 'Save Changes'} + {isSubmitting ? translate('childProfile.saving') : translate('childProfile.saveChanges')} diff --git a/frontend/apps/mobile/app/(app)/(tabs)/index.tsx b/frontend/apps/mobile/app/(app)/(tabs)/index.tsx index c45ba7e4..59ebfd31 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/index.tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/index.tsx @@ -16,6 +16,7 @@ import { useRouter } from "expo-router"; import { AppColors } from "@/constants/theme"; import { StarRating } from "@/components/StarRating"; import { formatDuration } from "@/utils/format"; +import { useTranslation } from "react-i18next"; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -37,6 +38,7 @@ function FilterChips({ active: string[]; onToggle: (f: string) => void; }) { + const { t: translate } = useTranslation(); return ( - {isActive ? `× ${f}` : f} + {isActive ? `× ${translate(`interests.${f}`, { defaultValue: f })}` : translate(`interests.${f}`, { defaultValue: f })} ); @@ -130,7 +132,11 @@ function DiscoverBanner({ event }: { event: EventOccurrence }) { function EventCard({ item }: { item: EventOccurrence }) { const router = useRouter(); - const duration = formatDuration(item.start_time, item.end_time); + const { t: translate } = useTranslation(); + const duration = formatDuration(item.start_time, item.end_time, { + hr: translate('event.hr'), + min: translate('event.min'), + }); const ageLabel = item.event.age_range_min != null ? `${item.event.age_range_min}${item.event.age_range_max != null ? `–${item.event.age_range_max}` : ""}+` : null; @@ -188,6 +194,7 @@ function EventOccurrencesList() { const { data: response, isLoading, error } = useGetAllEventOccurrences(); const [activeFilters, setActiveFilters] = useState([]); const [search, setSearch] = useState(""); + const { t: translate } = useTranslation(); const toggleFilter = (f: string) => setActiveFilters((prev) => @@ -198,7 +205,7 @@ function EventOccurrencesList() { return ( - Loading events... + {translate('common.loadingEvents')} ); } @@ -206,8 +213,8 @@ function EventOccurrencesList() { if (error) { return ( - Error loading events - {error.detail || "An error occurred"} + {translate('common.errorLoadingEvents')} + {error.detail || translate('common.errorOccurred')} ); } @@ -250,7 +257,7 @@ function EventOccurrencesList() { {/* Title */} - My Dashboard + {translate('dashboard.title')} @@ -266,7 +273,7 @@ function EventOccurrencesList() { - Discover Weekly + {translate('dashboard.discoverWeekly')} {featuredEvent && } @@ -287,7 +294,7 @@ function EventOccurrencesList() { A - For You + {translate('dashboard.forYou')} {["#10B981", "#6366F1"].map((c, i) => ( 0 ? -8 : 0 }} /> @@ -296,8 +303,8 @@ function EventOccurrencesList() { - Based on - upcoming events + {translate('dashboard.basedOn')} + {translate('dashboard.upcomingEvents')} )} @@ -308,7 +315,7 @@ function EventOccurrencesList() { renderItem={({ item }) => } ListEmptyComponent={ - No upcoming events + {translate('common.noUpcomingEvents')} } /> diff --git a/frontend/apps/mobile/app/(app)/(tabs)/language.tsx b/frontend/apps/mobile/app/(app)/(tabs)/language.tsx index eac3fa1f..61664cf0 100644 --- a/frontend/apps/mobile/app/(app)/(tabs)/language.tsx +++ b/frontend/apps/mobile/app/(app)/(tabs)/language.tsx @@ -6,6 +6,12 @@ import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors } from '@/constants/theme'; +import { useTranslation } from 'react-i18next' +import { useEffect } from 'react'; +import { useGuardian } from '@/hooks/use-guardian'; +import { useUpdateGuardian, getGetGuardianByIdQueryKey, setCurrentLanguage } from '@skillspark/api-client'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import * as SecureStore from "expo-secure-store"; const LANGUAGES = [ { code: 'en', label: 'English', flag: '🇺🇸' }, @@ -16,11 +22,38 @@ export default function LanguageScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); + const { t: translate, i18n } = useTranslation(); + const theme = Colors[colorScheme ?? 'light']; + const dividerColor = colorScheme === 'dark' ? '#3a3a3c' : '#E5E7EB'; - const [selected, setSelected] = useState('en'); + const [selected, setSelected] = useState(i18n.language ?? 'en'); + const { guardian, guardianId } = useGuardian(); + const updateGuardianMutation = useUpdateGuardian(); + const queryClient = useQueryClient(); + + const updateLanguageData = async (langCode: string) => { + setSelected(langCode); + await i18n.changeLanguage(langCode); + setCurrentLanguage(langCode); + await SecureStore.setItemAsync('language_preference', langCode); + queryClient.invalidateQueries({ refetchType: 'all' }); + + if (guardian) { + updateGuardianMutation.mutate({ + id: guardianId, + data: { + name: guardian.name, + email: guardian.email, + username: guardian.username, + language_preference: langCode, + }, + }); + } + + } return ( @@ -32,20 +65,22 @@ export default function LanguageScreen() { > - Settings + {translate('settings.title')} - Language + {translate('settings.language')} {LANGUAGES.map((lang, index) => ( setSelected(lang.code)} + onPress={() => { + updateLanguageData(lang.code) + }} activeOpacity={0.6} > {lang.flag} - {lang.label} + {translate(`settings.languages.${lang.code}`)} - Loading Map Data... + {translate('common.loadingMapData')} ); } @@ -80,10 +83,9 @@ export default function MapScreen() { return ( - Permission to access location was denied. Please enable it in - settings. + {translate('common.locationDenied')} -