Files
collective-lists/apps/web/messages/en.json
Oier Bravo Urtasun 2806e06db2 feat(fase-4): global search + RLS isolation audit (211 tests green)
4.0 Tests primero
  Vitest: search.test.ts (10 — S-01..S-04), rls-audit.test.ts (42 —
    3 intruders × 14 cross-collective ops proving zero leakage)
  pgTAP: 007_search_tsvector.sql (9 — columns, GIN indexes, function
    signature, generated-vector invariant)
  Playwright: search.test.ts (2 — SR-01 happy-path, SR-02 filter toggle)

4.1 Búsqueda global
  Migration 010_search_vectors.sql: STORED GENERATED tsvector columns
    on shopping_items/tasks/notes + GIN indexes + search_result_type
    enum + search_in_collective() SECURITY INVOKER function (RLS-aware,
    trash-excluded per RN-08) + GRANT EXECUTE to anon/authenticated.
    Uses `simple` config (no stemmer — bilingual en/es).
  Store apps/web/src/lib/stores/search.ts — RPC wrapper
  /search: debounced input (300ms, ≥2 chars), type filter chips with
    multi-toggle, results grouped per type with data-testid hooks for
    Playwright, deep-link per type (shopping_item → /lists/[id], task →
    /tasks/[id], note → /notes/[id])

4.4 Security
  rls-audit.test.ts replaces the curl-based manual audit with a
    parametrised matrix — zero cross-collective reads/writes under
    three different attacker roles

Realtime flake hardening
  Add 250ms settle after SUBSCRIBED in realtime-helpers.ts — the Phoenix
    socket reports SUBSCRIBED slightly before the backend finishes
    propagating the filter to the WAL subscription, so mutations fired
    immediately after subscribe could miss the filter under load.

4.Z Verification
  just test-all → 211 verdes, 2 skipped
    34 pgTAP (was 25, +9 search)
    140 Vitest integration (was 88, +10 search +42 rls-audit; 2 skipped)
    6 Vitest unit (unchanged)
    31 Playwright (was 29, +2 search)
  Deferred (prod-deploy scope):
    PWA install/push (needs real iOS/Android hardware)
    Kong rate-limit plugin config
    JWT_SECRET / ANON_KEY rotation
    Lighthouse PWA ≥ 90 manual validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:47:38 +02:00

162 lines
6.2 KiB
JSON

{
"$schema": "https://inlang.com/schema/inlang-message-format",
"nav_lists": "Lists",
"nav_tasks": "Tasks",
"nav_notes": "Notes",
"nav_search": "Search",
"nav_settings": "Settings",
"nav_collective": "Collective",
"app_name": "Colectivo",
"loading": "Loading…",
"error_generic": "Something went wrong. Please try again.",
"login_title": "Sign in to Colectivo",
"login_button": "Sign in",
"logout_button": "Sign out",
"auth_back_to_home": "Back to home",
"onboarding_title": "Welcome to Colectivo",
"onboarding_subtitle": "Get started by creating a new collective or joining an existing one.",
"onboarding_create_tab": "Create",
"onboarding_join_tab": "Join",
"onboarding_create_collective": "Create a collective",
"onboarding_join_collective": "Join with an invitation link",
"onboarding_collective_name": "Collective name",
"onboarding_collective_name_placeholder": "e.g. Our Home",
"onboarding_collective_emoji": "Icon",
"onboarding_invite_link_label": "Invitation link",
"onboarding_invite_link_placeholder": "Paste the invite link here…",
"onboarding_create_btn": "Create collective",
"onboarding_join_btn": "Join collective",
"collective_name_label": "Collective name",
"collective_name_placeholder": "e.g. Our Home",
"invitation_title": "You're invited",
"invitation_subtitle": "You've been invited to join a collective.",
"invitation_accept_btn": "Accept invitation",
"invitation_success": "You've joined the collective!",
"invitation_invalid_link": "That doesn't look like a valid invitation link.",
"invitation_error_not_found": "This invitation link is not valid.",
"invitation_error_already_used": "This invitation has already been used.",
"invitation_error_expired": "This invitation has expired.",
"invitation_error_already_member": "You are already a member of this collective.",
"invitation_error_unauthenticated": "You must be signed in to accept an invitation.",
"invitation_error_unknown": "Something went wrong. Please try again.",
"manage_title": "Manage collective",
"manage_members": "Members",
"manage_you": "you",
"manage_remove": "Remove member",
"manage_invite_title": "Invite someone",
"manage_invite_role": "Role",
"manage_generate_link": "Generate invite link",
"manage_copy": "Copy",
"manage_copied": "Copied!",
"manage_link_expires": "Link expires in 7 days.",
"role_admin": "Admin",
"role_member": "Member",
"role_guest": "Guest",
"settings_title": "Settings",
"settings_display_name": "Display name",
"settings_saving": "Saving…",
"settings_language": "Language",
"settings_avatar": "Avatar",
"settings_avatar_initials": "Initials",
"settings_avatar_emoji": "Emoji",
"settings_avatar_upload": "Photo",
"settings_avatar_upload_btn": "Upload photo…",
"settings_account": "Account",
"settings_keycloak_link": "Change email or password",
"settings_logout": "Sign out",
"settings_save": "Save changes",
"settings_saved": "Changes saved",
"invite_member": "Invite member",
"invite_email_label": "Email address",
"invite_send": "Send invitation",
"lists_title": "Shopping Lists",
"lists_new_list": "Create",
"lists_no_lists": "No lists yet",
"lists_create_first": "Create your first shopping list",
"lists_trash": "Trash",
"list_status_active": "Active List",
"list_status_completed": "Completed",
"list_status_archived": "Archived",
"list_name_placeholder": "e.g. Weekly shop",
"list_items_empty": "List is empty",
"list_items_empty_hint": "Add your first item below",
"list_reset": "Reset list",
"list_archive": "Archive",
"list_delete": "Move to trash",
"list_actions": "List actions",
"list_item_placeholder": "Add item (e.g. Yogurt 500g)",
"list_qty_label": "Qty",
"list_unit_label": "Unit",
"list_add_item": "Add item…",
"list_to_buy": "To buy",
"list_checked": "Checked",
"list_start_session": "Shop",
"list_finish_shopping": "Finish shopping",
"list_finish_confirm": "Mark this list as completed?",
"list_finish_confirm_yes": "Yes, finish",
"list_finish_confirm_no": "Keep shopping",
"tasks_title": "Tasks",
"tasks_no_lists": "No task lists yet",
"tasks_create_first": "Create your first task list",
"tasks_new_list_placeholder": "New task list",
"tasks_new_task_placeholder": "Add task",
"tasks_empty": "No tasks yet",
"tasks_empty_hint": "Add your first task below",
"tasks_completed_section": "Completed",
"tasks_pending_section": "To do",
"tasks_completed_by": "Completed by {name}",
"tasks_toggle": "Toggle task",
"tasks_uncheck": "Uncheck task",
"tasks_delete": "Delete task",
"tasks_delete_list": "Delete list",
"tasks_delete_list_confirm": "Delete this task list and all its tasks?",
"notes_title": "Notes",
"notes_new": "New note",
"notes_pinned_section": "Pinned",
"notes_active_section": "Notes",
"notes_archive": "Archive",
"notes_archive_view": "Archived notes",
"notes_archived_empty": "No archived notes",
"notes_trash_view": "Notes trash",
"notes_trash_empty": "Trash is empty",
"notes_pin": "Pin note",
"notes_unpin": "Unpin note",
"notes_unarchive": "Unarchive note",
"notes_send_to_trash": "Move to trash",
"notes_restore": "Restore",
"notes_duplicate": "Duplicate",
"notes_color": "Color",
"notes_title_placeholder": "Title",
"notes_content_placeholder": "Start writing…",
"notes_content_aria": "Content",
"notes_back_to_board": "Back to board",
"notes_saved": "Saved",
"notes_saving": "Saving…",
"notes_empty_board": "No notes yet",
"notes_empty_board_hint": "Click \"New note\" to start writing",
"search_title": "Search",
"search_placeholder": "Search lists, tasks, notes…",
"search_filter_all": "All",
"search_filter_lists": "Lists",
"search_filter_tasks": "Tasks",
"search_filter_notes": "Notes",
"search_group_shopping_items": "Shopping items",
"search_group_tasks": "Tasks",
"search_group_notes": "Notes",
"search_empty": "No matches",
"search_hint": "Type at least two characters",
"trash_restore": "Restore",
"trash_delete_permanent": "Delete permanently",
"trash_empty": "Trash is empty",
"trash_expires_in": "Expires in {days} days",
"confirm_delete": "Delete?",
"cancel": "Cancel",
"save": "Save",
"action_delete": "Delete",
"edit": "Edit",
"add": "Add",
"done": "Done",
"sync_offline": "You're offline — changes will sync when you reconnect",
"sync_syncing": "Syncing…"
}