feat(journey): the journal settings list who a linked trip lets in

Under the contributors, the journey settings on desktop and phone get a section of their own, Through trips, with everyone a trip's journal link lets in, their role and the trip they come through. It is read only there, since the trip's settings manage it, and it leaves out whoever is a contributor already. getJourneyFull answers it as trip_access.
This commit is contained in:
Maurice
2026-10-18 22:04:41 +02:00
parent 451167bde3
commit 532010aa79
35 changed files with 248 additions and 12 deletions
@@ -98,6 +98,19 @@ describe('JourneySettingsDialog', () => {
await screen.findByRole('button', { name: /create share link/i })
})
it('FE-JRN-SETTINGS-2683: lists the people a linked trip lets in under their own heading, and nothing without', () => {
const { unmount } = mountDialog()
expect(screen.queryByText('Through trips')).not.toBeInTheDocument()
unmount()
mountDialog(buildJourney({
trip_access: [{ user_id: 7, role: 'viewer', username: 'lena', avatar: null, trip_id: 5, trip_title: 'Italy Trip' }],
}))
expect(screen.getByText('Through trips')).toBeInTheDocument()
expect(screen.getByText('lena')).toBeInTheDocument()
expect(screen.getByText('via Italy Trip')).toBeInTheDocument()
})
it('FE-JRN-SETTINGS-002: saves the edited title and subtitle', async () => {
const user = userEvent.setup()
const { onSaved } = mountDialog()
@@ -7,7 +7,7 @@ import { DeleteButton, DialogButton, DialogFooter, DialogHeader, DialogSection,
import { AddRowButton, EditorField, INPUT, Segmented } from '../shared/dialogParts'
import JourneyShareSection from './JourneyShareSection'
import { useJourneySettings } from './useJourneySettings'
import type { JourneyContributor, JourneyDetail } from '../../store/journeyStore'
import type { JourneyContributor, JourneyDetail, JourneyTripAccess } from '../../store/journeyStore'
import { pickGradient } from '../../pages/journeyDetail/JourneyDetailPage.helpers'
import { AddTripDialog } from './JourneyDetailPageAddTripDialog'
import ToggleSwitch from '../Settings/ToggleSwitch'
@@ -277,6 +277,8 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite,
</div>
<AddRowButton onClick={onOpenInvite}>{t('journey.contributors.invite')}</AddRowButton>
</DialogSection>
<TripAccessSection access={journey.trip_access} roleLabel={roleLabel} />
</div>
</div>
@@ -324,3 +326,32 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite,
</>
)
}
/**
* Who a trip's linked journal lets in (#2683), under its own heading below the
* contributors: shown here, but managed in that trip's settings, so read only.
*/
function TripAccessSection({ access, roleLabel }: {
access: JourneyTripAccess[] | undefined
roleLabel: (role: JourneyContributor['role']) => string
}) {
const { t } = useTranslation()
if (!access?.length) return null
return (
<DialogSection label={t('journey.contributors.viaTrips')}>
<p className="m-0 mb-2.5 leading-normal text-content-faint" style={fs(11.5)}>{t('journey.contributors.viaTripsHint')}</p>
<div className={ROWS}>
{access.map(a => (
<div key={`${a.user_id}-${a.trip_id}`} className={ROW}>
<TripMemberAvatar username={a.username} avatarUrl={avatarSrc(a.avatar)} size={28} />
<div className="min-w-0 flex-1">
<div className="truncate font-semibold text-content" style={fs(13, 'body')}>{a.username}</div>
<div className="truncate text-content-faint" style={fs(11)}>{t('journey.contributors.viaTrip', { trip: a.trip_title })}</div>
</div>
<span className={`${BADGE} bg-surface-tertiary text-content-muted`} style={fs(10.5)}>{roleLabel(a.role)}</span>
</div>
))}
</div>
</DialogSection>
)
}
@@ -1,8 +1,5 @@
import { useRef, useState } from 'react'
import {
X, Image, Plus, Trash2, UserPlus, Link as LinkIcon,
List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2,
} from 'lucide-react'
import { X, Image, Plus, Trash2, UserPlus, Link as LinkIcon, List, Grid3x3, MapPin, Route, Archive, ArchiveRestore, Undo2, Link2 } from 'lucide-react'
import MSheet from '../../components/MSheet'
import MIconBtn from '../../components/MIconBtn'
import MToggle from '../../components/MToggle'
@@ -237,6 +234,26 @@ export default function MJourneySettingsSheet({
{t('journey.contributors.invite')}
</button>
{/* Through a trip's linked journal (#2683): shown, but changed in that trip. */}
{!!journey.trip_access?.length && (
<>
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.contributors.viaTrips')}</div>
<p className="m-0 mb-[6px] text-caption text-m-faint">{t('journey.contributors.viaTripsHint')}</p>
{journey.trip_access.map(a => (
<div key={`${a.user_id}-${a.trip_id}`} className="flex items-center gap-[10px] px-[2px] py-1">
<Link2 size={15} className="flex-none text-m-muted" />
<span className="min-w-0 flex-1">
<span className="block truncate text-caption font-bold">{a.username}</span>
<span className="block truncate text-caption text-m-faint">{t('journey.contributors.viaTrip', { trip: a.trip_title })}</span>
</span>
<span className="rounded-full bg-[color:var(--m-ic)] px-[10px] py-[3px] font-geist text-caption font-extrabold text-m-muted">
{a.role}
</span>
</div>
))}
</>
)}
{/* Public share */}
<div className={`${eyebrow} mb-[6px] mt-[14px]`}>{t('journey.share.publicShare')}</div>
{!shareLink ? (
+12
View File
@@ -141,11 +141,23 @@ export interface JourneyContributor {
avatar?: string | null
}
/** Someone a trip's linked journal lets in, with the trip they come through (#2683). */
export interface JourneyTripAccess {
user_id: number
role: 'editor' | 'viewer'
username: string
avatar?: string | null
trip_id: number
trip_title: string
}
export interface JourneyDetail extends Journey {
entries: JourneyEntry[]
gallery: GalleryPhoto[]
trips: JourneyTrip[]
contributors: JourneyContributor[]
/** Who the trips' journal links let in besides the contributors, managed in those trips. */
trip_access?: JourneyTripAccess[]
stats: { entries: number; photos: number; places: number }
hide_skeletons?: boolean
/** How many suggestions were waved away one at a time, so the settings sheet can offer them back. */
@@ -89,6 +89,17 @@ describe('MJourneySettingsSheet', () => {
expect(screen.getByText('8 places')).toBeInTheDocument();
});
it('FE-MOB-JSET-2683: lists the people a linked trip lets in, and nothing without', async () => {
const { unmount } = setup();
expect(screen.queryByText('Through trips')).not.toBeInTheDocument();
unmount();
setup({ trip_access: [{ user_id: 7, role: 'editor', username: 'lena', avatar: null, trip_id: 3, trip_title: 'Tokyo trip' }] });
expect(await screen.findByText('Through trips')).toBeInTheDocument();
expect(screen.getByText('lena')).toBeInTheDocument();
expect(screen.getByText('via Tokyo trip')).toBeInTheDocument();
});
it('FE-MOB-JSET-002: shows the empty hint when no trip is linked', async () => {
setup({ trips: [] });
expect(await screen.findByText('No trips linked')).toBeInTheDocument();
@@ -1,6 +1,12 @@
import type { JourneyContributors } from '../entities/JourneyContributors.entity';
import type { DB } from '../kysely/db';
import { readLinkHideSkeletons, readLinkRole, type TripJournalLinkKyselyDB } from './TripJournalLinks.repository';
import {
linkReach,
readLinkHideSkeletons,
readLinkRole,
type TripJournalLinkKyselyDB,
type TripJournalLinkRole,
} from './TripJournalLinks.repository';
import { TrekRepository } from './_shared/trek-repository';
/** JG18's contributor-list row (`getJourneyFull`) — the junction plus the joined username/avatar. */
@@ -16,6 +22,16 @@ export interface JourneyContributorListRow {
/** The `journey_contributors`/`users` tables {@link JourneyContributorsRepository.listForJourney} reads. */
type JourneyContributorListKyselyDB = Pick<DB, 'journey_contributors' | 'users'>;
/** Someone a trip's journal link lets in, with the trip they come through (#2683). */
export interface JourneyTripAccessRow {
user_id: number;
role: TripJournalLinkRole;
username: string;
avatar: string | null;
trip_id: number;
trip_title: string;
}
/** The table the access reads below take a person's own contributor row from. */
type JourneyContributorRowKyselyDB = Pick<DB, 'journey_contributors'>;
@@ -79,6 +95,24 @@ export class JourneyContributorsRepository extends TrekRepository<JourneyContrib
.execute();
}
/**
* #2683 — the people the trips' journal links let into this journal, one row
* per trip they come through, leaving out whoever is a contributor already:
* the settings show them apart, since they are managed in the trip.
*/
async listTripAccessForJourney(journeyId: number): Promise<JourneyTripAccessRow[]> {
const direct = new Set((await this.listForJourney(journeyId)).map((c) => c.user_id));
const rows = await linkReach(this.kysely<TripJournalLinkKyselyDB>())
.innerJoin('users as u', 'u.id', 'm.user_id')
.select(['m.user_id', 'm.role', 'u.username', 'u.avatar', 't.id as trip_id', 't.title as trip_title'])
.where('l.journey_id', '=', journeyId)
.orderBy('u.username', 'asc')
.execute();
return rows
.filter((r) => !direct.has(r.user_id))
.map((r) => ({ ...r, role: r.role === 'editor' ? 'editor' : 'viewer' }));
}
/** JG18 — `getJourneyFull`'s contributor list, joined to `users` for `username`/`avatar` (`avatar_url` is computed in the SERVICE, unchanged). */
async listForJourney(journeyId: number): Promise<JourneyContributorListRow[]> {
const rows = await this.kysely<JourneyContributorListKyselyDB>()
@@ -270,12 +270,10 @@ export class JourneyDomainService {
// linked trips (JG17)
const trips = await this.journeyTripsRepo.listForJourney(journeyId);
// contributors (JG18)
const contributorsRaw = await this.contributorsRepo.listForJourney(journeyId);
const contributors = contributorsRaw.map((c) => ({
...c,
avatar_url: avatarUrl(c),
}));
const withAvatar = <R extends { avatar: string | null }>(rows: R[]) => rows.map((r) => ({ ...r, avatar_url: avatarUrl(r) }));
const contributors = withAvatar(await this.contributorsRepo.listForJourney(journeyId)); // JG18
// Who a trip's journal link lets in besides them (#2683), shown apart in the settings.
const tripAccess = withAvatar(await this.contributorsRepo.listTripAccessForJourney(journeyId));
// stats
const entryCount = entries.filter((e) => e.type === 'entry').length;
@@ -306,6 +304,7 @@ export class JourneyDomainService {
gallery,
trips,
contributors,
trip_access: tripAccess,
stats: { entries: entryCount, photos: photoCount, places: places.length },
hide_skeletons: hideSkeletons,
// What the eye toggle cannot say: how much was waved away one at a time, and
@@ -171,4 +171,21 @@ describe('TripJournalLinksRepository', () => {
expect(await links.findForTrip(s.trip.id)).toBeNull();
expect(await links.listMembers(s.trip.id)).toEqual([]);
});
it('TJLREPO-008: the journal lists who the links let in, per trip, apart from its contributors', async () => {
const s = await scene();
await links.upsertLink(s.trip.id, s.journal.id, s.owner.id, 1);
await links.replaceMembers(
s.trip.id,
[
{ user_id: s.tom.id, role: 'editor' },
{ user_id: s.lena.id, role: 'viewer' },
],
1,
);
await addJourneyContributor(t, s.journal.id, s.tom.id, 'viewer');
const access = await contributors.listTripAccessForJourney(s.journal.id);
expect(access.map((a) => [a.username, a.role, a.trip_id])).toEqual([['Lena', 'viewer', s.trip.id]]);
expect(access[0]?.trip_title).toBe(s.trip.title);
});
});
+3
View File
@@ -241,6 +241,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'عبر الرحلات',
'journey.contributors.viaTripsHint': 'يمكنهم فتح هذه اليوميات لأن رحلة ترتبط بها. غيّر ذلك من إعدادات الرحلة.',
'journey.contributors.viaTrip': 'عبر {trip}',
'journey.share.publicShare': 'Public Share', // en-fallback
'journey.share.createLink': 'Create share link', // en-fallback
'journey.share.linkCreated': 'Share link created', // en-fallback
+4
View File
@@ -206,6 +206,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username} bu səyahət gündəliyindən çıxarılsın?',
'journey.contributors.removed': 'İştirakçı çıxarıldı',
'journey.contributors.removeFailed': 'İştirakçını çıxarmaq mümkün olmadı',
'journey.contributors.viaTrips': 'Səyahətlər vasitəsilə',
'journey.contributors.viaTripsHint':
'Bu gündəliyi aça bilirlər, çünki bir səyahət onu bağlayır. Bunu səyahətin parametrlərində dəyişin.',
'journey.contributors.viaTrip': '{trip} vasitəsilə',
'journey.share.publicShare': 'İctimai paylaşım',
'journey.share.createLink': 'Paylaşım linki yarat',
+4
View File
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Por meio de viagens',
'journey.contributors.viaTripsHint':
'Podem abrir este diário porque uma viagem o vincula. Altere isso nas configurações da viagem.',
'journey.contributors.viaTrip': 'por {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': "Vols eliminar {username} d'aquesta travesia?",
'journey.contributors.removed': 'Col·laborador eliminat',
'journey.contributors.removeFailed': "No s'ha pogut eliminar el col·laborador",
'journey.contributors.viaTrips': 'A través de viatges',
'journey.contributors.viaTripsHint':
'Poden obrir aquest diari perquè un viatge el vincula. Canvia-ho a la configuració del viatge.',
'journey.contributors.viaTrip': 'a través de {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -270,6 +270,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Přes cesty',
'journey.contributors.viaTripsHint':
'Mohou tento deník otevřít, protože ho cesta propojuje. Změňte to v nastavení cesty.',
'journey.contributors.viaTrip': 'přes {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -180,6 +180,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username} aus dieser Journey entfernen?',
'journey.contributors.removed': 'Mitwirkender entfernt',
'journey.contributors.removeFailed': 'Entfernen fehlgeschlagen',
'journey.contributors.viaTrips': 'Über Reisen',
'journey.contributors.viaTripsHint':
'Sie können dieses Journal öffnen, weil eine Reise es verknüpft. Ändern lässt sich das in den Einstellungen der Reise.',
'journey.contributors.viaTrip': 'über {trip}',
'journey.share.publicShare': 'Öffentlicher Link',
'journey.share.createLink': 'Link erstellen',
'journey.share.linkCreated': 'Link erstellt',
+4
View File
@@ -188,6 +188,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?',
'journey.contributors.removed': 'Contributor removed',
'journey.contributors.removeFailed': 'Failed to remove contributor',
'journey.contributors.viaTrips': 'Through trips',
'journey.contributors.viaTripsHint':
"They can open this journal because a trip links it. Change that in the trip's settings.",
'journey.contributors.viaTrip': 'via {trip}',
'journey.share.publicShare': 'Public Share',
'journey.share.createLink': 'Create share link',
'journey.share.linkCreated': 'Share link created',
+4
View File
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'A través de viajes',
'journey.contributors.viaTripsHint':
'Pueden abrir este diario porque un viaje lo vincula. Cámbialo en los ajustes del viaje.',
'journey.contributors.viaTrip': 'a través de {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -190,6 +190,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Kas eemaldada {username} sellest reisiloost?',
'journey.contributors.removed': 'Kaasautor eemaldatud',
'journey.contributors.removeFailed': 'Kaasautori eemaldamine ebaõnnestus',
'journey.contributors.viaTrips': 'Reiside kaudu',
'journey.contributors.viaTripsHint':
'Nad saavad selle päeviku avada, sest reis seob selle. Muuda seda reisi seadetes.',
'journey.contributors.viaTrip': '{trip} kaudu',
'journey.share.publicShare': 'Avalik jagamine',
'journey.share.createLink': 'Loo jagamislink',
'journey.share.linkCreated': 'Jagamislink loodud',
+4
View File
@@ -265,6 +265,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Via des voyages',
'journey.contributors.viaTripsHint':
"Ces personnes peuvent ouvrir ce journal parce qu'un voyage le lie. Modifiez-le dans les paramètres du voyage.",
'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -183,6 +183,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Αφαίρεση {username} από αυτό το ταξίδι;',
'journey.contributors.removed': 'Ο συνεισφέρων αφαιρέθηκε',
'journey.contributors.removeFailed': 'Αποτυχία αφαίρεσης συνεισφέροντα',
'journey.contributors.viaTrips': 'Μέσω ταξιδιών',
'journey.contributors.viaTripsHint':
'Μπορούν να ανοίξουν αυτό το ημερολόγιο επειδή ένα ταξίδι το συνδέει. Αλλάξτε το στις ρυθμίσεις του ταξιδιού.',
'journey.contributors.viaTrip': 'μέσω {trip}',
'journey.share.publicShare': 'Δημόσια Κοινοποίηση',
'journey.share.createLink': 'Δημιουργία συνδέσμου κοινής χρήσης',
'journey.share.linkCreated': 'Ο σύνδεσμος κοινής χρήσης δημιουργήθηκε',
+4
View File
@@ -264,6 +264,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Utazásokon át',
'journey.contributors.viaTripsHint':
'Azért nyithatják meg ezt a naplót, mert egy utazás kapcsolja. Ezt az utazás beállításaiban módosíthatod.',
'journey.contributors.viaTrip': '{trip} útján',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -257,6 +257,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Melalui perjalanan',
'journey.contributors.viaTripsHint':
'Mereka bisa membuka jurnal ini karena sebuah perjalanan menautkannya. Ubah di pengaturan perjalanan.',
'journey.contributors.viaTrip': 'melalui {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -264,6 +264,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Tramite viaggi',
'journey.contributors.viaTripsHint':
'Possono aprire questo diario perché un viaggio lo collega. Modificalo nelle impostazioni del viaggio.',
'journey.contributors.viaTrip': 'tramite {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+3
View File
@@ -173,6 +173,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username}をこの日記から削除しますか?',
'journey.contributors.removed': '参加者を削除しました',
'journey.contributors.removeFailed': '削除に失敗しました',
'journey.contributors.viaTrips': '旅行経由',
'journey.contributors.viaTripsHint': '旅行がこのジャーナルをリンクしているため開けます。変更は旅行の設定で行います。',
'journey.contributors.viaTrip': '{trip} 経由',
'journey.share.publicShare': '公開共有',
'journey.share.createLink': '共有リンクを作成',
'journey.share.linkCreated': '共有リンクを作成しました',
+3
View File
@@ -172,6 +172,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '{username}을(를) 이 Journey에서 제거할까요?',
'journey.contributors.removed': '기여자가 제거되었습니다',
'journey.contributors.removeFailed': '기여자 제거 실패',
'journey.contributors.viaTrips': '여행을 통해',
'journey.contributors.viaTripsHint': '여행이 이 저널을 연결하고 있어 열 수 있습니다. 여행 설정에서 변경하세요.',
'journey.contributors.viaTrip': '{trip} 통해',
'journey.share.publicShare': '공개 공유',
'journey.share.createLink': '공유 링크 만들기',
'journey.share.linkCreated': '공유 링크가 생성되었습니다',
+4
View File
@@ -266,6 +266,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Via reizen',
'journey.contributors.viaTripsHint':
'Zij kunnen dit dagboek openen omdat een reis het koppelt. Wijzig dat in de instellingen van de reis.',
'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -278,6 +278,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Przez podróże',
'journey.contributors.viaTripsHint':
'Mogą otworzyć ten dziennik, bo podróż go łączy. Zmień to w ustawieniach podróży.',
'journey.contributors.viaTrip': 'przez {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -278,6 +278,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': 'Через поездки',
'journey.contributors.viaTripsHint':
'Они могут открыть этот дневник, потому что его привязала поездка. Измените это в настройках поездки.',
'journey.contributors.viaTrip': 'через {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+4
View File
@@ -193,6 +193,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Odstrániť {username} z tohto cestovného denníka?',
'journey.contributors.removed': 'Prispievateľ odstránený',
'journey.contributors.removeFailed': 'Odstránenie prispievateľa zlyhalo',
'journey.contributors.viaTrips': 'Cez cesty',
'journey.contributors.viaTripsHint':
'Môžu tento denník otvoriť, pretože ho cesta prepája. Zmeňte to v nastaveniach cesty.',
'journey.contributors.viaTrip': 'cez {trip}',
'journey.share.publicShare': 'Verejné zdieľanie',
'journey.share.createLink': 'Vytvoriť odkaz na zdieľanie',
'journey.share.linkCreated': 'Odkaz na zdieľanie vytvorený',
+4
View File
@@ -181,6 +181,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Ta bort {username} från denna journey?',
'journey.contributors.removed': 'Medverkare borttagen',
'journey.contributors.removeFailed': 'Kunde inte ta bort medverkare',
'journey.contributors.viaTrips': 'Via resor',
'journey.contributors.viaTripsHint':
'De kan öppna dagboken eftersom en resa kopplar den. Ändra det i resans inställningar.',
'journey.contributors.viaTrip': 'via {trip}', // same-as-en
'journey.share.publicShare': 'Offentlig delning',
'journey.share.createLink': 'Skapa en delningslänk',
'journey.share.linkCreated': 'Länk för delning har skapats',
+3
View File
@@ -183,6 +183,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'ลบ {username} ออกจากการเดินทางนี้หรือไม่',
'journey.contributors.removed': 'ผู้ร่วมให้ข้อมูลถูกลบออก',
'journey.contributors.removeFailed': 'ไม่สามารถลบผู้ร่วมให้ข้อมูลได้',
'journey.contributors.viaTrips': 'ผ่านทริป',
'journey.contributors.viaTripsHint': 'พวกเขาเปิดบันทึกนี้ได้เพราะทริปหนึ่งเชื่อมไว้ เปลี่ยนได้ในการตั้งค่าของทริป',
'journey.contributors.viaTrip': 'ผ่าน {trip}',
'journey.share.publicShare': 'แบ่งปันสาธารณะ',
'journey.share.createLink': 'สร้างลิงก์แชร์',
'journey.share.linkCreated': 'สร้างลิงก์แชร์แล้ว',
+4
View File
@@ -178,6 +178,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': "{username} bu journey'den kaldırılsın mı?",
'journey.contributors.removed': 'Katkıda bulunan kaldırıldı',
'journey.contributors.removeFailed': 'Katkıda bulunan kaldırılamadı',
'journey.contributors.viaTrips': 'Seyahatler üzerinden',
'journey.contributors.viaTripsHint':
'Bir seyahat bu günlüğü bağladığı için açabiliyorlar. Bunu seyahatin ayarlarından değiştir.',
'journey.contributors.viaTrip': '{trip} üzerinden',
'journey.share.publicShare': 'Herkese açık paylaşım',
'journey.share.createLink': 'Paylaşım bağlantısı oluştur',
'journey.share.linkCreated': 'Paylaşım bağlantısı oluşturuldu',
+4
View File
@@ -187,6 +187,10 @@ const journey: TranslationStrings = {
'journey.contributors.remove': 'Вилучити учасника',
'journey.contributors.removeConfirm': 'Вилучити {username} з цієї подорожі?',
'journey.contributors.removeFailed': 'Не вдалося вилучити учасника',
'journey.contributors.viaTrips': 'Через подорожі',
'journey.contributors.viaTripsHint':
'Вони можуть відкрити цей щоденник, бо його привʼязала подорож. Змініть це в налаштуваннях подорожі.',
'journey.contributors.viaTrip': 'через {trip}',
'journey.contributors.removed': 'Учасника вилучено',
'journey.share.publicShare': 'Публічний доступ',
'journey.share.createLink': 'Створити посилання для спільного доступу',
+4
View File
@@ -176,6 +176,10 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Xóa {username} khỏi hành trình này?',
'journey.contributors.removed': 'Đã xóa người đóng góp',
'journey.contributors.removeFailed': 'Không thể xóa người đóng góp',
'journey.contributors.viaTrips': 'Qua chuyến đi',
'journey.contributors.viaTripsHint':
'Họ có thể mở nhật ký này vì một chuyến đi liên kết nó. Thay đổi trong cài đặt của chuyến đi.',
'journey.contributors.viaTrip': 'qua {trip}',
'journey.share.publicShare': 'Chia sẻ công khai',
'journey.share.createLink': 'Tạo liên kết chia sẻ',
'journey.share.linkCreated': 'Đã tạo liên kết chia sẻ',
+3
View File
@@ -253,6 +253,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': 'Remove {username} from this journey?', // en-fallback
'journey.contributors.removed': 'Contributor removed', // en-fallback
'journey.contributors.removeFailed': 'Failed to remove contributor', // en-fallback
'journey.contributors.viaTrips': '透過旅行',
'journey.contributors.viaTripsHint': '因為某個旅行連結了此日誌,他們可以開啟它。請在該旅行的設定中變更。',
'journey.contributors.viaTrip': '透過 {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback
+3
View File
@@ -253,6 +253,9 @@ const journey: TranslationStrings = {
'journey.contributors.removeConfirm': '要将 {username} 从此旅程中移除吗?',
'journey.contributors.removed': '已移除贡献者',
'journey.contributors.removeFailed': '移除贡献者失败',
'journey.contributors.viaTrips': '通过旅行',
'journey.contributors.viaTripsHint': '因为某个旅行关联了此日志,他们可以打开它。请在该旅行的设置中更改。',
'journey.contributors.viaTrip': '通过 {trip}',
'journey.editor.externalPhotos': 'External photos', // en-fallback
'journey.editor.externalPhotosFor': 'Photos for {date}', // en-fallback
'journey.editor.externalPhotosNearby': 'Nearby photos first', // en-fallback