feat(places): collaborative place ratings + collections MCP (#1435) (#1644)

Trip members can rate a place 1-5 stars; the place shows the average,
with a who-voted custom tooltip and a star-sort toggle in the places
sidebar. Saved collection places are ratable too (plus a min-rating
filter), and ratings travel with a place copied between a trip and a
collection — carrying only the votes of users who belong to both sides.

Adds a full Collections MCP surface (list/get, CRUD lists + places,
status, rate_collection_place, labels, sharing, copy-to-trip) under a
new collections:read/collections:write scope, plus rate_place for trip
places; rating aggregates flow through the existing place outputs.

Mobile trip + collection place sheets gain the rating row. New UI
strings translated across all 22 locales.
This commit is contained in:
Maurice
2026-07-21 21:00:26 +02:00
committed by GitHub
parent 88cb7f22dd
commit 399e3573c1
86 changed files with 1441 additions and 46 deletions
+5 -1
View File
@@ -1,4 +1,4 @@
import { placeCategorySchema } from '../place/place.schema';
import { placeCategorySchema, placeRatingVoteSchema } from '../place/place.schema';
import { tagSchema } from '../tag/tag.schema';
import { z } from 'zod';
@@ -70,6 +70,10 @@ export const collectionPlaceSchema = z.object({
tags: z.array(tagSchema.partial()).optional(),
/** Ids of the per-collection labels assigned to this place. */
label_ids: z.array(z.number()).optional(),
// Collaborative ratings (#1435): every member's vote + the displayed aggregate.
ratings: z.array(placeRatingVoteSchema).optional(),
rating_avg: z.number().nullable().optional(),
rating_count: z.number().optional(),
});
export type CollectionPlace = z.infer<typeof collectionPlaceSchema>;
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'الرحلات',
'oauth.scope.group.places': 'الأماكن',
'oauth.scope.group.collections': 'المجموعات',
'oauth.scope.group.packing': 'الأمتعة',
'oauth.scope.group.todos': 'المهام',
'oauth.scope.group.budget': 'الميزانية',
@@ -24,6 +25,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'قراءة الأماكن وتعيينات الأيام والعلامات والفئات',
'oauth.scope.places:write.label': 'إدارة الأماكن',
'oauth.scope.places:write.description': 'إنشاء وتحديث وحذف الأماكن والتعيينات والعلامات',
'oauth.scope.collections:read.label': 'عرض المجموعات',
'oauth.scope.collections:read.description': 'قراءة مجموعات الأماكن المحفوظة وأماكنها وتقييماتها وتسمياتها وأعضائها',
'oauth.scope.collections:write.label': 'إدارة المجموعات',
'oauth.scope.collections:write.description': 'إنشاء وتعديل المجموعات، وحفظ الأماكن وتقييمها وتسميتها ونسخها، ومشاركة القوائم',
'oauth.scope.atlas:read.label': 'عرض Atlas',
'oauth.scope.atlas:read.description': 'قراءة الدول والمناطق المزارة وقائمة الأمنيات',
'oauth.scope.atlas:write.label': 'إدارة Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'الكل',
'places.unplanned': 'غير مخطط',
'places.filterTracks': 'المسارات',
'places.sortByRating': 'الترتيب حسب التقييم',
'places.yourRating': 'تقييمك',
'places.notRated': 'لم يُقيَّم بعد',
'places.search': 'ابحث عن أماكن...',
'places.allCategories': 'كل الفئات',
'places.categoriesSelected': 'فئات',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Viagens',
'oauth.scope.group.places': 'Locais',
'oauth.scope.group.collections': 'Coleções',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Bagagem',
'oauth.scope.group.todos': 'Tarefas',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Ler locais, atribuições de dias, tags e categorias',
'oauth.scope.places:write.label': 'Gerenciar locais',
'oauth.scope.places:write.description': 'Criar, atualizar e excluir locais, atribuições e tags',
'oauth.scope.collections:read.label': 'Ver coleções',
'oauth.scope.collections:read.description': 'Ler coleções de lugares salvos, seus lugares, avaliações, etiquetas e membros',
'oauth.scope.collections:write.label': 'Gerenciar coleções',
'oauth.scope.collections:write.description': 'Criar/editar coleções, salvar, avaliar, etiquetar e copiar lugares, e compartilhar listas',
'oauth.scope.atlas:read.label': 'Ver Atlas',
'oauth.scope.atlas:read.description': 'Ler países visitados, regiões e lista de desejos',
'oauth.scope.atlas:write.label': 'Gerenciar Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Todos',
'places.unplanned': 'Não planejados',
'places.filterTracks': 'Trilhas',
'places.sortByRating': 'Ordenar por avaliação',
'places.yourRating': 'Sua avaliação',
'places.notRated': 'Ainda sem avaliações',
'places.search': 'Buscar lugares...',
'places.allCategories': 'Todas as categorias',
'places.categoriesSelected': 'categorias',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Viatges',
'oauth.scope.group.places': 'Llocs',
'oauth.scope.group.collections': 'Col·leccions',
'oauth.scope.group.atlas': 'Atles',
'oauth.scope.group.packing': 'Equipatge',
'oauth.scope.group.todos': 'Tasques',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Llegeix llocs, assignacions de dies, etiquetes i categories',
'oauth.scope.places:write.label': 'Gestiona els llocs',
'oauth.scope.places:write.description': 'Crea, actualitza i elimina llocs, assignacions i etiquetes',
'oauth.scope.collections:read.label': 'Mostra les col·leccions',
'oauth.scope.collections:read.description': 'Llegeix les col·leccions de llocs desats, els seus llocs, valoracions, etiquetes i membres',
'oauth.scope.collections:write.label': 'Gestiona les col·leccions',
'oauth.scope.collections:write.description': 'Crea i edita col·leccions; desa, valora, etiqueta i copia llocs, i comparteix llistes',
'oauth.scope.atlas:read.label': "Mostra l'Atles",
'oauth.scope.atlas:read.description': 'Llegeix països visitats, regions i llista de desitjos',
'oauth.scope.atlas:write.label': "Gestiona l'Atles",
+3
View File
@@ -41,6 +41,9 @@ const places: TranslationStrings = {
'places.all': 'Tot',
'places.unplanned': 'Sense planificar',
'places.filterTracks': 'Rutes',
'places.sortByRating': 'Ordena per valoració',
'places.yourRating': 'La teva valoració',
'places.notRated': 'Encara sense valoració',
'places.search': 'Cerca llocs...',
'places.allCategories': 'Totes les categories',
'places.categoriesSelected': 'categories',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Výlety',
'oauth.scope.group.places': 'Místa',
'oauth.scope.group.collections': 'Kolekce',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Balení',
'oauth.scope.group.todos': 'Úkoly',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Číst místa, denní přiřazení, štítky a kategorie',
'oauth.scope.places:write.label': 'Spravovat místa',
'oauth.scope.places:write.description': 'Vytvářet, aktualizovat a mazat místa, přiřazení a štítky',
'oauth.scope.collections:read.label': 'Zobrazit kolekce',
'oauth.scope.collections:read.description': 'Číst kolekce uložených míst, jejich místa, hodnocení, štítky a členy',
'oauth.scope.collections:write.label': 'Spravovat kolekce',
'oauth.scope.collections:write.description': 'Vytvářet a upravovat kolekce, ukládat, hodnotit, označovat a kopírovat místa a sdílet seznamy',
'oauth.scope.atlas:read.label': 'Zobrazit Atlas',
'oauth.scope.atlas:read.description': 'Číst navštívené země, regiony a seznam přání',
'oauth.scope.atlas:write.label': 'Spravovat Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Vše',
'places.unplanned': 'Nezařazené',
'places.filterTracks': 'Trasy',
'places.sortByRating': 'Seřadit podle hodnocení',
'places.yourRating': 'Tvé hodnocení',
'places.notRated': 'Zatím bez hodnocení',
'places.search': 'Hledat místa...',
'places.allCategories': 'Všechny kategorie',
'places.categoriesSelected': 'kategorií',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Reisen',
'oauth.scope.group.places': 'Orte',
'oauth.scope.group.collections': 'Sammlungen',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Packliste',
'oauth.scope.group.todos': 'Aufgaben',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Orte, Tageszuweisungen, Tags und Kategorien lesen',
'oauth.scope.places:write.label': 'Orte verwalten',
'oauth.scope.places:write.description': 'Orte, Zuweisungen und Tags erstellen, aktualisieren und löschen',
'oauth.scope.collections:read.label': 'Sammlungen ansehen',
'oauth.scope.collections:read.description': 'Sammlungen gespeicherter Orte, ihre Orte, Bewertungen, Labels und Mitglieder lesen',
'oauth.scope.collections:write.label': 'Sammlungen verwalten',
'oauth.scope.collections:write.description': 'Sammlungen erstellen/bearbeiten, Orte speichern, bewerten, labeln und kopieren, Listen teilen',
'oauth.scope.atlas:read.label': 'Atlas anzeigen',
'oauth.scope.atlas:read.description': 'Besuchte Länder, Regionen und Wunschliste lesen',
'oauth.scope.atlas:write.label': 'Atlas verwalten',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Alle',
'places.unplanned': 'Ungeplant',
'places.filterTracks': 'Tracks',
'places.sortByRating': 'Nach Bewertung sortieren',
'places.yourRating': 'Deine Bewertung',
'places.notRated': 'Noch nicht bewertet',
'places.search': 'Orte suchen...',
'places.allCategories': 'Alle Kategorien',
'places.categoriesSelected': 'Kategorien',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Trips',
'oauth.scope.group.places': 'Places',
'oauth.scope.group.collections': 'Collections',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Packing',
'oauth.scope.group.todos': 'To-dos',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Read places, day assignments, tags, and categories',
'oauth.scope.places:write.label': 'Manage places',
'oauth.scope.places:write.description': 'Create, update, and delete places, assignments, and tags',
'oauth.scope.collections:read.label': 'View collections',
'oauth.scope.collections:read.description': 'Read saved-place collections, their places, ratings, labels, and members',
'oauth.scope.collections:write.label': 'Manage collections',
'oauth.scope.collections:write.description': 'Create/edit collections, save, rate, label and copy places, and share lists',
'oauth.scope.atlas:read.label': 'View Atlas',
'oauth.scope.atlas:read.description': 'Read visited countries, regions, and bucket list',
'oauth.scope.atlas:write.label': 'Manage Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'All',
'places.unplanned': 'Unplanned',
'places.filterTracks': 'Tracks',
'places.sortByRating': 'Sort by rating',
'places.yourRating': 'Your rating',
'places.notRated': 'Not rated yet',
'places.search': 'Search places...',
'places.allCategories': 'All Categories',
'places.categoriesSelected': 'categories',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Viajes',
'oauth.scope.group.places': 'Lugares',
'oauth.scope.group.collections': 'Colecciones',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Equipaje',
'oauth.scope.group.todos': 'Tareas',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Leer lugares, asignaciones de días, etiquetas y categorías',
'oauth.scope.places:write.label': 'Gestionar lugares',
'oauth.scope.places:write.description': 'Crear, actualizar y eliminar lugares, asignaciones y etiquetas',
'oauth.scope.collections:read.label': 'Ver colecciones',
'oauth.scope.collections:read.description': 'Leer las colecciones de lugares guardados, sus lugares, valoraciones, etiquetas y miembros',
'oauth.scope.collections:write.label': 'Gestionar colecciones',
'oauth.scope.collections:write.description': 'Crear/editar colecciones, guardar, valorar, etiquetar y copiar lugares, y compartir listas',
'oauth.scope.atlas:read.label': 'Ver Atlas',
'oauth.scope.atlas:read.description': 'Leer países visitados, regiones y lista de deseos',
'oauth.scope.atlas:write.label': 'Gestionar Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Todo',
'places.unplanned': 'Sin planificar',
'places.filterTracks': 'Rutas',
'places.sortByRating': 'Ordenar por valoración',
'places.yourRating': 'Tu valoración',
'places.notRated': 'Sin valorar todavía',
'places.search': 'Buscar lugares...',
'places.allCategories': 'Todas las categorías',
'places.categoriesSelected': 'categorías',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Voyages',
'oauth.scope.group.places': 'Lieux',
'oauth.scope.group.collections': 'Collections',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Bagages',
'oauth.scope.group.todos': 'Tâches',
@@ -27,6 +28,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Lire les lieux, affectations de jours, étiquettes et catégories',
'oauth.scope.places:write.label': 'Gérer les lieux',
'oauth.scope.places:write.description': 'Créer, modifier et supprimer des lieux, affectations et étiquettes',
'oauth.scope.collections:read.label': 'Voir les collections',
'oauth.scope.collections:read.description': 'Consulter les collections de lieux enregistrés, ainsi que leurs lieux, notes, étiquettes et membres',
'oauth.scope.collections:write.label': 'Gérer les collections',
'oauth.scope.collections:write.description': 'Créer/modifier des collections, enregistrer, noter, étiqueter et copier des lieux, et partager des listes',
'oauth.scope.atlas:read.label': "Voir l'Atlas",
'oauth.scope.atlas:read.description': 'Lire les pays visités, régions et liste de souhaits',
'oauth.scope.atlas:write.label': "Gérer l'Atlas",
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Tous',
'places.unplanned': 'Non planifiés',
'places.filterTracks': 'Traces',
'places.sortByRating': 'Trier par note',
'places.yourRating': 'Ta note',
'places.notRated': 'Pas encore noté',
'places.search': 'Rechercher des lieux…',
'places.allCategories': 'Toutes les catégories',
'places.categoriesSelected': 'catégories',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Ταξίδια',
'oauth.scope.group.places': 'Τοποθεσίες',
'oauth.scope.group.collections': 'Συλλογές',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Αποσκευές',
'oauth.scope.group.todos': 'Εργασίες',
@@ -27,6 +28,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Ανάγνωση τοποθεσιών, αναθέσεων ημερών, ετικετών και κατηγοριών',
'oauth.scope.places:write.label': 'Διαχείριση τοποθεσιών',
'oauth.scope.places:write.description': 'Δημιουργία, ενημέρωση και διαγραφή τοποθεσιών, αναθέσεων και ετικετών',
'oauth.scope.collections:read.label': 'Προβολή συλλογών',
'oauth.scope.collections:read.description': 'Ανάγνωση συλλογών αποθηκευμένων τοποθεσιών, των τοποθεσιών τους, των βαθμολογιών, των ετικετών και των μελών τους',
'oauth.scope.collections:write.label': 'Διαχείριση συλλογών',
'oauth.scope.collections:write.description': 'Δημιουργία/επεξεργασία συλλογών, αποθήκευση, βαθμολόγηση, προσθήκη ετικετών και αντιγραφή τοποθεσιών και κοινή χρήση λιστών',
'oauth.scope.atlas:read.label': 'Προβολή Atlas',
'oauth.scope.atlas:read.description': 'Ανάγνωση περιοχών που έχουν επισκεφθεί, χωρών και λίστας επιθυμιών',
'oauth.scope.atlas:write.label': 'Διαχείριση Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Όλα',
'places.unplanned': 'Μη προγραμματισμένα',
'places.filterTracks': 'Ίχνη',
'places.sortByRating': 'Ταξινόμηση κατά βαθμολογία',
'places.yourRating': 'Η βαθμολογία σου',
'places.notRated': 'Χωρίς βαθμολογία ακόμη',
'places.search': 'Αναζήτηση μερών...',
'places.allCategories': 'Όλες οι Κατηγορίες',
'places.categoriesSelected': 'κατηγορίες',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Utazások',
'oauth.scope.group.places': 'Helyek',
'oauth.scope.group.collections': 'Gyűjtemények',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Csomagolás',
'oauth.scope.group.todos': 'Feladatok',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Helyek, napi hozzárendelések, címkék és kategóriák olvasása',
'oauth.scope.places:write.label': 'Helyek kezelése',
'oauth.scope.places:write.description': 'Helyek, hozzárendelések és címkék létrehozása, frissítése és törlése',
'oauth.scope.collections:read.label': 'Gyűjtemények megtekintése',
'oauth.scope.collections:read.description': 'Helygyűjtemények, azok helyeinek, értékeléseinek, címkéinek és tagjainak olvasása',
'oauth.scope.collections:write.label': 'Gyűjtemények kezelése',
'oauth.scope.collections:write.description': 'Gyűjtemények létrehozása/szerkesztése, helyek mentése, értékelése, címkézése és másolása, valamint listák megosztása',
'oauth.scope.atlas:read.label': 'Atlas megtekintése',
'oauth.scope.atlas:read.description': 'Meglátogatott országok, régiók és bakancslisták olvasása',
'oauth.scope.atlas:write.label': 'Atlas kezelése',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Összes',
'places.unplanned': 'Nem tervezett',
'places.filterTracks': 'Nyomvonalak',
'places.sortByRating': 'Rendezés értékelés szerint',
'places.yourRating': 'Az értékelésed',
'places.notRated': 'Még nincs értékelve',
'places.search': 'Helyek keresése...',
'places.allCategories': 'Összes kategória',
'places.categoriesSelected': 'kategória',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Perjalanan',
'oauth.scope.group.places': 'Tempat',
'oauth.scope.group.collections': 'Koleksi',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Perlengkapan',
'oauth.scope.group.todos': 'To-do',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Baca tempat, penugasan hari, tag, dan kategori',
'oauth.scope.places:write.label': 'Kelola tempat',
'oauth.scope.places:write.description': 'Buat, perbarui, dan hapus tempat, penugasan, dan tag',
'oauth.scope.collections:read.label': 'Lihat koleksi',
'oauth.scope.collections:read.description': 'Baca koleksi tempat tersimpan beserta tempat, rating, label, dan anggotanya',
'oauth.scope.collections:write.label': 'Kelola koleksi',
'oauth.scope.collections:write.description': 'Buat/edit koleksi, simpan, beri rating, beri label, dan salin tempat, serta bagikan daftar',
'oauth.scope.atlas:read.label': 'Lihat Atlas',
'oauth.scope.atlas:read.description': 'Baca negara yang dikunjungi, wilayah, dan daftar impian',
'oauth.scope.atlas:write.label': 'Kelola Atlas',
+3
View File
@@ -42,6 +42,9 @@ const places: TranslationStrings = {
'places.all': 'Semua',
'places.unplanned': 'Belum direncanakan',
'places.filterTracks': 'Trek',
'places.sortByRating': 'Urutkan berdasarkan rating',
'places.yourRating': 'Rating kamu',
'places.notRated': 'Belum dinilai',
'places.search': 'Cari tempat...',
'places.allCategories': 'Semua Kategori',
'places.categoriesSelected': 'kategori',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Viaggi',
'oauth.scope.group.places': 'Luoghi',
'oauth.scope.group.collections': 'Raccolte',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Bagagli',
'oauth.scope.group.todos': 'Attività',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Leggi luoghi, assegnazioni giornaliere, tag e categorie',
'oauth.scope.places:write.label': 'Gestisci luoghi',
'oauth.scope.places:write.description': 'Crea, aggiorna ed elimina luoghi, assegnazioni e tag',
'oauth.scope.collections:read.label': 'Visualizza raccolte',
'oauth.scope.collections:read.description': 'Leggi le raccolte di luoghi salvati, i relativi luoghi, valutazioni, etichette e membri',
'oauth.scope.collections:write.label': 'Gestisci raccolte',
'oauth.scope.collections:write.description': 'Crea/modifica raccolte, salva, valuta, etichetta e copia luoghi e condividi elenchi',
'oauth.scope.atlas:read.label': 'Visualizza Atlas',
'oauth.scope.atlas:read.description': 'Leggi paesi visitati, regioni e lista dei desideri',
'oauth.scope.atlas:write.label': 'Gestisci Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Tutti',
'places.unplanned': 'Non pianificati',
'places.filterTracks': 'Tracce',
'places.sortByRating': 'Ordina per valutazione',
'places.yourRating': 'La tua valutazione',
'places.notRated': 'Non ancora valutato',
'places.search': 'Cerca luoghi...',
'places.allCategories': 'Tutte le categorie',
'places.categoriesSelected': 'categorie',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': '旅行',
'oauth.scope.group.places': '場所',
'oauth.scope.group.collections': 'コレクション',
'oauth.scope.group.atlas': '地図',
'oauth.scope.group.packing': '持ち物',
'oauth.scope.group.todos': 'ToDo',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': '場所、日への割り当て、タグ、カテゴリを閲覧',
'oauth.scope.places:write.label': '場所を管理',
'oauth.scope.places:write.description': '場所、割り当て、タグの作成・更新・削除',
'oauth.scope.collections:read.label': 'コレクションを表示',
'oauth.scope.collections:read.description': '保存した場所のコレクションと、その中の場所、評価、ラベル、メンバーを読み取る',
'oauth.scope.collections:write.label': 'コレクションを管理',
'oauth.scope.collections:write.description': 'コレクションの作成・編集、場所の保存・評価・ラベル付け・コピー、リストの共有',
'oauth.scope.atlas:read.label': '地図を表示',
'oauth.scope.atlas:read.description': '訪問した国・地域、バケットリストを閲覧',
'oauth.scope.atlas:write.label': '地図を管理',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'すべて',
'places.unplanned': '未計画',
'places.filterTracks': 'トラック',
'places.sortByRating': '評価順に並べ替え',
'places.yourRating': 'あなたの評価',
'places.notRated': '未評価',
'places.search': '場所を検索…',
'places.allCategories': 'すべてのカテゴリ',
'places.categoriesSelected': 'カテゴリ',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': '여행',
'oauth.scope.group.places': '장소',
'oauth.scope.group.collections': '컬렉션',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': '짐 목록',
'oauth.scope.group.todos': '할 일',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': '장소, 날 배정, 태그, 카테고리 읽기',
'oauth.scope.places:write.label': '장소 관리',
'oauth.scope.places:write.description': '장소, 배정, 태그 만들기, 업데이트, 삭제',
'oauth.scope.collections:read.label': '컬렉션 보기',
'oauth.scope.collections:read.description': '저장된 장소 컬렉션과 장소, 평점, 라벨, 멤버 읽기',
'oauth.scope.collections:write.label': '컬렉션 관리',
'oauth.scope.collections:write.description': '컬렉션 생성·편집, 장소 저장·평가·라벨 지정·복사, 목록 공유',
'oauth.scope.atlas:read.label': 'Atlas 보기',
'oauth.scope.atlas:read.description': '방문한 나라, 지역, 버킷 리스트 읽기',
'oauth.scope.atlas:write.label': 'Atlas 관리',
+3
View File
@@ -42,6 +42,9 @@ const places: TranslationStrings = {
'places.all': '전체',
'places.unplanned': '미계획',
'places.filterTracks': '트랙',
'places.sortByRating': '평점순 정렬',
'places.yourRating': '내 평점',
'places.notRated': '아직 평점 없음',
'places.search': '장소 검색...',
'places.allCategories': '모든 카테고리',
'places.categoriesSelected': '카테고리',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Reizen',
'oauth.scope.group.places': 'Plaatsen',
'oauth.scope.group.collections': 'Collecties',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Paklijst',
'oauth.scope.group.todos': 'Taken',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Plaatsen, dagtoewijzingen, tags en categorieën lezen',
'oauth.scope.places:write.label': 'Plaatsen beheren',
'oauth.scope.places:write.description': 'Plaatsen, toewijzingen en tags aanmaken, bijwerken en verwijderen',
'oauth.scope.collections:read.label': 'Collecties bekijken',
'oauth.scope.collections:read.description': 'Collecties met opgeslagen plekken lezen, inclusief hun plekken, beoordelingen, labels en leden',
'oauth.scope.collections:write.label': 'Collecties beheren',
'oauth.scope.collections:write.description': 'Collecties maken/bewerken, plekken opslaan, beoordelen, labelen en kopiëren, en lijsten delen',
'oauth.scope.atlas:read.label': 'Atlas bekijken',
'oauth.scope.atlas:read.description': "Bezochte landen, regio's en bucketlist lezen",
'oauth.scope.atlas:write.label': 'Atlas beheren',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Alle',
'places.unplanned': 'Ongepland',
'places.filterTracks': 'Tracks',
'places.sortByRating': 'Sorteren op beoordeling',
'places.yourRating': 'Jouw beoordeling',
'places.notRated': 'Nog niet beoordeeld',
'places.search': 'Plaatsen zoeken...',
'places.allCategories': 'Alle categorieën',
'places.categoriesSelected': 'categorieën',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Podróże',
'oauth.scope.group.places': 'Miejsca',
'oauth.scope.group.collections': 'Kolekcje',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Pakowanie',
'oauth.scope.group.todos': 'Zadania',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Odczytuj miejsca, przypisania dni, tagi i kategorie',
'oauth.scope.places:write.label': 'Zarządzaj miejscami',
'oauth.scope.places:write.description': 'Twórz, aktualizuj i usuń miejsca, przypisania i tagi',
'oauth.scope.collections:read.label': 'Wyświetlanie kolekcji',
'oauth.scope.collections:read.description': 'Odczyt kolekcji zapisanych miejsc, zawartych w nich miejsc, ocen, etykiet i członków',
'oauth.scope.collections:write.label': 'Zarządzanie kolekcjami',
'oauth.scope.collections:write.description': 'Tworzenie/edytowanie kolekcji, zapisywanie, ocenianie, etykietowanie i kopiowanie miejsc oraz udostępnianie list',
'oauth.scope.atlas:read.label': 'Przeglądaj Atlas',
'oauth.scope.atlas:read.description': 'Odczytuj odwiedzone kraje, regiony i listę marzeń',
'oauth.scope.atlas:write.label': 'Zarządzaj Atlasem',
+3
View File
@@ -34,6 +34,9 @@ const places: TranslationStrings = {
'places.all': 'Wszystkie',
'places.unplanned': 'Niezaplanowane',
'places.filterTracks': 'Trasy',
'places.sortByRating': 'Sortuj według oceny',
'places.yourRating': 'Twoja ocena',
'places.notRated': 'Jeszcze nie oceniono',
'places.search': 'Szukaj miejsc...',
'places.allCategories': 'Wszystkie kategorie',
'places.categoriesSelected': 'kategorii',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Поездки',
'oauth.scope.group.places': 'Места',
'oauth.scope.group.collections': 'Коллекции',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Вещи',
'oauth.scope.group.todos': 'Задачи',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Чтение мест, назначений по дням, тегов и категорий',
'oauth.scope.places:write.label': 'Управление местами',
'oauth.scope.places:write.description': 'Создание, обновление и удаление мест, назначений и тегов',
'oauth.scope.collections:read.label': 'Просмотр коллекций',
'oauth.scope.collections:read.description': 'Чтение коллекций сохранённых мест, входящих в них мест, оценок, меток и участников',
'oauth.scope.collections:write.label': 'Управление коллекциями',
'oauth.scope.collections:write.description': 'Создание и изменение коллекций, сохранение, оценка, добавление меток и копирование мест, а также предоставление доступа к спискам',
'oauth.scope.atlas:read.label': 'Просмотр Atlas',
'oauth.scope.atlas:read.description': 'Чтение посещённых стран, регионов и списка желаний',
'oauth.scope.atlas:write.label': 'Управление Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Все',
'places.unplanned': 'Незапланированные',
'places.filterTracks': 'Треки',
'places.sortByRating': 'Сортировать по рейтингу',
'places.yourRating': 'Твоя оценка',
'places.notRated': 'Ещё нет оценок',
'places.search': 'Поиск мест...',
'places.allCategories': 'Все категории',
'places.categoriesSelected': 'категорий',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Resor',
'oauth.scope.group.places': 'Platser',
'oauth.scope.group.collections': 'Samlingar',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Packning',
'oauth.scope.group.todos': 'Att göra',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Läsa platser, dagsuppdrag, taggar och kategorier',
'oauth.scope.places:write.label': 'Hantera platser',
'oauth.scope.places:write.description': 'Skapa, uppdatera och ta bort platser, uppdrag och taggar',
'oauth.scope.collections:read.label': 'Visa samlingar',
'oauth.scope.collections:read.description': 'Läs samlingar med sparade platser, deras platser, betyg, etiketter och medlemmar',
'oauth.scope.collections:write.label': 'Hantera samlingar',
'oauth.scope.collections:write.description': 'Skapa/redigera samlingar, spara, betygsätt, etikettera och kopiera platser samt dela listor',
'oauth.scope.atlas:read.label': 'Visa Atlas',
'oauth.scope.atlas:read.description': 'Läs om besökta länder, regioner och bucketlist',
'oauth.scope.atlas:write.label': 'Hantera Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Alla',
'places.unplanned': 'Oplanerat',
'places.filterTracks': 'Spår',
'places.sortByRating': 'Sortera efter betyg',
'places.yourRating': 'Ditt betyg',
'places.notRated': 'Inte betygsatt ännu',
'places.search': 'Sök efter platser...',
'places.allCategories': 'Alla kategorier',
'places.categoriesSelected': 'kategorier',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Geziler',
'oauth.scope.group.places': 'Yer',
'oauth.scope.group.collections': 'Koleksiyonlar',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Ambalaj',
'oauth.scope.group.todos': 'Yapılacaklar',
@@ -27,6 +28,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Yerleri, gün atamalarını, etiketleri ve kategorileri oku',
'oauth.scope.places:write.label': 'Yerleri yönet',
'oauth.scope.places:write.description': 'Yerleri, atamaları ve etiketleri oluştur, güncelle ve sil',
'oauth.scope.collections:read.label': 'Koleksiyonları görüntüle',
'oauth.scope.collections:read.description': 'Kayıtlı yer koleksiyonlarını, içindeki yerleri, puanları, etiketleri ve üyeleri okuma',
'oauth.scope.collections:write.label': 'Koleksiyonları yönet',
'oauth.scope.collections:write.description': 'Koleksiyon oluşturma/düzenleme, yerleri kaydetme, puanlama, etiketleme ve kopyalama, listeleri paylaşma',
'oauth.scope.atlas:read.label': "Atlas'ı Görüntüle",
'oauth.scope.atlas:read.description': 'Ziyaret edilen ülkeleri, bölgeleri ve yapılacaklar listesini oku',
'oauth.scope.atlas:write.label': "Atlas'ı Yönet",
+3
View File
@@ -45,6 +45,9 @@ const places: TranslationStrings = {
'places.all': 'Tüm',
'places.unplanned': 'Planlanmamış',
'places.filterTracks': 'Parçalar',
'places.sortByRating': 'Puana göre sırala',
'places.yourRating': 'Senin puanın',
'places.notRated': 'Henüz puanlanmadı',
'places.search': 'Yer ara...',
'places.allCategories': 'Tüm Kategoriler',
'places.categoriesSelected': 'kategoriler',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Поїздки',
'oauth.scope.group.places': 'Місця',
'oauth.scope.group.collections': 'Колекції',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': 'Речі',
'oauth.scope.group.todos': 'Задачі',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Читання місць, призначень за днями, тегів і категорій',
'oauth.scope.places:write.label': 'Керування місцями',
'oauth.scope.places:write.description': 'Створення, оновлення та видалення місць, призначень і тегів',
'oauth.scope.collections:read.label': 'Перегляд колекцій',
'oauth.scope.collections:read.description': 'Читання колекцій збережених місць, їхніх місць, оцінок, міток і учасників',
'oauth.scope.collections:write.label': 'Керування колекціями',
'oauth.scope.collections:write.description': 'Створення й редагування колекцій, збереження, оцінювання, додавання міток і копіювання місць, а також надання доступу до списків',
'oauth.scope.atlas:read.label': 'Перегляд Atlas',
'oauth.scope.atlas:read.description': 'Читання відвіданих країн, регіонів і списку бажань',
'oauth.scope.atlas:write.label': 'Керування Atlas',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Усі',
'places.unplanned': 'Незаплановані',
'places.filterTracks': 'Треки',
'places.sortByRating': 'Сортувати за рейтингом',
'places.yourRating': 'Твоя оцінка',
'places.notRated': 'Ще не оцінено',
'places.search': 'Пошук місць...',
'places.allCategories': 'Всі категорії',
'places.categoriesSelected': 'категорій',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': 'Chuyến đi',
'oauth.scope.group.places': 'Địa điểm',
'oauth.scope.group.collections': 'Bộ sưu tập',
'oauth.scope.group.atlas': 'Bản đồ',
'oauth.scope.group.packing': 'Đóng gói',
'oauth.scope.group.todos': 'Việc cần làm',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': 'Đọc địa điểm, bài tập trong ngày, thẻ và danh mục',
'oauth.scope.places:write.label': 'Quản lý địa điểm',
'oauth.scope.places:write.description': 'Tạo, cập nhật và xóa địa điểm, bài tập và thẻ',
'oauth.scope.collections:read.label': 'Xem bộ sưu tập',
'oauth.scope.collections:read.description': 'Đọc bộ sưu tập địa điểm đã lưu cùng các địa điểm, đánh giá, nhãn và thành viên trong đó',
'oauth.scope.collections:write.label': 'Quản lý bộ sưu tập',
'oauth.scope.collections:write.description': 'Tạo/chỉnh sửa bộ sưu tập, lưu, đánh giá, gắn nhãn và sao chép địa điểm, chia sẻ danh sách',
'oauth.scope.atlas:read.label': 'Xem bản đồ',
'oauth.scope.atlas:read.description': 'Đọc các quốc gia, khu vực và danh sách nhóm đã ghé thăm',
'oauth.scope.atlas:write.label': 'Quản lý bản đồ',
+3
View File
@@ -43,6 +43,9 @@ const places: TranslationStrings = {
'places.all': 'Tất cả',
'places.unplanned': 'Không có kế hoạch',
'places.filterTracks': 'Bài hát',
'places.sortByRating': 'Sắp xếp theo đánh giá',
'places.yourRating': 'Đánh giá của bạn',
'places.notRated': 'Chưa có đánh giá',
'places.search': 'Tìm kiếm địa điểm...',
'places.allCategories': 'Tất cả danh mục',
'places.categoriesSelected': 'Thể loại',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': '行程',
'oauth.scope.group.places': '地點',
'oauth.scope.group.collections': '收藏集',
'oauth.scope.group.atlas': 'Atlas',
'oauth.scope.group.packing': '行李',
'oauth.scope.group.todos': '待辦事項',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': '讀取地點、每日指派、標籤及類別',
'oauth.scope.places:write.label': '管理地點',
'oauth.scope.places:write.description': '建立、更新及刪除地點、指派及標籤',
'oauth.scope.collections:read.label': '檢視收藏集',
'oauth.scope.collections:read.description': '讀取地點收藏集及其中的地點、評分、標籤與成員',
'oauth.scope.collections:write.label': '管理收藏集',
'oauth.scope.collections:write.description': '建立/編輯收藏集,儲存、評分、標記及複製地點,並分享清單',
'oauth.scope.atlas:read.label': '檢視 Atlas',
'oauth.scope.atlas:read.description': '讀取已造訪的國家、地區及願望清單',
'oauth.scope.atlas:write.label': '管理 Atlas',
+3
View File
@@ -42,6 +42,9 @@ const places: TranslationStrings = {
'places.all': '全部',
'places.unplanned': '未規劃',
'places.filterTracks': '路線',
'places.sortByRating': '依評分排序',
'places.yourRating': '你的評分',
'places.notRated': '尚未評分',
'places.search': '搜尋地點...',
'places.allCategories': '所有分類',
'places.categoriesSelected': '個分類',
+5
View File
@@ -3,6 +3,7 @@ import type { TranslationStrings } from '../types';
const oauth: TranslationStrings = {
'oauth.scope.group.trips': '行程',
'oauth.scope.group.places': '地点',
'oauth.scope.group.collections': '收藏夹',
'oauth.scope.group.atlas': '足迹',
'oauth.scope.group.packing': '行李',
'oauth.scope.group.todos': '待办事项',
@@ -26,6 +27,10 @@ const oauth: TranslationStrings = {
'oauth.scope.places:read.description': '读取地点、每日分配、标签和分类',
'oauth.scope.places:write.label': '管理地点',
'oauth.scope.places:write.description': '创建、更新和删除地点、分配和标签',
'oauth.scope.collections:read.label': '查看收藏夹',
'oauth.scope.collections:read.description': '读取收藏夹及其中的地点、评分、标签和成员',
'oauth.scope.collections:write.label': '管理收藏夹',
'oauth.scope.collections:write.description': '创建/编辑收藏夹,保存、评分、标注、复制地点,以及分享列表',
'oauth.scope.atlas:read.label': '查看足迹',
'oauth.scope.atlas:read.description': '读取已访问国家、地区和心愿清单',
'oauth.scope.atlas:write.label': '管理足迹',
+3
View File
@@ -42,6 +42,9 @@ const places: TranslationStrings = {
'places.all': '全部',
'places.unplanned': '未规划',
'places.filterTracks': '路线',
'places.sortByRating': '按评分排序',
'places.yourRating': '你的评分',
'places.notRated': '暂无评分',
'places.search': '搜索地点...',
'places.allCategories': '所有分类',
'places.categoriesSelected': '个分类',
+20
View File
@@ -38,6 +38,15 @@ export type PlaceCategory = z.infer<typeof placeCategorySchema>;
* projection and `tags` array. Numbers (lat/lng/price) are SQLite REAL, ids are
* INTEGER; provider-derived columns are nullable.
*/
// One member's star vote on a place (#1435), with display info for the tooltip.
export const placeRatingVoteSchema = z.object({
user_id: z.number(),
username: z.string(),
avatar: z.string().nullable().optional(),
rating: z.number(),
});
export type PlaceRatingVote = z.infer<typeof placeRatingVoteSchema>;
export const placeSchema = z.object({
id: z.number(),
trip_id: z.number(),
@@ -68,6 +77,11 @@ export const placeSchema = z.object({
updated_at: z.string().optional(),
category: placeCategorySchema.optional(),
tags: z.array(tagSchema.partial()).optional(),
// Collaborative ratings (#1435): every member's vote (for the who-voted
// tooltip) plus the aggregate the UI displays.
ratings: z.array(placeRatingVoteSchema).optional(),
rating_avg: z.number().nullable().optional(),
rating_count: z.number().optional(),
});
export type Place = z.infer<typeof placeSchema>;
@@ -111,6 +125,12 @@ export type PlaceCreateRequest = z.infer<typeof placeCreateRequestSchema>;
export const placeUpdateRequestSchema = open;
export type PlaceUpdateRequest = z.infer<typeof placeUpdateRequestSchema>;
// Collaborative ratings (#1435): one 1-5 star vote per user and place.
export const placeRatingRequestSchema = z.object({
rating: z.number().int().min(1).max(5),
});
export type PlaceRatingRequest = z.infer<typeof placeRatingRequestSchema>;
export const placeBulkDeleteRequestSchema = z.object({
ids: z.array(z.number()),
});