diff --git a/be/alembic/versions/05bf96a9e18b_add_chore_history_and_scheduling_tables.py b/be/alembic/versions/05bf96a9e18b_add_chore_history_and_scheduling_tables.py new file mode 100644 index 0000000..12395df --- /dev/null +++ b/be/alembic/versions/05bf96a9e18b_add_chore_history_and_scheduling_tables.py @@ -0,0 +1,75 @@ +"""Add chore history and scheduling tables + +Revision ID: 05bf96a9e18b +Revises: 91d00c100f5b +Create Date: 2025-06-08 00:41:10.516324 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '05bf96a9e18b' +down_revision: Union[str, None] = '91d00c100f5b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('chore_history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('chore_id', sa.Integer(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('event_type', sa.Enum('CREATED', 'UPDATED', 'DELETED', 'COMPLETED', 'REOPENED', 'ASSIGNED', 'UNASSIGNED', 'REASSIGNED', 'SCHEDULE_GENERATED', 'DUE_DATE_CHANGED', 'DETAILS_CHANGED', name='chorehistoryeventtypeenum'), nullable=False), + sa.Column('event_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('changed_by_user_id', sa.Integer(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['changed_by_user_id'], ['users.id'], ), + sa.ForeignKeyConstraint(['chore_id'], ['chores.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_chore_history_chore_id'), 'chore_history', ['chore_id'], unique=False) + op.create_index(op.f('ix_chore_history_group_id'), 'chore_history', ['group_id'], unique=False) + op.create_index(op.f('ix_chore_history_id'), 'chore_history', ['id'], unique=False) + op.create_table('chore_assignment_history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('assignment_id', sa.Integer(), nullable=False), + sa.Column('event_type', sa.Enum('CREATED', 'UPDATED', 'DELETED', 'COMPLETED', 'REOPENED', 'ASSIGNED', 'UNASSIGNED', 'REASSIGNED', 'SCHEDULE_GENERATED', 'DUE_DATE_CHANGED', 'DETAILS_CHANGED', name='chorehistoryeventtypeenum'), nullable=False), + sa.Column('event_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('changed_by_user_id', sa.Integer(), nullable=True), + sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['assignment_id'], ['chore_assignments.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['changed_by_user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_chore_assignment_history_assignment_id'), 'chore_assignment_history', ['assignment_id'], unique=False) + op.create_index(op.f('ix_chore_assignment_history_id'), 'chore_assignment_history', ['id'], unique=False) + op.drop_index('ix_apscheduler_jobs_next_run_time', table_name='apscheduler_jobs') + op.drop_table('apscheduler_jobs') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('apscheduler_jobs', + sa.Column('id', sa.VARCHAR(length=191), autoincrement=False, nullable=False), + sa.Column('next_run_time', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('job_state', postgresql.BYTEA(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='apscheduler_jobs_pkey') + ) + op.create_index('ix_apscheduler_jobs_next_run_time', 'apscheduler_jobs', ['next_run_time'], unique=False) + op.drop_index(op.f('ix_chore_assignment_history_id'), table_name='chore_assignment_history') + op.drop_index(op.f('ix_chore_assignment_history_assignment_id'), table_name='chore_assignment_history') + op.drop_table('chore_assignment_history') + op.drop_index(op.f('ix_chore_history_id'), table_name='chore_history') + op.drop_index(op.f('ix_chore_history_group_id'), table_name='chore_history') + op.drop_index(op.f('ix_chore_history_chore_id'), table_name='chore_history') + op.drop_table('chore_history') + # ### end Alembic commands ### diff --git a/be/app/api/v1/endpoints/chores.py b/be/app/api/v1/endpoints/chores.py index be38736..ba72bcd 100644 --- a/be/app/api/v1/endpoints/chores.py +++ b/be/app/api/v1/endpoints/chores.py @@ -8,8 +8,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_transactional_session, get_session from app.auth import current_active_user from app.models import User as UserModel, Chore as ChoreModel, ChoreTypeEnum -from app.schemas.chore import ChoreCreate, ChoreUpdate, ChorePublic, ChoreAssignmentCreate, ChoreAssignmentUpdate, ChoreAssignmentPublic +from app.schemas.chore import ( + ChoreCreate, ChoreUpdate, ChorePublic, + ChoreAssignmentCreate, ChoreAssignmentUpdate, ChoreAssignmentPublic, + ChoreHistoryPublic, ChoreAssignmentHistoryPublic +) from app.crud import chore as crud_chore +from app.crud import history as crud_history from app.core.exceptions import ChoreNotFoundError, PermissionDeniedError, GroupNotFoundError, DatabaseIntegrityError logger = logging.getLogger(__name__) @@ -450,4 +455,66 @@ async def complete_chore_assignment( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.detail) except DatabaseIntegrityError as e: logger.error(f"DatabaseIntegrityError completing assignment {assignment_id} for {current_user.email}: {e.detail}", exc_info=True) - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) \ No newline at end of file + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=e.detail) + +# === CHORE HISTORY ENDPOINTS === + +@router.get( + "/{chore_id}/history", + response_model=PyList[ChoreHistoryPublic], + summary="Get Chore History", + tags=["Chores", "History"] +) +async def get_chore_history( + chore_id: int, + db: AsyncSession = Depends(get_session), + current_user: UserModel = Depends(current_active_user), +): + """Retrieves the history of a specific chore.""" + # First, check if user has permission to view the chore itself + chore = await crud_chore.get_chore_by_id(db, chore_id) + if not chore: + raise ChoreNotFoundError(chore_id=chore_id) + + if chore.type == ChoreTypeEnum.personal and chore.created_by_id != current_user.id: + raise PermissionDeniedError("You can only view history for your own personal chores.") + + if chore.type == ChoreTypeEnum.group: + is_member = await crud_chore.is_user_member(db, chore.group_id, current_user.id) + if not is_member: + raise PermissionDeniedError("You must be a member of the group to view this chore's history.") + + logger.info(f"User {current_user.email} getting history for chore {chore_id}") + return await crud_history.get_chore_history(db=db, chore_id=chore_id) + +@router.get( + "/assignments/{assignment_id}/history", + response_model=PyList[ChoreAssignmentHistoryPublic], + summary="Get Chore Assignment History", + tags=["Chore Assignments", "History"] +) +async def get_chore_assignment_history( + assignment_id: int, + db: AsyncSession = Depends(get_session), + current_user: UserModel = Depends(current_active_user), +): + """Retrieves the history of a specific chore assignment.""" + assignment = await crud_chore.get_chore_assignment_by_id(db, assignment_id) + if not assignment: + raise ChoreNotFoundError(assignment_id=assignment_id) + + # Check permission by checking permission on the parent chore + chore = await crud_chore.get_chore_by_id(db, assignment.chore_id) + if not chore: + raise ChoreNotFoundError(chore_id=assignment.chore_id) # Should not happen if assignment exists + + if chore.type == ChoreTypeEnum.personal and chore.created_by_id != current_user.id: + raise PermissionDeniedError("You can only view history for assignments of your own personal chores.") + + if chore.type == ChoreTypeEnum.group: + is_member = await crud_chore.is_user_member(db, chore.group_id, current_user.id) + if not is_member: + raise PermissionDeniedError("You must be a member of the group to view this assignment's history.") + + logger.info(f"User {current_user.email} getting history for assignment {assignment_id}") + return await crud_history.get_assignment_history(db=db, assignment_id=assignment_id) \ No newline at end of file diff --git a/be/app/api/v1/endpoints/groups.py b/be/app/api/v1/endpoints/groups.py index 4aad4b9..64248f2 100644 --- a/be/app/api/v1/endpoints/groups.py +++ b/be/app/api/v1/endpoints/groups.py @@ -8,13 +8,16 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_transactional_session, get_session from app.auth import current_active_user from app.models import User as UserModel, UserRoleEnum # Import model and enum -from app.schemas.group import GroupCreate, GroupPublic +from app.schemas.group import GroupCreate, GroupPublic, GroupScheduleGenerateRequest from app.schemas.invite import InviteCodePublic from app.schemas.message import Message # For simple responses from app.schemas.list import ListPublic, ListDetail +from app.schemas.chore import ChoreHistoryPublic, ChoreAssignmentPublic from app.crud import group as crud_group from app.crud import invite as crud_invite from app.crud import list as crud_list +from app.crud import history as crud_history +from app.crud import schedule as crud_schedule from app.core.exceptions import ( GroupNotFoundError, GroupPermissionError, @@ -264,4 +267,55 @@ async def read_group_lists( lists = await crud_list.get_lists_for_user(db=db, user_id=current_user.id) group_lists = [list for list in lists if list.group_id == group_id] - return group_lists \ No newline at end of file + return group_lists + +@router.post( + "/{group_id}/chores/generate-schedule", + response_model=List[ChoreAssignmentPublic], + summary="Generate Group Chore Schedule", + tags=["Groups", "Chores"] +) +async def generate_group_chore_schedule( + group_id: int, + schedule_in: GroupScheduleGenerateRequest, + db: AsyncSession = Depends(get_transactional_session), + current_user: UserModel = Depends(current_active_user), +): + """Generates a round-robin chore schedule for a group.""" + logger.info(f"User {current_user.email} generating chore schedule for group {group_id}") + # Permission check: ensure user is a member (or owner/admin if stricter rules are needed) + if not await crud_group.is_user_member(db, group_id, current_user.id): + raise GroupMembershipError(group_id, "generate chore schedule for this group") + + try: + assignments = await crud_schedule.generate_group_chore_schedule( + db=db, + group_id=group_id, + start_date=schedule_in.start_date, + end_date=schedule_in.end_date, + user_id=current_user.id, + member_ids=schedule_in.member_ids, + ) + return assignments + except Exception as e: + logger.error(f"Error generating schedule for group {group_id}: {e}", exc_info=True) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) + +@router.get( + "/{group_id}/chores/history", + response_model=List[ChoreHistoryPublic], + summary="Get Group Chore History", + tags=["Groups", "Chores", "History"] +) +async def get_group_chore_history( + group_id: int, + db: AsyncSession = Depends(get_session), + current_user: UserModel = Depends(current_active_user), +): + """Retrieves all chore-related history for a specific group.""" + logger.info(f"User {current_user.email} requesting chore history for group {group_id}") + # Permission check + if not await crud_group.is_user_member(db, group_id, current_user.id): + raise GroupMembershipError(group_id, "view chore history for this group") + + return await crud_history.get_group_chore_history(db=db, group_id=group_id) \ No newline at end of file diff --git a/be/app/core/exceptions.py b/be/app/core/exceptions.py index aedd157..c9a2d1f 100644 --- a/be/app/core/exceptions.py +++ b/be/app/core/exceptions.py @@ -332,9 +332,17 @@ class UserOperationError(HTTPException): detail=detail ) +class ChoreOperationError(HTTPException): + """Raised when a chore-related operation fails.""" + def __init__(self, detail: str): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + detail=detail + ) + class ChoreNotFoundError(HTTPException): - """Raised when a chore is not found.""" - def __init__(self, chore_id: int, group_id: Optional[int] = None, detail: Optional[str] = None): + """Raised when a chore or assignment is not found.""" + def __init__(self, chore_id: int = None, assignment_id: int = None, group_id: Optional[int] = None, detail: Optional[str] = None): if detail: error_detail = detail elif group_id is not None: diff --git a/be/app/crud/chore.py b/be/app/crud/chore.py index cbac95e..f8ca622 100644 --- a/be/app/crud/chore.py +++ b/be/app/crud/chore.py @@ -6,10 +6,11 @@ from typing import List, Optional import logging from datetime import date, datetime -from app.models import Chore, Group, User, ChoreAssignment, ChoreFrequencyEnum, ChoreTypeEnum, UserGroup +from app.models import Chore, Group, User, ChoreAssignment, ChoreFrequencyEnum, ChoreTypeEnum, UserGroup, ChoreHistoryEventTypeEnum from app.schemas.chore import ChoreCreate, ChoreUpdate, ChoreAssignmentCreate, ChoreAssignmentUpdate from app.core.chore_utils import calculate_next_due_date from app.crud.group import get_group_by_id, is_user_member +from app.crud.history import create_chore_history_entry, create_assignment_history_entry from app.core.exceptions import ChoreNotFoundError, GroupNotFoundError, PermissionDeniedError, DatabaseIntegrityError logger = logging.getLogger(__name__) @@ -39,7 +40,9 @@ async def get_all_user_chores(db: AsyncSession, user_id: int) -> List[Chore]: personal_chores_query .options( selectinload(Chore.creator), - selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) .order_by(Chore.next_due_date, Chore.name) ) @@ -56,7 +59,9 @@ async def get_all_user_chores(db: AsyncSession, user_id: int) -> List[Chore]: .options( selectinload(Chore.creator), selectinload(Chore.group), - selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) .order_by(Chore.next_due_date, Chore.name) ) @@ -99,6 +104,16 @@ async def create_chore( db.add(db_chore) await db.flush() # Get the ID for the chore + # Log history + await create_chore_history_entry( + db, + chore_id=db_chore.id, + group_id=db_chore.group_id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.CREATED, + event_data={"chore_name": db_chore.name} + ) + try: # Load relationships for the response with eager loading result = await db.execute( @@ -107,7 +122,9 @@ async def create_chore( .options( selectinload(Chore.creator), selectinload(Chore.group), - selectinload(Chore.assignments) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) ) return result.scalar_one() @@ -120,7 +137,13 @@ async def get_chore_by_id(db: AsyncSession, chore_id: int) -> Optional[Chore]: result = await db.execute( select(Chore) .where(Chore.id == chore_id) - .options(selectinload(Chore.creator), selectinload(Chore.group), selectinload(Chore.assignments)) + .options( + selectinload(Chore.creator), + selectinload(Chore.group), + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) + ) ) return result.scalar_one_or_none() @@ -152,7 +175,9 @@ async def get_personal_chores( ) .options( selectinload(Chore.creator), - selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) .order_by(Chore.next_due_date, Chore.name) ) @@ -175,7 +200,9 @@ async def get_chores_by_group_id( ) .options( selectinload(Chore.creator), - selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) .order_by(Chore.next_due_date, Chore.name) ) @@ -194,6 +221,9 @@ async def update_chore( if not db_chore: raise ChoreNotFoundError(chore_id, group_id) + # Store original state for history + original_data = {field: getattr(db_chore, field) for field in chore_in.model_dump(exclude_unset=True)} + # Check permissions if db_chore.type == ChoreTypeEnum.group: if not group_id: @@ -245,6 +275,23 @@ async def update_chore( if db_chore.frequency == ChoreFrequencyEnum.custom and db_chore.custom_interval_days is None: raise ValueError("custom_interval_days must be set for custom frequency chores.") + # Log history for changes + changes = {} + for field, old_value in original_data.items(): + new_value = getattr(db_chore, field) + if old_value != new_value: + changes[field] = {"old": str(old_value), "new": str(new_value)} + + if changes: + await create_chore_history_entry( + db, + chore_id=chore_id, + group_id=db_chore.group_id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.UPDATED, + event_data=changes + ) + try: await db.flush() # Flush changes within the transaction result = await db.execute( @@ -253,7 +300,9 @@ async def update_chore( .options( selectinload(Chore.creator), selectinload(Chore.group), - selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user) + selectinload(Chore.assignments).selectinload(ChoreAssignment.assigned_user), + selectinload(Chore.assignments).selectinload(ChoreAssignment.history), + selectinload(Chore.history) ) ) return result.scalar_one() @@ -273,6 +322,16 @@ async def delete_chore( if not db_chore: raise ChoreNotFoundError(chore_id, group_id) + # Log history before deleting + await create_chore_history_entry( + db, + chore_id=chore_id, + group_id=db_chore.group_id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.DELETED, + event_data={"chore_name": db_chore.name} + ) + # Check permissions if db_chore.type == ChoreTypeEnum.group: if not group_id: @@ -324,6 +383,15 @@ async def create_chore_assignment( db.add(db_assignment) await db.flush() # Get the ID for the assignment + # Log history + await create_assignment_history_entry( + db, + assignment_id=db_assignment.id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.ASSIGNED, + event_data={"assigned_to_user_id": db_assignment.assigned_to_user_id, "due_date": db_assignment.due_date.isoformat()} + ) + try: # Load relationships for the response result = await db.execute( @@ -331,7 +399,8 @@ async def create_chore_assignment( .where(ChoreAssignment.id == db_assignment.id) .options( selectinload(ChoreAssignment.chore).selectinload(Chore.creator), - selectinload(ChoreAssignment.assigned_user) + selectinload(ChoreAssignment.assigned_user), + selectinload(ChoreAssignment.history) ) ) return result.scalar_one() @@ -346,7 +415,8 @@ async def get_chore_assignment_by_id(db: AsyncSession, assignment_id: int) -> Op .where(ChoreAssignment.id == assignment_id) .options( selectinload(ChoreAssignment.chore).selectinload(Chore.creator), - selectinload(ChoreAssignment.assigned_user) + selectinload(ChoreAssignment.assigned_user), + selectinload(ChoreAssignment.history) ) ) return result.scalar_one_or_none() @@ -364,7 +434,8 @@ async def get_user_assignments( query = query.options( selectinload(ChoreAssignment.chore).selectinload(Chore.creator), - selectinload(ChoreAssignment.assigned_user) + selectinload(ChoreAssignment.assigned_user), + selectinload(ChoreAssignment.history) ).order_by(ChoreAssignment.due_date, ChoreAssignment.id) result = await db.execute(query) @@ -393,7 +464,8 @@ async def get_chore_assignments( .where(ChoreAssignment.chore_id == chore_id) .options( selectinload(ChoreAssignment.chore).selectinload(Chore.creator), - selectinload(ChoreAssignment.assigned_user) + selectinload(ChoreAssignment.assigned_user), + selectinload(ChoreAssignment.history) ) .order_by(ChoreAssignment.due_date, ChoreAssignment.id) ) @@ -411,11 +483,10 @@ async def update_chore_assignment( if not db_assignment: raise ChoreNotFoundError(assignment_id=assignment_id) - # Load the chore for permission checking chore = await get_chore_by_id(db, db_assignment.chore_id) if not chore: raise ChoreNotFoundError(chore_id=db_assignment.chore_id) - + # Check permissions - only assignee can complete, but chore managers can reschedule can_manage = False if chore.type == ChoreTypeEnum.personal: @@ -427,19 +498,27 @@ async def update_chore_assignment( update_data = assignment_in.model_dump(exclude_unset=True) + original_assignee = db_assignment.assigned_to_user_id + original_due_date = db_assignment.due_date + # Check specific permissions for different updates if 'is_complete' in update_data and not can_complete: raise PermissionDeniedError(detail="Only the assignee can mark assignments as complete") - if 'due_date' in update_data and not can_manage: - raise PermissionDeniedError(detail="Only chore managers can reschedule assignments") - + if 'due_date' in update_data and update_data['due_date'] != original_due_date: + if not can_manage: + raise PermissionDeniedError(detail="Only chore managers can reschedule assignments") + await create_assignment_history_entry(db, assignment_id=assignment_id, changed_by_user_id=user_id, event_type=ChoreHistoryEventTypeEnum.DUE_DATE_CHANGED, event_data={"old": original_due_date.isoformat(), "new": update_data['due_date'].isoformat()}) + + if 'assigned_to_user_id' in update_data and update_data['assigned_to_user_id'] != original_assignee: + if not can_manage: + raise PermissionDeniedError(detail="Only chore managers can reassign assignments") + await create_assignment_history_entry(db, assignment_id=assignment_id, changed_by_user_id=user_id, event_type=ChoreHistoryEventTypeEnum.REASSIGNED, event_data={"old": original_assignee, "new": update_data['assigned_to_user_id']}) + # Handle completion logic - if 'is_complete' in update_data and update_data['is_complete']: - if not db_assignment.is_complete: # Only if not already complete + if 'is_complete' in update_data: + if update_data['is_complete'] and not db_assignment.is_complete: update_data['completed_at'] = datetime.utcnow() - - # Update parent chore's last_completed_at and recalculate next_due_date chore.last_completed_at = update_data['completed_at'] chore.next_due_date = calculate_next_due_date( current_due_date=chore.next_due_date, @@ -447,24 +526,25 @@ async def update_chore_assignment( custom_interval_days=chore.custom_interval_days, last_completed_date=chore.last_completed_at ) - elif 'is_complete' in update_data and not update_data['is_complete']: - # If marking as incomplete, clear completed_at - update_data['completed_at'] = None + await create_assignment_history_entry(db, assignment_id=assignment_id, changed_by_user_id=user_id, event_type=ChoreHistoryEventTypeEnum.COMPLETED) + elif not update_data['is_complete'] and db_assignment.is_complete: + update_data['completed_at'] = None + await create_assignment_history_entry(db, assignment_id=assignment_id, changed_by_user_id=user_id, event_type=ChoreHistoryEventTypeEnum.REOPENED) # Apply updates for field, value in update_data.items(): setattr(db_assignment, field, value) try: - await db.flush() # Flush changes within the transaction - + await db.flush() # Load relationships for the response result = await db.execute( select(ChoreAssignment) .where(ChoreAssignment.id == db_assignment.id) .options( selectinload(ChoreAssignment.chore).selectinload(Chore.creator), - selectinload(ChoreAssignment.assigned_user) + selectinload(ChoreAssignment.assigned_user), + selectinload(ChoreAssignment.history) ) ) return result.scalar_one() @@ -483,6 +563,15 @@ async def delete_chore_assignment( if not db_assignment: raise ChoreNotFoundError(assignment_id=assignment_id) + # Log history before deleting + await create_assignment_history_entry( + db, + assignment_id=assignment_id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.UNASSIGNED, + event_data={"unassigned_user_id": db_assignment.assigned_to_user_id} + ) + # Load the chore for permission checking chore = await get_chore_by_id(db, db_assignment.chore_id) if not chore: diff --git a/be/app/crud/group.py b/be/app/crud/group.py index 535e1f5..054fa08 100644 --- a/be/app/crud/group.py +++ b/be/app/crud/group.py @@ -79,7 +79,8 @@ async def get_user_groups(db: AsyncSession, user_id: int) -> List[GroupModel]: .options( selectinload(GroupModel.member_associations).options( selectinload(UserGroupModel.user) - ) + ), + selectinload(GroupModel.chore_history) # Eager load chore history ) ) return result.scalars().all() @@ -95,7 +96,8 @@ async def get_group_by_id(db: AsyncSession, group_id: int) -> Optional[GroupMode select(GroupModel) .where(GroupModel.id == group_id) .options( - selectinload(GroupModel.member_associations).selectinload(UserGroupModel.user) + selectinload(GroupModel.member_associations).selectinload(UserGroupModel.user), + selectinload(GroupModel.chore_history) # Eager load chore history ) ) return result.scalars().first() diff --git a/be/app/crud/history.py b/be/app/crud/history.py new file mode 100644 index 0000000..e5a8012 --- /dev/null +++ b/be/app/crud/history.py @@ -0,0 +1,83 @@ +# be/app/crud/history.py +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy.orm import selectinload +from typing import List, Optional, Any, Dict + +from app.models import ChoreHistory, ChoreAssignmentHistory, ChoreHistoryEventTypeEnum, User, Chore, Group +from app.schemas.chore import ChoreHistoryPublic, ChoreAssignmentHistoryPublic + +async def create_chore_history_entry( + db: AsyncSession, + *, + chore_id: Optional[int], + group_id: Optional[int], + changed_by_user_id: Optional[int], + event_type: ChoreHistoryEventTypeEnum, + event_data: Optional[Dict[str, Any]] = None, +) -> ChoreHistory: + """Logs an event in the chore history.""" + history_entry = ChoreHistory( + chore_id=chore_id, + group_id=group_id, + changed_by_user_id=changed_by_user_id, + event_type=event_type, + event_data=event_data or {}, + ) + db.add(history_entry) + await db.flush() + await db.refresh(history_entry) + return history_entry + +async def create_assignment_history_entry( + db: AsyncSession, + *, + assignment_id: int, + changed_by_user_id: int, + event_type: ChoreHistoryEventTypeEnum, + event_data: Optional[Dict[str, Any]] = None, +) -> ChoreAssignmentHistory: + """Logs an event in the chore assignment history.""" + history_entry = ChoreAssignmentHistory( + assignment_id=assignment_id, + changed_by_user_id=changed_by_user_id, + event_type=event_type, + event_data=event_data or {}, + ) + db.add(history_entry) + await db.flush() + await db.refresh(history_entry) + return history_entry + +async def get_chore_history(db: AsyncSession, chore_id: int) -> List[ChoreHistory]: + """Gets all history for a specific chore.""" + result = await db.execute( + select(ChoreHistory) + .where(ChoreHistory.chore_id == chore_id) + .options(selectinload(ChoreHistory.changed_by_user)) + .order_by(ChoreHistory.timestamp.desc()) + ) + return result.scalars().all() + +async def get_assignment_history(db: AsyncSession, assignment_id: int) -> List[ChoreAssignmentHistory]: + """Gets all history for a specific assignment.""" + result = await db.execute( + select(ChoreAssignmentHistory) + .where(ChoreAssignmentHistory.assignment_id == assignment_id) + .options(selectinload(ChoreAssignmentHistory.changed_by_user)) + .order_by(ChoreAssignmentHistory.timestamp.desc()) + ) + return result.scalars().all() + +async def get_group_chore_history(db: AsyncSession, group_id: int) -> List[ChoreHistory]: + """Gets all chore-related history for a group, including chore-specific and group-level events.""" + result = await db.execute( + select(ChoreHistory) + .where(ChoreHistory.group_id == group_id) + .options( + selectinload(ChoreHistory.changed_by_user), + selectinload(ChoreHistory.chore) # Also load chore info if available + ) + .order_by(ChoreHistory.timestamp.desc()) + ) + return result.scalars().all() \ No newline at end of file diff --git a/be/app/crud/schedule.py b/be/app/crud/schedule.py new file mode 100644 index 0000000..a42e0dc --- /dev/null +++ b/be/app/crud/schedule.py @@ -0,0 +1,120 @@ +# be/app/crud/schedule.py +import logging +from datetime import date, timedelta +from typing import List +from itertools import cycle + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.models import Chore, Group, User, ChoreAssignment, UserGroup, ChoreTypeEnum, ChoreHistoryEventTypeEnum +from app.crud.group import get_group_by_id +from app.crud.history import create_chore_history_entry +from app.core.exceptions import GroupNotFoundError, ChoreOperationError + +logger = logging.getLogger(__name__) + +async def generate_group_chore_schedule( + db: AsyncSession, + *, + group_id: int, + start_date: date, + end_date: date, + user_id: int, # The user initiating the action + member_ids: List[int] = None +) -> List[ChoreAssignment]: + """ + Generates a round-robin chore schedule for all group chores within a date range. + """ + if start_date > end_date: + raise ChoreOperationError("Start date cannot be after end date.") + + group = await get_group_by_id(db, group_id) + if not group: + raise GroupNotFoundError(group_id) + + if not member_ids: + # If no members are specified, use all members from the group + members_result = await db.execute( + select(UserGroup.user_id).where(UserGroup.group_id == group_id) + ) + member_ids = members_result.scalars().all() + + if not member_ids: + raise ChoreOperationError("Cannot generate schedule with no members.") + + # Fetch all chores belonging to this group + chores_result = await db.execute( + select(Chore).where(Chore.group_id == group_id, Chore.type == ChoreTypeEnum.group) + ) + group_chores = chores_result.scalars().all() + if not group_chores: + logger.info(f"No chores found in group {group_id} to generate a schedule for.") + return [] + + member_cycle = cycle(member_ids) + new_assignments = [] + + current_date = start_date + while current_date <= end_date: + for chore in group_chores: + # Check if a chore is due on the current day based on its frequency + # This is a simplified check. A more robust system would use the chore's next_due_date + # and frequency to see if it falls on the current_date. + # For this implementation, we assume we generate assignments for ALL chores on ALL days + # in the range, which might not be desired. + # A better approach is needed here. Let's assume for now we just create assignments for each chore + # on its *next* due date if it falls within the range. + + if start_date <= chore.next_due_date <= end_date: + # Check if an assignment for this chore on this due date already exists + existing_assignment_result = await db.execute( + select(ChoreAssignment.id) + .where(ChoreAssignment.chore_id == chore.id, ChoreAssignment.due_date == chore.next_due_date) + .limit(1) + ) + if existing_assignment_result.scalar_one_or_none(): + logger.info(f"Skipping assignment for chore '{chore.name}' on {chore.next_due_date} as it already exists.") + continue + + assigned_to_user_id = next(member_cycle) + + assignment = ChoreAssignment( + chore_id=chore.id, + assigned_to_user_id=assigned_to_user_id, + due_date=chore.next_due_date, # Assign on the chore's own next_due_date + is_complete=False + ) + db.add(assignment) + new_assignments.append(assignment) + logger.info(f"Created assignment for chore '{chore.name}' to user {assigned_to_user_id} on {chore.next_due_date}") + + current_date += timedelta(days=1) + + if not new_assignments: + logger.info(f"No new assignments were generated for group {group_id} in the specified date range.") + return [] + + # Log a single group-level event for the schedule generation + await create_chore_history_entry( + db, + chore_id=None, # This is a group-level event + group_id=group_id, + changed_by_user_id=user_id, + event_type=ChoreHistoryEventTypeEnum.SCHEDULE_GENERATED, + event_data={ + "start_date": start_date.isoformat(), + "end_date": end_date.isoformat(), + "member_ids": member_ids, + "assignments_created": len(new_assignments) + } + ) + + await db.flush() + + # Refresh assignments to load relationships if needed, although not strictly necessary + # as the objects are already in the session. + for assign in new_assignments: + await db.refresh(assign) + + return new_assignments \ No newline at end of file diff --git a/be/app/models.py b/be/app/models.py index 8c18196..6a4f513 100644 --- a/be/app/models.py +++ b/be/app/models.py @@ -24,6 +24,7 @@ from sqlalchemy import ( Date # Added Date for Chore model ) from sqlalchemy.orm import relationship, backref +from sqlalchemy.dialects.postgresql import JSONB from .database import Base @@ -71,6 +72,20 @@ class ChoreTypeEnum(enum.Enum): personal = "personal" group = "group" +class ChoreHistoryEventTypeEnum(str, enum.Enum): + CREATED = "created" + UPDATED = "updated" + DELETED = "deleted" + COMPLETED = "completed" + REOPENED = "reopened" + ASSIGNED = "assigned" + UNASSIGNED = "unassigned" + REASSIGNED = "reassigned" + SCHEDULE_GENERATED = "schedule_generated" + # Add more specific events as needed + DUE_DATE_CHANGED = "due_date_changed" + DETAILS_CHANGED = "details_changed" + # --- User Model --- class User(Base): __tablename__ = "users" @@ -109,6 +124,11 @@ class User(Base): assigned_chores = relationship("ChoreAssignment", back_populates="assigned_user", cascade="all, delete-orphan") # --- End Relationships for Chores --- + # --- History Relationships --- + chore_history_entries = relationship("ChoreHistory", back_populates="changed_by_user", cascade="all, delete-orphan") + assignment_history_entries = relationship("ChoreAssignmentHistory", back_populates="changed_by_user", cascade="all, delete-orphan") + # --- End History Relationships --- + # --- Group Model --- class Group(Base): @@ -137,6 +157,10 @@ class Group(Base): chores = relationship("Chore", back_populates="group", cascade="all, delete-orphan") # --- End Relationship for Chores --- + # --- History Relationships --- + chore_history = relationship("ChoreHistory", back_populates="group", cascade="all, delete-orphan") + # --- End History Relationships --- + # --- UserGroup Association Model --- class UserGroup(Base): @@ -383,6 +407,7 @@ class Chore(Base): group = relationship("Group", back_populates="chores") creator = relationship("User", back_populates="created_chores") assignments = relationship("ChoreAssignment", back_populates="chore", cascade="all, delete-orphan") + history = relationship("ChoreHistory", back_populates="chore", cascade="all, delete-orphan") # --- ChoreAssignment Model --- @@ -403,6 +428,7 @@ class ChoreAssignment(Base): # --- Relationships --- chore = relationship("Chore", back_populates="assignments") assigned_user = relationship("User", back_populates="assigned_chores") + history = relationship("ChoreAssignmentHistory", back_populates="assignment", cascade="all, delete-orphan") # === NEW: RecurrencePattern Model === @@ -430,3 +456,35 @@ class RecurrencePattern(Base): # === END: RecurrencePattern Model === + +# === NEW: Chore History Models === + +class ChoreHistory(Base): + __tablename__ = "chore_history" + + id = Column(Integer, primary_key=True, index=True) + chore_id = Column(Integer, ForeignKey("chores.id", ondelete="CASCADE"), nullable=True, index=True) + group_id = Column(Integer, ForeignKey("groups.id", ondelete="CASCADE"), nullable=True, index=True) # For group-level events + event_type = Column(SAEnum(ChoreHistoryEventTypeEnum, name="chorehistoryeventtypeenum", create_type=True), nullable=False) + event_data = Column(JSONB, nullable=True) # e.g., {'field': 'name', 'old': 'Old', 'new': 'New'} + changed_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Nullable if system-generated + timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + # --- Relationships --- + chore = relationship("Chore", back_populates="history") + group = relationship("Group", back_populates="chore_history") + changed_by_user = relationship("User", back_populates="chore_history_entries") + +class ChoreAssignmentHistory(Base): + __tablename__ = "chore_assignment_history" + + id = Column(Integer, primary_key=True, index=True) + assignment_id = Column(Integer, ForeignKey("chore_assignments.id", ondelete="CASCADE"), nullable=False, index=True) + event_type = Column(SAEnum(ChoreHistoryEventTypeEnum, name="chorehistoryeventtypeenum", create_type=True), nullable=False) # Reusing enum + event_data = Column(JSONB, nullable=True) + changed_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + # --- Relationships --- + assignment = relationship("ChoreAssignment", back_populates="history") + changed_by_user = relationship("User", back_populates="assignment_history_entries") diff --git a/be/app/schemas/chore.py b/be/app/schemas/chore.py index 7ba70f1..3605164 100644 --- a/be/app/schemas/chore.py +++ b/be/app/schemas/chore.py @@ -1,13 +1,37 @@ from datetime import date, datetime -from typing import Optional, List +from typing import Optional, List, Any from pydantic import BaseModel, ConfigDict, field_validator # Assuming ChoreFrequencyEnum is imported from models # Adjust the import path if necessary based on your project structure. # e.g., from app.models import ChoreFrequencyEnum -from ..models import ChoreFrequencyEnum, ChoreTypeEnum, User as UserModel # For UserPublic relation +from ..models import ChoreFrequencyEnum, ChoreTypeEnum, User as UserModel, ChoreHistoryEventTypeEnum # For UserPublic relation from .user import UserPublic # For embedding user information +# Forward declaration for circular dependencies +class ChoreAssignmentPublic(BaseModel): + pass + +# History Schemas +class ChoreHistoryPublic(BaseModel): + id: int + event_type: ChoreHistoryEventTypeEnum + event_data: Optional[dict[str, Any]] = None + changed_by_user: Optional[UserPublic] = None + timestamp: datetime + + model_config = ConfigDict(from_attributes=True) + +class ChoreAssignmentHistoryPublic(BaseModel): + id: int + event_type: ChoreHistoryEventTypeEnum + event_data: Optional[dict[str, Any]] = None + changed_by_user: Optional[UserPublic] = None + timestamp: datetime + + model_config = ConfigDict(from_attributes=True) + + # Chore Schemas class ChoreBase(BaseModel): name: str @@ -75,7 +99,8 @@ class ChorePublic(ChoreBase): created_at: datetime updated_at: datetime creator: Optional[UserPublic] = None # Embed creator UserPublic schema - # group: Optional[GroupPublic] = None # Embed GroupPublic schema if needed + assignments: List[ChoreAssignmentPublic] = [] + history: List[ChoreHistoryPublic] = [] model_config = ConfigDict(from_attributes=True) @@ -92,6 +117,7 @@ class ChoreAssignmentUpdate(BaseModel): # Only completion status and perhaps due_date can be updated for an assignment is_complete: Optional[bool] = None due_date: Optional[date] = None # If rescheduling an existing assignment is allowed + assigned_to_user_id: Optional[int] = None # For reassigning the chore class ChoreAssignmentPublic(ChoreAssignmentBase): id: int @@ -100,12 +126,13 @@ class ChoreAssignmentPublic(ChoreAssignmentBase): created_at: datetime updated_at: datetime # Embed ChorePublic and UserPublic for richer responses - chore: Optional[ChorePublic] = None + chore: Optional[ChorePublic] = None assigned_user: Optional[UserPublic] = None + history: List[ChoreAssignmentHistoryPublic] = [] model_config = ConfigDict(from_attributes=True) # To handle potential circular imports if ChorePublic needs GroupPublic and GroupPublic needs ChorePublic # We can update forward refs after all models are defined. -# ChorePublic.model_rebuild() # If using Pydantic v2 and forward refs were used with strings -# ChoreAssignmentPublic.model_rebuild() +ChorePublic.model_rebuild() +ChoreAssignmentPublic.model_rebuild() diff --git a/be/app/schemas/group.py b/be/app/schemas/group.py index 6773e83..ea43806 100644 --- a/be/app/schemas/group.py +++ b/be/app/schemas/group.py @@ -1,14 +1,21 @@ # app/schemas/group.py from pydantic import BaseModel, ConfigDict, computed_field -from datetime import datetime +from datetime import datetime, date from typing import Optional, List from .user import UserPublic # Import UserPublic to represent members +from .chore import ChoreHistoryPublic # Import for history # Properties to receive via API on creation class GroupCreate(BaseModel): name: str +# New schema for generating a schedule +class GroupScheduleGenerateRequest(BaseModel): + start_date: date + end_date: date + member_ids: Optional[List[int]] = None # Optional: if not provided, use all members + # Properties to return to client class GroupPublic(BaseModel): id: int @@ -16,6 +23,7 @@ class GroupPublic(BaseModel): created_by_id: int created_at: datetime member_associations: Optional[List["UserGroupPublic"]] = None + chore_history: Optional[List[ChoreHistoryPublic]] = [] @computed_field @property @@ -39,4 +47,7 @@ class UserGroupPublic(BaseModel): # Properties stored in DB (if needed, often GroupPublic is sufficient) # class GroupInDB(GroupPublic): -# pass \ No newline at end of file +# pass + +# We need to rebuild GroupPublic to resolve the forward reference to UserGroupPublic +GroupPublic.model_rebuild() \ No newline at end of file diff --git a/fe/src/assets/valerie-ui.scss b/fe/src/assets/valerie-ui.scss index f3d916b..95455cf 100644 --- a/fe/src/assets/valerie-ui.scss +++ b/fe/src/assets/valerie-ui.scss @@ -917,11 +917,13 @@ select.form-input { .modal-backdrop { position: fixed; inset: 0; - background-color: rgba(57, 62, 70, 0.7); + background-color: rgba(57, 62, 70, 0.9); + /* Increased opacity for better visibility */ display: flex; align-items: center; justify-content: center; - z-index: 1000; + z-index: 9999; + /* Increased z-index to ensure it's above other elements */ opacity: 0; visibility: hidden; transition: @@ -941,16 +943,18 @@ select.form-input { background-color: var(--light); border: var(--border); width: 90%; - max-width: 550px; + max-width: 850px; box-shadow: var(--shadow-lg); position: relative; - overflow-y: scroll; - /* Can cause tooltip clipping */ + overflow-y: auto; + /* Changed from scroll to auto */ transform: scale(0.95) translateY(-20px); transition: transform var(--transition-speed) var(--transition-ease-out); max-height: 90vh; display: flex; flex-direction: column; + z-index: 10000; + /* Ensure modal content is above backdrop */ } .modal-container::before { diff --git a/fe/src/config/api-config.ts b/fe/src/config/api-config.ts index 2f80e8e..e88ff90 100644 --- a/fe/src/config/api-config.ts +++ b/fe/src/config/api-config.ts @@ -2,7 +2,7 @@ export const API_VERSION = 'v1' // API Base URL -export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev' +export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000' // API Endpoints export const API_ENDPOINTS = { @@ -33,7 +33,6 @@ export const API_ENDPOINTS = { BASE: '/lists', BY_ID: (id: string) => `/lists/${id}`, STATUS: (id: string) => `/lists/${id}/status`, - STATUSES: '/lists/statuses', ITEMS: (listId: string) => `/lists/${listId}/items`, ITEM: (listId: string, itemId: string) => `/lists/${listId}/items/${itemId}`, EXPENSES: (listId: string) => `/lists/${listId}/expenses`, @@ -62,13 +61,15 @@ export const API_ENDPOINTS = { SETTINGS: (groupId: string) => `/groups/${groupId}/settings`, ROLES: (groupId: string) => `/groups/${groupId}/roles`, ROLE: (groupId: string, roleId: string) => `/groups/${groupId}/roles/${roleId}`, + GENERATE_SCHEDULE: (groupId: string) => `/groups/${groupId}/chores/generate-schedule`, + CHORE_HISTORY: (groupId: string) => `/groups/${groupId}/chores/history`, }, // Invites INVITES: { BASE: '/invites', BY_ID: (id: string) => `/invites/${id}`, - ACCEPT: '/invites/accept', + ACCEPT: (id: string) => `/invites/accept/${id}`, DECLINE: (id: string) => `/invites/decline/${id}`, REVOKE: (id: string) => `/invites/revoke/${id}`, LIST: '/invites', @@ -120,4 +121,12 @@ export const API_ENDPOINTS = { METRICS: '/health/metrics', LOGS: '/health/logs', }, + + CHORES: { + BASE: '/chores', + BY_ID: (id: number) => `/chores/${id}`, + HISTORY: (id: number) => `/chores/${id}/history`, + ASSIGNMENTS: (choreId: number) => `/chores/${choreId}/assignments`, + ASSIGNMENT_BY_ID: (id: number) => `/chores/assignments/${id}`, + }, } diff --git a/fe/src/config/api.ts b/fe/src/config/api.ts index 7cddb43..ed3b2b3 100644 --- a/fe/src/config/api.ts +++ b/fe/src/config/api.ts @@ -1,5 +1,6 @@ import { api } from '@/services/api'; -import { API_BASE_URL, API_VERSION, API_ENDPOINTS } from './api-config'; +import { API_BASE_URL, API_VERSION } from './api-config'; +export { API_ENDPOINTS } from './api-config'; // Helper function to get full API URL export const getApiUrl = (endpoint: string): string => { @@ -13,6 +14,4 @@ export const apiClient = { put: (endpoint: string, data = {}, config = {}) => api.put(getApiUrl(endpoint), data, config), patch: (endpoint: string, data = {}, config = {}) => api.patch(getApiUrl(endpoint), data, config), delete: (endpoint: string, config = {}) => api.delete(getApiUrl(endpoint), config), -}; - -export { API_ENDPOINTS }; \ No newline at end of file +}; \ No newline at end of file diff --git a/fe/src/i18n/de.json b/fe/src/i18n/de.json index ab6a3ff..a084114 100644 --- a/fe/src/i18n/de.json +++ b/fe/src/i18n/de.json @@ -3,559 +3,639 @@ "hello": "Hallo" }, "loginPage": { - "emailLabel": "DE: Email", - "passwordLabel": "DE: Password", - "togglePasswordVisibilityLabel": "DE: Toggle password visibility", - "loginButton": "DE: Login", - "signupLink": "DE: Don't have an account? Sign up", + "emailLabel": "E-Mail", + "passwordLabel": "Passwort", + "togglePasswordVisibilityLabel": "Passwort-Sichtbarkeit umschalten", + "loginButton": "Anmelden", + "signupLink": "Kein Konto? Registrieren", "errors": { - "emailRequired": "DE: Email is required", - "emailInvalid": "DE: Invalid email format", - "passwordRequired": "DE: Password is required", - "loginFailed": "DE: Login failed. Please check your credentials." + "emailRequired": "E-Mail ist erforderlich", + "emailInvalid": "Ungültiges E-Mail-Format", + "passwordRequired": "Passwort ist erforderlich", + "loginFailed": "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten." }, "notifications": { - "loginSuccess": "DE: Login successful" + "loginSuccess": "Anmeldung erfolgreich" } }, "listsPage": { - "retryButton": "DE: Retry", + "retryButton": "Erneut versuchen", "emptyState": { - "noListsForGroup": "DE: No lists found for this group.", - "noListsYet": "DE: You have no lists yet.", - "personalGlobalInfo": "DE: Create a personal list or join a group to see shared lists.", - "groupSpecificInfo": "DE: This group doesn't have any lists yet." + "noListsForGroup": "Keine Listen für diese Gruppe gefunden.", + "noListsYet": "Sie haben noch keine Listen.", + "personalGlobalInfo": "Erstellen Sie eine persönliche Liste oder treten Sie einer Gruppe bei, um geteilte Listen zu sehen.", + "groupSpecificInfo": "Diese Gruppe hat noch keine Listen." }, - "createNewListButton": "DE: Create New List", - "loadingLists": "DE: Loading lists...", - "noDescription": "DE: No description", - "addItemPlaceholder": "DE: Add new item...", + "createNewListButton": "Neue Liste erstellen", + "loadingLists": "Listen werden geladen...", + "noDescription": "Keine Beschreibung", + "addItemPlaceholder": "Neues Element hinzufügen...", "createCard": { - "title": "DE: + Create a new list" + "title": "+ Liste" }, "pageTitle": { - "forGroup": "DE: Lists for {groupName}", - "forGroupId": "DE: Lists for Group {groupId}", - "myLists": "DE: My Lists" + "forGroup": "Listen für {groupName}", + "forGroupId": "Listen für Gruppe {groupId}", + "myLists": "Meine Listen" }, "errors": { - "fetchFailed": "DE: Failed to fetch lists." + "fetchFailed": "Abrufen der Listen fehlgeschlagen." } }, "groupsPage": { - "retryButton": "DE: Retry", + "retryButton": "Erneut versuchen", "emptyState": { - "title": "DE: No Groups Yet!", - "description": "DE: You are not a member of any groups yet. Create one or join using an invite code.", - "createButton": "DE: Create New Group" + "title": "Noch keine Gruppen!", + "description": "Sie sind noch kein Mitglied einer Gruppe. Erstellen Sie eine oder treten Sie mit einem Einladungscode bei.", + "createButton": "Neue Gruppe erstellen" }, "groupCard": { - "newListButton": "DE: List" + "newListButton": "Liste" }, "createCard": { - "title": "DE: + Group" + "title": "+ Gruppe" }, "joinGroup": { - "title": "DE: Join a Group with Invite Code", - "inputLabel": "DE: Enter Invite Code", - "inputPlaceholder": "DE: Enter Invite Code", - "joinButton": "DE: Join" + "title": "Gruppe mit Einladungscode beitreten", + "inputLabel": "Einladungscode eingeben", + "inputPlaceholder": "Einladungscode eingeben", + "joinButton": "Beitreten" }, "createDialog": { - "title": "DE: Create New Group", - "closeButtonLabel": "DE: Close", - "groupNameLabel": "DE: Group Name", - "cancelButton": "DE: Cancel", - "createButton": "DE: Create" + "title": "Neue Gruppe erstellen", + "closeButtonLabel": "Schließen", + "groupNameLabel": "Gruppenname", + "cancelButton": "Abbrechen", + "createButton": "Erstellen" }, "errors": { - "fetchFailed": "DE: Failed to load groups", - "groupNameRequired": "DE: Group name is required", - "createFailed": "DE: Failed to create group. Please try again.", - "inviteCodeRequired": "DE: Invite code is required", - "joinFailed": "DE: Failed to join group. Please check the invite code and try again." + "fetchFailed": "Laden der Gruppen fehlgeschlagen", + "groupNameRequired": "Gruppenname ist erforderlich", + "createFailed": "Erstellen der Gruppe fehlgeschlagen. Bitte versuchen Sie es erneut.", + "inviteCodeRequired": "Einladungscode ist erforderlich", + "joinFailed": "Beitritt zur Gruppe fehlgeschlagen. Bitte überprüfen Sie den Einladungscode und versuchen Sie es erneut.", + "invalidDataFromServer": "Ungültige Daten vom Server empfangen.", + "createFailedConsole": "Fehler beim Erstellen der Gruppe:", + "joinFailedConsole": "Fehler beim Beitreten zur Gruppe:" }, "notifications": { - "groupCreatedSuccess": "DE: Group '{groupName}' created successfully.", - "joinSuccessNamed": "DE: Successfully joined group '{groupName}'.", - "joinSuccessGeneric": "DE: Successfully joined group.", - "listCreatedSuccess": "DE: List '{listName}' created successfully." + "groupCreatedSuccess": "Gruppe '{groupName}' erfolgreich erstellt.", + "joinSuccessNamed": "Erfolgreich der Gruppe '{groupName}' beigetreten.", + "joinSuccessGeneric": "Erfolgreich der Gruppe beigetreten.", + "listCreatedSuccess": "Liste '{listName}' erfolgreich erstellt." } }, "authCallbackPage": { - "redirecting": "DE: Redirecting...", + "redirecting": "Weiterleitung...", "errors": { - "authenticationFailed": "DE: Authentication failed" + "authenticationFailed": "Authentifizierung fehlgeschlagen", + "noTokenProvided": "Kein Token bereitgestellt" } }, "choresPage": { - "title": "DE: Chores", + "title": "Aufgaben", "tabs": { - "overdue": "DE: Overdue", - "today": "DE: Today", - "upcoming": "DE: Upcoming", - "allPending": "DE: All Pending", - "completed": "DE: Completed" + "overdue": "Überfällig", + "today": "Heute", + "upcoming": "Anstehend", + "allPending": "Alle ausstehenden", + "completed": "Abgeschlossen" }, "viewToggle": { - "calendarLabel": "DE: Calendar View", - "calendarText": "DE: Calendar", - "listLabel": "DE: List View", - "listText": "DE: List" + "calendarLabel": "Kalenderansicht", + "calendarText": "Kalender", + "listLabel": "Listenansicht", + "listText": "Liste" }, - "newChoreButtonLabel": "DE: New Chore", - "newChoreButtonText": "DE: New Chore", + "newChoreButtonLabel": "Neue Aufgabe", + "newChoreButtonText": "Neue Aufgabe", "loadingState": { - "loadingChores": "DE: Loading chores..." + "loadingChores": "Aufgaben werden geladen..." }, "calendar": { - "prevMonthLabel": "DE: Previous month", - "nextMonthLabel": "DE: Next month", + "prevMonthLabel": "Vorheriger Monat", + "nextMonthLabel": "Nächster Monat", "weekdays": { - "sun": "DE: Sun", - "mon": "DE: Mon", - "tue": "DE: Tue", - "wed": "DE: Wed", - "thu": "DE: Thu", - "fri": "DE: Fri", - "sat": "DE: Sat" + "sun": "So", + "mon": "Mo", + "tue": "Di", + "wed": "Mi", + "thu": "Do", + "fri": "Fr", + "sat": "Sa" }, - "addChoreToDayLabel": "DE: Add chore to this day", - "emptyState": "DE: No chores to display for this period." + "addChoreToDayLabel": "Aufgabe zu diesem Tag hinzufügen", + "emptyState": "Keine Aufgaben für diesen Zeitraum anzuzeigen." }, "listView": { - "choreTypePersonal": "DE: Personal", - "choreTypeGroupFallback": "DE: Group", - "completedDatePrefix": "DE: Completed:", + "choreTypePersonal": "Persönlich", + "choreTypeGroupFallback": "Gruppe", + "completedDatePrefix": "Abgeschlossen:", "actions": { - "doneTitle": "DE: Mark as Done", - "doneText": "DE: Done", - "undoTitle": "DE: Mark as Not Done", - "undoText": "DE: Undo", - "editTitle": "DE: Edit", - "editLabel": "DE: Edit chore", - "editText": "DE: Edit", - "deleteTitle": "DE: Delete", - "deleteLabel": "DE: Delete chore", - "deleteText": "DE: Delete" + "doneTitle": "Als erledigt markieren", + "doneText": "Erledigt", + "undoTitle": "Als nicht erledigt markieren", + "undoText": "Rückgängig", + "editTitle": "Bearbeiten", + "editLabel": "Aufgabe bearbeiten", + "editText": "Bearbeiten", + "deleteTitle": "Löschen", + "deleteLabel": "Aufgabe löschen", + "deleteText": "Löschen" }, "emptyState": { - "message": "DE: No chores in this view. Well done!", - "viewAllButton": "DE: View All Pending" + "message": "Keine Aufgaben in dieser Ansicht. Gut gemacht!", + "viewAllButton": "Alle ausstehenden anzeigen" } }, "choreModal": { - "editTitle": "DE: Edit Chore", - "newTitle": "DE: New Chore", - "closeButtonLabel": "DE: Close modal", - "nameLabel": "DE: Name", - "namePlaceholder": "DE: Enter chore name", - "typeLabel": "DE: Type", - "typePersonal": "DE: Personal", - "typeGroup": "DE: Group", - "groupLabel": "DE: Group", - "groupSelectDefault": "DE: Select a group", - "descriptionLabel": "DE: Description", - "descriptionPlaceholder": "DE: Add a description (optional)", - "frequencyLabel": "DE: Frequency", - "intervalLabel": "DE: Interval (days)", - "intervalPlaceholder": "DE: e.g. 3", - "dueDateLabel": "DE: Due Date", - "quickDueDateToday": "DE: Today", - "quickDueDateTomorrow": "DE: Tomorrow", - "quickDueDateNextWeek": "DE: Next Week", - "cancelButton": "DE: Cancel", - "saveButton": "DE: Save" + "editTitle": "Aufgabe bearbeiten", + "newTitle": "Neue Aufgabe", + "closeButtonLabel": "Modal schließen", + "nameLabel": "Name", + "namePlaceholder": "Aufgabennamen eingeben", + "typeLabel": "Typ", + "typePersonal": "Persönlich", + "typeGroup": "Gruppe", + "groupLabel": "Gruppe", + "groupSelectDefault": "Gruppe auswählen", + "descriptionLabel": "Beschreibung", + "descriptionPlaceholder": "Beschreibung hinzufügen (optional)", + "frequencyLabel": "Häufigkeit", + "intervalLabel": "Intervall (Tage)", + "intervalPlaceholder": "z.B. 10", + "dueDateLabel": "Fälligkeitsdatum", + "quickDueDateToday": "Heute", + "quickDueDateTomorrow": "Morgen", + "quickDueDateNextWeek": "Nächste Woche", + "cancelButton": "Abbrechen", + "saveButton": "Speichern" + }, + "consoleErrors": { + "loadFailed": "Laden aller Aufgaben fehlgeschlagen:", + "loadGroupsFailed": "Laden der Gruppen fehlgeschlagen", + "createAssignmentForNewChoreFailed": "Zuweisung für neue Aufgabe konnte nicht erstellt werden:", + "saveFailed": "Speichern der Aufgabe fehlgeschlagen:", + "deleteFailed": "Löschen der Aufgabe fehlgeschlagen:", + "createAssignmentFailed": "Zuweisung konnte nicht erstellt werden:", + "updateCompletionStatusFailed": "Abschlussstatus der Aufgabe konnte nicht aktualisiert werden:" }, "deleteDialog": { - "title": "DE: Delete Chore", - "confirmationText": "DE: Are you sure you want to delete this chore? This action cannot be undone.", - "deleteButton": "DE: Delete" + "title": "Aufgabe löschen", + "confirmationText": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteButton": "Löschen" }, "shortcutsModal": { - "title": "DE: Keyboard Shortcuts", - "descNewChore": "DE: New Chore", - "descToggleView": "DE: Toggle View (List/Calendar)", - "descToggleShortcuts": "DE: Show/Hide Shortcuts", - "descCloseModal": "DE: Close any open Modal/Dialog" + "title": "Tastaturkürzel", + "descNewChore": "Neue Aufgabe", + "descToggleView": "Ansicht umschalten (Liste/Kalender)", + "descToggleShortcuts": "Kürzel anzeigen/ausblenden", + "descCloseModal": "Geöffnetes Modal/Dialog schließen" }, "frequencyOptions": { - "oneTime": "DE: One Time", - "daily": "DE: Daily", - "weekly": "DE: Weekly", - "monthly": "DE: Monthly", - "custom": "DE: Custom" + "oneTime": "Einmalig", + "daily": "Täglich", + "weekly": "Wöchentlich", + "monthly": "Monatlich", + "custom": "Benutzerdefiniert" + }, + "frequency": { + "customInterval": "Alle {n} Tag | Alle {n} Tage" }, "formatters": { - "noDueDate": "DE: No due date", - "dueToday": "DE: Due Today", - "dueTomorrow": "DE: Due Tomorrow", - "overdueFull": "DE: Overdue: {date}", - "dueFull": "DE: Due {date}", - "invalidDate": "DE: Invalid Date" + "noDueDate": "Kein Fälligkeitsdatum", + "dueToday": "Heute fällig", + "dueTomorrow": "Morgen fällig", + "overdueFull": "Überfällig: {date}", + "dueFull": "Fällig am {date}", + "invalidDate": "Ungültiges Datum" }, "notifications": { - "loadFailed": "DE: Failed to load chores", - "updateSuccess": "DE: Chore '{name}' updated successfully", - "createSuccess": "DE: Chore '{name}' created successfully", - "updateFailed": "DE: Failed to update chore", - "createFailed": "DE: Failed to create chore", - "deleteSuccess": "DE: Chore '{name}' deleted successfully", - "deleteFailed": "DE: Failed to delete chore", - "markedDone": "DE: {name} marked as done.", - "markedNotDone": "DE: {name} marked as not done.", - "statusUpdateFailed": "DE: Failed to update chore status." + "loadFailed": "Laden der Aufgaben fehlgeschlagen.", + "loadGroupsFailed": "Laden der Gruppen fehlgeschlagen.", + "updateSuccess": "Aufgabe '{name}' erfolgreich aktualisiert.", + "createSuccess": "Aufgabe '{name}' erfolgreich erstellt.", + "updateFailed": "Aktualisieren der Aufgabe fehlgeschlagen.", + "createFailed": "Erstellen der Aufgabe fehlgeschlagen.", + "deleteSuccess": "Aufgabe '{name}' erfolgreich gelöscht.", + "deleteFailed": "Löschen der Aufgabe fehlgeschlagen.", + "markedDone": "{name} als erledigt markiert.", + "markedNotDone": "{name} als nicht erledigt markiert.", + "statusUpdateFailed": "Status der Aufgabe konnte nicht aktualisiert werden.", + "createAssignmentFailed": "Zuweisung für Aufgabe konnte nicht erstellt werden." }, "validation": { - "nameRequired": "DE: Chore name is required.", - "groupRequired": "DE: Please select a group for group chores.", - "intervalRequired": "DE: Custom interval must be at least 1 day.", - "dueDateRequired": "DE: Due date is required.", - "invalidDueDate": "DE: Invalid due date format." + "nameRequired": "Aufgabenname ist erforderlich.", + "groupRequired": "Bitte wählen Sie eine Gruppe für Gruppenaufgaben.", + "intervalRequired": "Benutzerdefiniertes Intervall muss mindestens 1 Tag betragen.", + "dueDateRequired": "Fälligkeitsdatum ist erforderlich.", + "invalidDueDate": "Ungültiges Fälligkeitsdatumformat." }, - "unsavedChangesConfirmation": "DE: You have unsaved changes in the chore form. Are you sure you want to leave?" + "unsavedChangesConfirmation": "Sie haben ungespeicherte Änderungen im Aufgabenformular. Sind Sie sicher, dass Sie die Seite verlassen möchten?" }, "errorNotFoundPage": { - "errorCode": "DE: 404", - "errorMessage": "DE: Oops. Nothing here...", - "goHomeButton": "DE: Go Home" + "errorCode": "404", + "errorMessage": "Upps. Hier gibt es nichts...", + "goHomeButton": "Zur Startseite" }, "groupDetailPage": { - "loadingLabel": "DE: Loading group details...", - "retryButton": "DE: Retry", - "groupNotFound": "DE: Group not found or an error occurred.", + "loadingLabel": "Gruppendetails werden geladen...", + "retryButton": "Erneut versuchen", + "groupNotFound": "Gruppe nicht gefunden oder ein Fehler ist aufgetreten.", "members": { - "title": "DE: Group Members", - "defaultRole": "DE: Member", - "removeButton": "DE: Remove", - "emptyState": "DE: No members found." + "title": "Gruppenmitglieder", + "defaultRole": "Mitglied", + "removeButton": "Entfernen", + "emptyState": "Keine Mitglieder gefunden.", + "closeMenuLabel": "Menü schließen" }, "invites": { - "title": "DE: Invite Members", - "regenerateButton": "DE: Regenerate Invite Code", - "generateButton": "DE: Generate Invite Code", - "activeCodeLabel": "DE: Current Active Invite Code:", - "copyButtonLabel": "DE: Copy invite code", - "copySuccess": "DE: Invite code copied to clipboard!", - "emptyState": "DE: No active invite code. Click the button above to generate one.", + "title": "Mitglieder einladen", + "description": "Laden Sie neue Mitglieder ein, indem Sie einen teilbaren Code generieren.", + "addMemberButtonLabel": "Mitglied hinzufügen", + "closeInviteLabel": "Einladung schließen", + "regenerateButton": "Einladungscode neu generieren", + "generateButton": "Einladungscode generieren", + "activeCodeLabel": "Aktueller aktiver Einladungscode:", + "copyButtonLabel": "Einladungscode kopieren", + "copySuccess": "Einladungscode in die Zwischenablage kopiert!", + "emptyState": "Kein aktiver Einladungscode. Klicken Sie auf die Schaltfläche oben, um einen zu generieren.", "errors": { - "newDataInvalid": "DE: New invite code data is invalid." + "newDataInvalid": "Daten des neuen Einladungscodes sind ungültig." } }, + "errors": { + "failedToFetchActiveInvite": "Abrufen des aktiven Einladungscodes fehlgeschlagen.", + "failedToFetchGroupDetails": "Abrufen der Gruppendetails fehlgeschlagen.", + "failedToLoadUpcomingChores": "Fehler beim Laden anstehender Aufgaben:", + "failedToLoadRecentExpenses": "Fehler beim Laden der letzten Ausgaben:" + }, + "console": { + "noActiveInvite": "Kein aktiver Einladungscode für diese Gruppe gefunden." + }, "chores": { - "title": "DE: Group Chores", - "manageButton": "DE: Manage Chores", - "duePrefix": "DE: Due:", - "emptyState": "DE: No chores scheduled. Click \"Manage Chores\" to create some!" + "title": "Gruppenaufgaben", + "manageButton": "Aufgaben verwalten", + "duePrefix": "Fällig:", + "emptyState": "Keine Aufgaben geplant. Klicken Sie auf \"Aufgaben verwalten\", um welche zu erstellen!" }, "expenses": { - "title": "DE: Group Expenses", - "manageButton": "DE: Manage Expenses", - "emptyState": "DE: No expenses recorded. Click \"Manage Expenses\" to add some!", + "title": "Gruppenausgaben", + "manageButton": "Ausgaben verwalten", + "emptyState": "Keine Ausgaben erfasst. Klicken Sie auf \"Ausgaben verwalten\", um welche hinzuzufügen!", + "paidBy": "Bezahlt von:", + "owes": "schuldet", + "paidAmount": "Bezahlt:", + "onDate": "am", + "settleShareButton": "Meinen Anteil begleichen", + "activityLabel": "Aktivität:", + "byUser": "von", + "fallbackUserName": "Benutzer-ID: {userId}", + "activityByUserFallback": "Benutzer {userId}", "splitTypes": { - "equal": "DE: Equal", - "exactAmounts": "DE: Exact Amounts", - "percentage": "DE: Percentage", - "shares": "DE: Shares", - "itemBased": "DE: Item Based" + "equal": "Gleichmäßig", + "exactAmounts": "Genaue Beträge", + "percentage": "Prozentual", + "shares": "Anteile", + "itemBased": "Artikelbasiert" } }, "notifications": { - "fetchDetailsFailed": "DE: Failed to fetch group details.", - "fetchInviteFailed": "DE: Failed to fetch active invite code.", - "generateInviteSuccess": "DE: New invite code generated successfully!", - "generateInviteError": "DE: Failed to generate invite code.", - "clipboardNotSupported": "DE: Clipboard not supported or no code to copy.", - "copyInviteFailed": "DE: Failed to copy invite code.", - "removeMemberSuccess": "DE: Member removed successfully", - "removeMemberFailed": "DE: Failed to remove member" + "fetchDetailsFailed": "Abrufen der Gruppendetails fehlgeschlagen.", + "fetchInviteFailed": "Abrufen des aktiven Einladungscodes fehlgeschlagen.", + "generateInviteSuccess": "Neuer Einladungscode erfolgreich generiert!", + "generateInviteError": "Generieren des Einladungscodes fehlgeschlagen.", + "clipboardNotSupported": "Zwischenablage nicht unterstützt oder kein Code zum Kopieren vorhanden.", + "copyInviteFailed": "Kopieren des Einladungscodes fehlgeschlagen.", + "removeMemberSuccess": "Mitglied erfolgreich entfernt", + "removeMemberFailed": "Entfernen des Mitglieds fehlgeschlagen", + "loadExpensesFailed": "Laden der letzten Ausgaben fehlgeschlagen.", + "cannotSettleOthersShares": "Sie können nur Ihre eigenen Anteile begleichen.", + "settlementDataMissing": "Abrechnung kann nicht verarbeitet werden: fehlende Daten.", + "settleShareSuccess": "Anteil erfolgreich beglichen!", + "settleShareFailed": "Begleichen des Anteils fehlgeschlagen." + }, + "loading": { + "settlement": "Abrechnung wird verarbeitet..." + }, + "settleShareModal": { + "title": "Anteil begleichen", + "settleAmountFor": "Betrag für {userName} begleichen:", + "amountLabel": "Betrag", + "cancelButton": "Abbrechen", + "confirmButton": "Bestätigen", + "errors": { + "enterAmount": "Bitte geben Sie einen Betrag ein.", + "positiveAmount": "Bitte geben Sie einen positiven Betrag ein.", + "exceedsRemaining": "Betrag darf den Restbetrag nicht überschreiten: {amount}.", + "noSplitSelected": "Fehler: Keine Aufteilung ausgewählt." + } + }, + "status": { + "settled": "Beglichen", + "partiallySettled": "Teilweise beglichen", + "unsettled": "Unbeglichen", + "paid": "Bezahlt", + "partiallyPaid": "Teilweise bezahlt", + "unpaid": "Unbezahlt", + "unknown": "Unbekannter Status" } }, "accountPage": { - "title": "Account Settings", - "loadingProfile": "Loading profile...", - "retryButton": "Retry", + "title": "Kontoeinstellungen", + "loadingProfile": "Profil wird geladen...", + "retryButton": "Erneut versuchen", "profileSection": { - "header": "Profile Information", + "header": "Profilinformationen", "nameLabel": "Name", - "emailLabel": "Email", - "saveButton": "Save Changes" + "emailLabel": "E-Mail", + "saveButton": "Änderungen speichern" }, "passwordSection": { - "header": "Change Password", - "currentPasswordLabel": "Current Password", - "newPasswordLabel": "New Password", - "changeButton": "Change Password" + "header": "Passwort ändern", + "currentPasswordLabel": "Aktuelles Passwort", + "newPasswordLabel": "Neues Passwort", + "changeButton": "Passwort ändern" }, "notificationsSection": { - "header": "Notification Preferences", - "emailNotificationsLabel": "Email Notifications", - "emailNotificationsDescription": "Receive email notifications for important updates", - "listUpdatesLabel": "List Updates", - "listUpdatesDescription": "Get notified when lists are updated", - "groupActivitiesLabel": "Group Activities", - "groupActivitiesDescription": "Receive notifications for group activities" + "header": "Benachrichtigungseinstellungen", + "emailNotificationsLabel": "E-Mail-Benachrichtigungen", + "emailNotificationsDescription": "E-Mail-Benachrichtigungen für wichtige Updates erhalten", + "listUpdatesLabel": "Listen-Updates", + "listUpdatesDescription": "Benachrichtigungen erhalten, wenn Listen aktualisiert werden", + "groupActivitiesLabel": "Gruppenaktivitäten", + "groupActivitiesDescription": "Benachrichtigungen für Gruppenaktivitäten erhalten" }, "notifications": { - "profileLoadFailed": "Failed to load profile", - "profileUpdateSuccess": "Profile updated successfully", - "profileUpdateFailed": "Failed to update profile", - "passwordFieldsRequired": "Please fill in both current and new password fields.", - "passwordTooShort": "New password must be at least 8 characters long.", - "passwordChangeSuccess": "Password changed successfully", - "passwordChangeFailed": "Failed to change password", - "preferencesUpdateSuccess": "Preferences updated successfully", - "preferencesUpdateFailed": "Failed to update preferences" + "profileLoadFailed": "Laden des Profils fehlgeschlagen", + "profileUpdateSuccess": "Profil erfolgreich aktualisiert", + "profileUpdateFailed": "Aktualisieren des Profils fehlgeschlagen", + "passwordFieldsRequired": "Bitte füllen Sie sowohl das aktuelle als auch das neue Passwortfeld aus.", + "passwordTooShort": "Neues Passwort muss mindestens 8 Zeichen lang sein.", + "passwordChangeSuccess": "Passwort erfolgreich geändert", + "passwordChangeFailed": "Ändern des Passworts fehlgeschlagen", + "preferencesUpdateSuccess": "Einstellungen erfolgreich aktualisiert", + "preferencesUpdateFailed": "Aktualisieren der Einstellungen fehlgeschlagen" }, - "saving": "Saving..." + "saving": "Speichern..." }, "signupPage": { - "header": "Sign Up", - "fullNameLabel": "Full Name", - "emailLabel": "Email", - "passwordLabel": "Password", - "confirmPasswordLabel": "Confirm Password", - "togglePasswordVisibility": "Toggle password visibility", - "submitButton": "Sign Up", - "loginLink": "Already have an account? Login", + "header": "Registrieren", + "fullNameLabel": "Vollständiger Name", + "emailLabel": "E-Mail", + "passwordLabel": "Passwort", + "confirmPasswordLabel": "Passwort bestätigen", + "togglePasswordVisibility": "Passwort-Sichtbarkeit umschalten", + "submitButton": "Registrieren", + "loginLink": "Bereits ein Konto? Anmelden", "validation": { - "nameRequired": "Name is required", - "emailRequired": "Email is required", - "emailInvalid": "Invalid email format", - "passwordRequired": "Password is required", - "passwordLength": "Password must be at least 8 characters", - "confirmPasswordRequired": "Please confirm your password", - "passwordsNoMatch": "Passwords do not match" + "nameRequired": "Name ist erforderlich", + "emailRequired": "E-Mail ist erforderlich", + "emailInvalid": "Ungültiges E-Mail-Format", + "passwordRequired": "Passwort ist erforderlich", + "passwordLength": "Passwort muss mindestens 8 Zeichen lang sein", + "confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort", + "passwordsNoMatch": "Passwörter stimmen nicht überein" }, "notifications": { - "signupFailed": "Signup failed. Please try again.", - "signupSuccess": "Account created successfully. Please login." + "signupFailed": "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "signupSuccess": "Konto erfolgreich erstellt. Bitte anmelden." } }, "listDetailPage": { "loading": { - "list": "Loading list...", - "items": "Loading items...", - "ocrProcessing": "Processing image...", - "addingOcrItems": "Adding OCR items...", - "costSummary": "Loading summary...", - "expenses": "Loading expenses...", - "settlement": "Processing settlement..." + "list": "Liste wird geladen...", + "items": "Elemente werden geladen...", + "ocrProcessing": "Bild wird verarbeitet...", + "addingOcrItems": "OCR-Elemente werden hinzugefügt...", + "costSummary": "Zusammenfassung wird geladen...", + "expenses": "Ausgaben werden geladen...", + "settlement": "Abrechnung wird verarbeitet..." }, "errors": { - "fetchFailed": "Failed to load list details.", - "genericLoadFailure": "Group not found or an error occurred.", - "ocrNoItems": "No items extracted from the image.", - "ocrFailed": "Failed to process image.", - "addItemFailed": "Failed to add item.", - "updateItemFailed": "Failed to update item.", - "updateItemPriceFailed": "Failed to update item price.", - "deleteItemFailed": "Failed to delete item.", - "addOcrItemsFailed": "Failed to add OCR items.", - "fetchItemsFailed": "Failed to load items: {errorMessage}", - "loadCostSummaryFailed": "Failed to load cost summary." + "fetchFailed": "Laden der Listendetails fehlgeschlagen.", + "genericLoadFailure": "Gruppe nicht gefunden oder ein Fehler ist aufgetreten.", + "ocrNoItems": "Keine Elemente aus dem Bild extrahiert.", + "ocrFailed": "Verarbeitung des Bildes fehlgeschlagen.", + "addItemFailed": "Hinzufügen des Elements fehlgeschlagen.", + "updateItemFailed": "Aktualisieren des Elements fehlgeschlagen.", + "updateItemPriceFailed": "Aktualisieren des Elementpreises fehlgeschlagen.", + "deleteItemFailed": "Löschen des Elements fehlgeschlagen.", + "addOcrItemsFailed": "Hinzufügen der OCR-Elemente fehlgeschlagen.", + "fetchItemsFailed": "Laden der Elemente fehlgeschlagen: {errorMessage}", + "loadCostSummaryFailed": "Laden der Kostenzusammenfassung fehlgeschlagen." }, - "retryButton": "Retry", + "retryButton": "Erneut versuchen", "buttons": { - "addViaOcr": "Add via OCR", - "addItem": "Add", - "addItems": "Add Items", - "cancel": "Cancel", - "confirm": "Confirm", - "saveChanges": "Save Changes", - "close": "Close", - "costSummary": "Cost Summary" + "addViaOcr": "Über OCR hinzufügen", + "addItem": "Hinzufügen", + "addItems": "Elemente hinzufügen", + "cancel": "Abbrechen", + "confirm": "Bestätigen", + "saveChanges": "Änderungen speichern", + "close": "Schließen", + "costSummary": "Kostenzusammenfassung" }, "badges": { - "groupList": "Group List", - "personalList": "Personal List" + "groupList": "Gruppenliste", + "personalList": "Persönliche Liste" }, "items": { "emptyState": { - "title": "No Items Yet!", - "message": "Add some items using the form below." + "title": "Noch keine Elemente!", + "message": "Fügen Sie einige Elemente über das Formular unten hinzu." }, "addItemForm": { - "placeholder": "Add a new item", - "quantityPlaceholder": "Qty", - "itemNameSrLabel": "New item name", - "quantitySrLabel": "Quantity" + "placeholder": "Neues Element hinzufügen", + "quantityPlaceholder": "Menge", + "itemNameSrLabel": "Name des neuen Elements", + "quantitySrLabel": "Menge" }, - "pricePlaceholder": "Price", - "editItemAriaLabel": "Edit item", - "deleteItemAriaLabel": "Delete item" + "pricePlaceholder": "Preis", + "editItemAriaLabel": "Element bearbeiten", + "deleteItemAriaLabel": "Element löschen" }, "modals": { "ocr": { - "title": "Add Items via OCR", - "uploadLabel": "Upload Image" + "title": "Elemente über OCR hinzufügen", + "uploadLabel": "Bild hochladen" }, "confirmation": { - "title": "Confirmation" + "title": "Bestätigung" }, "editItem": { - "title": "Edit Item", - "nameLabel": "Item Name", - "quantityLabel": "Quantity" + "title": "Element bearbeiten", + "nameLabel": "Elementname", + "quantityLabel": "Menge" }, "costSummary": { - "title": "List Cost Summary", - "totalCostLabel": "Total List Cost:", - "equalShareLabel": "Equal Share Per User:", - "participantsLabel": "Participating Users:", - "userBalancesHeader": "User Balances", + "title": "Listenkostenzusammenfassung", + "totalCostLabel": "Gesamtkosten der Liste:", + "equalShareLabel": "Gleicher Anteil pro Benutzer:", + "participantsLabel": "Teilnehmende Benutzer:", + "userBalancesHeader": "Benutzersalden", "tableHeaders": { - "user": "User", - "itemsAddedValue": "Items Added Value", - "amountDue": "Amount Due", - "balance": "Balance" + "user": "Benutzer", + "itemsAddedValue": "Wert der hinzugefügten Elemente", + "amountDue": "Fälliger Betrag", + "balance": "Saldo" }, - "emptyState": "No cost summary available." + "emptyState": "Keine Kostenzusammenfassung verfügbar." }, "settleShare": { - "title": "Settle Share", - "settleAmountFor": "Settle amount for {userName}:", - "amountLabel": "Amount", + "title": "Anteil begleichen", + "settleAmountFor": "Betrag für {userName} begleichen:", + "amountLabel": "Betrag", "errors": { - "enterAmount": "Please enter an amount.", - "positiveAmount": "Please enter a positive amount.", - "exceedsRemaining": "Amount cannot exceed remaining: {amount}.", - "noSplitSelected": "Error: No split selected." + "enterAmount": "Bitte geben Sie einen Betrag ein.", + "positiveAmount": "Bitte geben Sie einen positiven Betrag ein.", + "exceedsRemaining": "Betrag darf den Restbetrag nicht überschreiten: {amount}.", + "noSplitSelected": "Fehler: Keine Aufteilung ausgewählt." } } }, "confirmations": { - "updateMessage": "Mark '{itemName}' as {status}?", - "statusComplete": "complete", - "statusIncomplete": "incomplete", - "deleteMessage": "Delete '{itemName}'? This cannot be undone." + "updateMessage": "'{itemName}' als {status} markieren?", + "statusComplete": "abgeschlossen", + "statusIncomplete": "unvollständig", + "deleteMessage": "'{itemName}' löschen? Dies kann nicht rückgängig gemacht werden." }, "notifications": { - "itemAddedSuccess": "Item added successfully.", - "itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.", - "itemUpdatedSuccess": "Item updated successfully.", - "itemDeleteSuccess": "Item deleted successfully.", - "enterItemName": "Please enter an item name.", - "costSummaryLoadFailed": "Failed to load cost summary.", - "cannotSettleOthersShares": "You can only settle your own shares.", - "settlementDataMissing": "Cannot process settlement: missing data.", - "settleShareSuccess": "Share settled successfully!", - "settleShareFailed": "Failed to settle share." + "itemAddedSuccess": "Element erfolgreich hinzugefügt.", + "itemsAddedSuccessOcr": "{count} Element(e) erfolgreich über OCR hinzugefügt.", + "itemUpdatedSuccess": "Element erfolgreich aktualisiert.", + "itemDeleteSuccess": "Element erfolgreich gelöscht.", + "enterItemName": "Bitte geben Sie einen Elementnamen ein.", + "costSummaryLoadFailed": "Laden der Kostenzusammenfassung fehlgeschlagen.", + "cannotSettleOthersShares": "Sie können nur Ihre eigenen Anteile begleichen.", + "settlementDataMissing": "Abrechnung kann nicht verarbeitet werden: fehlende Daten.", + "settleShareSuccess": "Anteil erfolgreich beglichen!", + "settleShareFailed": "Begleichen des Anteils fehlgeschlagen." }, "expensesSection": { - "title": "Expenses", - "addExpenseButton": "Add Expense", - "loading": "Loading expenses...", - "emptyState": "No expenses recorded for this list yet.", - "paidBy": "Paid by:", - "onDate": "on", - "owes": "owes", - "paidAmount": "Paid:", - "activityLabel": "Activity:", - "byUser": "by", - "settleShareButton": "Settle My Share", - "retryButton": "Retry" + "title": "Ausgaben", + "addExpenseButton": "Ausgabe hinzufügen", + "loading": "Ausgaben werden geladen...", + "emptyState": "Noch keine Ausgaben für diese Liste erfasst.", + "paidBy": "Bezahlt von:", + "onDate": "am", + "owes": "schuldet", + "paidAmount": "Bezahlt:", + "activityLabel": "Aktivität:", + "byUser": "von", + "settleShareButton": "Meinen Anteil begleichen", + "retryButton": "Erneut versuchen" }, "status": { - "settled": "Settled", - "partiallySettled": "Partially Settled", - "unsettled": "Unsettled", - "paid": "Paid", - "partiallyPaid": "Partially Paid", - "unpaid": "Unpaid", - "unknown": "Unknown Status" + "settled": "Beglichen", + "partiallySettled": "Teilweise beglichen", + "unsettled": "Unbeglichen", + "paid": "Bezahlt", + "partiallyPaid": "Teilweise bezahlt", + "unpaid": "Unbezahlt", + "unknown": "Unbekannter Status" } }, "myChoresPage": { - "title": "My Assigned Chores", - "showCompletedToggle": "Show Completed", + "title": "Meine zugewiesenen Aufgaben", + "showCompletedToggle": "Abgeschlossene anzeigen", "timelineHeaders": { - "overdue": "Overdue", - "today": "Due Today", - "thisWeek": "This Week", - "later": "Later", - "completed": "Completed" + "overdue": "Überfällig", + "today": "Heute fällig", + "thisWeek": "Diese Woche", + "later": "Später", + "completed": "Abgeschlossen" }, "choreCard": { - "personal": "Personal", - "group": "Group", - "duePrefix": "Due", - "completedPrefix": "Completed", - "dueToday": "Due Today", - "markCompleteButton": "Mark Complete" + "personal": "Persönlich", + "group": "Gruppe", + "duePrefix": "Fällig", + "completedPrefix": "Abgeschlossen", + "dueToday": "Heute fällig", + "markCompleteButton": "Als erledigt markieren" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Einmalig", + "daily": "Täglich", + "weekly": "Wöchentlich", + "monthly": "Monatlich", + "custom": "Benutzerdefiniert", + "unknown": "Unbekannte Häufigkeit" }, "dates": { - "invalidDate": "Invalid Date", - "unknownDate": "Unknown Date" + "invalidDate": "Ungültiges Datum", + "unknownDate": "Unbekanntes Datum" }, "emptyState": { - "title": "No Assignments Yet!", - "noAssignmentsPending": "You have no pending chore assignments.", - "noAssignmentsAll": "You have no chore assignments (completed or pending).", - "viewAllChoresButton": "View All Chores" + "title": "Noch keine Zuweisungen!", + "noAssignmentsPending": "Sie haben keine ausstehenden Aufgabenzuweisungen.", + "noAssignmentsAll": "Sie haben keine Aufgabenzuweisungen (abgeschlossen oder ausstehend).", + "viewAllChoresButton": "Alle Aufgaben anzeigen" }, "notifications": { - "loadFailed": "Failed to load assignments", - "markedComplete": "Marked \"{choreName}\" as complete!", - "markCompleteFailed": "Failed to mark assignment as complete" + "loadFailed": "Laden der Zuweisungen fehlgeschlagen", + "markedComplete": "\"{choreName}\" als erledigt markiert!", + "markCompleteFailed": "Markieren der Zuweisung als erledigt fehlgeschlagen" } }, "personalChoresPage": { - "title": "Personal Chores", - "newChoreButton": "New Chore", - "editButton": "Edit", - "deleteButton": "Delete", - "cancelButton": "Cancel", - "saveButton": "Save", + "title": "Persönliche Aufgaben", + "newChoreButton": "Neue Aufgabe", + "editButton": "Bearbeiten", + "deleteButton": "Löschen", + "cancelButton": "Abbrechen", + "saveButton": "Speichern", "modals": { - "editChoreTitle": "Edit Chore", - "newChoreTitle": "New Chore", - "deleteChoreTitle": "Delete Chore" + "editChoreTitle": "Aufgabe bearbeiten", + "newChoreTitle": "Neue Aufgabe", + "deleteChoreTitle": "Aufgabe löschen" }, "form": { "nameLabel": "Name", - "descriptionLabel": "Description", - "frequencyLabel": "Frequency", - "intervalLabel": "Interval (days)", - "dueDateLabel": "Next Due Date" + "descriptionLabel": "Beschreibung", + "frequencyLabel": "Häufigkeit", + "intervalLabel": "Intervall (Tage)", + "dueDateLabel": "Nächstes Fälligkeitsdatum" }, "deleteDialog": { - "confirmationText": "Are you sure you want to delete this chore?" + "confirmationText": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten?" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Einmalig", + "daily": "Täglich", + "weekly": "Wöchentlich", + "monthly": "Monatlich", + "custom": "Benutzerdefiniert", + "unknown": "Unbekannte Häufigkeit" }, "dates": { - "invalidDate": "Invalid Date", - "duePrefix": "Due" + "invalidDate": "Ungültiges Datum", + "duePrefix": "Fällig" }, "notifications": { - "loadFailed": "Failed to load personal chores", - "updateSuccess": "Personal chore updated successfully", - "createSuccess": "Personal chore created successfully", - "saveFailed": "Failed to save personal chore", - "deleteSuccess": "Personal chore deleted successfully", - "deleteFailed": "Failed to delete personal chore" + "loadFailed": "Laden der persönlichen Aufgaben fehlgeschlagen", + "updateSuccess": "Persönliche Aufgabe erfolgreich aktualisiert", + "createSuccess": "Persönliche Aufgabe erfolgreich erstellt", + "saveFailed": "Speichern der persönlichen Aufgabe fehlgeschlagen", + "deleteSuccess": "Persönliche Aufgabe erfolgreich gelöscht", + "deleteFailed": "Löschen der persönlichen Aufgabe fehlgeschlagen" } }, "indexPage": { - "welcomeMessage": "Welcome to Valerie UI App", - "mainPageInfo": "This is the main index page.", - "sampleTodosHeader": "Sample Todos (from IndexPage data)", - "totalCountLabel": "Total count from meta:", - "noTodos": "No todos to display." + "welcomeMessage": "Willkommen bei der Valerie UI App", + "mainPageInfo": "Dies ist die Hauptindexseite.", + "sampleTodosHeader": "Beispiel-Todos (aus IndexPage-Daten)", + "totalCountLabel": "Gesamtzahl aus Meta:", + "noTodos": "Keine Todos zum Anzeigen." + }, + "languageSelector": { + "title": "Sprache", + "languages": { + "en": "English", + "de": "Deutsch", + "nl": "Nederlands", + "fr": "Français", + "es": "Español" + } } -} +} \ No newline at end of file diff --git a/fe/src/i18n/en.json b/fe/src/i18n/en.json index 62afc82..d81ccf4 100644 --- a/fe/src/i18n/en.json +++ b/fe/src/i18n/en.json @@ -73,7 +73,10 @@ "groupNameRequired": "Group name is required", "createFailed": "Failed to create group. Please try again.", "inviteCodeRequired": "Invite code is required", - "joinFailed": "Failed to join group. Please check the invite code and try again." + "joinFailed": "Failed to join group. Please check the invite code and try again.", + "invalidDataFromServer": "Invalid data received from server.", + "createFailedConsole": "Error creating group:", + "joinFailedConsole": "Error joining group:" }, "notifications": { "groupCreatedSuccess": "Group '{groupName}' created successfully.", @@ -85,7 +88,8 @@ "authCallbackPage": { "redirecting": "Redirecting...", "errors": { - "authenticationFailed": "Authentication failed" + "authenticationFailed": "Authentication failed", + "noTokenProvided": "No token provided" } }, "choresPage": { @@ -125,7 +129,17 @@ "save": "Save Changes", "create": "Create", "editChore": "Edit Chore", - "createChore": "Create Chore" + "createChore": "Create Chore", + "intervalPlaceholder": "e.g., 10" + }, + "consoleErrors": { + "loadFailed": "Failed to load all chores:", + "loadGroupsFailed": "Failed to load groups", + "createAssignmentForNewChoreFailed": "Failed to create assignment for new chore:", + "saveFailed": "Failed to save chore:", + "deleteFailed": "Failed to delete chore:", + "createAssignmentFailed": "Failed to create assignment:", + "updateCompletionStatusFailed": "Failed to update chore completion status:" }, "deleteConfirm": { "title": "Confirm Deletion", @@ -160,10 +174,14 @@ "title": "Group Members", "defaultRole": "Member", "removeButton": "Remove", - "emptyState": "No members found." + "emptyState": "No members found.", + "closeMenuLabel": "Close menu" }, "invites": { "title": "Invite Members", + "description": "Invite new members by generating a shareable code.", + "addMemberButtonLabel": "Add member", + "closeInviteLabel": "Close invite", "regenerateButton": "Regenerate Invite Code", "generateButton": "Generate Invite Code", "activeCodeLabel": "Current Active Invite Code:", @@ -174,6 +192,15 @@ "newDataInvalid": "New invite code data is invalid." } }, + "errors": { + "failedToFetchActiveInvite": "Failed to fetch active invite code.", + "failedToFetchGroupDetails": "Failed to fetch group details.", + "failedToLoadUpcomingChores": "Error loading upcoming chores:", + "failedToLoadRecentExpenses": "Error loading recent expenses:" + }, + "console": { + "noActiveInvite": "No active invite code found for this group." + }, "chores": { "title": "Group Chores", "manageButton": "Manage Chores", @@ -191,6 +218,8 @@ "settleShareButton": "Settle My Share", "activityLabel": "Activity:", "byUser": "by", + "fallbackUserName": "User ID: {userId}", + "activityByUserFallback": "User {userId}", "splitTypes": { "equal": "Equal", "exactAmounts": "Exact Amounts", @@ -526,5 +555,15 @@ "sampleTodosHeader": "Sample Todos (from IndexPage data)", "totalCountLabel": "Total count from meta:", "noTodos": "No todos to display." + }, + "languageSelector": { + "title": "Language", + "languages": { + "en": "English", + "de": "Deutsch", + "nl": "Nederlands", + "fr": "Français", + "es": "Español" + } } } \ No newline at end of file diff --git a/fe/src/i18n/es.json b/fe/src/i18n/es.json index 665870e..2398631 100644 --- a/fe/src/i18n/es.json +++ b/fe/src/i18n/es.json @@ -3,559 +3,639 @@ "hello": "Hola" }, "loginPage": { - "emailLabel": "ES: Email", - "passwordLabel": "ES: Password", - "togglePasswordVisibilityLabel": "ES: Toggle password visibility", - "loginButton": "ES: Login", - "signupLink": "ES: Don't have an account? Sign up", + "emailLabel": "Correo electrónico", + "passwordLabel": "Contraseña", + "togglePasswordVisibilityLabel": "Alternar visibilidad de contraseña", + "loginButton": "Iniciar sesión", + "signupLink": "¿No tienes una cuenta? Regístrate", "errors": { - "emailRequired": "ES: Email is required", - "emailInvalid": "ES: Invalid email format", - "passwordRequired": "ES: Password is required", - "loginFailed": "ES: Login failed. Please check your credentials." + "emailRequired": "El correo electrónico es obligatorio", + "emailInvalid": "Formato de correo electrónico inválido", + "passwordRequired": "La contraseña es obligatoria", + "loginFailed": "Inicio de sesión fallido. Por favor, comprueba tus credenciales." }, "notifications": { - "loginSuccess": "ES: Login successful" + "loginSuccess": "Inicio de sesión exitoso" } }, "listsPage": { - "retryButton": "ES: Retry", + "retryButton": "Reintentar", "emptyState": { - "noListsForGroup": "ES: No lists found for this group.", - "noListsYet": "ES: You have no lists yet.", - "personalGlobalInfo": "ES: Create a personal list or join a group to see shared lists.", - "groupSpecificInfo": "ES: This group doesn't have any lists yet." + "noListsForGroup": "No se encontraron listas para este grupo.", + "noListsYet": "Aún no tienes listas.", + "personalGlobalInfo": "Crea una lista personal o únete a un grupo para ver listas compartidas.", + "groupSpecificInfo": "Este grupo aún no tiene listas." }, - "createNewListButton": "ES: Create New List", - "loadingLists": "ES: Loading lists...", - "noDescription": "ES: No description", - "addItemPlaceholder": "ES: Add new item...", + "createNewListButton": "Crear nueva lista", + "loadingLists": "Cargando listas...", + "noDescription": "Sin descripción", + "addItemPlaceholder": "Añadir nuevo artículo...", "createCard": { - "title": "ES: + Create a new list" + "title": "+ Lista" }, "pageTitle": { - "forGroup": "ES: Lists for {groupName}", - "forGroupId": "ES: Lists for Group {groupId}", - "myLists": "ES: My Lists" + "forGroup": "Listas para {groupName}", + "forGroupId": "Listas para el Grupo {groupId}", + "myLists": "Mis Listas" }, "errors": { - "fetchFailed": "ES: Failed to fetch lists." + "fetchFailed": "Error al obtener las listas." } }, "groupsPage": { - "retryButton": "ES: Retry", + "retryButton": "Reintentar", "emptyState": { - "title": "ES: No Groups Yet!", - "description": "ES: You are not a member of any groups yet. Create one or join using an invite code.", - "createButton": "ES: Create New Group" + "title": "¡Aún no hay grupos!", + "description": "Aún no eres miembro de ningún grupo. Crea uno o únete usando un código de invitación.", + "createButton": "Crear nuevo grupo" }, "groupCard": { - "newListButton": "ES: List" + "newListButton": "Lista" }, "createCard": { - "title": "ES: + Group" + "title": "+ Grupo" }, "joinGroup": { - "title": "ES: Join a Group with Invite Code", - "inputLabel": "ES: Enter Invite Code", - "inputPlaceholder": "ES: Enter Invite Code", - "joinButton": "ES: Join" + "title": "Unirse a un grupo con código de invitación", + "inputLabel": "Ingresar código de invitación", + "inputPlaceholder": "Ingresar código de invitación", + "joinButton": "Unirse" }, "createDialog": { - "title": "ES: Create New Group", - "closeButtonLabel": "ES: Close", - "groupNameLabel": "ES: Group Name", - "cancelButton": "ES: Cancel", - "createButton": "ES: Create" + "title": "Crear nuevo grupo", + "closeButtonLabel": "Cerrar", + "groupNameLabel": "Nombre del grupo", + "cancelButton": "Cancelar", + "createButton": "Crear" }, "errors": { - "fetchFailed": "ES: Failed to load groups", - "groupNameRequired": "ES: Group name is required", - "createFailed": "ES: Failed to create group. Please try again.", - "inviteCodeRequired": "ES: Invite code is required", - "joinFailed": "ES: Failed to join group. Please check the invite code and try again." + "fetchFailed": "Error al cargar los grupos", + "groupNameRequired": "El nombre del grupo es obligatorio", + "createFailed": "Error al crear el grupo. Por favor, inténtalo de nuevo.", + "inviteCodeRequired": "El código de invitación es obligatorio", + "joinFailed": "Error al unirse al grupo. Por favor, comprueba el código de invitación e inténtalo de nuevo.", + "invalidDataFromServer": "Datos inválidos recibidos del servidor.", + "createFailedConsole": "Error creando grupo:", + "joinFailedConsole": "Error al unirse al grupo:" }, "notifications": { - "groupCreatedSuccess": "ES: Group '{groupName}' created successfully.", - "joinSuccessNamed": "ES: Successfully joined group '{groupName}'.", - "joinSuccessGeneric": "ES: Successfully joined group.", - "listCreatedSuccess": "ES: List '{listName}' created successfully." + "groupCreatedSuccess": "Grupo '{groupName}' creado exitosamente.", + "joinSuccessNamed": "Te uniste exitosamente al grupo '{groupName}'.", + "joinSuccessGeneric": "Te uniste exitosamente al grupo.", + "listCreatedSuccess": "Lista '{listName}' creada exitosamente." } }, "authCallbackPage": { - "redirecting": "ES: Redirecting...", + "redirecting": "Redirigiendo...", "errors": { - "authenticationFailed": "ES: Authentication failed" + "authenticationFailed": "Autenticación fallida", + "noTokenProvided": "No se proporcionó ningún token" } }, "choresPage": { - "title": "ES: Chores", + "title": "Tareas", "tabs": { - "overdue": "ES: Overdue", - "today": "ES: Today", - "upcoming": "ES: Upcoming", - "allPending": "ES: All Pending", - "completed": "ES: Completed" + "overdue": "Vencidas", + "today": "Hoy", + "upcoming": "Próximas", + "allPending": "Todas pendientes", + "completed": "Completadas" }, "viewToggle": { - "calendarLabel": "ES: Calendar View", - "calendarText": "ES: Calendar", - "listLabel": "ES: List View", - "listText": "ES: List" + "calendarLabel": "Vista de calendario", + "calendarText": "Calendario", + "listLabel": "Vista de lista", + "listText": "Lista" }, - "newChoreButtonLabel": "ES: New Chore", - "newChoreButtonText": "ES: New Chore", + "newChoreButtonLabel": "Nueva tarea", + "newChoreButtonText": "Nueva tarea", "loadingState": { - "loadingChores": "ES: Loading chores..." + "loadingChores": "Cargando tareas..." }, "calendar": { - "prevMonthLabel": "ES: Previous month", - "nextMonthLabel": "ES: Next month", + "prevMonthLabel": "Mes anterior", + "nextMonthLabel": "Mes siguiente", "weekdays": { - "sun": "ES: Sun", - "mon": "ES: Mon", - "tue": "ES: Tue", - "wed": "ES: Wed", - "thu": "ES: Thu", - "fri": "ES: Fri", - "sat": "ES: Sat" + "sun": "Dom", + "mon": "Lun", + "tue": "Mar", + "wed": "Mié", + "thu": "Jue", + "fri": "Vie", + "sat": "Sáb" }, - "addChoreToDayLabel": "ES: Add chore to this day", - "emptyState": "ES: No chores to display for this period." + "addChoreToDayLabel": "Añadir tarea a este día", + "emptyState": "No hay tareas para mostrar en este período." }, "listView": { - "choreTypePersonal": "ES: Personal", - "choreTypeGroupFallback": "ES: Group", - "completedDatePrefix": "ES: Completed:", + "choreTypePersonal": "Personal", + "choreTypeGroupFallback": "Grupo", + "completedDatePrefix": "Completada:", "actions": { - "doneTitle": "ES: Mark as Done", - "doneText": "ES: Done", - "undoTitle": "ES: Mark as Not Done", - "undoText": "ES: Undo", - "editTitle": "ES: Edit", - "editLabel": "ES: Edit chore", - "editText": "ES: Edit", - "deleteTitle": "ES: Delete", - "deleteLabel": "ES: Delete chore", - "deleteText": "ES: Delete" + "doneTitle": "Marcar como hecha", + "doneText": "Hecha", + "undoTitle": "Marcar como no hecha", + "undoText": "Deshacer", + "editTitle": "Editar", + "editLabel": "Editar tarea", + "editText": "Editar", + "deleteTitle": "Eliminar", + "deleteLabel": "Eliminar tarea", + "deleteText": "Eliminar" }, "emptyState": { - "message": "ES: No chores in this view. Well done!", - "viewAllButton": "ES: View All Pending" + "message": "No hay tareas en esta vista. ¡Bien hecho!", + "viewAllButton": "Ver todas las pendientes" } }, "choreModal": { - "editTitle": "ES: Edit Chore", - "newTitle": "ES: New Chore", - "closeButtonLabel": "ES: Close modal", - "nameLabel": "ES: Name", - "namePlaceholder": "ES: Enter chore name", - "typeLabel": "ES: Type", - "typePersonal": "ES: Personal", - "typeGroup": "ES: Group", - "groupLabel": "ES: Group", - "groupSelectDefault": "ES: Select a group", - "descriptionLabel": "ES: Description", - "descriptionPlaceholder": "ES: Add a description (optional)", - "frequencyLabel": "ES: Frequency", - "intervalLabel": "ES: Interval (days)", - "intervalPlaceholder": "ES: e.g. 3", - "dueDateLabel": "ES: Due Date", - "quickDueDateToday": "ES: Today", - "quickDueDateTomorrow": "ES: Tomorrow", - "quickDueDateNextWeek": "ES: Next Week", - "cancelButton": "ES: Cancel", - "saveButton": "ES: Save" + "editTitle": "Editar tarea", + "newTitle": "Nueva tarea", + "closeButtonLabel": "Cerrar modal", + "nameLabel": "Nombre", + "namePlaceholder": "Ingresar nombre de la tarea", + "typeLabel": "Tipo", + "typePersonal": "Personal", + "typeGroup": "Grupo", + "groupLabel": "Grupo", + "groupSelectDefault": "Seleccionar un grupo", + "descriptionLabel": "Descripción", + "descriptionPlaceholder": "Añadir una descripción (opcional)", + "frequencyLabel": "Frecuencia", + "intervalLabel": "Intervalo (días)", + "intervalPlaceholder": "p.ej. 10", + "dueDateLabel": "Fecha de vencimiento", + "quickDueDateToday": "Hoy", + "quickDueDateTomorrow": "Mañana", + "quickDueDateNextWeek": "Próxima semana", + "cancelButton": "Cancelar", + "saveButton": "Guardar" + }, + "consoleErrors": { + "loadFailed": "Error al cargar todas las tareas:", + "loadGroupsFailed": "Error al cargar grupos", + "createAssignmentForNewChoreFailed": "Error al crear asignación para nueva tarea:", + "saveFailed": "Error al guardar tarea:", + "deleteFailed": "Error al eliminar tarea:", + "createAssignmentFailed": "Error al crear asignación:", + "updateCompletionStatusFailed": "Error al actualizar estado de finalización de la tarea:" }, "deleteDialog": { - "title": "ES: Delete Chore", - "confirmationText": "ES: Are you sure you want to delete this chore? This action cannot be undone.", - "deleteButton": "ES: Delete" + "title": "Eliminar tarea", + "confirmationText": "¿Estás seguro de que quieres eliminar esta tarea? Esta acción no se puede deshacer.", + "deleteButton": "Eliminar" }, "shortcutsModal": { - "title": "ES: Keyboard Shortcuts", - "descNewChore": "ES: New Chore", - "descToggleView": "ES: Toggle View (List/Calendar)", - "descToggleShortcuts": "ES: Show/Hide Shortcuts", - "descCloseModal": "ES: Close any open Modal/Dialog" + "title": "Atajos de teclado", + "descNewChore": "Nueva tarea", + "descToggleView": "Alternar vista (Lista/Calendario)", + "descToggleShortcuts": "Mostrar/Ocultar atajos", + "descCloseModal": "Cerrar cualquier Modal/Diálogo abierto" }, "frequencyOptions": { - "oneTime": "ES: One Time", - "daily": "ES: Daily", - "weekly": "ES: Weekly", - "monthly": "ES: Monthly", - "custom": "ES: Custom" + "oneTime": "Una vez", + "daily": "Diariamente", + "weekly": "Semanalmente", + "monthly": "Mensualmente", + "custom": "Personalizado" + }, + "frequency": { + "customInterval": "Cada {n} día | Cada {n} días" }, "formatters": { - "noDueDate": "ES: No due date", - "dueToday": "ES: Due Today", - "dueTomorrow": "ES: Due Tomorrow", - "overdueFull": "ES: Overdue: {date}", - "dueFull": "ES: Due {date}", - "invalidDate": "ES: Invalid Date" + "noDueDate": "Sin fecha de vencimiento", + "dueToday": "Vence hoy", + "dueTomorrow": "Vence mañana", + "overdueFull": "Vencida: {date}", + "dueFull": "Vence el {date}", + "invalidDate": "Fecha inválida" }, "notifications": { - "loadFailed": "ES: Failed to load chores", - "updateSuccess": "ES: Chore '{name}' updated successfully", - "createSuccess": "ES: Chore '{name}' created successfully", - "updateFailed": "ES: Failed to update chore", - "createFailed": "ES: Failed to create chore", - "deleteSuccess": "ES: Chore '{name}' deleted successfully", - "deleteFailed": "ES: Failed to delete chore", - "markedDone": "ES: {name} marked as done.", - "markedNotDone": "ES: {name} marked as not done.", - "statusUpdateFailed": "ES: Failed to update chore status." + "loadFailed": "Error al cargar las tareas.", + "loadGroupsFailed": "Error al cargar los grupos.", + "updateSuccess": "Tarea '{name}' actualizada exitosamente.", + "createSuccess": "Tarea '{name}' creada exitosamente.", + "updateFailed": "Error al actualizar la tarea.", + "createFailed": "Error al crear la tarea.", + "deleteSuccess": "Tarea '{name}' eliminada exitosamente.", + "deleteFailed": "Error al eliminar la tarea.", + "markedDone": "{name} marcada como hecha.", + "markedNotDone": "{name} marcada como no hecha.", + "statusUpdateFailed": "Error al actualizar el estado de la tarea.", + "createAssignmentFailed": "Error al crear la asignación para la tarea." }, "validation": { - "nameRequired": "ES: Chore name is required.", - "groupRequired": "ES: Please select a group for group chores.", - "intervalRequired": "ES: Custom interval must be at least 1 day.", - "dueDateRequired": "ES: Due date is required.", - "invalidDueDate": "ES: Invalid due date format." + "nameRequired": "El nombre de la tarea es obligatorio.", + "groupRequired": "Por favor, selecciona un grupo para tareas grupales.", + "intervalRequired": "El intervalo personalizado debe ser de al menos 1 día.", + "dueDateRequired": "La fecha de vencimiento es obligatoria.", + "invalidDueDate": "Formato de fecha de vencimiento inválido." }, - "unsavedChangesConfirmation": "ES: You have unsaved changes in the chore form. Are you sure you want to leave?" + "unsavedChangesConfirmation": "Tienes cambios sin guardar en el formulario de la tarea. ¿Estás seguro de que quieres salir?" }, "errorNotFoundPage": { - "errorCode": "ES: 404", - "errorMessage": "ES: Oops. Nothing here...", - "goHomeButton": "ES: Go Home" + "errorCode": "404", + "errorMessage": "Vaya. Aquí no hay nada...", + "goHomeButton": "Ir al inicio" }, "groupDetailPage": { - "loadingLabel": "ES: Loading group details...", - "retryButton": "ES: Retry", - "groupNotFound": "ES: Group not found or an error occurred.", + "loadingLabel": "Cargando detalles del grupo...", + "retryButton": "Reintentar", + "groupNotFound": "Grupo no encontrado o se produjo un error.", "members": { - "title": "ES: Group Members", - "defaultRole": "ES: Member", - "removeButton": "ES: Remove", - "emptyState": "ES: No members found." + "title": "Miembros del grupo", + "defaultRole": "Miembro", + "removeButton": "Eliminar", + "emptyState": "No se encontraron miembros.", + "closeMenuLabel": "Cerrar menú" }, "invites": { - "title": "ES: Invite Members", - "regenerateButton": "ES: Regenerate Invite Code", - "generateButton": "ES: Generate Invite Code", - "activeCodeLabel": "ES: Current Active Invite Code:", - "copyButtonLabel": "ES: Copy invite code", - "copySuccess": "ES: Invite code copied to clipboard!", - "emptyState": "ES: No active invite code. Click the button above to generate one.", + "title": "Invitar miembros", + "description": "Invita a nuevos miembros generando un código compartible.", + "addMemberButtonLabel": "Añadir miembro", + "closeInviteLabel": "Cerrar invitación", + "regenerateButton": "Regenerar código de invitación", + "generateButton": "Generar código de invitación", + "activeCodeLabel": "Código de invitación activo actual:", + "copyButtonLabel": "Copiar código de invitación", + "copySuccess": "¡Código de invitación copiado al portapapeles!", + "emptyState": "No hay código de invitación activo. Haz clic en el botón de arriba para generar uno.", "errors": { - "newDataInvalid": "ES: New invite code data is invalid." + "newDataInvalid": "Los datos del nuevo código de invitación son inválidos." } }, + "errors": { + "failedToFetchActiveInvite": "Error al obtener el código de invitación activo.", + "failedToFetchGroupDetails": "Error al obtener los detalles del grupo.", + "failedToLoadUpcomingChores": "Error al cargar las próximas tareas:", + "failedToLoadRecentExpenses": "Error al cargar los gastos recientes:" + }, + "console": { + "noActiveInvite": "No se encontró ningún código de invitación activo para este grupo." + }, "chores": { - "title": "ES: Group Chores", - "manageButton": "ES: Manage Chores", - "duePrefix": "ES: Due:", - "emptyState": "ES: No chores scheduled. Click \"Manage Chores\" to create some!" + "title": "Tareas del grupo", + "manageButton": "Gestionar tareas", + "duePrefix": "Vence:", + "emptyState": "No hay tareas programadas. ¡Haz clic en \"Gestionar tareas\" para crear algunas!" }, "expenses": { - "title": "ES: Group Expenses", - "manageButton": "ES: Manage Expenses", - "emptyState": "ES: No expenses recorded. Click \"Manage Expenses\" to add some!", + "title": "Gastos del grupo", + "manageButton": "Gestionar gastos", + "emptyState": "No hay gastos registrados. ¡Haz clic en \"Gestionar gastos\" para añadir algunos!", + "paidBy": "Pagado por:", + "owes": "debe", + "paidAmount": "Pagado:", + "onDate": "el", + "settleShareButton": "Saldar mi parte", + "activityLabel": "Actividad:", + "byUser": "por", + "fallbackUserName": "ID de usuario: {userId}", + "activityByUserFallback": "Usuario {userId}", "splitTypes": { - "equal": "ES: Equal", - "exactAmounts": "ES: Exact Amounts", - "percentage": "ES: Percentage", - "shares": "ES: Shares", - "itemBased": "ES: Item Based" + "equal": "Igual", + "exactAmounts": "Cantidades exactas", + "percentage": "Porcentaje", + "shares": "Partes", + "itemBased": "Basado en artículos" } }, "notifications": { - "fetchDetailsFailed": "ES: Failed to fetch group details.", - "fetchInviteFailed": "ES: Failed to fetch active invite code.", - "generateInviteSuccess": "ES: New invite code generated successfully!", - "generateInviteError": "ES: Failed to generate invite code.", - "clipboardNotSupported": "ES: Clipboard not supported or no code to copy.", - "copyInviteFailed": "ES: Failed to copy invite code.", - "removeMemberSuccess": "ES: Member removed successfully", - "removeMemberFailed": "ES: Failed to remove member" + "fetchDetailsFailed": "Error al obtener los detalles del grupo.", + "fetchInviteFailed": "Error al obtener el código de invitación activo.", + "generateInviteSuccess": "¡Nuevo código de invitación generado exitosamente!", + "generateInviteError": "Error al generar el código de invitación.", + "clipboardNotSupported": "Portapapeles no compatible o no hay código para copiar.", + "copyInviteFailed": "Error al copiar el código de invitación.", + "removeMemberSuccess": "Miembro eliminado exitosamente", + "removeMemberFailed": "Error al eliminar miembro", + "loadExpensesFailed": "Error al cargar los gastos recientes.", + "cannotSettleOthersShares": "Solo puedes saldar tus propias partes.", + "settlementDataMissing": "No se puede procesar el saldo: faltan datos.", + "settleShareSuccess": "¡Parte saldada exitosamente!", + "settleShareFailed": "Error al saldar la parte." + }, + "loading": { + "settlement": "Procesando saldo..." + }, + "settleShareModal": { + "title": "Saldar parte", + "settleAmountFor": "Saldar cantidad para {userName}:", + "amountLabel": "Cantidad", + "cancelButton": "Cancelar", + "confirmButton": "Confirmar", + "errors": { + "enterAmount": "Por favor, ingresa una cantidad.", + "positiveAmount": "Por favor, ingresa una cantidad positiva.", + "exceedsRemaining": "La cantidad no puede exceder el restante: {amount}.", + "noSplitSelected": "Error: No se seleccionó ninguna división." + } + }, + "status": { + "settled": "Saldado", + "partiallySettled": "Parcialmente saldado", + "unsettled": "Pendiente", + "paid": "Pagado", + "partiallyPaid": "Parcialmente pagado", + "unpaid": "No pagado", + "unknown": "Estado desconocido" } }, "accountPage": { - "title": "Account Settings", - "loadingProfile": "Loading profile...", - "retryButton": "Retry", + "title": "Configuración de la cuenta", + "loadingProfile": "Cargando perfil...", + "retryButton": "Reintentar", "profileSection": { - "header": "Profile Information", - "nameLabel": "Name", - "emailLabel": "Email", - "saveButton": "Save Changes" + "header": "Información del perfil", + "nameLabel": "Nombre", + "emailLabel": "Correo electrónico", + "saveButton": "Guardar cambios" }, "passwordSection": { - "header": "Change Password", - "currentPasswordLabel": "Current Password", - "newPasswordLabel": "New Password", - "changeButton": "Change Password" + "header": "Cambiar contraseña", + "currentPasswordLabel": "Contraseña actual", + "newPasswordLabel": "Nueva contraseña", + "changeButton": "Cambiar contraseña" }, "notificationsSection": { - "header": "Notification Preferences", - "emailNotificationsLabel": "Email Notifications", - "emailNotificationsDescription": "Receive email notifications for important updates", - "listUpdatesLabel": "List Updates", - "listUpdatesDescription": "Get notified when lists are updated", - "groupActivitiesLabel": "Group Activities", - "groupActivitiesDescription": "Receive notifications for group activities" + "header": "Preferencias de notificación", + "emailNotificationsLabel": "Notificaciones por correo electrónico", + "emailNotificationsDescription": "Recibir notificaciones por correo electrónico para actualizaciones importantes", + "listUpdatesLabel": "Actualizaciones de listas", + "listUpdatesDescription": "Recibir notificaciones cuando se actualicen las listas", + "groupActivitiesLabel": "Actividades de grupo", + "groupActivitiesDescription": "Recibir notificaciones para actividades de grupo" }, "notifications": { - "profileLoadFailed": "Failed to load profile", - "profileUpdateSuccess": "Profile updated successfully", - "profileUpdateFailed": "Failed to update profile", - "passwordFieldsRequired": "Please fill in both current and new password fields.", - "passwordTooShort": "New password must be at least 8 characters long.", - "passwordChangeSuccess": "Password changed successfully", - "passwordChangeFailed": "Failed to change password", - "preferencesUpdateSuccess": "Preferences updated successfully", - "preferencesUpdateFailed": "Failed to update preferences" + "profileLoadFailed": "Error al cargar el perfil", + "profileUpdateSuccess": "Perfil actualizado exitosamente", + "profileUpdateFailed": "Error al actualizar el perfil", + "passwordFieldsRequired": "Por favor, completa los campos de contraseña actual y nueva.", + "passwordTooShort": "La nueva contraseña debe tener al menos 8 caracteres.", + "passwordChangeSuccess": "Contraseña cambiada exitosamente", + "passwordChangeFailed": "Error al cambiar la contraseña", + "preferencesUpdateSuccess": "Preferencias actualizadas exitosamente", + "preferencesUpdateFailed": "Error al actualizar las preferencias" }, - "saving": "Saving..." + "saving": "Guardando..." }, "signupPage": { - "header": "Sign Up", - "fullNameLabel": "Full Name", - "emailLabel": "Email", - "passwordLabel": "Password", - "confirmPasswordLabel": "Confirm Password", - "togglePasswordVisibility": "Toggle password visibility", - "submitButton": "Sign Up", - "loginLink": "Already have an account? Login", + "header": "Regístrate", + "fullNameLabel": "Nombre completo", + "emailLabel": "Correo electrónico", + "passwordLabel": "Contraseña", + "confirmPasswordLabel": "Confirmar contraseña", + "togglePasswordVisibility": "Alternar visibilidad de contraseña", + "submitButton": "Regístrate", + "loginLink": "¿Ya tienes una cuenta? Iniciar sesión", "validation": { - "nameRequired": "Name is required", - "emailRequired": "Email is required", - "emailInvalid": "Invalid email format", - "passwordRequired": "Password is required", - "passwordLength": "Password must be at least 8 characters", - "confirmPasswordRequired": "Please confirm your password", - "passwordsNoMatch": "Passwords do not match" + "nameRequired": "El nombre es obligatorio", + "emailRequired": "El correo electrónico es obligatorio", + "emailInvalid": "Formato de correo electrónico inválido", + "passwordRequired": "La contraseña es obligatoria", + "passwordLength": "La contraseña debe tener al menos 8 caracteres", + "confirmPasswordRequired": "Por favor, confirma tu contraseña", + "passwordsNoMatch": "Las contraseñas no coinciden" }, "notifications": { - "signupFailed": "Signup failed. Please try again.", - "signupSuccess": "Account created successfully. Please login." + "signupFailed": "Registro fallido. Por favor, inténtalo de nuevo.", + "signupSuccess": "Cuenta creada exitosamente. Por favor, inicia sesión." } }, "listDetailPage": { "loading": { - "list": "Loading list...", - "items": "Loading items...", - "ocrProcessing": "Processing image...", - "addingOcrItems": "Adding OCR items...", - "costSummary": "Loading summary...", - "expenses": "Loading expenses...", - "settlement": "Processing settlement..." + "list": "Cargando lista...", + "items": "Cargando artículos...", + "ocrProcessing": "Procesando imagen...", + "addingOcrItems": "Añadiendo artículos OCR...", + "costSummary": "Cargando resumen...", + "expenses": "Cargando gastos...", + "settlement": "Procesando saldo..." }, "errors": { - "fetchFailed": "Failed to load list details.", - "genericLoadFailure": "Group not found or an error occurred.", - "ocrNoItems": "No items extracted from the image.", - "ocrFailed": "Failed to process image.", - "addItemFailed": "Failed to add item.", - "updateItemFailed": "Failed to update item.", - "updateItemPriceFailed": "Failed to update item price.", - "deleteItemFailed": "Failed to delete item.", - "addOcrItemsFailed": "Failed to add OCR items.", - "fetchItemsFailed": "Failed to load items: {errorMessage}", - "loadCostSummaryFailed": "Failed to load cost summary." + "fetchFailed": "Error al cargar los detalles de la lista.", + "genericLoadFailure": "Grupo no encontrado o se produjo un error.", + "ocrNoItems": "No se extrajeron artículos de la imagen.", + "ocrFailed": "Error al procesar la imagen.", + "addItemFailed": "Error al añadir el artículo.", + "updateItemFailed": "Error al actualizar el artículo.", + "updateItemPriceFailed": "Error al actualizar el precio del artículo.", + "deleteItemFailed": "Error al eliminar el artículo.", + "addOcrItemsFailed": "Error al añadir artículos OCR.", + "fetchItemsFailed": "Error al cargar artículos: {errorMessage}", + "loadCostSummaryFailed": "Error al cargar el resumen de costos." }, - "retryButton": "Retry", + "retryButton": "Reintentar", "buttons": { - "addViaOcr": "Add via OCR", - "addItem": "Add", - "addItems": "Add Items", - "cancel": "Cancel", - "confirm": "Confirm", - "saveChanges": "Save Changes", - "close": "Close", - "costSummary": "Cost Summary" + "addViaOcr": "Añadir vía OCR", + "addItem": "Añadir", + "addItems": "Añadir artículos", + "cancel": "Cancelar", + "confirm": "Confirmar", + "saveChanges": "Guardar cambios", + "close": "Cerrar", + "costSummary": "Resumen de costos" }, "badges": { - "groupList": "Group List", - "personalList": "Personal List" + "groupList": "Lista de grupo", + "personalList": "Lista personal" }, "items": { "emptyState": { - "title": "No Items Yet!", - "message": "Add some items using the form below." + "title": "¡Aún no hay artículos!", + "message": "Añade algunos artículos usando el formulario de abajo." }, "addItemForm": { - "placeholder": "Add a new item", - "quantityPlaceholder": "Qty", - "itemNameSrLabel": "New item name", - "quantitySrLabel": "Quantity" + "placeholder": "Añadir un nuevo artículo", + "quantityPlaceholder": "Cant.", + "itemNameSrLabel": "Nombre del nuevo artículo", + "quantitySrLabel": "Cantidad" }, - "pricePlaceholder": "Price", - "editItemAriaLabel": "Edit item", - "deleteItemAriaLabel": "Delete item" + "pricePlaceholder": "Precio", + "editItemAriaLabel": "Editar artículo", + "deleteItemAriaLabel": "Eliminar artículo" }, "modals": { "ocr": { - "title": "Add Items via OCR", - "uploadLabel": "Upload Image" + "title": "Añadir artículos vía OCR", + "uploadLabel": "Subir imagen" }, "confirmation": { - "title": "Confirmation" + "title": "Confirmación" }, "editItem": { - "title": "Edit Item", - "nameLabel": "Item Name", - "quantityLabel": "Quantity" + "title": "Editar artículo", + "nameLabel": "Nombre del artículo", + "quantityLabel": "Cantidad" }, "costSummary": { - "title": "List Cost Summary", - "totalCostLabel": "Total List Cost:", - "equalShareLabel": "Equal Share Per User:", - "participantsLabel": "Participating Users:", - "userBalancesHeader": "User Balances", + "title": "Resumen de costos de la lista", + "totalCostLabel": "Costo total de la lista:", + "equalShareLabel": "Parte igual por usuario:", + "participantsLabel": "Usuarios participantes:", + "userBalancesHeader": "Saldos de usuarios", "tableHeaders": { - "user": "User", - "itemsAddedValue": "Items Added Value", - "amountDue": "Amount Due", - "balance": "Balance" + "user": "Usuario", + "itemsAddedValue": "Valor de los artículos añadidos", + "amountDue": "Cantidad debida", + "balance": "Saldo" }, - "emptyState": "No cost summary available." + "emptyState": "No hay resumen de costos disponible." }, "settleShare": { - "title": "Settle Share", - "settleAmountFor": "Settle amount for {userName}:", - "amountLabel": "Amount", + "title": "Saldar parte", + "settleAmountFor": "Saldar cantidad para {userName}:", + "amountLabel": "Cantidad", "errors": { - "enterAmount": "Please enter an amount.", - "positiveAmount": "Please enter a positive amount.", - "exceedsRemaining": "Amount cannot exceed remaining: {amount}.", - "noSplitSelected": "Error: No split selected." + "enterAmount": "Por favor, ingresa una cantidad.", + "positiveAmount": "Por favor, ingresa una cantidad positiva.", + "exceedsRemaining": "La cantidad no puede exceder el restante: {amount}.", + "noSplitSelected": "Error: No se seleccionó ninguna división." } } }, "confirmations": { - "updateMessage": "Mark '{itemName}' as {status}?", - "statusComplete": "complete", - "statusIncomplete": "incomplete", - "deleteMessage": "Delete '{itemName}'? This cannot be undone." + "updateMessage": "¿Marcar '{itemName}' como {status}?", + "statusComplete": "completado", + "statusIncomplete": "incompleto", + "deleteMessage": "¿Eliminar '{itemName}'? Esto no se puede deshacer." }, "notifications": { - "itemAddedSuccess": "Item added successfully.", - "itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.", - "itemUpdatedSuccess": "Item updated successfully.", - "itemDeleteSuccess": "Item deleted successfully.", - "enterItemName": "Please enter an item name.", - "costSummaryLoadFailed": "Failed to load cost summary.", - "cannotSettleOthersShares": "You can only settle your own shares.", - "settlementDataMissing": "Cannot process settlement: missing data.", - "settleShareSuccess": "Share settled successfully!", - "settleShareFailed": "Failed to settle share." + "itemAddedSuccess": "Artículo añadido exitosamente.", + "itemsAddedSuccessOcr": "{count} artículo(s) añadido(s) exitosamente desde OCR.", + "itemUpdatedSuccess": "Artículo actualizado exitosamente.", + "itemDeleteSuccess": "Artículo eliminado exitosamente.", + "enterItemName": "Por favor, ingresa un nombre de artículo.", + "costSummaryLoadFailed": "Error al cargar el resumen de costos.", + "cannotSettleOthersShares": "Solo puedes saldar tus propias partes.", + "settlementDataMissing": "No se puede procesar el saldo: faltan datos.", + "settleShareSuccess": "¡Parte saldada exitosamente!", + "settleShareFailed": "Error al saldar la parte." }, "expensesSection": { - "title": "Expenses", - "addExpenseButton": "Add Expense", - "loading": "Loading expenses...", - "emptyState": "No expenses recorded for this list yet.", - "paidBy": "Paid by:", - "onDate": "on", - "owes": "owes", - "paidAmount": "Paid:", - "activityLabel": "Activity:", - "byUser": "by", - "settleShareButton": "Settle My Share", - "retryButton": "Retry" + "title": "Gastos", + "addExpenseButton": "Añadir gasto", + "loading": "Cargando gastos...", + "emptyState": "Aún no hay gastos registrados para esta lista.", + "paidBy": "Pagado por:", + "onDate": "el", + "owes": "debe", + "paidAmount": "Pagado:", + "activityLabel": "Actividad:", + "byUser": "por", + "settleShareButton": "Saldar mi parte", + "retryButton": "Reintentar" }, "status": { - "settled": "Settled", - "partiallySettled": "Partially Settled", - "unsettled": "Unsettled", - "paid": "Paid", - "partiallyPaid": "Partially Paid", - "unpaid": "Unpaid", - "unknown": "Unknown Status" + "settled": "Saldado", + "partiallySettled": "Parcialmente saldado", + "unsettled": "Pendiente", + "paid": "Pagado", + "partiallyPaid": "Parcialmente pagado", + "unpaid": "No pagado", + "unknown": "Estado desconocido" } }, "myChoresPage": { - "title": "My Assigned Chores", - "showCompletedToggle": "Show Completed", + "title": "Mis tareas asignadas", + "showCompletedToggle": "Mostrar completadas", "timelineHeaders": { - "overdue": "Overdue", - "today": "Due Today", - "thisWeek": "This Week", - "later": "Later", - "completed": "Completed" + "overdue": "Vencidas", + "today": "Vencen hoy", + "thisWeek": "Esta semana", + "later": "Más tarde", + "completed": "Completadas" }, "choreCard": { "personal": "Personal", - "group": "Group", - "duePrefix": "Due", - "completedPrefix": "Completed", - "dueToday": "Due Today", - "markCompleteButton": "Mark Complete" + "group": "Grupo", + "duePrefix": "Vence", + "completedPrefix": "Completada", + "dueToday": "Vence hoy", + "markCompleteButton": "Marcar como completada" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Una vez", + "daily": "Diariamente", + "weekly": "Semanalmente", + "monthly": "Mensualmente", + "custom": "Personalizado", + "unknown": "Frecuencia desconocida" }, "dates": { - "invalidDate": "Invalid Date", - "unknownDate": "Unknown Date" + "invalidDate": "Fecha inválida", + "unknownDate": "Fecha desconocida" }, "emptyState": { - "title": "No Assignments Yet!", - "noAssignmentsPending": "You have no pending chore assignments.", - "noAssignmentsAll": "You have no chore assignments (completed or pending).", - "viewAllChoresButton": "View All Chores" + "title": "¡Aún no hay asignaciones!", + "noAssignmentsPending": "No tienes asignaciones de tareas pendientes.", + "noAssignmentsAll": "No tienes asignaciones de tareas (completadas o pendientes).", + "viewAllChoresButton": "Ver todas las tareas" }, "notifications": { - "loadFailed": "Failed to load assignments", - "markedComplete": "Marked \"{choreName}\" as complete!", - "markCompleteFailed": "Failed to mark assignment as complete" + "loadFailed": "Error al cargar las asignaciones", + "markedComplete": "¡Se marcó \"{choreName}\" como completada!", + "markCompleteFailed": "Error al marcar la asignación como completada" } }, "personalChoresPage": { - "title": "Personal Chores", - "newChoreButton": "New Chore", - "editButton": "Edit", - "deleteButton": "Delete", - "cancelButton": "Cancel", - "saveButton": "Save", + "title": "Tareas personales", + "newChoreButton": "Nueva tarea", + "editButton": "Editar", + "deleteButton": "Eliminar", + "cancelButton": "Cancelar", + "saveButton": "Guardar", "modals": { - "editChoreTitle": "Edit Chore", - "newChoreTitle": "New Chore", - "deleteChoreTitle": "Delete Chore" + "editChoreTitle": "Editar tarea", + "newChoreTitle": "Nueva tarea", + "deleteChoreTitle": "Eliminar tarea" }, "form": { - "nameLabel": "Name", - "descriptionLabel": "Description", - "frequencyLabel": "Frequency", - "intervalLabel": "Interval (days)", - "dueDateLabel": "Next Due Date" + "nameLabel": "Nombre", + "descriptionLabel": "Descripción", + "frequencyLabel": "Frecuencia", + "intervalLabel": "Intervalo (días)", + "dueDateLabel": "Próxima fecha de vencimiento" }, "deleteDialog": { - "confirmationText": "Are you sure you want to delete this chore?" + "confirmationText": "¿Estás seguro de que quieres eliminar esta tarea?" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Una vez", + "daily": "Diariamente", + "weekly": "Semanalmente", + "monthly": "Mensualmente", + "custom": "Personalizado", + "unknown": "Frecuencia desconocida" }, "dates": { - "invalidDate": "Invalid Date", - "duePrefix": "Due" + "invalidDate": "Fecha inválida", + "duePrefix": "Vence" }, "notifications": { - "loadFailed": "Failed to load personal chores", - "updateSuccess": "Personal chore updated successfully", - "createSuccess": "Personal chore created successfully", - "saveFailed": "Failed to save personal chore", - "deleteSuccess": "Personal chore deleted successfully", - "deleteFailed": "Failed to delete personal chore" + "loadFailed": "Error al cargar las tareas personales", + "updateSuccess": "Tarea personal actualizada exitosamente", + "createSuccess": "Tarea personal creada exitosamente", + "saveFailed": "Error al guardar la tarea personal", + "deleteSuccess": "Tarea personal eliminada exitosamente", + "deleteFailed": "Error al eliminar la tarea personal" } }, "indexPage": { - "welcomeMessage": "Welcome to Valerie UI App", - "mainPageInfo": "This is the main index page.", - "sampleTodosHeader": "Sample Todos (from IndexPage data)", - "totalCountLabel": "Total count from meta:", - "noTodos": "No todos to display." + "welcomeMessage": "Bienvenido a la aplicación Valerie UI", + "mainPageInfo": "Esta es la página de índice principal.", + "sampleTodosHeader": "Tareas de ejemplo (de datos de IndexPage)", + "totalCountLabel": "Recuento total de meta:", + "noTodos": "No hay tareas para mostrar." + }, + "languageSelector": { + "title": "Idioma", + "languages": { + "en": "English", + "de": "Deutsch", + "nl": "Nederlands", + "fr": "Français", + "es": "Español" + } } -} +} \ No newline at end of file diff --git a/fe/src/i18n/fr.json b/fe/src/i18n/fr.json index cf8c4fc..7554798 100644 --- a/fe/src/i18n/fr.json +++ b/fe/src/i18n/fr.json @@ -3,559 +3,639 @@ "hello": "Bonjour" }, "loginPage": { - "emailLabel": "FR: Email", - "passwordLabel": "FR: Password", - "togglePasswordVisibilityLabel": "FR: Toggle password visibility", - "loginButton": "FR: Login", - "signupLink": "FR: Don't have an account? Sign up", + "emailLabel": "E-mail", + "passwordLabel": "Mot de passe", + "togglePasswordVisibilityLabel": "Basculer la visibilité du mot de passe", + "loginButton": "Connexion", + "signupLink": "Vous n'avez pas de compte ? S'inscrire", "errors": { - "emailRequired": "FR: Email is required", - "emailInvalid": "FR: Invalid email format", - "passwordRequired": "FR: Password is required", - "loginFailed": "FR: Login failed. Please check your credentials." + "emailRequired": "L'e-mail est requis", + "emailInvalid": "Format d'e-mail invalide", + "passwordRequired": "Le mot de passe est requis", + "loginFailed": "Échec de la connexion. Veuillez vérifier vos identifiants." }, "notifications": { - "loginSuccess": "FR: Login successful" + "loginSuccess": "Connexion réussie" } }, "listsPage": { - "retryButton": "FR: Retry", + "retryButton": "Réessayer", "emptyState": { - "noListsForGroup": "FR: No lists found for this group.", - "noListsYet": "FR: You have no lists yet.", - "personalGlobalInfo": "FR: Create a personal list or join a group to see shared lists.", - "groupSpecificInfo": "FR: This group doesn't have any lists yet." + "noListsForGroup": "Aucune liste trouvée pour ce groupe.", + "noListsYet": "Vous n'avez pas encore de listes.", + "personalGlobalInfo": "Créez une liste personnelle ou rejoignez un groupe pour voir les listes partagées.", + "groupSpecificInfo": "Ce groupe n'a pas encore de listes." }, - "createNewListButton": "FR: Create New List", - "loadingLists": "FR: Loading lists...", - "noDescription": "FR: No description", - "addItemPlaceholder": "FR: Add new item...", + "createNewListButton": "Créer une nouvelle liste", + "loadingLists": "Chargement des listes...", + "noDescription": "Pas de description", + "addItemPlaceholder": "Ajouter un nouvel article...", "createCard": { - "title": "FR: + Create a new list" + "title": "+ Liste" }, "pageTitle": { - "forGroup": "FR: Lists for {groupName}", - "forGroupId": "FR: Lists for Group {groupId}", - "myLists": "FR: My Lists" + "forGroup": "Listes pour {groupName}", + "forGroupId": "Listes pour le groupe {groupId}", + "myLists": "Mes listes" }, "errors": { - "fetchFailed": "FR: Failed to fetch lists." + "fetchFailed": "Échec de la récupération des listes." } }, "groupsPage": { - "retryButton": "FR: Retry", + "retryButton": "Réessayer", "emptyState": { - "title": "FR: No Groups Yet!", - "description": "FR: You are not a member of any groups yet. Create one or join using an invite code.", - "createButton": "FR: Create New Group" + "title": "Pas encore de groupes !", + "description": "Vous n'êtes membre d'aucun groupe pour le moment. Créez-en un ou rejoignez-en un à l'aide d'un code d'invitation.", + "createButton": "Créer un nouveau groupe" }, "groupCard": { - "newListButton": "FR: List" + "newListButton": "Liste" }, "createCard": { - "title": "FR: + Group" + "title": "+ Groupe" }, "joinGroup": { - "title": "FR: Join a Group with Invite Code", - "inputLabel": "FR: Enter Invite Code", - "inputPlaceholder": "FR: Enter Invite Code", - "joinButton": "FR: Join" + "title": "Rejoindre un groupe avec un code d'invitation", + "inputLabel": "Entrez le code d'invitation", + "inputPlaceholder": "Entrez le code d'invitation", + "joinButton": "Rejoindre" }, "createDialog": { - "title": "FR: Create New Group", - "closeButtonLabel": "FR: Close", - "groupNameLabel": "FR: Group Name", - "cancelButton": "FR: Cancel", - "createButton": "FR: Create" + "title": "Créer un nouveau groupe", + "closeButtonLabel": "Fermer", + "groupNameLabel": "Nom du groupe", + "cancelButton": "Annuler", + "createButton": "Créer" }, "errors": { - "fetchFailed": "FR: Failed to load groups", - "groupNameRequired": "FR: Group name is required", - "createFailed": "FR: Failed to create group. Please try again.", - "inviteCodeRequired": "FR: Invite code is required", - "joinFailed": "FR: Failed to join group. Please check the invite code and try again." + "fetchFailed": "Échec du chargement des groupes", + "groupNameRequired": "Le nom du groupe est requis", + "createFailed": "Échec de la création du groupe. Veuillez réessayer.", + "inviteCodeRequired": "Le code d'invitation est requis", + "joinFailed": "Échec pour rejoindre le groupe. Veuillez vérifier le code d'invitation et réessayer.", + "invalidDataFromServer": "Données invalides reçues du serveur.", + "createFailedConsole": "Erreur lors de la création du groupe :", + "joinFailedConsole": "Erreur pour rejoindre le groupe :" }, "notifications": { - "groupCreatedSuccess": "FR: Group '{groupName}' created successfully.", - "joinSuccessNamed": "FR: Successfully joined group '{groupName}'.", - "joinSuccessGeneric": "FR: Successfully joined group.", - "listCreatedSuccess": "FR: List '{listName}' created successfully." + "groupCreatedSuccess": "Groupe '{groupName}' créé avec succès.", + "joinSuccessNamed": "Vous avez rejoint le groupe '{groupName}' avec succès.", + "joinSuccessGeneric": "Vous avez rejoint le groupe avec succès.", + "listCreatedSuccess": "Liste '{listName}' créée avec succès." } }, "authCallbackPage": { - "redirecting": "FR: Redirecting...", + "redirecting": "Redirection...", "errors": { - "authenticationFailed": "FR: Authentication failed" + "authenticationFailed": "Échec de l'authentification", + "noTokenProvided": "Aucun jeton fourni" } }, "choresPage": { - "title": "FR: Chores", + "title": "Tâches", "tabs": { - "overdue": "FR: Overdue", - "today": "FR: Today", - "upcoming": "FR: Upcoming", - "allPending": "FR: All Pending", - "completed": "FR: Completed" + "overdue": "En retard", + "today": "Aujourd'hui", + "upcoming": "À venir", + "allPending": "Toutes en attente", + "completed": "Terminées" }, "viewToggle": { - "calendarLabel": "FR: Calendar View", - "calendarText": "FR: Calendar", - "listLabel": "FR: List View", - "listText": "FR: List" + "calendarLabel": "Vue Calendrier", + "calendarText": "Calendrier", + "listLabel": "Vue Liste", + "listText": "Liste" }, - "newChoreButtonLabel": "FR: New Chore", - "newChoreButtonText": "FR: New Chore", + "newChoreButtonLabel": "Nouvelle tâche", + "newChoreButtonText": "Nouvelle tâche", "loadingState": { - "loadingChores": "FR: Loading chores..." + "loadingChores": "Chargement des tâches..." }, "calendar": { - "prevMonthLabel": "FR: Previous month", - "nextMonthLabel": "FR: Next month", + "prevMonthLabel": "Mois précédent", + "nextMonthLabel": "Mois suivant", "weekdays": { - "sun": "FR: Sun", - "mon": "FR: Mon", - "tue": "FR: Tue", - "wed": "FR: Wed", - "thu": "FR: Thu", - "fri": "FR: Fri", - "sat": "FR: Sat" + "sun": "Dim", + "mon": "Lun", + "tue": "Mar", + "wed": "Mer", + "thu": "Jeu", + "fri": "Ven", + "sat": "Sam" }, - "addChoreToDayLabel": "FR: Add chore to this day", - "emptyState": "FR: No chores to display for this period." + "addChoreToDayLabel": "Ajouter une tâche à ce jour", + "emptyState": "Aucune tâche à afficher pour cette période." }, "listView": { - "choreTypePersonal": "FR: Personal", - "choreTypeGroupFallback": "FR: Group", - "completedDatePrefix": "FR: Completed:", + "choreTypePersonal": "Personnel", + "choreTypeGroupFallback": "Groupe", + "completedDatePrefix": "Terminée le :", "actions": { - "doneTitle": "FR: Mark as Done", - "doneText": "FR: Done", - "undoTitle": "FR: Mark as Not Done", - "undoText": "FR: Undo", - "editTitle": "FR: Edit", - "editLabel": "FR: Edit chore", - "editText": "FR: Edit", - "deleteTitle": "FR: Delete", - "deleteLabel": "FR: Delete chore", - "deleteText": "FR: Delete" + "doneTitle": "Marquer comme terminée", + "doneText": "Terminée", + "undoTitle": "Marquer comme non terminée", + "undoText": "Annuler", + "editTitle": "Modifier", + "editLabel": "Modifier la tâche", + "editText": "Modifier", + "deleteTitle": "Supprimer", + "deleteLabel": "Supprimer la tâche", + "deleteText": "Supprimer" }, "emptyState": { - "message": "FR: No chores in this view. Well done!", - "viewAllButton": "FR: View All Pending" + "message": "Aucune tâche dans cette vue. Bien joué !", + "viewAllButton": "Voir toutes les tâches en attente" } }, "choreModal": { - "editTitle": "FR: Edit Chore", - "newTitle": "FR: New Chore", - "closeButtonLabel": "FR: Close modal", - "nameLabel": "FR: Name", - "namePlaceholder": "FR: Enter chore name", - "typeLabel": "FR: Type", - "typePersonal": "FR: Personal", - "typeGroup": "FR: Group", - "groupLabel": "FR: Group", - "groupSelectDefault": "FR: Select a group", - "descriptionLabel": "FR: Description", - "descriptionPlaceholder": "FR: Add a description (optional)", - "frequencyLabel": "FR: Frequency", - "intervalLabel": "FR: Interval (days)", - "intervalPlaceholder": "FR: e.g. 3", - "dueDateLabel": "FR: Due Date", - "quickDueDateToday": "FR: Today", - "quickDueDateTomorrow": "FR: Tomorrow", - "quickDueDateNextWeek": "FR: Next Week", - "cancelButton": "FR: Cancel", - "saveButton": "FR: Save" + "editTitle": "Modifier la tâche", + "newTitle": "Nouvelle tâche", + "closeButtonLabel": "Fermer la modale", + "nameLabel": "Nom", + "namePlaceholder": "Entrez le nom de la tâche", + "typeLabel": "Type", + "typePersonal": "Personnel", + "typeGroup": "Groupe", + "groupLabel": "Groupe", + "groupSelectDefault": "Sélectionnez un groupe", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Ajoutez une description (facultatif)", + "frequencyLabel": "Fréquence", + "intervalLabel": "Intervalle (jours)", + "intervalPlaceholder": "ex: 10", + "dueDateLabel": "Date d'échéance", + "quickDueDateToday": "Aujourd'hui", + "quickDueDateTomorrow": "Demain", + "quickDueDateNextWeek": "Semaine prochaine", + "cancelButton": "Annuler", + "saveButton": "Enregistrer" + }, + "consoleErrors": { + "loadFailed": "Échec du chargement de toutes les tâches :", + "loadGroupsFailed": "Échec du chargement des groupes", + "createAssignmentForNewChoreFailed": "Échec de la création de l'affectation pour la nouvelle tâche :", + "saveFailed": "Échec de l'enregistrement de la tâche :", + "deleteFailed": "Échec de la suppression de la tâche :", + "createAssignmentFailed": "Échec de la création de l'affectation :", + "updateCompletionStatusFailed": "Échec de la mise à jour du statut d'achèvement de la tâche :" }, "deleteDialog": { - "title": "FR: Delete Chore", - "confirmationText": "FR: Are you sure you want to delete this chore? This action cannot be undone.", - "deleteButton": "FR: Delete" + "title": "Supprimer la tâche", + "confirmationText": "Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action est irréversible.", + "deleteButton": "Supprimer" }, "shortcutsModal": { - "title": "FR: Keyboard Shortcuts", - "descNewChore": "FR: New Chore", - "descToggleView": "FR: Toggle View (List/Calendar)", - "descToggleShortcuts": "FR: Show/Hide Shortcuts", - "descCloseModal": "FR: Close any open Modal/Dialog" + "title": "Raccourcis clavier", + "descNewChore": "Nouvelle tâche", + "descToggleView": "Basculer la vue (Liste/Calendrier)", + "descToggleShortcuts": "Afficher/Masquer les raccourcis", + "descCloseModal": "Fermer toute modale/dialogue ouvert" }, "frequencyOptions": { - "oneTime": "FR: One Time", - "daily": "FR: Daily", - "weekly": "FR: Weekly", - "monthly": "FR: Monthly", - "custom": "FR: Custom" + "oneTime": "Une fois", + "daily": "Quotidien", + "weekly": "Hebdomadaire", + "monthly": "Mensuel", + "custom": "Personnalisé" + }, + "frequency": { + "customInterval": "Tous les {n} jour | Tous les {n} jours" }, "formatters": { - "noDueDate": "FR: No due date", - "dueToday": "FR: Due Today", - "dueTomorrow": "FR: Due Tomorrow", - "overdueFull": "FR: Overdue: {date}", - "dueFull": "FR: Due {date}", - "invalidDate": "FR: Invalid Date" + "noDueDate": "Pas de date d'échéance", + "dueToday": "Pour aujourd'hui", + "dueTomorrow": "Pour demain", + "overdueFull": "En retard : {date}", + "dueFull": "Échéance le {date}", + "invalidDate": "Date invalide" }, "notifications": { - "loadFailed": "FR: Failed to load chores", - "updateSuccess": "FR: Chore '{name}' updated successfully", - "createSuccess": "FR: Chore '{name}' created successfully", - "updateFailed": "FR: Failed to update chore", - "createFailed": "FR: Failed to create chore", - "deleteSuccess": "FR: Chore '{name}' deleted successfully", - "deleteFailed": "FR: Failed to delete chore", - "markedDone": "FR: {name} marked as done.", - "markedNotDone": "FR: {name} marked as not done.", - "statusUpdateFailed": "FR: Failed to update chore status." + "loadFailed": "Échec du chargement des tâches.", + "loadGroupsFailed": "Échec du chargement des groupes.", + "updateSuccess": "Tâche '{name}' mise à jour avec succès.", + "createSuccess": "Tâche '{name}' créée avec succès.", + "updateFailed": "Échec de la mise à jour de la tâche.", + "createFailed": "Échec de la création de la tâche.", + "deleteSuccess": "Tâche '{name}' supprimée avec succès.", + "deleteFailed": "Échec de la suppression de la tâche.", + "markedDone": "{name} marquée comme terminée.", + "markedNotDone": "{name} marquée comme non terminée.", + "statusUpdateFailed": "Échec de la mise à jour du statut de la tâche.", + "createAssignmentFailed": "Échec de la création de l'affectation pour la tâche." }, "validation": { - "nameRequired": "FR: Chore name is required.", - "groupRequired": "FR: Please select a group for group chores.", - "intervalRequired": "FR: Custom interval must be at least 1 day.", - "dueDateRequired": "FR: Due date is required.", - "invalidDueDate": "FR: Invalid due date format." + "nameRequired": "Le nom de la tâche est requis.", + "groupRequired": "Veuillez sélectionner un groupe pour les tâches de groupe.", + "intervalRequired": "L'intervalle personnalisé doit être d'au moins 1 jour.", + "dueDateRequired": "La date d'échéance est requise.", + "invalidDueDate": "Format de date d'échéance invalide." }, - "unsavedChangesConfirmation": "FR: You have unsaved changes in the chore form. Are you sure you want to leave?" + "unsavedChangesConfirmation": "Vous avez des modifications non enregistrées dans le formulaire de tâche. Êtes-vous sûr de vouloir quitter ?" }, "errorNotFoundPage": { - "errorCode": "FR: 404", - "errorMessage": "FR: Oops. Nothing here...", - "goHomeButton": "FR: Go Home" + "errorCode": "404", + "errorMessage": "Oups. Rien ici...", + "goHomeButton": "Aller à l'accueil" }, "groupDetailPage": { - "loadingLabel": "FR: Loading group details...", - "retryButton": "FR: Retry", - "groupNotFound": "FR: Group not found or an error occurred.", + "loadingLabel": "Chargement des détails du groupe...", + "retryButton": "Réessayer", + "groupNotFound": "Groupe non trouvé ou une erreur s'est produite.", "members": { - "title": "FR: Group Members", - "defaultRole": "FR: Member", - "removeButton": "FR: Remove", - "emptyState": "FR: No members found." + "title": "Membres du groupe", + "defaultRole": "Membre", + "removeButton": "Retirer", + "emptyState": "Aucun membre trouvé.", + "closeMenuLabel": "Fermer le menu" }, "invites": { - "title": "FR: Invite Members", - "regenerateButton": "FR: Regenerate Invite Code", - "generateButton": "FR: Generate Invite Code", - "activeCodeLabel": "FR: Current Active Invite Code:", - "copyButtonLabel": "FR: Copy invite code", - "copySuccess": "FR: Invite code copied to clipboard!", - "emptyState": "FR: No active invite code. Click the button above to generate one.", + "title": "Inviter des membres", + "description": "Invitez de nouveaux membres en générant un code partageable.", + "addMemberButtonLabel": "Ajouter un membre", + "closeInviteLabel": "Fermer l'invitation", + "regenerateButton": "Régénérer le code d'invitation", + "generateButton": "Générer le code d'invitation", + "activeCodeLabel": "Code d'invitation actif actuel :", + "copyButtonLabel": "Copier le code d'invitation", + "copySuccess": "Code d'invitation copié dans le presse-papiers !", + "emptyState": "Aucun code d'invitation actif. Cliquez sur le bouton ci-dessus pour en générer un.", "errors": { - "newDataInvalid": "FR: New invite code data is invalid." + "newDataInvalid": "Les données du nouveau code d'invitation sont invalides." } }, + "errors": { + "failedToFetchActiveInvite": "Échec de la récupération du code d'invitation actif.", + "failedToFetchGroupDetails": "Échec de la récupération des détails du groupe.", + "failedToLoadUpcomingChores": "Erreur lors du chargement des tâches à venir :", + "failedToLoadRecentExpenses": "Erreur lors du chargement des dépenses récentes :" + }, + "console": { + "noActiveInvite": "Aucun code d'invitation actif trouvé pour ce groupe." + }, "chores": { - "title": "FR: Group Chores", - "manageButton": "FR: Manage Chores", - "duePrefix": "FR: Due:", - "emptyState": "FR: No chores scheduled. Click \"Manage Chores\" to create some!" + "title": "Tâches du groupe", + "manageButton": "Gérer les tâches", + "duePrefix": "Échéance :", + "emptyState": "Aucune tâche planifiée. Cliquez sur \"Gérer les tâches\" pour en créer !" }, "expenses": { - "title": "FR: Group Expenses", - "manageButton": "FR: Manage Expenses", - "emptyState": "FR: No expenses recorded. Click \"Manage Expenses\" to add some!", + "title": "Dépenses du groupe", + "manageButton": "Gérer les dépenses", + "emptyState": "Aucune dépense enregistrée. Cliquez sur \"Gérer les dépenses\" pour en ajouter !", + "paidBy": "Payé par :", + "owes": "doit", + "paidAmount": "Payé :", + "onDate": "le", + "settleShareButton": "Régler ma part", + "activityLabel": "Activité :", + "byUser": "par", + "fallbackUserName": "ID utilisateur : {userId}", + "activityByUserFallback": "Utilisateur {userId}", "splitTypes": { - "equal": "FR: Equal", - "exactAmounts": "FR: Exact Amounts", - "percentage": "FR: Percentage", - "shares": "FR: Shares", - "itemBased": "FR: Item Based" + "equal": "Égal", + "exactAmounts": "Montants exacts", + "percentage": "Pourcentage", + "shares": "Parts", + "itemBased": "Basé sur l'article" } }, "notifications": { - "fetchDetailsFailed": "FR: Failed to fetch group details.", - "fetchInviteFailed": "FR: Failed to fetch active invite code.", - "generateInviteSuccess": "FR: New invite code generated successfully!", - "generateInviteError": "FR: Failed to generate invite code.", - "clipboardNotSupported": "FR: Clipboard not supported or no code to copy.", - "copyInviteFailed": "FR: Failed to copy invite code.", - "removeMemberSuccess": "FR: Member removed successfully", - "removeMemberFailed": "FR: Failed to remove member" + "fetchDetailsFailed": "Échec de la récupération des détails du groupe.", + "fetchInviteFailed": "Échec de la récupération du code d'invitation actif.", + "generateInviteSuccess": "Nouveau code d'invitation généré avec succès !", + "generateInviteError": "Échec de la génération du code d'invitation.", + "clipboardNotSupported": "Presse-papiers non pris en charge ou aucun code à copier.", + "copyInviteFailed": "Échec de la copie du code d'invitation.", + "removeMemberSuccess": "Membre retiré avec succès", + "removeMemberFailed": "Échec du retrait du membre", + "loadExpensesFailed": "Échec du chargement des dépenses récentes.", + "cannotSettleOthersShares": "Vous ne pouvez régler que vos propres parts.", + "settlementDataMissing": "Impossible de traiter le règlement : données manquantes.", + "settleShareSuccess": "Part réglée avec succès !", + "settleShareFailed": "Échec du règlement de la part." + }, + "loading": { + "settlement": "Traitement du règlement..." + }, + "settleShareModal": { + "title": "Régler la part", + "settleAmountFor": "Régler le montant pour {userName} :", + "amountLabel": "Montant", + "cancelButton": "Annuler", + "confirmButton": "Confirmer", + "errors": { + "enterAmount": "Veuillez entrer un montant.", + "positiveAmount": "Veuillez entrer un montant positif.", + "exceedsRemaining": "Le montant ne peut pas dépasser le reste : {amount}.", + "noSplitSelected": "Erreur : Aucune répartition sélectionnée." + } + }, + "status": { + "settled": "Réglé", + "partiallySettled": "Partiellement réglé", + "unsettled": "Non réglé", + "paid": "Payé", + "partiallyPaid": "Partiellement payé", + "unpaid": "Non payé", + "unknown": "Statut inconnu" } }, "accountPage": { - "title": "Account Settings", - "loadingProfile": "Loading profile...", - "retryButton": "Retry", + "title": "Paramètres du compte", + "loadingProfile": "Chargement du profil...", + "retryButton": "Réessayer", "profileSection": { - "header": "Profile Information", - "nameLabel": "Name", - "emailLabel": "Email", - "saveButton": "Save Changes" + "header": "Informations du profil", + "nameLabel": "Nom", + "emailLabel": "E-mail", + "saveButton": "Enregistrer les modifications" }, "passwordSection": { - "header": "Change Password", - "currentPasswordLabel": "Current Password", - "newPasswordLabel": "New Password", - "changeButton": "Change Password" + "header": "Changer le mot de passe", + "currentPasswordLabel": "Mot de passe actuel", + "newPasswordLabel": "Nouveau mot de passe", + "changeButton": "Changer le mot de passe" }, "notificationsSection": { - "header": "Notification Preferences", - "emailNotificationsLabel": "Email Notifications", - "emailNotificationsDescription": "Receive email notifications for important updates", - "listUpdatesLabel": "List Updates", - "listUpdatesDescription": "Get notified when lists are updated", - "groupActivitiesLabel": "Group Activities", - "groupActivitiesDescription": "Receive notifications for group activities" + "header": "Préférences de notification", + "emailNotificationsLabel": "Notifications par e-mail", + "emailNotificationsDescription": "Recevoir des notifications par e-mail pour les mises à jour importantes", + "listUpdatesLabel": "Mises à jour des listes", + "listUpdatesDescription": "Être notifié lorsque les listes sont mises à jour", + "groupActivitiesLabel": "Activités de groupe", + "groupActivitiesDescription": "Recevoir des notifications pour les activités de groupe" }, "notifications": { - "profileLoadFailed": "Failed to load profile", - "profileUpdateSuccess": "Profile updated successfully", - "profileUpdateFailed": "Failed to update profile", - "passwordFieldsRequired": "Please fill in both current and new password fields.", - "passwordTooShort": "New password must be at least 8 characters long.", - "passwordChangeSuccess": "Password changed successfully", - "passwordChangeFailed": "Failed to change password", - "preferencesUpdateSuccess": "Preferences updated successfully", - "preferencesUpdateFailed": "Failed to update preferences" + "profileLoadFailed": "Échec du chargement du profil", + "profileUpdateSuccess": "Profil mis à jour avec succès", + "profileUpdateFailed": "Échec de la mise à jour du profil", + "passwordFieldsRequired": "Veuillez remplir les champs du mot de passe actuel et du nouveau mot de passe.", + "passwordTooShort": "Le nouveau mot de passe doit comporter au moins 8 caractères.", + "passwordChangeSuccess": "Mot de passe changé avec succès", + "passwordChangeFailed": "Échec du changement de mot de passe", + "preferencesUpdateSuccess": "Préférences mises à jour avec succès", + "preferencesUpdateFailed": "Échec de la mise à jour des préférences" }, - "saving": "Saving..." + "saving": "Enregistrement..." }, "signupPage": { - "header": "Sign Up", - "fullNameLabel": "Full Name", - "emailLabel": "Email", - "passwordLabel": "Password", - "confirmPasswordLabel": "Confirm Password", - "togglePasswordVisibility": "Toggle password visibility", - "submitButton": "Sign Up", - "loginLink": "Already have an account? Login", + "header": "S'inscrire", + "fullNameLabel": "Nom complet", + "emailLabel": "E-mail", + "passwordLabel": "Mot de passe", + "confirmPasswordLabel": "Confirmer le mot de passe", + "togglePasswordVisibility": "Basculer la visibilité du mot de passe", + "submitButton": "S'inscrire", + "loginLink": "Vous avez déjà un compte ? Connexion", "validation": { - "nameRequired": "Name is required", - "emailRequired": "Email is required", - "emailInvalid": "Invalid email format", - "passwordRequired": "Password is required", - "passwordLength": "Password must be at least 8 characters", - "confirmPasswordRequired": "Please confirm your password", - "passwordsNoMatch": "Passwords do not match" + "nameRequired": "Le nom est requis", + "emailRequired": "L'e-mail est requis", + "emailInvalid": "Format d'e-mail invalide", + "passwordRequired": "Le mot de passe est requis", + "passwordLength": "Le mot de passe doit comporter au moins 8 caractères", + "confirmPasswordRequired": "Veuillez confirmer votre mot de passe", + "passwordsNoMatch": "Les mots de passe ne correspondent pas" }, "notifications": { - "signupFailed": "Signup failed. Please try again.", - "signupSuccess": "Account created successfully. Please login." + "signupFailed": "Échec de l'inscription. Veuillez réessayer.", + "signupSuccess": "Compte créé avec succès. Veuillez vous connecter." } }, "listDetailPage": { "loading": { - "list": "Loading list...", - "items": "Loading items...", - "ocrProcessing": "Processing image...", - "addingOcrItems": "Adding OCR items...", - "costSummary": "Loading summary...", - "expenses": "Loading expenses...", - "settlement": "Processing settlement..." + "list": "Chargement de la liste...", + "items": "Chargement des articles...", + "ocrProcessing": "Traitement de l'image...", + "addingOcrItems": "Ajout des articles OCR...", + "costSummary": "Chargement du résumé...", + "expenses": "Chargement des dépenses...", + "settlement": "Traitement du règlement..." }, "errors": { - "fetchFailed": "Failed to load list details.", - "genericLoadFailure": "Group not found or an error occurred.", - "ocrNoItems": "No items extracted from the image.", - "ocrFailed": "Failed to process image.", - "addItemFailed": "Failed to add item.", - "updateItemFailed": "Failed to update item.", - "updateItemPriceFailed": "Failed to update item price.", - "deleteItemFailed": "Failed to delete item.", - "addOcrItemsFailed": "Failed to add OCR items.", - "fetchItemsFailed": "Failed to load items: {errorMessage}", - "loadCostSummaryFailed": "Failed to load cost summary." + "fetchFailed": "Échec du chargement des détails de la liste.", + "genericLoadFailure": "Groupe non trouvé ou une erreur s'est produite.", + "ocrNoItems": "Aucun article extrait de l'image.", + "ocrFailed": "Échec du traitement de l'image.", + "addItemFailed": "Échec de l'ajout de l'article.", + "updateItemFailed": "Échec de la mise à jour de l'article.", + "updateItemPriceFailed": "Échec de la mise à jour du prix de l'article.", + "deleteItemFailed": "Échec de la suppression de l'article.", + "addOcrItemsFailed": "Échec de l'ajout des articles OCR.", + "fetchItemsFailed": "Échec du chargement des articles : {errorMessage}", + "loadCostSummaryFailed": "Échec du chargement du résumé des coûts." }, - "retryButton": "Retry", + "retryButton": "Réessayer", "buttons": { - "addViaOcr": "Add via OCR", - "addItem": "Add", - "addItems": "Add Items", - "cancel": "Cancel", - "confirm": "Confirm", - "saveChanges": "Save Changes", - "close": "Close", - "costSummary": "Cost Summary" + "addViaOcr": "Ajouter via OCR", + "addItem": "Ajouter", + "addItems": "Ajouter des articles", + "cancel": "Annuler", + "confirm": "Confirmer", + "saveChanges": "Enregistrer les modifications", + "close": "Fermer", + "costSummary": "Résumé des coûts" }, "badges": { - "groupList": "Group List", - "personalList": "Personal List" + "groupList": "Liste de groupe", + "personalList": "Liste personnelle" }, "items": { "emptyState": { - "title": "No Items Yet!", - "message": "Add some items using the form below." + "title": "Pas encore d'articles !", + "message": "Ajoutez des articles en utilisant le formulaire ci-dessous." }, "addItemForm": { - "placeholder": "Add a new item", - "quantityPlaceholder": "Qty", - "itemNameSrLabel": "New item name", - "quantitySrLabel": "Quantity" + "placeholder": "Ajouter un nouvel article", + "quantityPlaceholder": "Qté", + "itemNameSrLabel": "Nom du nouvel article", + "quantitySrLabel": "Quantité" }, - "pricePlaceholder": "Price", - "editItemAriaLabel": "Edit item", - "deleteItemAriaLabel": "Delete item" + "pricePlaceholder": "Prix", + "editItemAriaLabel": "Modifier l'article", + "deleteItemAriaLabel": "Supprimer l'article" }, "modals": { "ocr": { - "title": "Add Items via OCR", - "uploadLabel": "Upload Image" + "title": "Ajouter des articles via OCR", + "uploadLabel": "Télécharger une image" }, "confirmation": { "title": "Confirmation" }, "editItem": { - "title": "Edit Item", - "nameLabel": "Item Name", - "quantityLabel": "Quantity" + "title": "Modifier l'article", + "nameLabel": "Nom de l'article", + "quantityLabel": "Quantité" }, "costSummary": { - "title": "List Cost Summary", - "totalCostLabel": "Total List Cost:", - "equalShareLabel": "Equal Share Per User:", - "participantsLabel": "Participating Users:", - "userBalancesHeader": "User Balances", + "title": "Résumé des coûts de la liste", + "totalCostLabel": "Coût total de la liste :", + "equalShareLabel": "Part égale par utilisateur :", + "participantsLabel": "Utilisateurs participants :", + "userBalancesHeader": "Soldes des utilisateurs", "tableHeaders": { - "user": "User", - "itemsAddedValue": "Items Added Value", - "amountDue": "Amount Due", - "balance": "Balance" + "user": "Utilisateur", + "itemsAddedValue": "Valeur des articles ajoutés", + "amountDue": "Montant dû", + "balance": "Solde" }, - "emptyState": "No cost summary available." + "emptyState": "Aucun résumé des coûts disponible." }, "settleShare": { - "title": "Settle Share", - "settleAmountFor": "Settle amount for {userName}:", - "amountLabel": "Amount", + "title": "Régler la part", + "settleAmountFor": "Régler le montant pour {userName} :", + "amountLabel": "Montant", "errors": { - "enterAmount": "Please enter an amount.", - "positiveAmount": "Please enter a positive amount.", - "exceedsRemaining": "Amount cannot exceed remaining: {amount}.", - "noSplitSelected": "Error: No split selected." + "enterAmount": "Veuillez entrer un montant.", + "positiveAmount": "Veuillez entrer un montant positif.", + "exceedsRemaining": "Le montant ne peut pas dépasser le reste : {amount}.", + "noSplitSelected": "Erreur : Aucune répartition sélectionnée." } } }, "confirmations": { - "updateMessage": "Mark '{itemName}' as {status}?", - "statusComplete": "complete", - "statusIncomplete": "incomplete", - "deleteMessage": "Delete '{itemName}'? This cannot be undone." + "updateMessage": "Marquer '{itemName}' comme {status} ?", + "statusComplete": "terminé", + "statusIncomplete": "incomplet", + "deleteMessage": "Supprimer '{itemName}' ? Ceci ne peut pas être annulé." }, "notifications": { - "itemAddedSuccess": "Item added successfully.", - "itemsAddedSuccessOcr": "{count} item(s) added successfully from OCR.", - "itemUpdatedSuccess": "Item updated successfully.", - "itemDeleteSuccess": "Item deleted successfully.", - "enterItemName": "Please enter an item name.", - "costSummaryLoadFailed": "Failed to load cost summary.", - "cannotSettleOthersShares": "You can only settle your own shares.", - "settlementDataMissing": "Cannot process settlement: missing data.", - "settleShareSuccess": "Share settled successfully!", - "settleShareFailed": "Failed to settle share." + "itemAddedSuccess": "Article ajouté avec succès.", + "itemsAddedSuccessOcr": "{count} article(s) ajouté(s) avec succès depuis l'OCR.", + "itemUpdatedSuccess": "Article mis à jour avec succès.", + "itemDeleteSuccess": "Article supprimé avec succès.", + "enterItemName": "Veuillez entrer un nom d'article.", + "costSummaryLoadFailed": "Échec du chargement du résumé des coûts.", + "cannotSettleOthersShares": "Vous ne pouvez régler que vos propres parts.", + "settlementDataMissing": "Impossible de traiter le règlement : données manquantes.", + "settleShareSuccess": "Part réglée avec succès !", + "settleShareFailed": "Échec du règlement de la part." }, "expensesSection": { - "title": "Expenses", - "addExpenseButton": "Add Expense", - "loading": "Loading expenses...", - "emptyState": "No expenses recorded for this list yet.", - "paidBy": "Paid by:", - "onDate": "on", - "owes": "owes", - "paidAmount": "Paid:", - "activityLabel": "Activity:", - "byUser": "by", - "settleShareButton": "Settle My Share", - "retryButton": "Retry" + "title": "Dépenses", + "addExpenseButton": "Ajouter une dépense", + "loading": "Chargement des dépenses...", + "emptyState": "Aucune dépense enregistrée pour cette liste pour le moment.", + "paidBy": "Payé par :", + "onDate": "le", + "owes": "doit", + "paidAmount": "Payé :", + "activityLabel": "Activité :", + "byUser": "par", + "settleShareButton": "Régler ma part", + "retryButton": "Réessayer" }, "status": { - "settled": "Settled", - "partiallySettled": "Partially Settled", - "unsettled": "Unsettled", - "paid": "Paid", - "partiallyPaid": "Partially Paid", - "unpaid": "Unpaid", - "unknown": "Unknown Status" + "settled": "Réglé", + "partiallySettled": "Partiellement réglé", + "unsettled": "Non réglé", + "paid": "Payé", + "partiallyPaid": "Partiellement payé", + "unpaid": "Non payé", + "unknown": "Statut inconnu" } }, "myChoresPage": { - "title": "My Assigned Chores", - "showCompletedToggle": "Show Completed", + "title": "Mes tâches assignées", + "showCompletedToggle": "Afficher les tâches terminées", "timelineHeaders": { - "overdue": "Overdue", - "today": "Due Today", - "thisWeek": "This Week", - "later": "Later", - "completed": "Completed" + "overdue": "En retard", + "today": "Pour aujourd'hui", + "thisWeek": "Cette semaine", + "later": "Plus tard", + "completed": "Terminées" }, "choreCard": { - "personal": "Personal", - "group": "Group", - "duePrefix": "Due", - "completedPrefix": "Completed", - "dueToday": "Due Today", - "markCompleteButton": "Mark Complete" + "personal": "Personnel", + "group": "Groupe", + "duePrefix": "Échéance", + "completedPrefix": "Terminée", + "dueToday": "Pour aujourd'hui", + "markCompleteButton": "Marquer comme terminée" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Une fois", + "daily": "Quotidien", + "weekly": "Hebdomadaire", + "monthly": "Mensuel", + "custom": "Personnalisé", + "unknown": "Fréquence inconnue" }, "dates": { - "invalidDate": "Invalid Date", - "unknownDate": "Unknown Date" + "invalidDate": "Date invalide", + "unknownDate": "Date inconnue" }, "emptyState": { - "title": "No Assignments Yet!", - "noAssignmentsPending": "You have no pending chore assignments.", - "noAssignmentsAll": "You have no chore assignments (completed or pending).", - "viewAllChoresButton": "View All Chores" + "title": "Pas encore d'affectations !", + "noAssignmentsPending": "Vous n'avez aucune affectation de tâche en attente.", + "noAssignmentsAll": "Vous n'avez aucune affectation de tâche (terminée ou en attente).", + "viewAllChoresButton": "Voir toutes les tâches" }, "notifications": { - "loadFailed": "Failed to load assignments", - "markedComplete": "Marked \"{choreName}\" as complete!", - "markCompleteFailed": "Failed to mark assignment as complete" + "loadFailed": "Échec du chargement des affectations", + "markedComplete": "\"{choreName}\" marquée comme terminée !", + "markCompleteFailed": "Échec du marquage de l'affectation comme terminée" } }, "personalChoresPage": { - "title": "Personal Chores", - "newChoreButton": "New Chore", - "editButton": "Edit", - "deleteButton": "Delete", - "cancelButton": "Cancel", - "saveButton": "Save", + "title": "Tâches personnelles", + "newChoreButton": "Nouvelle tâche", + "editButton": "Modifier", + "deleteButton": "Supprimer", + "cancelButton": "Annuler", + "saveButton": "Enregistrer", "modals": { - "editChoreTitle": "Edit Chore", - "newChoreTitle": "New Chore", - "deleteChoreTitle": "Delete Chore" + "editChoreTitle": "Modifier la tâche", + "newChoreTitle": "Nouvelle tâche", + "deleteChoreTitle": "Supprimer la tâche" }, "form": { - "nameLabel": "Name", + "nameLabel": "Nom", "descriptionLabel": "Description", - "frequencyLabel": "Frequency", - "intervalLabel": "Interval (days)", - "dueDateLabel": "Next Due Date" + "frequencyLabel": "Fréquence", + "intervalLabel": "Intervalle (jours)", + "dueDateLabel": "Prochaine date d'échéance" }, "deleteDialog": { - "confirmationText": "Are you sure you want to delete this chore?" + "confirmationText": "Êtes-vous sûr de vouloir supprimer cette tâche ?" }, "frequencies": { - "one_time": "One Time", - "daily": "Daily", - "weekly": "Weekly", - "monthly": "Monthly", - "custom": "Custom", - "unknown": "Unknown Frequency" + "one_time": "Une fois", + "daily": "Quotidien", + "weekly": "Hebdomadaire", + "monthly": "Mensuel", + "custom": "Personnalisé", + "unknown": "Fréquence inconnue" }, "dates": { - "invalidDate": "Invalid Date", - "duePrefix": "Due" + "invalidDate": "Date invalide", + "duePrefix": "Échéance" }, "notifications": { - "loadFailed": "Failed to load personal chores", - "updateSuccess": "Personal chore updated successfully", - "createSuccess": "Personal chore created successfully", - "saveFailed": "Failed to save personal chore", - "deleteSuccess": "Personal chore deleted successfully", - "deleteFailed": "Failed to delete personal chore" + "loadFailed": "Échec du chargement des tâches personnelles", + "updateSuccess": "Tâche personnelle mise à jour avec succès", + "createSuccess": "Tâche personnelle créée avec succès", + "saveFailed": "Échec de l'enregistrement de la tâche personnelle", + "deleteSuccess": "Tâche personnelle supprimée avec succès", + "deleteFailed": "Échec de la suppression de la tâche personnelle" } }, "indexPage": { - "welcomeMessage": "Welcome to Valerie UI App", - "mainPageInfo": "This is the main index page.", - "sampleTodosHeader": "Sample Todos (from IndexPage data)", - "totalCountLabel": "Total count from meta:", - "noTodos": "No todos to display." + "welcomeMessage": "Bienvenue sur l'application Valerie UI", + "mainPageInfo": "Ceci est la page d'index principale.", + "sampleTodosHeader": "Exemples de tâches (depuis les données IndexPage)", + "totalCountLabel": "Nombre total depuis meta :", + "noTodos": "Aucune tâche à afficher." + }, + "languageSelector": { + "title": "Langue", + "languages": { + "en": "English", + "de": "Deutsch", + "nl": "Nederlands", + "fr": "Français", + "es": "Español" + } } -} +} \ No newline at end of file diff --git a/fe/src/i18n/index.ts b/fe/src/i18n/index.ts index cf5cbc9..fc030cf 100644 --- a/fe/src/i18n/index.ts +++ b/fe/src/i18n/index.ts @@ -1,11 +1,13 @@ import en from './en.json'; import de from './de.json'; +import nl from './nl.json'; import fr from './fr.json'; import es from './es.json'; export default { en, de, + nl, fr, es }; diff --git a/fe/src/i18n/nl.json b/fe/src/i18n/nl.json new file mode 100644 index 0000000..ce065d1 --- /dev/null +++ b/fe/src/i18n/nl.json @@ -0,0 +1,641 @@ +{ + "message": { + "hello": "Hallo" + }, + "loginPage": { + "emailLabel": "E-mail", + "passwordLabel": "Wachtwoord", + "togglePasswordVisibilityLabel": "Wachtwoord zichtbaarheid wisselen", + "loginButton": "Inloggen", + "signupLink": "Geen account? Aanmelden", + "errors": { + "emailRequired": "E-mail is vereist", + "emailInvalid": "Ongeldig e-mailformaat", + "passwordRequired": "Wachtwoord is vereist", + "loginFailed": "Inloggen mislukt. Controleer uw gegevens." + }, + "notifications": { + "loginSuccess": "Succesvol ingelogd" + } + }, + "listsPage": { + "retryButton": "Opnieuw proberen", + "emptyState": { + "noListsForGroup": "Geen lijsten gevonden voor deze groep.", + "noListsYet": "U heeft nog geen lijsten.", + "personalGlobalInfo": "Maak een persoonlijke lijst of word lid van een groep om gedeelde lijsten te zien.", + "groupSpecificInfo": "Deze groep heeft nog geen lijsten." + }, + "createNewListButton": "Nieuwe lijst maken", + "loadingLists": "Lijsten laden...", + "noDescription": "Geen beschrijving", + "addItemPlaceholder": "Nieuw item toevoegen...", + "createCard": { + "title": "+ Lijst" + }, + "pageTitle": { + "forGroup": "Lijsten voor {groupName}", + "forGroupId": "Lijsten voor Groep {groupId}", + "myLists": "Mijn Lijsten" + }, + "errors": { + "fetchFailed": "Ophalen van lijsten mislukt." + } + }, + "groupsPage": { + "retryButton": "Opnieuw proberen", + "emptyState": { + "title": "Nog geen groepen!", + "description": "U bent nog geen lid van groepen. Maak er een aan of word lid met een uitnodigingscode.", + "createButton": "Nieuwe groep maken" + }, + "groupCard": { + "newListButton": "Lijst" + }, + "createCard": { + "title": "+ Groep" + }, + "joinGroup": { + "title": "Lid worden van een groep met uitnodigingscode", + "inputLabel": "Voer uitnodigingscode in", + "inputPlaceholder": "Voer uitnodigingscode in", + "joinButton": "Deelnemen" + }, + "createDialog": { + "title": "Nieuwe groep maken", + "closeButtonLabel": "Sluiten", + "groupNameLabel": "Groepsnaam", + "cancelButton": "Annuleren", + "createButton": "Maken" + }, + "errors": { + "fetchFailed": "Laden van groepen mislukt", + "groupNameRequired": "Groepsnaam is vereist", + "createFailed": "Maken van groep mislukt. Probeer het opnieuw.", + "inviteCodeRequired": "Uitnodigingscode is vereist", + "joinFailed": "Deelnemen aan groep mislukt. Controleer de uitnodigingscode en probeer het opnieuw.", + "invalidDataFromServer": "Ongeldige gegevens ontvangen van server.", + "createFailedConsole": "Fout bij het maken van groep:", + "joinFailedConsole": "Fout bij het deelnemen aan groep:" + }, + "notifications": { + "groupCreatedSuccess": "Groep '{groupName}' succesvol aangemaakt.", + "joinSuccessNamed": "Succesvol lid geworden van groep '{groupName}'.", + "joinSuccessGeneric": "Succesvol lid geworden van groep.", + "listCreatedSuccess": "Lijst '{listName}' succesvol aangemaakt." + } + }, + "authCallbackPage": { + "redirecting": "Bezig met omleiden...", + "errors": { + "authenticationFailed": "Authenticatie mislukt", + "noTokenProvided": "Geen token opgegeven" + } + }, + "choresPage": { + "title": "Taken", + "tabs": { + "overdue": "Achterstallig", + "today": "Vandaag", + "upcoming": "Aankomend", + "allPending": "Alle openstaande", + "completed": "Voltooid" + }, + "viewToggle": { + "calendarLabel": "Kalenderweergave", + "calendarText": "Kalender", + "listLabel": "Lijstweergave", + "listText": "Lijst" + }, + "newChoreButtonLabel": "Nieuwe taak", + "newChoreButtonText": "Nieuwe taak", + "loadingState": { + "loadingChores": "Taken laden..." + }, + "calendar": { + "prevMonthLabel": "Vorige maand", + "nextMonthLabel": "Volgende maand", + "weekdays": { + "sun": "Zo", + "mon": "Ma", + "tue": "Di", + "wed": "Wo", + "thu": "Do", + "fri": "Vr", + "sat": "Za" + }, + "addChoreToDayLabel": "Taak aan deze dag toevoegen", + "emptyState": "Geen taken om weer te geven voor deze periode." + }, + "listView": { + "choreTypePersonal": "Persoonlijk", + "choreTypeGroupFallback": "Groep", + "completedDatePrefix": "Voltooid:", + "actions": { + "doneTitle": "Markeer als voltooid", + "doneText": "Gedaan", + "undoTitle": "Markeer als niet voltooid", + "undoText": "Ongedaan maken", + "editTitle": "Bewerken", + "editLabel": "Taak bewerken", + "editText": "Bewerken", + "deleteTitle": "Verwijderen", + "deleteLabel": "Taak verwijderen", + "deleteText": "Verwijderen" + }, + "emptyState": { + "message": "Geen taken in deze weergave. Goed gedaan!", + "viewAllButton": "Alle openstaande bekijken" + } + }, + "choreModal": { + "editTitle": "Taak bewerken", + "newTitle": "Nieuwe taak", + "closeButtonLabel": "Modal sluiten", + "nameLabel": "Naam", + "namePlaceholder": "Voer taaknaam in", + "typeLabel": "Type", + "typePersonal": "Persoonlijk", + "typeGroup": "Groep", + "groupLabel": "Groep", + "groupSelectDefault": "Selecteer een groep", + "descriptionLabel": "Beschrijving", + "descriptionPlaceholder": "Voeg een beschrijving toe (optioneel)", + "frequencyLabel": "Frequentie", + "intervalLabel": "Interval (dagen)", + "intervalPlaceholder": "bijv. 10", + "dueDateLabel": "Vervaldatum", + "quickDueDateToday": "Vandaag", + "quickDueDateTomorrow": "Morgen", + "quickDueDateNextWeek": "Volgende week", + "cancelButton": "Annuleren", + "saveButton": "Opslaan" + }, + "consoleErrors": { + "loadFailed": "Laden van alle taken mislukt:", + "loadGroupsFailed": "Laden van groepen mislukt", + "createAssignmentForNewChoreFailed": "Toewijzing voor nieuwe taak kon niet worden gemaakt:", + "saveFailed": "Opslaan van taak mislukt:", + "deleteFailed": "Verwijderen van taak mislukt:", + "createAssignmentFailed": "Toewijzing kon niet worden gemaakt:", + "updateCompletionStatusFailed": "Voltooiingsstatus van taak kon niet worden bijgewerkt:" + }, + "deleteDialog": { + "title": "Taak verwijderen", + "confirmationText": "Weet u zeker dat u deze taak wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.", + "deleteButton": "Verwijderen" + }, + "shortcutsModal": { + "title": "Sneltoetsen", + "descNewChore": "Nieuwe taak", + "descToggleView": "Weergave wisselen (Lijst/Kalender)", + "descToggleShortcuts": "Sneltoetsen tonen/verbergen", + "descCloseModal": "Open Modal/Dialoog sluiten" + }, + "frequencyOptions": { + "oneTime": "Eenmalig", + "daily": "Dagelijks", + "weekly": "Wekelijks", + "monthly": "Maandelijks", + "custom": "Aangepast" + }, + "frequency": { + "customInterval": "Elke {n} dag | Elke {n} dagen" + }, + "formatters": { + "noDueDate": "Geen vervaldatum", + "dueToday": "Vandaag te doen", + "dueTomorrow": "Morgen te doen", + "overdueFull": "Achterstallig: {date}", + "dueFull": "Vervalt op {date}", + "invalidDate": "Ongeldige datum" + }, + "notifications": { + "loadFailed": "Laden van taken mislukt.", + "loadGroupsFailed": "Laden van groepen mislukt.", + "updateSuccess": "Taak '{name}' succesvol bijgewerkt.", + "createSuccess": "Taak '{name}' succesvol aangemaakt.", + "updateFailed": "Bijwerken van taak mislukt.", + "createFailed": "Aanmaken van taak mislukt.", + "deleteSuccess": "Taak '{name}' succesvol verwijderd.", + "deleteFailed": "Verwijderen van taak mislukt.", + "markedDone": "{name} gemarkeerd als voltooid.", + "markedNotDone": "{name} gemarkeerd als niet voltooid.", + "statusUpdateFailed": "Status van taak kon niet worden bijgewerkt.", + "createAssignmentFailed": "Toewijzing voor taak kon niet worden gemaakt." + }, + "validation": { + "nameRequired": "Taaknaam is vereist.", + "groupRequired": "Selecteer een groep voor groepstaken.", + "intervalRequired": "Aangepast interval moet minimaal 1 dag zijn.", + "dueDateRequired": "Vervaldatum is vereist.", + "invalidDueDate": "Ongeldig formaat vervaldatum." + }, + "unsavedChangesConfirmation": "U heeft niet-opgeslagen wijzigingen in het taakformulier. Weet u zeker dat u wilt vertrekken?" + }, + "errorNotFoundPage": { + "errorCode": "404", + "errorMessage": "Oeps. Hier is niets...", + "goHomeButton": "Naar de startpagina" + }, + "groupDetailPage": { + "loadingLabel": "Groepsdetails laden...", + "retryButton": "Opnieuw proberen", + "groupNotFound": "Groep niet gevonden of er is een fout opgetreden.", + "members": { + "title": "Groepsleden", + "defaultRole": "Lid", + "removeButton": "Verwijderen", + "emptyState": "Geen leden gevonden.", + "closeMenuLabel": "Menu sluiten" + }, + "invites": { + "title": "Leden uitnodigen", + "description": "Nodig nieuwe leden uit door een deelbare code te genereren.", + "addMemberButtonLabel": "Lid toevoegen", + "closeInviteLabel": "Uitnodiging sluiten", + "regenerateButton": "Uitnodigingscode opnieuw genereren", + "generateButton": "Uitnodigingscode genereren", + "activeCodeLabel": "Huidige actieve uitnodigingscode:", + "copyButtonLabel": "Kopieer uitnodigingscode", + "copySuccess": "Uitnodigingscode gekopieerd naar klembord!", + "emptyState": "Geen actieve uitnodigingscode. Klik op de knop hierboven om er een te genereren.", + "errors": { + "newDataInvalid": "Gegevens van nieuwe uitnodigingscode zijn ongeldig." + } + }, + "errors": { + "failedToFetchActiveInvite": "Ophalen van actieve uitnodigingscode mislukt.", + "failedToFetchGroupDetails": "Ophalen van groepsdetails mislukt.", + "failedToLoadUpcomingChores": "Fout bij het laden van aankomende taken:", + "failedToLoadRecentExpenses": "Fout bij het laden van recente uitgaven:" + }, + "console": { + "noActiveInvite": "Geen actieve uitnodigingscode gevonden voor deze groep." + }, + "chores": { + "title": "Groepstaken", + "manageButton": "Taken beheren", + "duePrefix": "Vervalt:", + "emptyState": "Geen taken gepland. Klik op \"Taken beheren\" om er enkele aan te maken!" + }, + "expenses": { + "title": "Groepsuitgaven", + "manageButton": "Uitgaven beheren", + "emptyState": "Geen uitgaven geregistreerd. Klik op \"Uitgaven beheren\" om er enkele toe te voegen!", + "paidBy": "Betaald door:", + "owes": "is verschuldigd", + "paidAmount": "Betaald:", + "onDate": "op", + "settleShareButton": "Mijn deel vereffenen", + "activityLabel": "Activiteit:", + "byUser": "door", + "fallbackUserName": "Gebruikers-ID: {userId}", + "activityByUserFallback": "Gebruiker {userId}", + "splitTypes": { + "equal": "Gelijk", + "exactAmounts": "Exacte bedragen", + "percentage": "Percentage", + "shares": "Aandelen", + "itemBased": "Op item gebaseerd" + } + }, + "notifications": { + "fetchDetailsFailed": "Ophalen van groepsdetails mislukt.", + "fetchInviteFailed": "Ophalen van actieve uitnodigingscode mislukt.", + "generateInviteSuccess": "Nieuwe uitnodigingscode succesvol gegenereerd!", + "generateInviteError": "Genereren van uitnodigingscode mislukt.", + "clipboardNotSupported": "Klembord niet ondersteund of geen code om te kopiëren.", + "copyInviteFailed": "Kopiëren van uitnodigingscode mislukt.", + "removeMemberSuccess": "Lid succesvol verwijderd", + "removeMemberFailed": "Verwijderen van lid mislukt", + "loadExpensesFailed": "Laden van recente uitgaven mislukt.", + "cannotSettleOthersShares": "U kunt alleen uw eigen aandelen vereffenen.", + "settlementDataMissing": "Kan vereffening niet verwerken: gegevens ontbreken.", + "settleShareSuccess": "Aandeel succesvol vereffend!", + "settleShareFailed": "Vereffenen van aandeel mislukt." + }, + "loading": { + "settlement": "Bezig met vereffenen..." + }, + "settleShareModal": { + "title": "Aandeel vereffenen", + "settleAmountFor": "Bedrag vereffenen voor {userName}:", + "amountLabel": "Bedrag", + "cancelButton": "Annuleren", + "confirmButton": "Bevestigen", + "errors": { + "enterAmount": "Voer een bedrag in.", + "positiveAmount": "Voer een positief bedrag in.", + "exceedsRemaining": "Bedrag mag resterend bedrag niet overschrijden: {amount}.", + "noSplitSelected": "Fout: Geen verdeling geselecteerd." + } + }, + "status": { + "settled": "Vereffend", + "partiallySettled": "Gedeeltelijk vereffend", + "unsettled": "Openstaand", + "paid": "Betaald", + "partiallyPaid": "Gedeeltelijk betaald", + "unpaid": "Onbetaald", + "unknown": "Onbekende status" + } + }, + "accountPage": { + "title": "Accountinstellingen", + "loadingProfile": "Profiel laden...", + "retryButton": "Opnieuw proberen", + "profileSection": { + "header": "Profielinformatie", + "nameLabel": "Naam", + "emailLabel": "E-mail", + "saveButton": "Wijzigingen opslaan" + }, + "passwordSection": { + "header": "Wachtwoord wijzigen", + "currentPasswordLabel": "Huidig wachtwoord", + "newPasswordLabel": "Nieuw wachtwoord", + "changeButton": "Wachtwoord wijzigen" + }, + "notificationsSection": { + "header": "Notificatievoorkeuren", + "emailNotificationsLabel": "E-mailnotificaties", + "emailNotificationsDescription": "Ontvang e-mailnotificaties voor belangrijke updates", + "listUpdatesLabel": "Lijstupdates", + "listUpdatesDescription": "Ontvang een melding wanneer lijsten worden bijgewerkt", + "groupActivitiesLabel": "Groepsactiviteiten", + "groupActivitiesDescription": "Ontvang meldingen voor groepsactiviteiten" + }, + "notifications": { + "profileLoadFailed": "Laden van profiel mislukt", + "profileUpdateSuccess": "Profiel succesvol bijgewerkt", + "profileUpdateFailed": "Bijwerken van profiel mislukt", + "passwordFieldsRequired": "Vul zowel het huidige als het nieuwe wachtwoordveld in.", + "passwordTooShort": "Nieuw wachtwoord moet minimaal 8 tekens lang zijn.", + "passwordChangeSuccess": "Wachtwoord succesvol gewijzigd", + "passwordChangeFailed": "Wijzigen van wachtwoord mislukt", + "preferencesUpdateSuccess": "Voorkeuren succesvol bijgewerkt", + "preferencesUpdateFailed": "Bijwerken van voorkeuren mislukt" + }, + "saving": "Opslaan..." + }, + "signupPage": { + "header": "Aanmelden", + "fullNameLabel": "Volledige naam", + "emailLabel": "E-mail", + "passwordLabel": "Wachtwoord", + "confirmPasswordLabel": "Bevestig wachtwoord", + "togglePasswordVisibility": "Wachtwoord zichtbaarheid wisselen", + "submitButton": "Aanmelden", + "loginLink": "Heeft u al een account? Inloggen", + "validation": { + "nameRequired": "Naam is vereist", + "emailRequired": "E-mail is vereist", + "emailInvalid": "Ongeldig e-mailformaat", + "passwordRequired": "Wachtwoord is vereist", + "passwordLength": "Wachtwoord moet minimaal 8 tekens lang zijn", + "confirmPasswordRequired": "Bevestig uw wachtwoord", + "passwordsNoMatch": "Wachtwoorden komen niet overeen" + }, + "notifications": { + "signupFailed": "Aanmelden mislukt. Probeer het opnieuw.", + "signupSuccess": "Account succesvol aangemaakt. Log in alstublieft." + } + }, + "listDetailPage": { + "loading": { + "list": "Lijst laden...", + "items": "Items laden...", + "ocrProcessing": "Afbeelding verwerken...", + "addingOcrItems": "OCR-items toevoegen...", + "costSummary": "Samenvatting laden...", + "expenses": "Uitgaven laden...", + "settlement": "Bezig met vereffenen..." + }, + "errors": { + "fetchFailed": "Laden van lijstdetails mislukt.", + "genericLoadFailure": "Groep niet gevonden of er is een fout opgetreden.", + "ocrNoItems": "Geen items uit de afbeelding gehaald.", + "ocrFailed": "Verwerken van afbeelding mislukt.", + "addItemFailed": "Toevoegen van item mislukt.", + "updateItemFailed": "Bijwerken van item mislukt.", + "updateItemPriceFailed": "Bijwerken van itemprijs mislukt.", + "deleteItemFailed": "Verwijderen van item mislukt.", + "addOcrItemsFailed": "Toevoegen van OCR-items mislukt.", + "fetchItemsFailed": "Laden van items mislukt: {errorMessage}", + "loadCostSummaryFailed": "Laden van kostensamenvatting mislukt." + }, + "retryButton": "Opnieuw proberen", + "buttons": { + "addViaOcr": "Toevoegen via OCR", + "addItem": "Toevoegen", + "addItems": "Items toevoegen", + "cancel": "Annuleren", + "confirm": "Bevestigen", + "saveChanges": "Wijzigingen opslaan", + "close": "Sluiten", + "costSummary": "Kostensamenvatting" + }, + "badges": { + "groupList": "Groepslijst", + "personalList": "Persoonlijke lijst" + }, + "items": { + "emptyState": { + "title": "Nog geen items!", + "message": "Voeg items toe via het onderstaande formulier." + }, + "addItemForm": { + "placeholder": "Nieuw item toevoegen", + "quantityPlaceholder": "Aantal", + "itemNameSrLabel": "Naam nieuw item", + "quantitySrLabel": "Hoeveelheid" + }, + "pricePlaceholder": "Prijs", + "editItemAriaLabel": "Item bewerken", + "deleteItemAriaLabel": "Item verwijderen" + }, + "modals": { + "ocr": { + "title": "Items toevoegen via OCR", + "uploadLabel": "Afbeelding uploaden" + }, + "confirmation": { + "title": "Bevestiging" + }, + "editItem": { + "title": "Item bewerken", + "nameLabel": "Itemnaam", + "quantityLabel": "Hoeveelheid" + }, + "costSummary": { + "title": "Kostensamenvatting lijst", + "totalCostLabel": "Totale kosten lijst:", + "equalShareLabel": "Gelijk deel per gebruiker:", + "participantsLabel": "Deelnemende gebruikers:", + "userBalancesHeader": "Gebruikerssaldi", + "tableHeaders": { + "user": "Gebruiker", + "itemsAddedValue": "Waarde toegevoegde items", + "amountDue": "Verschuldigd bedrag", + "balance": "Saldo" + }, + "emptyState": "Geen kostensamenvatting beschikbaar." + }, + "settleShare": { + "title": "Aandeel vereffenen", + "settleAmountFor": "Bedrag vereffenen voor {userName}:", + "amountLabel": "Bedrag", + "errors": { + "enterAmount": "Voer een bedrag in.", + "positiveAmount": "Voer een positief bedrag in.", + "exceedsRemaining": "Bedrag mag resterend bedrag niet overschrijden: {amount}.", + "noSplitSelected": "Fout: Geen verdeling geselecteerd." + } + } + }, + "confirmations": { + "updateMessage": "'{itemName}' markeren als {status}?", + "statusComplete": "voltooid", + "statusIncomplete": "onvolledig", + "deleteMessage": "'{itemName}' verwijderen? Dit kan niet ongedaan worden gemaakt." + }, + "notifications": { + "itemAddedSuccess": "Item succesvol toegevoegd.", + "itemsAddedSuccessOcr": "{count} item(s) succesvol toegevoegd via OCR.", + "itemUpdatedSuccess": "Item succesvol bijgewerkt.", + "itemDeleteSuccess": "Item succesvol verwijderd.", + "enterItemName": "Voer een itemnaam in.", + "costSummaryLoadFailed": "Laden van kostensamenvatting mislukt.", + "cannotSettleOthersShares": "U kunt alleen uw eigen aandelen vereffenen.", + "settlementDataMissing": "Kan vereffening niet verwerken: gegevens ontbreken.", + "settleShareSuccess": "Aandeel succesvol vereffend!", + "settleShareFailed": "Vereffenen van aandeel mislukt." + }, + "expensesSection": { + "title": "Uitgaven", + "addExpenseButton": "Uitgave toevoegen", + "loading": "Uitgaven laden...", + "emptyState": "Nog geen uitgaven geregistreerd voor deze lijst.", + "paidBy": "Betaald door:", + "onDate": "op", + "owes": "is verschuldigd", + "paidAmount": "Betaald:", + "activityLabel": "Activiteit:", + "byUser": "door", + "settleShareButton": "Mijn deel vereffenen", + "retryButton": "Opnieuw proberen" + }, + "status": { + "settled": "Vereffend", + "partiallySettled": "Gedeeltelijk vereffend", + "unsettled": "Openstaand", + "paid": "Betaald", + "partiallyPaid": "Gedeeltelijk betaald", + "unpaid": "Onbetaald", + "unknown": "Onbekende status" + } + }, + "myChoresPage": { + "title": "Mijn toegewezen taken", + "showCompletedToggle": "Voltooide tonen", + "timelineHeaders": { + "overdue": "Achterstallig", + "today": "Vandaag te doen", + "thisWeek": "Deze week", + "later": "Later", + "completed": "Voltooid" + }, + "choreCard": { + "personal": "Persoonlijk", + "group": "Groep", + "duePrefix": "Vervalt", + "completedPrefix": "Voltooid", + "dueToday": "Vandaag te doen", + "markCompleteButton": "Markeer als voltooid" + }, + "frequencies": { + "one_time": "Eenmalig", + "daily": "Dagelijks", + "weekly": "Wekelijks", + "monthly": "Maandelijks", + "custom": "Aangepast", + "unknown": "Onbekende frequentie" + }, + "dates": { + "invalidDate": "Ongeldige datum", + "unknownDate": "Onbekende datum" + }, + "emptyState": { + "title": "Nog geen toewijzingen!", + "noAssignmentsPending": "U heeft geen openstaande taaktoewijzingen.", + "noAssignmentsAll": "U heeft geen taaktoewijzingen (voltooid of openstaand).", + "viewAllChoresButton": "Alle taken bekijken" + }, + "notifications": { + "loadFailed": "Laden van toewijzingen mislukt", + "markedComplete": "\"{choreName}\" gemarkeerd als voltooid!", + "markCompleteFailed": "Markeren van toewijzing als voltooid mislukt" + } + }, + "personalChoresPage": { + "title": "Persoonlijke taken", + "newChoreButton": "Nieuwe taak", + "editButton": "Bewerken", + "deleteButton": "Verwijderen", + "cancelButton": "Annuleren", + "saveButton": "Opslaan", + "modals": { + "editChoreTitle": "Taak bewerken", + "newChoreTitle": "Nieuwe taak", + "deleteChoreTitle": "Taak verwijderen" + }, + "form": { + "nameLabel": "Naam", + "descriptionLabel": "Beschrijving", + "frequencyLabel": "Frequentie", + "intervalLabel": "Interval (dagen)", + "dueDateLabel": "Volgende vervaldatum" + }, + "deleteDialog": { + "confirmationText": "Weet u zeker dat u deze taak wilt verwijderen?" + }, + "frequencies": { + "one_time": "Eenmalig", + "daily": "Dagelijks", + "weekly": "Wekelijks", + "monthly": "Maandelijks", + "custom": "Aangepast", + "unknown": "Onbekende frequentie" + }, + "dates": { + "invalidDate": "Ongeldige datum", + "duePrefix": "Vervalt" + }, + "notifications": { + "loadFailed": "Laden van persoonlijke taken mislukt", + "updateSuccess": "Persoonlijke taak succesvol bijgewerkt", + "createSuccess": "Persoonlijke taak succesvol aangemaakt", + "saveFailed": "Opslaan van persoonlijke taak mislukt", + "deleteSuccess": "Persoonlijke taak succesvol verwijderd", + "deleteFailed": "Verwijderen van persoonlijke taak mislukt" + } + }, + "indexPage": { + "welcomeMessage": "Welkom bij de Valerie UI App", + "mainPageInfo": "Dit is de hoofdindexpagina.", + "sampleTodosHeader": "Voorbeeldtaken (uit IndexPage-gegevens)", + "totalCountLabel": "Totaal aantal uit meta:", + "noTodos": "Geen taken om weer te geven." + }, + "languageSelector": { + "title": "Taal", + "languages": { + "en": "English", + "de": "Deutsch", + "nl": "Nederlands", + "fr": "Français", + "es": "Español" + } + } +} \ No newline at end of file diff --git a/fe/src/layouts/MainLayout.vue b/fe/src/layouts/MainLayout.vue index f5dda86..7a47c33 100644 --- a/fe/src/layouts/MainLayout.vue +++ b/fe/src/layouts/MainLayout.vue @@ -2,17 +2,40 @@
mitlist
-
- -
@@ -53,13 +76,14 @@