feat(flashcards): révision SM-2, génération IA et page /revision
Livre US-FLASHCARDS avec decks, session de révision, stats et migration Prisma. Finalise le Web Clipper (i18n 15 langues) et corrige les erreurs ESLint bloquant la CI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,11 +14,11 @@
|
|||||||
| **US-LIVING-BLOCKS** | Blocs Vivants (Transclusion Bidirectionnelle) | ✅ **LIVRÉ** | `tiptap-unique-id-extension.ts`, `tiptap-live-block-extension.tsx`, `block-picker.tsx`, `app/api/blocks/*`, migration `LiveBlockRef` |
|
| **US-LIVING-BLOCKS** | Blocs Vivants (Transclusion Bidirectionnelle) | ✅ **LIVRÉ** | `tiptap-unique-id-extension.ts`, `tiptap-live-block-extension.tsx`, `block-picker.tsx`, `app/api/blocks/*`, migration `LiveBlockRef` |
|
||||||
| **US-MEMORY-ECHO** | Résonance Sémantique + Embed depuis Echo | ✅ **LIVRÉ** | `memory-echo-section.tsx`, `/api/notes/[id]/live-block-refs`, `/api/blocks/resolve` |
|
| **US-MEMORY-ECHO** | Résonance Sémantique + Embed depuis Echo | ✅ **LIVRÉ** | `memory-echo-section.tsx`, `/api/notes/[id]/live-block-refs`, `/api/blocks/resolve` |
|
||||||
| **US-INFO-RÉSEAU** | Panneau Info + Réseau Local | ✅ **LIVRÉ** | `note-network-tab.tsx`, `sync-note-links.ts`, migration `NoteLink`, picker `[[` |
|
| **US-INFO-RÉSEAU** | Panneau Info + Réseau Local | ✅ **LIVRÉ** | `note-network-tab.tsx`, `sync-note-links.ts`, migration `NoteLink`, picker `[[` |
|
||||||
| **US-CLIPPER** | Web Clipper | 🚧 **En cours** | `extension/`, `/api/clip/*`, migration `sourceUrl`, badge panneau Info |
|
| **US-CLIPPER** | Web Clipper | ✅ **LIVRÉ** | `extension/`, `/api/clip/*`, migration `sourceUrl`, badge panneau Info |
|
||||||
| **US-GRAPH** | Graphe de Connaissance Global enrichi | ✅ **LIVRÉ** | `note-graph-view.tsx` — filtres liens, seuil sémantique, focus voisinage, couleurs carnets, double-clic ouverture |
|
| **US-GRAPH** | Graphe de Connaissance Global enrichi | ✅ **LIVRÉ** | `note-graph-view.tsx` — filtres liens, seuil sémantique, focus voisinage, couleurs carnets, double-clic ouverture |
|
||||||
| **US-INSIGHTS** | Clusters Sémantiques + Bridge Notes | 🚧 **EN COURS** | clusters en base mais page masquait les résultats périmés — correction affichage |
|
| **US-INSIGHTS** | Clusters Sémantiques + Bridge Notes | 🚧 **EN COURS** | clusters en base mais page masquait les résultats périmés — correction affichage |
|
||||||
| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏳ À faire | — |
|
| **US-TEMPORAL** | Prédictions d'accès temporelles | ⏳ À faire | — |
|
||||||
| **US-FLASHCARDS** | Révision IA — Répétition espacée SM-2 | ⏳ À faire | — |
|
| **US-FLASHCARDS** | Révision IA — Répétition espacée SM-2 | ✅ **LIVRÉ** | `/revision`, `/api/flashcards/*`, SM-2, génération IA depuis l'éditeur |
|
||||||
| **US-STRUCTURED-VIEWS** | Vues Structurées (Tableau/Kanban/Galerie) | ⏳ À faire | — |
|
| **US-STRUCTURED-VIEWS** | Vues Structurées (Tableau/Kanban/Galerie) | ⏳ À faire | — |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
16
memento-note/app/(main)/revision/page.tsx
Normal file
16
memento-note/app/(main)/revision/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
|
const RevisionView = dynamic(
|
||||||
|
() => import('@/components/flashcards/revision-view').then((m) => m.RevisionView),
|
||||||
|
{ ssr: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function RevisionPage() {
|
||||||
|
return (
|
||||||
|
<div className="h-full min-h-0 flex flex-col">
|
||||||
|
<RevisionView />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -157,7 +157,7 @@ Response format (COPY this structure):
|
|||||||
let parsed: SuggestChartsResponse
|
let parsed: SuggestChartsResponse
|
||||||
try {
|
try {
|
||||||
// Clean the response - remove markdown code blocks
|
// Clean the response - remove markdown code blocks
|
||||||
let cleanText = text
|
const cleanText = text
|
||||||
.replace(/```json\n?/gi, '')
|
.replace(/```json\n?/gi, '')
|
||||||
.replace(/```\n?/gi, '')
|
.replace(/```\n?/gi, '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
65
memento-note/app/api/flashcards/[id]/review/route.ts
Normal file
65
memento-note/app/api/flashcards/[id]/review/route.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { computeSm2Update } from '@/lib/flashcards/sm2'
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: cardId } = await params
|
||||||
|
const body = await request.json()
|
||||||
|
const grade = typeof body.grade === 'number' ? body.grade : Number(body.grade)
|
||||||
|
|
||||||
|
if (!Number.isFinite(grade) || grade < 1 || grade > 4) {
|
||||||
|
return NextResponse.json({ error: 'Grade must be 1–4' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = await prisma.flashcard.findFirst({
|
||||||
|
where: {
|
||||||
|
id: cardId,
|
||||||
|
deck: { userId: session.user.id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!card) {
|
||||||
|
return NextResponse.json({ error: 'Card not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = computeSm2Update(grade, {
|
||||||
|
easinessFactor: card.easinessFactor,
|
||||||
|
interval: card.interval,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [savedCard] = await prisma.$transaction([
|
||||||
|
prisma.flashcard.update({
|
||||||
|
where: { id: cardId },
|
||||||
|
data: {
|
||||||
|
easinessFactor: updated.easinessFactor,
|
||||||
|
interval: updated.interval,
|
||||||
|
nextReviewAt: updated.nextReviewAt,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.flashcardReview.create({
|
||||||
|
data: { cardId, grade: Math.round(grade) },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
card: {
|
||||||
|
id: savedCard.id,
|
||||||
|
interval: savedCard.interval,
|
||||||
|
easinessFactor: savedCard.easinessFactor,
|
||||||
|
nextReviewAt: savedCard.nextReviewAt.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/[id]/review]', error)
|
||||||
|
return NextResponse.json({ error: 'Review failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
37
memento-note/app/api/flashcards/decks/[id]/route.ts
Normal file
37
memento-note/app/api/flashcards/decks/[id]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import { getDeckDetail } from '@/lib/flashcards/deck-queries'
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params
|
||||||
|
const deck = await getDeckDetail(session.user.id, id)
|
||||||
|
if (!deck) {
|
||||||
|
return NextResponse.json({ error: 'Deck not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const dueCount = deck.cards.filter((c) => c.due).length
|
||||||
|
const masteredCount = deck.cards.filter((c) => c.mastered).length
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
deck,
|
||||||
|
stats: {
|
||||||
|
total: deck.cards.length,
|
||||||
|
due: dueCount,
|
||||||
|
mastered: masteredCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/decks/[id] GET]', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to load deck' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
51
memento-note/app/api/flashcards/decks/route.ts
Normal file
51
memento-note/app/api/flashcards/decks/route.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import { listDeckSummaries } from '@/lib/flashcards/deck-queries'
|
||||||
|
import { getOrCreateDeckForNotebook } from '@/lib/flashcards/deck-utils'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const decks = await listDeckSummaries(session.user.id)
|
||||||
|
return NextResponse.json({ decks })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/decks GET]', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to load decks' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const name = typeof body.name === 'string' ? body.name.trim() : ''
|
||||||
|
if (!name) {
|
||||||
|
return NextResponse.json({ error: 'Name required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const deck = await getOrCreateDeckForNotebook({
|
||||||
|
userId: session.user.id,
|
||||||
|
notebookId: null,
|
||||||
|
manualName: name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
deck: {
|
||||||
|
id: deck.id,
|
||||||
|
name: deck.name,
|
||||||
|
notebookId: deck.notebookId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/decks POST]', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to create deck' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
81
memento-note/app/api/flashcards/generate/route.ts
Normal file
81
memento-note/app/api/flashcards/generate/route.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { getAISettings } from '@/app/actions/ai-settings'
|
||||||
|
import { checkEntitlementOrThrow, QuotaExceededError, incrementUsageAsync } from '@/lib/entitlements'
|
||||||
|
import { hasUserAiConsent } from '@/lib/consent/server-consent'
|
||||||
|
import { generateFlashcardsFromNote, type FlashcardStyle } from '@/lib/flashcards/generate-flashcards'
|
||||||
|
import { stripHtmlToText } from '@/lib/flashcards/deck-utils'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await hasUserAiConsent())) {
|
||||||
|
return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userSettings = await getAISettings(session.user.id)
|
||||||
|
if (userSettings.paragraphRefactor === false) {
|
||||||
|
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const noteId = typeof body.noteId === 'string' ? body.noteId : ''
|
||||||
|
const count = typeof body.count === 'number' ? body.count : 10
|
||||||
|
const styleRaw = typeof body.style === 'string' ? body.style : 'qa'
|
||||||
|
const style: FlashcardStyle = ['qa', 'cloze', 'concept'].includes(styleRaw)
|
||||||
|
? (styleRaw as FlashcardStyle)
|
||||||
|
: 'qa'
|
||||||
|
|
||||||
|
if (!noteId) {
|
||||||
|
return NextResponse.json({ error: 'noteId required' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await prisma.note.findFirst({
|
||||||
|
where: { id: noteId, userId: session.user.id, trashedAt: null },
|
||||||
|
select: { id: true, title: true, content: true, language: true },
|
||||||
|
})
|
||||||
|
if (!note) {
|
||||||
|
return NextResponse.json({ error: 'Note not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = stripHtmlToText(note.content)
|
||||||
|
if (textContent.length < 80) {
|
||||||
|
return NextResponse.json({ error: 'Not enough content to generate flashcards' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await checkEntitlementOrThrow(session.user.id, 'reformulate')
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof QuotaExceededError) {
|
||||||
|
const isTierLocked = err.currentQuota === 0
|
||||||
|
return NextResponse.json({
|
||||||
|
error: isTierLocked ? 'feature_locked' : 'quota_exceeded',
|
||||||
|
errorKey: isTierLocked ? 'ai.featureLocked' : 'ai.quotaExceeded',
|
||||||
|
quotaExceeded: true,
|
||||||
|
}, { status: 402 })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = await generateFlashcardsFromNote({
|
||||||
|
title: note.title || 'Untitled',
|
||||||
|
textContent,
|
||||||
|
count,
|
||||||
|
style,
|
||||||
|
language: note.language || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
incrementUsageAsync(session.user.id, 'reformulate')
|
||||||
|
|
||||||
|
return NextResponse.json({ cards, noteId: note.id, style })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/generate]', error)
|
||||||
|
const message = error instanceof Error ? error.message : 'Generation failed'
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
108
memento-note/app/api/flashcards/save/route.ts
Normal file
108
memento-note/app/api/flashcards/save/route.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { getOrCreateDeckForNotebook } from '@/lib/flashcards/deck-utils'
|
||||||
|
import type { FlashcardStyle } from '@/lib/flashcards/generate-flashcards'
|
||||||
|
|
||||||
|
interface CardInput {
|
||||||
|
front: string
|
||||||
|
back: string
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json()
|
||||||
|
const noteId = typeof body.noteId === 'string' ? body.noteId : null
|
||||||
|
const deckIdInput = typeof body.deckId === 'string' ? body.deckId : null
|
||||||
|
const cards = Array.isArray(body.cards) ? body.cards as CardInput[] : []
|
||||||
|
|
||||||
|
if (cards.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'No cards to save' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
let notebookId: string | null = null
|
||||||
|
let fallbackDeckName: string | undefined
|
||||||
|
if (noteId) {
|
||||||
|
const note = await prisma.note.findFirst({
|
||||||
|
where: { id: noteId, userId: session.user.id },
|
||||||
|
select: { notebookId: true, title: true },
|
||||||
|
})
|
||||||
|
if (!note) {
|
||||||
|
return NextResponse.json({ error: 'Note not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
notebookId = note.notebookId
|
||||||
|
if (!notebookId) {
|
||||||
|
fallbackDeckName = note.title?.trim() || 'General'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let deckId = deckIdInput
|
||||||
|
if (!deckId) {
|
||||||
|
if (noteId) {
|
||||||
|
const existingFromNote = await prisma.flashcard.findFirst({
|
||||||
|
where: { noteId, deck: { userId: session.user.id } },
|
||||||
|
select: { deckId: true },
|
||||||
|
})
|
||||||
|
if (existingFromNote) {
|
||||||
|
deckId = existingFromNote.deckId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!deckId) {
|
||||||
|
const deck = await getOrCreateDeckForNotebook({
|
||||||
|
userId: session.user.id,
|
||||||
|
notebookId,
|
||||||
|
manualName: fallbackDeckName,
|
||||||
|
})
|
||||||
|
deckId = deck.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const deck = await prisma.flashcardDeck.findFirst({
|
||||||
|
where: { id: deckId, userId: session.user.id },
|
||||||
|
})
|
||||||
|
if (!deck) {
|
||||||
|
return NextResponse.json({ error: 'Deck not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = cards
|
||||||
|
.map((c) => ({
|
||||||
|
front: typeof c.front === 'string' ? c.front.trim().slice(0, 500) : '',
|
||||||
|
back: typeof c.back === 'string' ? c.back.trim().slice(0, 800) : '',
|
||||||
|
type: (['qa', 'cloze', 'concept'].includes(c.type || '') ? c.type : 'qa') as FlashcardStyle,
|
||||||
|
}))
|
||||||
|
.filter((c) => c.front && c.back)
|
||||||
|
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
return NextResponse.json({ error: 'No valid cards' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.flashcard.createMany({
|
||||||
|
data: sanitized.map((c) => ({
|
||||||
|
deckId: deckId!,
|
||||||
|
noteId,
|
||||||
|
front: c.front,
|
||||||
|
back: c.back,
|
||||||
|
type: c.type,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.flashcardDeck.update({
|
||||||
|
where: { id: deckId! },
|
||||||
|
data: { updatedAt: new Date() },
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
deckId,
|
||||||
|
savedCount: sanitized.length,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/save]', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to save flashcards' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
80
memento-note/app/api/flashcards/stats/route.ts
Normal file
80
memento-note/app/api/flashcards/stats/route.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { isCardMastered } from '@/lib/flashcards/sm2'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const [reviews, allCards] = await Promise.all([
|
||||||
|
prisma.flashcardReview.findMany({
|
||||||
|
where: { card: { deck: { userId } } },
|
||||||
|
select: { reviewedAt: true, grade: true },
|
||||||
|
orderBy: { reviewedAt: 'desc' },
|
||||||
|
take: 500,
|
||||||
|
}),
|
||||||
|
prisma.flashcard.findMany({
|
||||||
|
where: { deck: { userId } },
|
||||||
|
select: { id: true, interval: true, easinessFactor: true, front: true, deck: { select: { name: true } } },
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
const heatmapMap = new Map<string, number>()
|
||||||
|
for (const r of reviews) {
|
||||||
|
const day = r.reviewedAt.toISOString().slice(0, 10)
|
||||||
|
heatmapMap.set(day, (heatmapMap.get(day) || 0) + 1)
|
||||||
|
}
|
||||||
|
const heatmap = Array.from(heatmapMap.entries())
|
||||||
|
.map(([date, count]) => ({ date, count }))
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date))
|
||||||
|
.slice(-90)
|
||||||
|
|
||||||
|
const totalCards = allCards.length
|
||||||
|
const masteredCount = allCards.filter((c) => isCardMastered(c.interval)).length
|
||||||
|
const retentionRate = totalCards > 0 ? Math.round((masteredCount / totalCards) * 100) : 0
|
||||||
|
|
||||||
|
const weekMs = 7 * 24 * 60 * 60 * 1000
|
||||||
|
const retentionByWeek: { week: string; rate: number }[] = []
|
||||||
|
for (let i = 7; i >= 0; i--) {
|
||||||
|
const weekEnd = new Date(now.getTime() - i * weekMs)
|
||||||
|
const weekStart = new Date(weekEnd.getTime() - weekMs)
|
||||||
|
const cardsAtWeek = allCards.filter((c) => c.interval >= 7 * (8 - i))
|
||||||
|
const masteredAtWeek = cardsAtWeek.filter((c) => isCardMastered(c.interval)).length
|
||||||
|
const rate = cardsAtWeek.length > 0
|
||||||
|
? Math.round((masteredAtWeek / cardsAtWeek.length) * 100)
|
||||||
|
: retentionRate
|
||||||
|
retentionByWeek.push({
|
||||||
|
week: weekStart.toISOString().slice(0, 10),
|
||||||
|
rate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const difficultCards = [...allCards]
|
||||||
|
.sort((a, b) => a.easinessFactor - b.easinessFactor)
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
front: c.front.slice(0, 120),
|
||||||
|
easinessFactor: c.easinessFactor,
|
||||||
|
deckName: c.deck.name,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
heatmap,
|
||||||
|
retentionRate,
|
||||||
|
retentionByWeek,
|
||||||
|
difficultCards,
|
||||||
|
totalReviews: reviews.length,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[flashcards/stats]', error)
|
||||||
|
return NextResponse.json({ error: 'Failed to load stats' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ export async function POST() {
|
|||||||
if (wikilinks.length === 0) continue
|
if (wikilinks.length === 0) continue
|
||||||
|
|
||||||
for (const { title, snippet } of wikilinks) {
|
for (const { title, snippet } of wikilinks) {
|
||||||
let targetNote = await prisma.note.findFirst({
|
const targetNote = await prisma.note.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
title: { equals: title, mode: 'insensitive' },
|
title: { equals: title, mode: 'insensitive' },
|
||||||
|
|||||||
221
memento-note/components/flashcards/flashcard-generate-dialog.tsx
Normal file
221
memento-note/components/flashcards/flashcard-generate-dialog.tsx
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { GraduationCap, Loader2, Sparkles, X } from 'lucide-react'
|
||||||
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
import { useAiConsent } from '@/components/legal/ai-consent-provider'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type FlashcardStyle = 'qa' | 'cloze' | 'concept'
|
||||||
|
|
||||||
|
export interface PreviewCard {
|
||||||
|
front: string
|
||||||
|
back: string
|
||||||
|
type: FlashcardStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlashcardGenerateDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
noteId: string
|
||||||
|
noteTitle: string
|
||||||
|
onSaved?: (deckId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlashcardGenerateDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
noteId,
|
||||||
|
noteTitle,
|
||||||
|
onSaved,
|
||||||
|
}: FlashcardGenerateDialogProps) {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
const { requestAiConsent } = useAiConsent()
|
||||||
|
const [count, setCount] = useState(10)
|
||||||
|
const [style, setStyle] = useState<FlashcardStyle>('qa')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [cards, setCards] = useState<PreviewCard[] | null>(null)
|
||||||
|
|
||||||
|
const handleGenerate = useCallback(async () => {
|
||||||
|
if (!(await requestAiConsent())) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/flashcards/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ noteId, count, style }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) {
|
||||||
|
if (data.errorKey) {
|
||||||
|
toast.error(t(data.errorKey) || data.error)
|
||||||
|
} else {
|
||||||
|
toast.error(data.error || t('flashcards.generateFailed'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCards(data.cards || [])
|
||||||
|
} catch {
|
||||||
|
toast.error(t('flashcards.generateFailed'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [count, noteId, requestAiConsent, style, t])
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
if (!cards?.length) return
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/flashcards/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ noteId, cards }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(data.error || t('flashcards.saveFailed'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.success(t('flashcards.savedCount', { count: data.savedCount }))
|
||||||
|
onSaved?.(data.deckId)
|
||||||
|
onClose()
|
||||||
|
setCards(null)
|
||||||
|
} catch {
|
||||||
|
toast.error(t('flashcards.saveFailed'))
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}, [cards, noteId, onClose, onSaved, t])
|
||||||
|
|
||||||
|
const updateCard = (index: number, field: 'front' | 'back', value: string) => {
|
||||||
|
setCards((prev) => {
|
||||||
|
if (!prev) return prev
|
||||||
|
const next = [...prev]
|
||||||
|
next[index] = { ...next[index], [field]: value }
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-card border border-border rounded-2xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-hidden flex flex-col"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-border/60">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GraduationCap size={18} className="text-brand-accent" />
|
||||||
|
<div>
|
||||||
|
<h2 className="text-sm font-semibold">{t('flashcards.generateTitle')}</h2>
|
||||||
|
<p className="text-[11px] text-muted-foreground truncate max-w-[280px]">{noteTitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} className="p-1.5 rounded-lg hover:bg-black/5">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-5 space-y-5">
|
||||||
|
{!cards ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="text-[11px] font-bold uppercase tracking-wider text-concrete block mb-2">
|
||||||
|
{t('flashcards.cardCount')} ({count})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={5}
|
||||||
|
max={20}
|
||||||
|
value={count}
|
||||||
|
onChange={(e) => setCount(Number(e.target.value))}
|
||||||
|
className="w-full accent-brand-accent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[11px] font-bold uppercase tracking-wider text-concrete block mb-2">
|
||||||
|
{t('flashcards.styleLabel')}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(['qa', 'cloze', 'concept'] as const).map((s) => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStyle(s)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
|
||||||
|
style === s
|
||||||
|
? 'bg-brand-accent/10 border-brand-accent/40 text-brand-accent'
|
||||||
|
: 'border-border text-muted-foreground hover:border-brand-accent/30',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`flashcards.style.${s}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('flashcards.previewHint')}</p>
|
||||||
|
{cards.map((card, i) => (
|
||||||
|
<div key={i} className="p-3 rounded-xl border border-border/60 space-y-2 bg-paper/30">
|
||||||
|
<input
|
||||||
|
value={card.front}
|
||||||
|
onChange={(e) => updateCard(i, 'front', e.target.value)}
|
||||||
|
className="w-full text-sm font-medium bg-transparent border-b border-border/40 pb-1 outline-none"
|
||||||
|
placeholder={t('flashcards.frontPlaceholder')}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
value={card.back}
|
||||||
|
onChange={(e) => updateCard(i, 'back', e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
className="w-full text-xs text-muted-foreground bg-transparent outline-none resize-none"
|
||||||
|
placeholder={t('flashcards.backPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 py-4 border-t border-border/60 flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-xs font-medium rounded-lg border border-border hover:bg-black/[0.03]"
|
||||||
|
>
|
||||||
|
{t('general.cancel')}
|
||||||
|
</button>
|
||||||
|
{!cards ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-xs font-bold rounded-lg bg-brand-accent text-white flex items-center gap-1.5 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
||||||
|
{t('flashcards.generateAction')}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="px-4 py-2 text-xs font-bold rounded-lg bg-brand-accent text-white flex items-center gap-1.5 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{saving && <Loader2 size={14} className="animate-spin" />}
|
||||||
|
{t('flashcards.confirmSave')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
69
memento-note/components/flashcards/revision-heatmap.tsx
Normal file
69
memento-note/components/flashcards/revision-heatmap.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
|
||||||
|
interface HeatmapDay {
|
||||||
|
date: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RevisionHeatmapProps {
|
||||||
|
data: HeatmapDay[]
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function intensityClass(count: number, max: number): string {
|
||||||
|
if (count <= 0) return 'bg-black/[0.04] dark:bg-white/[0.06]'
|
||||||
|
const ratio = count / Math.max(max, 1)
|
||||||
|
if (ratio >= 0.75) return 'bg-brand-accent'
|
||||||
|
if (ratio >= 0.5) return 'bg-brand-accent/70'
|
||||||
|
if (ratio >= 0.25) return 'bg-brand-accent/40'
|
||||||
|
return 'bg-brand-accent/20'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RevisionHeatmap({ data, className }: RevisionHeatmapProps) {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
|
||||||
|
const { cells, maxCount } = useMemo(() => {
|
||||||
|
const map = new Map(data.map((d) => [d.date, d.count]))
|
||||||
|
const today = new Date()
|
||||||
|
const cells: { date: string; count: number; label: string }[] = []
|
||||||
|
for (let i = 89; i >= 0; i--) {
|
||||||
|
const d = new Date(today)
|
||||||
|
d.setDate(d.getDate() - i)
|
||||||
|
const key = d.toISOString().slice(0, 10)
|
||||||
|
cells.push({
|
||||||
|
date: key,
|
||||||
|
count: map.get(key) || 0,
|
||||||
|
label: d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const maxCount = Math.max(1, ...cells.map((c) => c.count))
|
||||||
|
return { cells, maxCount }
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('space-y-3', className)}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-widest text-concrete">
|
||||||
|
{t('flashcards.heatmapTitle')}
|
||||||
|
</p>
|
||||||
|
<span className="text-[10px] text-concrete/60">{t('flashcards.heatmapLast90')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[repeat(15,minmax(0,1fr))] gap-1 sm:grid-cols-[repeat(18,minmax(0,1fr))]">
|
||||||
|
{cells.map((cell) => (
|
||||||
|
<div
|
||||||
|
key={cell.date}
|
||||||
|
title={`${cell.label}: ${cell.count}`}
|
||||||
|
className={cn(
|
||||||
|
'aspect-square rounded-[3px] transition-colors',
|
||||||
|
intensityClass(cell.count, maxCount),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
575
memento-note/components/flashcards/revision-view.tsx
Normal file
575
memento-note/components/flashcards/revision-view.tsx
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'motion/react'
|
||||||
|
import {
|
||||||
|
GraduationCap,
|
||||||
|
Layers,
|
||||||
|
ArrowLeft,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar,
|
||||||
|
Plus,
|
||||||
|
BarChart3,
|
||||||
|
Loader2,
|
||||||
|
BookOpen,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import { useLanguage } from '@/lib/i18n'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { NoteChart } from '@/components/note-chart'
|
||||||
|
import { RevisionHeatmap } from './revision-heatmap'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
interface DeckSummary {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
notebookId: string | null
|
||||||
|
totalCards: number
|
||||||
|
dueCount: number
|
||||||
|
masteredCount: number
|
||||||
|
lastReviewedAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlashcardItem {
|
||||||
|
id: string
|
||||||
|
front: string
|
||||||
|
back: string
|
||||||
|
type: string
|
||||||
|
due: boolean
|
||||||
|
mastered: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatsResponse {
|
||||||
|
heatmap: { date: string; count: number }[]
|
||||||
|
retentionRate: number
|
||||||
|
retentionByWeek: { week: string; rate: number }[]
|
||||||
|
difficultCards: { id: string; front: string; easinessFactor: number; deckName: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageTab = 'decks' | 'progress'
|
||||||
|
type SessionGrade = 1 | 2 | 3 | 4
|
||||||
|
|
||||||
|
export function RevisionView() {
|
||||||
|
const { t } = useLanguage()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const initialDeckId = searchParams.get('deckId')
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<PageTab>('decks')
|
||||||
|
const [decks, setDecks] = useState<DeckSummary[]>([])
|
||||||
|
const [loadingDecks, setLoadingDecks] = useState(true)
|
||||||
|
const [stats, setStats] = useState<StatsResponse | null>(null)
|
||||||
|
const [loadingStats, setLoadingStats] = useState(false)
|
||||||
|
|
||||||
|
const [activeDeckId, setActiveDeckId] = useState<string | null>(null)
|
||||||
|
const [deckCards, setDeckCards] = useState<FlashcardItem[]>([])
|
||||||
|
const [deckStats, setDeckStats] = useState({ total: 0, due: 0, mastered: 0 })
|
||||||
|
const [loadingDeck, setLoadingDeck] = useState(false)
|
||||||
|
|
||||||
|
const [isSessionActive, setIsSessionActive] = useState(false)
|
||||||
|
const [isSessionFinished, setIsSessionFinished] = useState(false)
|
||||||
|
const [sessionCards, setSessionCards] = useState<FlashcardItem[]>([])
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
|
const [isFlipped, setIsFlipped] = useState(false)
|
||||||
|
const [sessionGrades, setSessionGrades] = useState<Record<string, SessionGrade>>({})
|
||||||
|
const [reviewing, setReviewing] = useState(false)
|
||||||
|
|
||||||
|
const [newDeckName, setNewDeckName] = useState('')
|
||||||
|
const [creatingDeck, setCreatingDeck] = useState(false)
|
||||||
|
|
||||||
|
const loadDecks = useCallback(async () => {
|
||||||
|
setLoadingDecks(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/flashcards/decks')
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) setDecks(data.decks || [])
|
||||||
|
} finally {
|
||||||
|
setLoadingDecks(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadStats = useCallback(async () => {
|
||||||
|
setLoadingStats(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/flashcards/stats')
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) setStats(data)
|
||||||
|
} finally {
|
||||||
|
setLoadingStats(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadDeck = useCallback(async (deckId: string) => {
|
||||||
|
setLoadingDeck(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/flashcards/decks/${deckId}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) {
|
||||||
|
toast.error(data.error || t('flashcards.loadDeckFailed'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setActiveDeckId(deckId)
|
||||||
|
setDeckCards(data.deck.cards || [])
|
||||||
|
setDeckStats(data.stats)
|
||||||
|
} finally {
|
||||||
|
setLoadingDeck(false)
|
||||||
|
}
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDecks()
|
||||||
|
}, [loadDecks])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tab === 'progress') loadStats()
|
||||||
|
}, [tab, loadStats])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialDeckId && !activeDeckId) {
|
||||||
|
loadDeck(initialDeckId)
|
||||||
|
}
|
||||||
|
}, [initialDeckId, activeDeckId, loadDeck])
|
||||||
|
|
||||||
|
const activeDeck = useMemo(
|
||||||
|
() => decks.find((d) => d.id === activeDeckId),
|
||||||
|
[decks, activeDeckId],
|
||||||
|
)
|
||||||
|
|
||||||
|
const sessionChartData = useMemo(() => {
|
||||||
|
const counts = { 1: 0, 2: 0, 3: 0, 4: 0 }
|
||||||
|
Object.values(sessionGrades).forEach((g) => { counts[g] += 1 })
|
||||||
|
return [
|
||||||
|
{ label: t('flashcards.grade.hard'), value: counts[1] },
|
||||||
|
{ label: t('flashcards.grade.difficult'), value: counts[2] },
|
||||||
|
{ label: t('flashcards.grade.good'), value: counts[3] },
|
||||||
|
{ label: t('flashcards.grade.easy'), value: counts[4] },
|
||||||
|
]
|
||||||
|
}, [sessionGrades, t])
|
||||||
|
|
||||||
|
const startSession = useCallback((deckId: string, cardsInput?: FlashcardItem[], dueOnly = true) => {
|
||||||
|
const cards = cardsInput ?? (activeDeckId === deckId ? deckCards : [])
|
||||||
|
if (cards.length === 0) {
|
||||||
|
void loadDeck(deckId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let toReview = dueOnly ? cards.filter((c) => c.due) : cards
|
||||||
|
if (toReview.length === 0) toReview = cards
|
||||||
|
const shuffled = [...toReview].sort(() => Math.random() - 0.5)
|
||||||
|
setSessionCards(shuffled)
|
||||||
|
setCurrentIndex(0)
|
||||||
|
setIsFlipped(false)
|
||||||
|
setSessionGrades({})
|
||||||
|
setIsSessionActive(true)
|
||||||
|
setIsSessionFinished(false)
|
||||||
|
setActiveDeckId(deckId)
|
||||||
|
}, [activeDeckId, deckCards, loadDeck])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialDeckId) return
|
||||||
|
let cancelled = false
|
||||||
|
;(async () => {
|
||||||
|
const res = await fetch(`/api/flashcards/decks/${initialDeckId}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (cancelled || !res.ok) return
|
||||||
|
const cards: FlashcardItem[] = data.deck?.cards || []
|
||||||
|
setActiveDeckId(initialDeckId)
|
||||||
|
setDeckCards(cards)
|
||||||
|
setDeckStats(data.stats || { total: 0, due: 0, mastered: 0 })
|
||||||
|
startSession(initialDeckId, cards)
|
||||||
|
})()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [initialDeckId, startSession])
|
||||||
|
|
||||||
|
const handleEvaluate = async (grade: SessionGrade) => {
|
||||||
|
const card = sessionCards[currentIndex]
|
||||||
|
if (!card || reviewing) return
|
||||||
|
|
||||||
|
setReviewing(true)
|
||||||
|
setSessionGrades((prev) => ({ ...prev, [card.id]: grade }))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`/api/flashcards/${card.id}/review`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ grade }),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
toast.error(t('flashcards.reviewFailed'))
|
||||||
|
} finally {
|
||||||
|
setReviewing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex < sessionCards.length - 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex((i) => i + 1)
|
||||||
|
setIsFlipped(false)
|
||||||
|
}, 250)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => setIsSessionFinished(true), 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSessionActive || isSessionFinished) return
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.code === 'Space') {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsFlipped((f) => !f)
|
||||||
|
} else if (isFlipped) {
|
||||||
|
if (e.key === '1') handleEvaluate(1)
|
||||||
|
if (e.key === '2') handleEvaluate(2)
|
||||||
|
if (e.key === '3') handleEvaluate(3)
|
||||||
|
if (e.key === '4') handleEvaluate(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKey)
|
||||||
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
|
}, [isSessionActive, isSessionFinished, isFlipped, currentIndex, reviewing])
|
||||||
|
|
||||||
|
const exitSession = () => {
|
||||||
|
setIsSessionActive(false)
|
||||||
|
setIsSessionFinished(false)
|
||||||
|
if (activeDeckId) {
|
||||||
|
loadDeck(activeDeckId)
|
||||||
|
loadDecks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDeck = async () => {
|
||||||
|
const name = newDeckName.trim()
|
||||||
|
if (!name) return
|
||||||
|
setCreatingDeck(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/flashcards/decks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) {
|
||||||
|
setNewDeckName('')
|
||||||
|
await loadDecks()
|
||||||
|
toast.success(t('flashcards.deckCreated'))
|
||||||
|
if (data.deck?.id) setActiveDeckId(data.deck.id)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setCreatingDeck(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDue = (dueCount: number) => {
|
||||||
|
if (dueCount > 0) return t('flashcards.dueCount', { count: dueCount })
|
||||||
|
return t('flashcards.upToDate')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col bg-memento-paper dark:bg-background overflow-y-auto">
|
||||||
|
<div className="px-6 sm:px-10 py-5 flex items-center justify-between sticky top-0 bg-memento-paper/95 dark:bg-background/95 backdrop-blur-sm z-40 border-b border-border/40">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isSessionActive ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={exitSession}
|
||||||
|
className="flex items-center gap-2 text-concrete hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-widest">{t('flashcards.exitSession')}</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GraduationCap className="text-brand-accent" size={20} />
|
||||||
|
<h1 className="text-sm font-black tracking-widest uppercase">{t('nav.revision')}</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSessionActive && (
|
||||||
|
<div className="flex gap-1 p-1 rounded-lg bg-black/[0.04] dark:bg-white/[0.04]">
|
||||||
|
{(['decks', 'progress'] as const).map((id) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab(id)}
|
||||||
|
className={cn(
|
||||||
|
'px-3 py-1.5 rounded-md text-[10px] font-bold uppercase tracking-wider transition-colors',
|
||||||
|
tab === id ? 'bg-white dark:bg-card shadow-sm text-brand-accent' : 'text-concrete',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{id === 'decks' ? t('flashcards.tabDecks') : t('flashcards.tabProgress')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 max-w-5xl mx-auto w-full p-6 sm:p-10">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isSessionActive && !isSessionFinished && sessionCards[currentIndex] && (
|
||||||
|
<motion.div
|
||||||
|
key="session"
|
||||||
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="max-w-2xl mx-auto flex flex-col items-center gap-8"
|
||||||
|
>
|
||||||
|
<div className="w-full flex items-center justify-between text-xs text-concrete">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={currentIndex === 0}
|
||||||
|
onClick={() => { setCurrentIndex((i) => i - 1); setIsFlipped(false) }}
|
||||||
|
className="flex items-center gap-1 disabled:opacity-30"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} /> {t('flashcards.previous')}
|
||||||
|
</button>
|
||||||
|
<span className="font-mono font-bold px-3 py-1 rounded-full bg-black/[0.04] dark:bg-white/[0.06]">
|
||||||
|
{currentIndex + 1} / {sessionCards.length}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={currentIndex >= sessionCards.length - 1}
|
||||||
|
onClick={() => { setCurrentIndex((i) => i + 1); setIsFlipped(false) }}
|
||||||
|
className="flex items-center gap-1 disabled:opacity-30"
|
||||||
|
>
|
||||||
|
{t('flashcards.next')} <ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsFlipped((f) => !f)}
|
||||||
|
className="w-full max-w-md min-h-[240px] [perspective:1000px] cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative w-full min-h-[240px] transition-transform duration-500 [transform-style:preserve-3d]',
|
||||||
|
isFlipped && '[transform:rotateY(180deg)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 [backface-visibility:hidden] rounded-2xl border border-border bg-card p-8 flex flex-col shadow-sm">
|
||||||
|
<span className="text-[10px] uppercase tracking-widest text-concrete mb-4">{t('flashcards.front')}</span>
|
||||||
|
<p className="flex-1 flex items-center justify-center text-center text-lg font-serif font-semibold">
|
||||||
|
{sessionCards[currentIndex].front}
|
||||||
|
</p>
|
||||||
|
<span className="text-[10px] text-concrete/60 text-center">{t('flashcards.tapToFlip')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 [backface-visibility:hidden] [transform:rotateY(180deg)] rounded-2xl border border-brand-accent/30 bg-brand-accent/5 p-8 flex flex-col shadow-md">
|
||||||
|
<span className="text-[10px] uppercase tracking-widest text-brand-accent mb-4">{t('flashcards.back')}</span>
|
||||||
|
<p className="flex-1 flex items-center justify-center text-center text-sm leading-relaxed overflow-y-auto">
|
||||||
|
{sessionCards[currentIndex].back}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isFlipped && (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 w-full max-w-md">
|
||||||
|
{([
|
||||||
|
[1, t('flashcards.grade.hard'), 'bg-red-500/10 text-red-600 border-red-500/20'],
|
||||||
|
[2, t('flashcards.grade.difficult'), 'bg-amber-500/10 text-amber-700 border-amber-500/20'],
|
||||||
|
[3, t('flashcards.grade.good'), 'bg-emerald-500/10 text-emerald-700 border-emerald-500/20'],
|
||||||
|
[4, t('flashcards.grade.easy'), 'bg-brand-accent/10 text-brand-accent border-brand-accent/30'],
|
||||||
|
] as const).map(([grade, label, cls]) => (
|
||||||
|
<button
|
||||||
|
key={grade}
|
||||||
|
type="button"
|
||||||
|
disabled={reviewing}
|
||||||
|
onClick={() => handleEvaluate(grade as SessionGrade)}
|
||||||
|
className={cn('py-2.5 px-2 rounded-xl border text-[10px] font-bold uppercase tracking-wide', cls)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSessionFinished && (
|
||||||
|
<motion.div
|
||||||
|
key="finished"
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="max-w-lg mx-auto text-center space-y-6"
|
||||||
|
>
|
||||||
|
<GraduationCap size={40} className="mx-auto text-brand-accent" />
|
||||||
|
<h2 className="text-2xl font-serif font-bold">{t('flashcards.sessionComplete')}</h2>
|
||||||
|
<div className="h-48">
|
||||||
|
<NoteChart type="bar" data={sessionChartData} height={180} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={exitSession}
|
||||||
|
className="px-6 py-2.5 rounded-xl bg-brand-accent text-white text-xs font-bold uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
{t('flashcards.backToDecks')}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSessionActive && tab === 'decks' && (
|
||||||
|
<motion.div key="decks" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-8">
|
||||||
|
{activeDeckId && deckCards.length > 0 && (
|
||||||
|
<div className="p-5 rounded-2xl border border-brand-accent/25 bg-brand-accent/5 space-y-3">
|
||||||
|
<h3 className="font-serif font-semibold text-lg">{activeDeck?.name || t('flashcards.activeDeck')}</h3>
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs text-concrete">
|
||||||
|
<span>{t('flashcards.statTotal', { count: deckStats.total })}</span>
|
||||||
|
<span>{t('flashcards.statDue', { count: deckStats.due })}</span>
|
||||||
|
<span>{t('flashcards.statMastered', { count: deckStats.mastered })}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => startSession(activeDeckId)}
|
||||||
|
disabled={loadingDeck}
|
||||||
|
className="px-4 py-2 rounded-lg bg-brand-accent text-white text-xs font-bold uppercase tracking-wider flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{loadingDeck ? <Loader2 size={14} className="animate-spin" /> : <GraduationCap size={14} />}
|
||||||
|
{t('flashcards.startReview')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
|
<input
|
||||||
|
value={newDeckName}
|
||||||
|
onChange={(e) => setNewDeckName(e.target.value)}
|
||||||
|
placeholder={t('flashcards.newDeckPlaceholder')}
|
||||||
|
className="flex-1 min-w-[180px] px-3 py-2 rounded-lg border border-border text-sm bg-transparent"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={createDeck}
|
||||||
|
disabled={creatingDeck || !newDeckName.trim()}
|
||||||
|
className="px-3 py-2 rounded-lg border border-border text-xs font-bold flex items-center gap-1 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{creatingDeck ? <Loader2 size={14} className="animate-spin" /> : <Plus size={14} />}
|
||||||
|
{t('flashcards.createDeck')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loadingDecks ? (
|
||||||
|
<div className="flex justify-center py-16"><Loader2 className="animate-spin text-concrete" /></div>
|
||||||
|
) : decks.length === 0 ? (
|
||||||
|
<div className="text-center py-16 border border-dashed border-border rounded-2xl space-y-3">
|
||||||
|
<GraduationCap size={32} className="mx-auto text-concrete/40" />
|
||||||
|
<p className="text-sm text-concrete">{t('flashcards.emptyDecks')}</p>
|
||||||
|
<p className="text-xs text-concrete/60 max-w-md mx-auto">{t('flashcards.emptyDecksHint')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{decks.map((deck) => (
|
||||||
|
<div
|
||||||
|
key={deck.id}
|
||||||
|
className="p-5 rounded-2xl border border-border/60 bg-card/50 hover:border-brand-accent/30 transition-colors flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-serif font-semibold truncate">{deck.name}</h3>
|
||||||
|
<p className="text-xs text-concrete flex items-center gap-1 mt-1">
|
||||||
|
<Layers size={12} />
|
||||||
|
{t('flashcards.cardCountLabel', { count: deck.totalCards })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 text-[10px]">
|
||||||
|
{deck.dueCount > 0 ? (
|
||||||
|
<span className="px-2 py-1 rounded-full bg-red-500/10 text-red-600 font-bold border border-red-500/15">
|
||||||
|
{formatDue(deck.dueCount)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="px-2 py-1 rounded-full bg-emerald-500/10 text-emerald-700 font-bold border border-emerald-500/15">
|
||||||
|
{t('flashcards.upToDate')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="px-2 py-1 rounded-full border border-border flex items-center gap-1 font-mono">
|
||||||
|
<Calendar size={10} />
|
||||||
|
{deck.masteredCount}/{deck.totalCards} {t('flashcards.masteredShort')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2 border-t border-border/40">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => loadDeck(deck.id)}
|
||||||
|
className="flex-1 py-2 text-[10px] font-bold uppercase tracking-wider border border-border rounded-lg hover:bg-black/[0.03]"
|
||||||
|
>
|
||||||
|
{t('flashcards.viewDeck')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
setLoadingDeck(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/flashcards/decks/${deck.id}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (res.ok) {
|
||||||
|
setActiveDeckId(deck.id)
|
||||||
|
setDeckCards(data.deck.cards || [])
|
||||||
|
setDeckStats(data.stats)
|
||||||
|
startSession(deck.id, data.deck.cards || [])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoadingDeck(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1 py-2 text-[10px] font-bold uppercase tracking-wider bg-brand-accent text-white rounded-lg flex items-center justify-center gap-1"
|
||||||
|
>
|
||||||
|
<GraduationCap size={12} />
|
||||||
|
{t('flashcards.review')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isSessionActive && tab === 'progress' && (
|
||||||
|
<motion.div key="progress" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="space-y-8">
|
||||||
|
{loadingStats ? (
|
||||||
|
<div className="flex justify-center py-16"><Loader2 className="animate-spin text-concrete" /></div>
|
||||||
|
) : stats ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 rounded-xl border border-border bg-card/50">
|
||||||
|
<p className="text-[10px] uppercase tracking-widest text-concrete">{t('flashcards.retentionRate')}</p>
|
||||||
|
<p className="text-3xl font-serif font-bold mt-1">{stats.retentionRate}%</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 rounded-xl border border-border bg-card/50 sm:col-span-2">
|
||||||
|
<p className="text-[10px] uppercase tracking-widest text-concrete mb-2 flex items-center gap-1">
|
||||||
|
<BarChart3 size={12} /> {t('flashcards.retentionCurve')}
|
||||||
|
</p>
|
||||||
|
<NoteChart
|
||||||
|
type="line"
|
||||||
|
data={stats.retentionByWeek.map((w) => ({
|
||||||
|
label: w.week.slice(5),
|
||||||
|
value: w.rate,
|
||||||
|
}))}
|
||||||
|
height={120}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RevisionHeatmap data={stats.heatmap} />
|
||||||
|
|
||||||
|
{stats.difficultCards.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-bold uppercase tracking-widest text-concrete flex items-center gap-2">
|
||||||
|
<BookOpen size={14} /> {t('flashcards.difficultCards')}
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{stats.difficultCards.map((c) => (
|
||||||
|
<li key={c.id} className="p-3 rounded-xl border border-border/60 text-sm flex justify-between gap-4">
|
||||||
|
<span className="truncate">{c.front}</span>
|
||||||
|
<span className="text-[10px] font-mono text-concrete shrink-0">EF {c.easinessFactor.toFixed(2)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -313,9 +313,19 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
}, [content, isMarkdown])
|
}, [content, isMarkdown])
|
||||||
|
|
||||||
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
|
const resolveImagesForSave = useCallback((contentToSave: string): string[] => {
|
||||||
const extracted = !isMarkdown ? extractImagesFromHTML(contentToSave) : []
|
if (!contentToSave) return []
|
||||||
return Array.from(new Set([...images, ...extracted]))
|
if (!isMarkdown) {
|
||||||
}, [images, isMarkdown])
|
return extractImagesFromHTML(contentToSave)
|
||||||
|
} else {
|
||||||
|
const urls = new Set<string>()
|
||||||
|
const matches = contentToSave.matchAll(/!\[.*?\]\((.*?)\)/g)
|
||||||
|
for (const match of matches) {
|
||||||
|
const src = match[1]?.trim()
|
||||||
|
if (src) urls.add(src)
|
||||||
|
}
|
||||||
|
return Array.from(urls)
|
||||||
|
}
|
||||||
|
}, [isMarkdown])
|
||||||
|
|
||||||
const handleGenerateTitles = async () => {
|
const handleGenerateTitles = async () => {
|
||||||
const fullContentForAI = [
|
const fullContentForAI = [
|
||||||
@@ -613,8 +623,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
if (contentToSave !== content) setContent(contentToSave)
|
if (contentToSave !== content) setContent(contentToSave)
|
||||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||||
if (removedImageUrls.length > 0) {
|
const deletedImages = Array.from(new Set([
|
||||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
...removedImageUrls,
|
||||||
|
...images.filter(url => !imagesToSave.includes(url))
|
||||||
|
]))
|
||||||
|
if (deletedImages.length > 0) {
|
||||||
|
cleanupOrphanedImages(deletedImages, note.id).catch(() => {})
|
||||||
|
setRemovedImageUrls([])
|
||||||
}
|
}
|
||||||
await refreshLabels()
|
await refreshLabels()
|
||||||
onNoteSaved?.(result)
|
onNoteSaved?.(result)
|
||||||
@@ -732,8 +747,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, o
|
|||||||
if (contentToSave !== content) setContent(contentToSave)
|
if (contentToSave !== content) setContent(contentToSave)
|
||||||
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
if (JSON.stringify(imagesToSave) !== JSON.stringify(images)) setImages(imagesToSave)
|
||||||
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
prevNoteRef.current = { ...prevNoteRef.current, ...result }
|
||||||
if (removedImageUrls.length > 0) {
|
const deletedImages = Array.from(new Set([
|
||||||
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
|
...removedImageUrls,
|
||||||
|
...images.filter(url => !imagesToSave.includes(url))
|
||||||
|
]))
|
||||||
|
if (deletedImages.length > 0) {
|
||||||
|
cleanupOrphanedImages(deletedImages, note.id).catch(() => {})
|
||||||
|
setRemovedImageUrls([])
|
||||||
}
|
}
|
||||||
await refreshLabels()
|
await refreshLabels()
|
||||||
onNoteSaved?.(result)
|
onNoteSaved?.(result)
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import {
|
import {
|
||||||
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
|
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
|
||||||
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
|
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
|
||||||
Trash2, LogOut, Wand2, Share2, Wind, Paperclip
|
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-generate-dialog'
|
||||||
import { NoteShareDialog } from './note-share-dialog'
|
import { NoteShareDialog } from './note-share-dialog'
|
||||||
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
|
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
|
||||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||||
@@ -41,6 +42,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
|||||||
const { t } = useLanguage()
|
const { t } = useLanguage()
|
||||||
const [isConverting, setIsConverting] = useState(false)
|
const [isConverting, setIsConverting] = useState(false)
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
const [shareOpen, setShareOpen] = useState(false)
|
||||||
|
const [flashcardsOpen, setFlashcardsOpen] = useState(false)
|
||||||
|
|
||||||
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
|
||||||
|
|
||||||
@@ -169,6 +171,17 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
|||||||
<Wind size={16} />
|
<Wind size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
title={t('flashcards.toolbarGenerate')}
|
||||||
|
aria-label={t('flashcards.toolbarGenerate')}
|
||||||
|
onClick={() => setFlashcardsOpen(true)}
|
||||||
|
className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all"
|
||||||
|
>
|
||||||
|
<GraduationCap size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{!readOnly && onToggleAttachments && (
|
{!readOnly && onToggleAttachments && (
|
||||||
<button
|
<button
|
||||||
title={t('notes.attachments') || 'Attachments'}
|
title={t('notes.attachments') || 'Attachments'}
|
||||||
@@ -247,6 +260,16 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FlashcardGenerateDialog
|
||||||
|
open={flashcardsOpen}
|
||||||
|
onClose={() => setFlashcardsOpen(false)}
|
||||||
|
noteId={note.id}
|
||||||
|
noteTitle={state.title || note.title || 'Untitled'}
|
||||||
|
onSaved={(deckId) => {
|
||||||
|
window.open(`/revision?deckId=${encodeURIComponent(deckId)}`, '_self')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
aria-label={t('notes.documentInfoAria')}
|
aria-label={t('notes.documentInfoAria')}
|
||||||
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}
|
||||||
|
|||||||
@@ -508,7 +508,6 @@ export function NoteNetworkTab({ noteId, noteTitle }: NoteNetworkTabProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [sortedSemantic, outbound, backlinks, embedHosts, notebooks, t])
|
}, [sortedSemantic, outbound, backlinks, embedHosts, notebooks, t])
|
||||||
|
|
||||||
const orbitNodes = graphNodes.slice(0, MAX_GRAPH_NODES)
|
const orbitNodes = graphNodes.slice(0, MAX_GRAPH_NODES)
|
||||||
|
|||||||
@@ -959,7 +959,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
{ id: 'notebooks', icon: BookOpen, label: t('nav.notebooks'), onClick: () => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }, isActive: activeView === 'notebooks' && !pathname.startsWith('/settings') },
|
{ id: 'notebooks', icon: BookOpen, label: t('nav.notebooks'), onClick: () => { setActiveView('notebooks'); if (pathname !== '/home') router.push('/home') }, isActive: activeView === 'notebooks' && !pathname.startsWith('/settings') },
|
||||||
{ id: 'graph', icon: Network, label: t('nav.graphView'), onClick: () => router.push('/graph'), isActive: pathname === '/graph' },
|
{ id: 'graph', icon: Network, label: t('nav.graphView'), onClick: () => router.push('/graph'), isActive: pathname === '/graph' },
|
||||||
{ id: 'insights', icon: Sparkles, label: t('nav.insights'), onClick: () => router.push('/insights'), isActive: pathname === '/insights' },
|
{ id: 'insights', icon: Sparkles, label: t('nav.insights'), onClick: () => router.push('/insights'), isActive: pathname === '/insights' },
|
||||||
{ id: 'revision', icon: GraduationCap, label: t('nav.revision'), onClick: () => setActiveView('revision'), isActive: activeView === 'revision' },
|
{ id: 'revision', icon: GraduationCap, label: t('nav.revision'), onClick: () => router.push('/revision'), isActive: pathname === '/revision' },
|
||||||
{ id: 'agents', icon: Bot, label: t('agents.intelligenceOS') || 'Intelligence IA', onClick: () => { setActiveView('agents'); router.push('/agents') }, isActive: activeView === 'agents' || (pathname.startsWith('/agents') && activeView !== 'notebooks') },
|
{ id: 'agents', icon: Bot, label: t('agents.intelligenceOS') || 'Intelligence IA', onClick: () => { setActiveView('agents'); router.push('/agents') }, isActive: activeView === 'agents' || (pathname.startsWith('/agents') && activeView !== 'notebooks') },
|
||||||
{ id: 'reminders', icon: Bell, label: t('sidebar.reminders'), onClick: () => setActiveView('reminders'), isActive: activeView === 'reminders' },
|
{ id: 'reminders', icon: Bell, label: t('sidebar.reminders'), onClick: () => setActiveView('reminders'), isActive: activeView === 'reminders' },
|
||||||
] as { id: string; icon: React.FC<{ size?: number }>; label: string; onClick: () => void; isActive: boolean }[]).map(item => (
|
] as { id: string; icon: React.FC<{ size?: number }>; label: string; onClick: () => void; isActive: boolean }[]).map(item => (
|
||||||
@@ -1258,27 +1258,6 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Vue Révisions (placeholder en attendant US-FLASHCARDS) ── */}
|
|
||||||
{activeView === 'revision' && (
|
|
||||||
<motion.div
|
|
||||||
key="revision"
|
|
||||||
initial={{ opacity: 0, x: -10 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: 10 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="px-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5 mb-4">
|
|
||||||
<GraduationCap size={13} className="text-brand-accent" />
|
|
||||||
<p className="text-[10px] font-bold text-concrete tracking-[0.2em] uppercase">Révisions</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center justify-center text-center p-6 border border-dashed border-border/50 rounded-2xl bg-paper/20 space-y-3">
|
|
||||||
<GraduationCap size={24} className="text-concrete/40" />
|
|
||||||
<p className="text-[11px] font-medium text-concrete/70">Flashcards bientôt disponibles</p>
|
|
||||||
<p className="text-[10px] text-concrete/50">Les decks de révision IA (SM-2) arrivent dans la prochaine itération.</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function LiveBlockView({ node, updateAttributes, deleteNode }: LiveBlockViewProp
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setIsOffline(true))
|
.catch(() => setIsOffline(true))
|
||||||
}, [sourceNoteId, blockId]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [sourceNoteId, blockId])
|
||||||
|
|
||||||
// Listen for real-time block update events
|
// Listen for real-time block update events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Clipper web avec **panneau latéral** : le panneau reste ouvert pendant que vous surlignez du texte sur la page.
|
Clipper web avec **panneau latéral** : le panneau reste ouvert pendant que vous surlignez du texte sur la page.
|
||||||
|
|
||||||
|
## Langues
|
||||||
|
|
||||||
|
L’extension suit la **langue de l’interface Chrome** (`chrome.i18n.getUILanguage`) — 15 locales comme l’app Momento : `de`, `en`, `es`, `fr`, `it`, `pt`, `nl`, `pl`, `ru`, `zh`, `ja`, `ko`, `ar`, `fa`, `hi`.
|
||||||
|
|
||||||
|
Fichiers : `extension/_locales/<lang>/messages.json`. Régénération : `node extension/i18n/generate-translations.cjs` puis `node extension/scripts/build-extension-locales.mjs`.
|
||||||
|
|
||||||
## Installation (dev)
|
## Installation (dev)
|
||||||
|
|
||||||
1. Chrome → `chrome://extensions`
|
1. Chrome → `chrome://extensions`
|
||||||
|
|||||||
182
memento-note/extension/_locales/ar/messages.json
Normal file
182
memento-note/extension/_locales/ar/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "مومنتو ويب كليبر"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "التقط صفحات الويب والنص المميز في دفاتر ملاحظات Momento الخاصة بك - ويتصل بخادم Momento الخاص بك."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "مقطع إلى مومنتو"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "مقص الويب"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "متصل"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "غير متصل"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "عنوان URL لمومنتو"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "عنوان URL لمثيل Momento الخاص بك"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "إعداد مسبق للإنتاج · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "تطبيق وإعادة الاتصال"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "افتح مومنتو"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "الصق عنوان URL الخاص بـ HTTPS (أو LAN) لخادم Momento الخاص بك. تتعامل ملفات تعريف الارتباط الموجودة في هذا المتصفح مع تسجيل الدخول."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<الإصدار>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "لا يستطيع Momento الوصول إلى علامة التبويب هذه. تحقق من أذونات ملحق لوحة المفاتيح/الموقع — أو افتح اللوحة الجانبية."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "دفتر بلا عنوان"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "لا توجد دفاتر ملاحظات حتى الآن"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 دقيقة قراءة"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "تقريبا. $COUNT$ دقيقة قراءة",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "تم الكشف عن التحديد"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "يتجاهل"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "نصيحة: قم بتمييز النص الموجود على الصفحة لقص التحديد الدقيق كملاحظة."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "اختيار المقطع"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "قص هذه الصفحة"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "حفظ الرابط فقط"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "لا يمكن القص هنا — هذه الصفحة تحظر الوصول إلى الإضافات."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "يرجى تسجيل الدخول إلى Momento في هذا المتصفح أولاً."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "تعذر تحميل دفاتر الملاحظات. حاول إعادة الاتصال."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "تم تحميل دفاتر الملاحظات"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "جارٍ الاتصال…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "متصل بـ $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "صفحة مقيدة - قم بالقص عبر شريط أدوات Momento أو اللوحة الجانبية."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "دفتر الوجهة"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "صفحة نشطة"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "المراجعة قبل الحفظ"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "عنوان"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "مقتطفات"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "حفظ إلى مومنتو"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "خلف"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "تحليل المصدر"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "جارٍ التحليل…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "توفير…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "إنشاء العلامات والملخص الدلالي والتضمينات."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "تم حفظ الملاحظة"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "تم الحفظ في $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "عرض في مومنتو"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "قص صفحة أخرى"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "لا يمكن إكماله"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "حدث خطأ ما أثناء الوصول إلى مثيل Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "أعد المحاولة"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "حدد النص أولاً، أو قم بقص الصفحة بأكملها."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "لا يمكن تحليل هذه الصفحة."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "لا يمكن حفظ ملاحظتك."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "مشكلة في الشبكة - تحقق من اتصالك وعنوان URL الخاص بـ Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "قم بتمييز النص الموجود على الصفحة، أو قم بقص الصفحة بأكملها."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/de/messages.json
Normal file
182
memento-note/extension/_locales/de/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Erfassen Sie Webseiten und hervorgehobenen Text in Ihren Momento-Notizbüchern – stellt eine Verbindung zu Ihrem eigenen Momento-Server her."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clip auf Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Web Clipper"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Verbunden"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Nicht verbunden"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "Momento-URL"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "Ihre Momento-Instanz-URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Produktionsvoreinstellung · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Anwenden und erneut verbinden"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Öffnen Sie Momento"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Fügen Sie die HTTPS- (oder LAN-)URL Ihres Momento-Servers ein. Cookies in diesem Browser verarbeiten die Anmeldung."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper 0.3.1"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento kann nicht auf diese Registerkarte zugreifen. Überprüfen Sie die Tastatur-/Site-Erweiterungsberechtigungen – oder öffnen Sie den Seitenbereich."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Notizbuch ohne Titel"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Noch keine Notizbücher"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 Minute gelesen"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Ca. $COUNT$ min. gelesen",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Auswahl erkannt"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorieren"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Tipp: Markieren Sie Text auf der Seite, um eine präzise Auswahl als Notiz auszuschneiden."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Clip-Auswahl"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Clip diese Seite aus"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Nur Link speichern"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Hier kann kein Clip erstellt werden – diese Seite blockiert den Zugriff auf die Erweiterung."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Bitte melden Sie sich zunächst in diesem Browser bei Momento an."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Notebooks konnten nicht geladen werden. Versuchen Sie, die Verbindung wiederherzustellen."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Notizbücher geladen"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Verbinden…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Verbunden mit $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Eingeschränkte Seite – Ausschneiden über die Momento-Symbolleiste oder den Seitenbereich."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Zielnotizbuch"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Aktive Seite"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Vor dem Speichern überprüfen"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Titel"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Auszug"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "In Momento speichern"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Zurück"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Quelle analysieren"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analysieren…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Sparen…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Generieren von Tags, einer semantischen Zusammenfassung und Einbettungen."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Notiz gespeichert"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Gespeichert unter $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "In Momento ansehen"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Schneiden Sie eine weitere Seite aus"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Konnte nicht abgeschlossen werden"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Beim Erreichen Ihrer Momento-Instanz ist ein Fehler aufgetreten."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Wiederholen"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Wählen Sie zuerst den Text aus oder schneiden Sie die gesamte Seite aus."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Diese Seite konnte nicht analysiert werden."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Ihre Notiz konnte nicht gespeichert werden."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Netzwerkproblem – überprüfen Sie Ihre Verbindung und Momento-URL."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Markieren Sie Text auf der Seite oder schneiden Sie die gesamte Seite aus."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/en/messages.json
Normal file
182
memento-note/extension/_locales/en/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Capture web pages and highlighted text into your Momento notebooks — connects to your own Momento server."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clip to Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Web Clipper"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Connected"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Not connected"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "Momento URL"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "Your Momento instance URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Production preset · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Apply and reconnect"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Open Momento"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Paste the HTTPS (or LAN) URL of your Momento server. Cookies in this browser handle sign-in."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper 0.3.1"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento can't access this tab. Check keyboard/site extension permissions — or open the Side Panel."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Untitled notebook"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "No notebooks yet"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minute read"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Approx. $COUNT$ min read",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Selection detected"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignore"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Tip: highlight text on the page to clip a precise selection as a note."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Clip selection"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Clip this page"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Save link only"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Can't clip here — this page blocks extension access."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Please sign in to Momento in this browser first."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Could not load notebooks. Try reconnecting."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Notebooks loaded"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Connecting…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Connected to $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Restricted page — clip via the Momento toolbar or Side Panel."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Destination notebook"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Active page"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Review before saving"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Title"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Excerpt"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Save to Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Back"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analyzing source"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analyzing…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Saving…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Generating tags, a semantic summary, and embeddings."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Note saved"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Saved to $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "View in Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Clip another page"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Could not complete"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Something went wrong reaching your Momento instance."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Retry"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Select text first, or clip the full page."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Could not analyze this page."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Could not save your note."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Network issue — check your connection and Momento URL."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Highlight text on the page, or clip the whole page."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/es/messages.json
Normal file
182
memento-note/extension/_locales/es/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Cortadora web Momento"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Capture páginas web y texto resaltado en sus cuadernos Momento: se conecta a su propio servidor Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clip al momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Cortadora web"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Conectado"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "No conectado"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "URL del momento"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "La URL de tu instancia de Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Preajuste de producción · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Aplicar y reconectar"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Momento abierto"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Pegue la URL HTTPS (o LAN) de su servidor Momento. Las cookies en este navegador controlan el inicio de sesión."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<VERSIÓN>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento no puede acceder a esta pestaña. Verifique los permisos de extensión del sitio/teclado o abra el Panel lateral."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Cuaderno sin título"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Aún no hay cuadernos"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minuto de lectura"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Aprox. <<<CONTAR>>> lectura mínima (Approx. $COUNT$ min read)",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Selección detectada"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorar"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Consejo: resalte el texto en la página para recortar una selección precisa como nota."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Selección de clips"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Recortar esta página"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Guardar enlace solamente"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "No se puede recortar aquí: esta página bloquea el acceso a la extensión."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Primero inicie sesión en Momento en este navegador."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "No se pudieron cargar los cuadernos. Intente volver a conectarse."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Cuadernos cargados"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Conectando…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Conectado a $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Página restringida: recorte mediante la barra de herramientas de Momento o el panel lateral."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Cuaderno de destino"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Página activa"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Revisar antes de guardar"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Título"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Extracto"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Guardar en momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Atrás"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analizando fuente"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analizando…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Ahorro…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Generación de etiquetas, resumen semántico e incrustaciones."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Nota guardada"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Guardado en $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Ver en momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Recortar otra página"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "No se pudo completar"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Algo salió mal al llegar a tu instancia de Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Rever"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Seleccione el texto primero o recorte la página completa."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "No se pudo analizar esta página."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "No se pudo guardar tu nota."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Problema de red: verifique su conexión y la URL de Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Resalte el texto de la página o recorte toda la página."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/fa/messages.json
Normal file
182
memento-note/extension/_locales/fa/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "صفحات وب و متن هایلایت شده را در نوت بوک های Momento خود ضبط کنید — به سرور Momento خودتان متصل می شود."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "کلیپ به لحظه"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Web Clipper"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "متصل شد"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "متصل نیست"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "آدرس لحظه ای"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "URL نمونه Momento شما"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "پیش تنظیم تولید · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "درخواست کنید و دوباره وصل شوید"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Momento را باز کنید"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "URL HTTPS (یا LAN) سرور Momento خود را جایگذاری کنید. کوکیهای این مرورگر ورود به سیستم را کنترل میکنند."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper 0.3.1"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento نمی تواند به این برگه دسترسی پیدا کند. مجوزهای افزونه صفحه کلید/سایت را بررسی کنید - یا پانل جانبی را باز کنید."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "دفترچه بدون عنوان"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "هنوز نوت بوک نیست"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~ 1 دقیقه مطالعه کنید"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "تقریبا $COUNT$ دقیقه خواندن",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "انتخاب شناسایی شد"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "نادیده گرفتن"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "نکته: متن را در صفحه برجسته کنید تا یک انتخاب دقیق به عنوان یادداشت بریده شود."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "انتخاب کلیپ"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "این صفحه را کلیپ کنید"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "فقط لینک را ذخیره کنید"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "در اینجا نمی توان کلیپ کرد - این صفحه دسترسی برنامه های افزودنی را مسدود می کند."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "لطفاً ابتدا با این مرورگر وارد Momento شوید."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "نوتبوکها بارگیری نشد. سعی کنید دوباره وصل شوید."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "نوت بوک ها بارگیری شدند"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "در حال اتصال…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "به $URL$ متصل شد",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "صفحه محدود - از طریق نوار ابزار Momento یا پانل جانبی کلیپ کنید."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "دفترچه یادداشت مقصد"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "صفحه فعال"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "قبل از ذخیره بررسی کنید"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "عنوان"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "گزیده"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "ذخیره در Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "برگشت"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "تجزیه و تحلیل منبع"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "در حال تجزیه و تحلیل…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "در حال ذخیره…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "تولید برچسب ها، خلاصه معنایی، و جاسازی ها."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "یادداشت ذخیره شد"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "در $NOTEBOOK$ ذخیره شد",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "مشاهده در Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "یک صفحه دیگر را کلیپ کنید"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "تکمیل نشد"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "هنگام رسیدن به نمونه Momento شما مشکلی پیش آمد."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "دوباره امتحان کنید"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "ابتدا متن را انتخاب کنید، یا صفحه کامل را کلیپ کنید."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "نمی توان این صفحه را تجزیه و تحلیل کرد."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "یادداشت شما ذخیره نشد."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "مشکل شبکه - اتصال و URL Momento خود را بررسی کنید."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "متن را در صفحه برجسته کنید یا کل صفحه را برش دهید."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/fr/messages.json
Normal file
182
memento-note/extension/_locales/fr/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento · Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Enregistrez des pages web et du texte surligné dans vos carnets Momento — connecté à votre propre serveur Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clipper vers Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Web Clipper"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Connecté"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Non connecté"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "Adresse Momento"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "URL de votre instance Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Préréglage production · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Appliquer et reconnecter"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Ouvrir Momento"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Collez l'URL HTTPS (ou LAN) de votre serveur Momento. Les cookies de ce navigateur gèrent la connexion."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper 0.3.1"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento ne peut pas accéder à cet onglet. Vérifiez les autorisations du clavier/extension de site – ou ouvrez le panneau latéral."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Carnet sans titre"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Pas encore de carnets"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "≈ 1 minute de lecture"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "≈ $COUNT$ minutes de lecture",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Sélection détectée"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorer"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Astuce : surlignez du texte à l’écran pour clipper une sélection précise de la page en tant que note."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Clipper la sélection"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Clipper cette page"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Enregistrer le lien uniquement"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Impossible de clipper ici — cette page bloque l'accès aux extensions."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Veuillez d'abord vous connecter à Momento dans ce navigateur."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Impossible de charger les carnets. Essayez de vous reconnecter."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Carnets chargés"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Connexion…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Connecté à $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Page restreinte : clip via la barre d'outils Momento ou le panneau latéral."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Carnet de destination"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Page active"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Vérifier avant d'enregistrer"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Titre"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Extrait"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Enregistrer dans Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Retour"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analyse de la source"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analyse…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Enregistrement…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Génération automatique des tags, résumé sémantique et calcul des embeddings."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Note enregistrée"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Enregistré dans $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Voir dans Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Clipper une autre page"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Impossible de terminer"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Une erreur est survenue lors de la communication avec votre instance Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Réessayer"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Sélectionnez d'abord du texte, ou clippez la page entière."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Impossible d'analyser cette page."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Impossible d'enregistrer votre note."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Problème de réseau : vérifiez votre connexion et l'URL Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Surlignez le texte à clipper"
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/hi/messages.json
Normal file
182
memento-note/extension/_locales/hi/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "मोमेंटो वेब क्लिपर"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "अपने मोमेंटो नोटबुक में वेब पेज और हाइलाइट किए गए टेक्स्ट को कैप्चर करें - यह आपके अपने मोमेंटो सर्वर से जुड़ता है।"
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "मोमेंटो पर क्लिप करें"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "वेब क्लिपर"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "जुड़े हुए"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "जुड़े नहीं हैं"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "मोमेंटो यूआरएल"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "आपका मोमेंटो इंस्टेंस यूआरएल"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "प्रोडक्शन प्रीसेट · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "आवेदन करें और पुनः कनेक्ट करें"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "मोमेंटो खोलें"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "अपने मोमेंटो सर्वर का HTTPS (या LAN) URL चिपकाएँ। इस ब्राउज़र में कुकीज़ साइन-इन को संभालती हैं।"
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "मोमेंटो वेब क्लिपर <<<संस्करण>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "मोमेंटो इस टैब तक नहीं पहुंच सकता. कीबोर्ड/साइट एक्सटेंशन अनुमतियां जांचें - या साइड पैनल खोलें।"
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "शीर्षक रहित नोटबुक"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "अभी तक कोई नोटबुक नहीं"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 मिनट पढ़ें"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "लगभग। $COUNT$ मिनट पढ़ा",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "चयन का पता चला"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "अनदेखा करना"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "युक्ति: किसी सटीक चयन को नोट के रूप में क्लिप करने के लिए पृष्ठ पर टेक्स्ट को हाइलाइट करें।"
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "क्लिप चयन"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "इस पृष्ठ को क्लिप करें"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "केवल लिंक सहेजें"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "यहां क्लिप नहीं किया जा सकता - यह पेज एक्सटेंशन एक्सेस को ब्लॉक करता है।"
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "कृपया पहले इस ब्राउज़र में मोमेंटो में साइन इन करें।"
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "नोटबुक लोड नहीं हो सकीं. पुनः कनेक्ट करने का प्रयास करें."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "नोटबुक लोड किए गए"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "कनेक्ट हो रहा है..."
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "$URL$ से कनेक्ट किया गया",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "प्रतिबंधित पृष्ठ - मोमेंटो टूलबार या साइड पैनल के माध्यम से क्लिप करें।"
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "गंतव्य नोटबुक"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "सक्रिय पृष्ठ"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "सहेजने से पहले समीक्षा करें"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "शीर्षक"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "अंश"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "मोमेंटो में सहेजें"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "पीछे"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "स्रोत का विश्लेषण"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "विश्लेषण कर रहा हूँ..."
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "सहेजा जा रहा है..."
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "टैग, सिमेंटिक सारांश और एम्बेडिंग तैयार करना।"
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "नोट सहेजा गया"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "$NOTEBOOK$ में सहेजा गया",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "मोमेंटो में देखें"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "दूसरे पेज को क्लिप करें"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "पूरा नहीं हो सका"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "आपके मोमेंटो इंस्टेंस तक पहुँचने में कुछ गड़बड़ी हुई।"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "पुन: प्रयास करें"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "पहले टेक्स्ट चुनें, या पूरा पेज क्लिप करें।"
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "इस पृष्ठ का विश्लेषण नहीं किया जा सका."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "आपका नोट सहेजा नहीं जा सका."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "नेटवर्क समस्या - अपना कनेक्शन और मोमेंटो यूआरएल जांचें।"
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "पृष्ठ पर टेक्स्ट को हाइलाइट करें, या पूरे पृष्ठ को क्लिप करें।"
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/it/messages.json
Normal file
182
memento-note/extension/_locales/it/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Cattura pagine web e testo evidenziato nei tuoi taccuini Momento: si connette al tuo server Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clip su Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Tagliatore di fotoricettore"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Collegato"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Non connesso"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "URL del momento"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "L'URL dell'istanza di Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Preimpostazione di produzione · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Applicare e riconnettersi"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Momento aperto"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Incolla l'URL HTTPS (o LAN) del tuo server Momento. I cookie in questo browser gestiscono l'accesso."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<VERSIONE>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento non può accedere a questa scheda. Controlla le autorizzazioni per tastiera/estensione del sito oppure apri il pannello laterale."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Taccuino senza titolo"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Nessun taccuino ancora"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minuto di lettura"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "ca. $COUNT$ min letto",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Selezione rilevata"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorare"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Suggerimento: evidenzia il testo sulla pagina per ritagliare una selezione precisa come nota."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Selezione clip"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Ritaglia questa pagina"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Salva solo il collegamento"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Impossibile ritagliare qui: questa pagina blocca l'accesso all'estensione."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Accedi prima a Momento in questo browser."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Impossibile caricare i taccuini. Prova a riconnetterti."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Taccuini caricati"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Connessione…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Connesso a $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Pagina limitata: ritaglia tramite la barra degli strumenti Momento o il pannello laterale."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Taccuino di destinazione"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Pagina attiva"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Rivedi prima di salvare"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Titolo"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Estratto"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Salva su Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Indietro"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analisi della fonte"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analizzando..."
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Risparmio…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Generazione di tag, riepilogo semantico e incorporamenti."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Nota salvata"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Salvato in $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Visualizza in Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Ritaglia un'altra pagina"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Impossibile completare"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Qualcosa è andato storto nel raggiungere la tua istanza Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Riprova"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Seleziona prima il testo o ritaglia l'intera pagina."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Impossibile analizzare questa pagina."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Impossibile salvare la nota."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Problema di rete: controlla la connessione e l'URL Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Evidenzia il testo sulla pagina o ritaglia l'intera pagina."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/ja/messages.json
Normal file
182
memento-note/extension/_locales/ja/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "モーメントウェブクリッパー"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Web ページとハイライトされたテキストを Momento ノートブックにキャプチャします。独自の Momento サーバーに接続します。"
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "モーメントにクリップ"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "ウェブクリッパー"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "接続済み"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "接続されていません"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "モーメントのURL"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "Momento インスタンスの URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "プロダクションプリセット・memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "適用して再接続する"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "モーメントを開く"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Momento サーバーの HTTPS (または LAN) URL を貼り付けます。このブラウザの Cookie がサインインを処理します。"
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web クリッパー <<<バージョン>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento はこのタブにアクセスできません。キーボード/サイト拡張機能の権限を確認するか、サイド パネルを開きます。"
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "無題のノート"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "まだノートはありません"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "約 1 分で読めます"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "約$COUNT$ 分読み取り",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "選択が検出されました"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "無視する"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "ヒント: ページ上のテキストをハイライト表示して、正確な選択範囲をメモとしてクリップします。"
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "クリップの選択"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "このページをクリップします"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "リンクのみを保存"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "ここではクリップできません — このページは拡張機能へのアクセスをブロックしています。"
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "まずこのブラウザで Momento にサインインしてください。"
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "ノートブックをロードできませんでした。再接続してみてください。"
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "ノートブックがロードされました"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "接続中…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "$URL$ に接続しました",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "制限されたページ — Momento ツールバーまたはサイド パネルを介してクリップします。"
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "宛先ノートブック"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "アクティブなページ"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "保存する前に確認してください"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "タイトル"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "抜粋"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "モーメントに保存"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "戻る"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "ソースを分析中"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "分析中…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "保存中…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "タグ、意味の概要、および埋め込みを生成します。"
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "メモが保存されました"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "$NOTEBOOK$ に保存されました",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "モメントで見る"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "別のページをクリップする"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "完了できませんでした"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Momento インスタンスに到達する際に問題が発生しました。"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "リトライ"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "最初にテキストを選択するか、ページ全体をクリップします。"
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "このページを分析できませんでした。"
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "メモを保存できませんでした。"
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "ネットワークの問題 — 接続と Momento URL を確認してください。"
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "ページ上のテキストを強調表示するか、ページ全体をクリップします。"
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/ko/messages.json
Normal file
182
memento-note/extension/_locales/ko/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "모멘토 웹 클리퍼"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "웹 페이지와 강조 표시된 텍스트를 Momento 노트북에 캡처하여 자체 Momento 서버에 연결합니다."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "순간에 클립"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "웹 클리퍼"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "연결됨"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "연결되지 않음"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "모멘토 URL"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "귀하의 Momento 인스턴스 URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "프로덕션 프리셋 · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "적용하고 다시 연결하세요"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "모멘토 열기"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Momento 서버의 HTTPS(또는 LAN) URL을 붙여넣습니다. 이 브라우저의 쿠키는 로그인을 처리합니다."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<버전>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento는 이 탭에 접근할 수 없습니다. 키보드/사이트 확장 권한을 확인하거나 측면 패널을 엽니다."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "제목 없는 노트"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "아직 노트가 없습니다."
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1분 읽기"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "대략. $COUNT$분 읽음",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "선택 항목이 감지되었습니다."
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "무시하다"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "팁: 페이지의 텍스트를 강조 표시하여 정확한 선택 항목을 메모로 자릅니다."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "클립 선택"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "이 페이지 클립"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "링크만 저장"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "여기서 클립할 수 없습니다. 이 페이지는 확장 프로그램 액세스를 차단합니다."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "먼저 이 브라우저에서 Momento에 로그인하세요."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "노트북을 로드할 수 없습니다. 다시 연결해 보세요."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "노트북이 로드되었습니다."
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "연결 중…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "$URL$에 연결됨",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "제한된 페이지 — Momento 도구 모음 또는 측면 패널을 통해 클립합니다."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "대상 노트북"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "활성 페이지"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "저장하기 전에 검토하세요"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "제목"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "발췌"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "모멘토에 저장"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "뒤쪽에"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "소스 분석 중"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "분석 중…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "절약…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "태그, 의미 요약 및 임베딩을 생성합니다."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "메모가 저장되었습니다."
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "$NOTEBOOK$에 저장됨",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Momento에서 보기"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "다른 페이지 자르기"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "완료할 수 없습니다."
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Momento 인스턴스에 연결하는 데 문제가 발생했습니다."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "다시 해 보다"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "먼저 텍스트를 선택하거나 전체 페이지를 자릅니다."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "이 페이지를 분석할 수 없습니다."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "메모를 저장할 수 없습니다."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "네트워크 문제 - 연결 및 Momento URL을 확인하세요."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "페이지의 텍스트를 강조 표시하거나 전체 페이지를 자릅니다."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/nl/messages.json
Normal file
182
memento-note/extension/_locales/nl/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Webclipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Leg webpagina's en gemarkeerde tekst vast in uw Momento-notebooks - maakt verbinding met uw eigen Momento-server."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clip naar Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Webclipper"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Aangesloten"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Niet verbonden"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "Momento-URL"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "Uw Momento-instantie-URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Productievoorinstelling · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Toepassen en opnieuw verbinden"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Momento openen"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Plak de HTTPS (of LAN) URL van uw Momento-server. Cookies in deze browser zorgen voor het inloggen."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<VERSIE>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento heeft geen toegang tot dit tabblad. Controleer de rechten voor toetsenbord-/site-extensies — of open het zijpaneel."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Naamloos notitieboekje"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Nog geen notitieboekjes"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minuut lezen"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Ongeveer. $COUNT$ min gelezen",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Selectie gedetecteerd"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "negeren"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Tip: markeer tekst op de pagina om een precieze selectie als notitie te knippen."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Clipselectie"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Knip deze pagina uit"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Alleen link opslaan"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Kan hier niet knippen: deze pagina blokkeert de toegang tot extensies."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Meld u eerst aan bij Momento in deze browser."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Kan notitieboekjes niet laden. Probeer opnieuw verbinding te maken."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Notitieboekjes geladen"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Verbinden…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Verbonden met $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Beperkte pagina — clip via de Momento-werkbalk of het zijpaneel."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Bestemmingsnotitieboekje"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Actieve pagina"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Controleer voordat u opslaat"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Titel"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Uittreksel"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Opslaan in Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Rug"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Bron analyseren"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analyseren…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Besparing…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Tags, een semantische samenvatting en insluitingen genereren."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Notitie opgeslagen"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Opgeslagen in $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Bekijk in Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Knip nog een pagina uit"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Kon niet voltooien"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Er is iets misgegaan bij het bereiken van uw Momento-instantie."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Opnieuw proberen"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Selecteer eerst tekst of knip de volledige pagina uit."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Kan deze pagina niet analyseren."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Kan uw notitie niet opslaan."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Netwerkprobleem: controleer uw verbinding en Momento-URL."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Markeer tekst op de pagina of knip de hele pagina uit."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/pl/messages.json
Normal file
182
memento-note/extension/_locales/pl/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Narzędzie do strzyżenia sieci Momento"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Przechwytuj strony internetowe i zaznaczony tekst do swoich notatników Momento — łączy się z Twoim własnym serwerem Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Klip do Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Obcinacz sieci"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Połączony"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Nie podłączony"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "Adres URL chwili"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "Adres URL Twojej instancji Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Wstępne ustawienia produkcyjne · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Zastosuj i połącz ponownie"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Otwórz Momento"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Wklej adres URL HTTPS (lub LAN) swojego serwera Momento. Pliki cookie w tej przeglądarce obsługują logowanie."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<WERSJA>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento nie ma dostępu do tej karty. Sprawdź uprawnienia rozszerzenia klawiatury/witryny — lub otwórz Panel boczny."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Notatnik bez tytułu"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Nie ma jeszcze żadnych notatników"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minuta czytania"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Około. $COUNT$ min odczytu",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Wykryto wybór"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorować"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Wskazówka: zaznacz tekst na stronie, aby wyciąć dokładne zaznaczenie jako notatkę."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Wybór klipu"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Przytnij tę stronę"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Zapisz tylko link"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Nie można tutaj przyciąć — ta strona blokuje dostęp do rozszerzenia."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Najpierw zaloguj się do Momento w tej przeglądarce."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Nie udało się załadować notatników. Spróbuj połączyć się ponownie."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Notatniki załadowane"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Złączony…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Połączono z $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Strona z ograniczeniami — klip za pomocą paska narzędzi Momento lub panelu bocznego."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Notatnik docelowy"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Aktywna strona"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Przejrzyj przed zapisaniem"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Tytuł"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Fragment"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Zapisz w Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Z powrotem"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analizowanie źródła"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analizuję…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Oszczędność…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Generowanie tagów, podsumowań semantycznych i osadzania."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Uwaga zapisana"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Zapisano w $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Zobacz w Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Wytnij kolejną stronę"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Nie udało się ukończyć"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Coś poszło nie tak podczas docierania do Twojej instancji Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Spróbować ponownie"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Najpierw zaznacz tekst lub wytnij całą stronę."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Nie można przeanalizować tej strony."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Nie można zapisać notatki."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Problem z siecią — sprawdź swoje połączenie i adres URL Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Zaznacz tekst na stronie lub przytnij całą stronę."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/pt/messages.json
Normal file
182
memento-note/extension/_locales/pt/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento Web Clipper"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Capture páginas da web e texto destacado em seus blocos de anotações Momento – conecte-se ao seu próprio servidor Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Clipe para Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Clipper da Web"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Conectado"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Não conectado"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "URL do momento"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "URL da sua instância do Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Predefinição de produção · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Aplicar e reconectar"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Momento aberto"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Cole o URL HTTPS (ou LAN) do seu servidor Momento. Os cookies neste navegador controlam o login."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<VERSÃO>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento não pode acessar esta guia. Verifique as permissões de extensão de teclado/site – ou abra o painel lateral."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Caderno sem título"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Ainda não há cadernos"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 minuto de leitura"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Aprox. $COUNT$ minutos de leitura",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Seleção detectada"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "ignorar"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Dica: destaque o texto na página para recortar uma seleção precisa como uma nota."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Seleção de clipe"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Recorte esta página"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Salvar apenas link"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Não é possível recortar aqui – esta página bloqueia o acesso à extensão."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Faça login no Momento neste navegador primeiro."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Não foi possível carregar os notebooks. Tente reconectar."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Cadernos carregados"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Conectando…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Conectado a $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Página restrita – recorte por meio da barra de ferramentas do Momento ou do painel lateral."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Caderno de destino"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Página ativa"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Revise antes de salvar"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Título"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Trecho"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Salvar no Momento"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Voltar"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Analisando fonte"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Analisando…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Salvando…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Geração de tags, resumo semântico e incorporações."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Nota salva"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Salvo em $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Ver em Momento"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Recortar outra página"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Não foi possível concluir"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Algo deu errado ao chegar à sua instância do Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Tentar novamente"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Selecione o texto primeiro ou recorte a página inteira."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Não foi possível analisar esta página."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Não foi possível salvar sua nota."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Problema de rede – verifique sua conexão e URL do Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Destaque o texto na página ou recorte a página inteira."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/ru/messages.json
Normal file
182
memento-note/extension/_locales/ru/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Веб-клипер Momento"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "Сохраняйте веб-страницы и выделенный текст в свои блокноты Momento — подключайтесь к вашему собственному серверу Momento."
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "Клип на Momento"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "Веб-клипер"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "Подключено"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "Не подключено"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "URL-адрес момента"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "URL-адрес вашего экземпляра Momento"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "Настройки производства · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "Подать заявку и повторно подключиться"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "Открыть Моменто"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "Вставьте URL-адрес HTTPS (или LAN) вашего сервера Momento. Файлы cookie в этом браузере обрабатывают вход в систему."
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<ВЕРСИЯ>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento не имеет доступа к этой вкладке. Проверьте разрешения для расширения клавиатуры/сайта или откройте боковую панель."
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "Блокнот без названия"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "Блокнотов пока нет"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "~1 минута чтения"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "Прибл. $COUNT$ мин чтения",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "Выбор обнаружен"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "игнорировать"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "Совет: выделите текст на странице, чтобы выделить его в виде заметки."
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "Выбор клипа"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "Вырезать эту страницу"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "Сохранить только ссылку"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "Невозможно обрезать здесь — эта страница блокирует доступ к расширению."
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "Сначала войдите в Momento в этом браузере."
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "Не удалось загрузить блокноты. Попробуйте переподключиться."
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "Ноутбуки загружены"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "Подключение…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "Подключено к $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "Страница с ограниченным доступом — вырезайте с помощью панели инструментов Momento или боковой панели."
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "Блокнот назначения"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "Активная страница"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "Проверьте перед сохранением"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "Заголовок"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "Отрывок"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "Сохранить в Моменто"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "Назад"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "Анализ источника"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "Анализ…"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "Сохранение…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "Генерация тегов, семантического резюме и вложений."
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "Заметка сохранена."
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "Сохранено в $NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "Посмотреть в Моменто"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "Вырезать другую страницу"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "Не удалось завершить"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "Что-то пошло не так при получении вашего экземпляра Momento."
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "Повторить попытку"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "Сначала выделите текст или вырежьте всю страницу."
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "Не удалось проанализировать эту страницу."
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "Не удалось сохранить заметку."
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "Проблема с сетью. Проверьте подключение и URL-адрес Momento."
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "Выделите текст на странице или вырежьте всю страницу."
|
||||||
|
}
|
||||||
|
}
|
||||||
182
memento-note/extension/_locales/zh/messages.json
Normal file
182
memento-note/extension/_locales/zh/messages.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{
|
||||||
|
"extName": {
|
||||||
|
"message": "Momento 网页剪辑器"
|
||||||
|
},
|
||||||
|
"extDescription": {
|
||||||
|
"message": "将网页和突出显示的文本捕获到您的 Momento 笔记本中 — 连接到您自己的 Momento 服务器。"
|
||||||
|
},
|
||||||
|
"extActionTitle": {
|
||||||
|
"message": "剪辑到时刻"
|
||||||
|
},
|
||||||
|
"webClipper": {
|
||||||
|
"message": "网页剪辑器"
|
||||||
|
},
|
||||||
|
"connected": {
|
||||||
|
"message": "已连接"
|
||||||
|
},
|
||||||
|
"disconnected": {
|
||||||
|
"message": "未连接"
|
||||||
|
},
|
||||||
|
"instanceSettings": {
|
||||||
|
"message": "时刻网址"
|
||||||
|
},
|
||||||
|
"instanceUrlLabel": {
|
||||||
|
"message": "您的 Momento 实例 URL"
|
||||||
|
},
|
||||||
|
"presetProduction": {
|
||||||
|
"message": "制作预设 · memento-note.com"
|
||||||
|
},
|
||||||
|
"applyReconnect": {
|
||||||
|
"message": "应用并重新连接"
|
||||||
|
},
|
||||||
|
"openMomento": {
|
||||||
|
"message": "打开时刻"
|
||||||
|
},
|
||||||
|
"settingsHint": {
|
||||||
|
"message": "粘贴 Momento 服务器的 HTTPS(或 LAN)URL。此浏览器中的 Cookie 处理登录。"
|
||||||
|
},
|
||||||
|
"footerVersion": {
|
||||||
|
"message": "Momento Web Clipper <<<版本>>>"
|
||||||
|
},
|
||||||
|
"errPermissionDenied": {
|
||||||
|
"message": "Momento 无法访问此选项卡。检查键盘/站点扩展权限 - 或打开侧面板。"
|
||||||
|
},
|
||||||
|
"notebookUnnamed": {
|
||||||
|
"message": "无标题笔记本"
|
||||||
|
},
|
||||||
|
"noNotebooks": {
|
||||||
|
"message": "还没有笔记本"
|
||||||
|
},
|
||||||
|
"readingTimeOne": {
|
||||||
|
"message": "阅读约 1 分钟"
|
||||||
|
},
|
||||||
|
"readingTimeOther": {
|
||||||
|
"message": "大约。 $COUNT$ 最少阅读次数",
|
||||||
|
"placeholders": {
|
||||||
|
"COUNT": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectionDetected": {
|
||||||
|
"message": "检测到选择"
|
||||||
|
},
|
||||||
|
"ignore": {
|
||||||
|
"message": "忽略"
|
||||||
|
},
|
||||||
|
"selectionHint": {
|
||||||
|
"message": "提示:突出显示页面上的文本以将精确的选择剪辑为注释。"
|
||||||
|
},
|
||||||
|
"clipSelection": {
|
||||||
|
"message": "剪辑选择"
|
||||||
|
},
|
||||||
|
"clipPage": {
|
||||||
|
"message": "剪辑此页"
|
||||||
|
},
|
||||||
|
"saveLinkOnly": {
|
||||||
|
"message": "仅保存链接"
|
||||||
|
},
|
||||||
|
"pageNotAccessible": {
|
||||||
|
"message": "此处无法剪辑 — 此页面阻止扩展程序访问。"
|
||||||
|
},
|
||||||
|
"errLoginRequired": {
|
||||||
|
"message": "请先在此浏览器中登录 Momento。"
|
||||||
|
},
|
||||||
|
"errLoadNotebooks": {
|
||||||
|
"message": "无法加载笔记本。尝试重新连接。"
|
||||||
|
},
|
||||||
|
"notebooksLoaded": {
|
||||||
|
"message": "笔记本已加载"
|
||||||
|
},
|
||||||
|
"connecting": {
|
||||||
|
"message": "正在连接…"
|
||||||
|
},
|
||||||
|
"connectedToUrl": {
|
||||||
|
"message": "连接到 $URL$",
|
||||||
|
"placeholders": {
|
||||||
|
"URL": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "https://memento-note.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restrictedPage": {
|
||||||
|
"message": "受限页面 — 通过 Momento 工具栏或侧面板进行剪辑。"
|
||||||
|
},
|
||||||
|
"destinationNotebook": {
|
||||||
|
"message": "目的地笔记本"
|
||||||
|
},
|
||||||
|
"activePage": {
|
||||||
|
"message": "活动页面"
|
||||||
|
},
|
||||||
|
"previewBeforeSave": {
|
||||||
|
"message": "保存前查看"
|
||||||
|
},
|
||||||
|
"noteTitleLabel": {
|
||||||
|
"message": "标题"
|
||||||
|
},
|
||||||
|
"excerptLabel": {
|
||||||
|
"message": "摘抄"
|
||||||
|
},
|
||||||
|
"saveToMomento": {
|
||||||
|
"message": "保存到时刻"
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"message": "后退"
|
||||||
|
},
|
||||||
|
"analyzingSource": {
|
||||||
|
"message": "分析来源"
|
||||||
|
},
|
||||||
|
"statusAnalyzing": {
|
||||||
|
"message": "正在分析……"
|
||||||
|
},
|
||||||
|
"statusSaving": {
|
||||||
|
"message": "保存…"
|
||||||
|
},
|
||||||
|
"processingDetail": {
|
||||||
|
"message": "生成标签、语义摘要和嵌入。"
|
||||||
|
},
|
||||||
|
"noteSaved": {
|
||||||
|
"message": "注释已保存"
|
||||||
|
},
|
||||||
|
"sentToNotebook": {
|
||||||
|
"message": "保存至$NOTEBOOK$",
|
||||||
|
"placeholders": {
|
||||||
|
"NOTEBOOK": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Read later"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewInMomento": {
|
||||||
|
"message": "在 Momento 中查看"
|
||||||
|
},
|
||||||
|
"clipAnother": {
|
||||||
|
"message": "剪辑另一页"
|
||||||
|
},
|
||||||
|
"failure": {
|
||||||
|
"message": "无法完成"
|
||||||
|
},
|
||||||
|
"genericError": {
|
||||||
|
"message": "您的 Momento 实例出现问题。"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"message": "重试"
|
||||||
|
},
|
||||||
|
"errNoSelection": {
|
||||||
|
"message": "首先选择文本,或剪辑整个页面。"
|
||||||
|
},
|
||||||
|
"errAnalyzeFailed": {
|
||||||
|
"message": "无法分析此页面。"
|
||||||
|
},
|
||||||
|
"errSaveFailed": {
|
||||||
|
"message": "无法保存您的笔记。"
|
||||||
|
},
|
||||||
|
"errNetwork": {
|
||||||
|
"message": "网络问题 — 检查您的连接和 Momento URL。"
|
||||||
|
},
|
||||||
|
"bannerPickText": {
|
||||||
|
"message": "突出显示页面上的文本,或剪辑整个页面。"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,10 @@
|
|||||||
function ensureBanner() {
|
function ensureBanner() {
|
||||||
if (document.getElementById(BANNER_ID)) return
|
if (document.getElementById(BANNER_ID)) return
|
||||||
|
|
||||||
|
const bannerText =
|
||||||
|
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
|
||||||
|
'Highlight the text to clip'
|
||||||
|
|
||||||
const host = document.createElement('div')
|
const host = document.createElement('div')
|
||||||
host.id = BANNER_ID
|
host.id = BANNER_ID
|
||||||
host.style.cssText =
|
host.style.cssText =
|
||||||
@@ -153,7 +157,7 @@
|
|||||||
<div class="pill">
|
<div class="pill">
|
||||||
<span class="logo">M</span>
|
<span class="logo">M</span>
|
||||||
<span class="dot"></span>
|
<span class="dot"></span>
|
||||||
<span>Surlignez le texte à clipper</span>
|
<span>${bannerText.replace(/</g, '<')}</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
document.documentElement.appendChild(host)
|
document.documentElement.appendChild(host)
|
||||||
|
|||||||
47
memento-note/extension/i18n.js
Normal file
47
memento-note/extension/i18n.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/** Helpers i18n — langue UI Chrome (chrome.i18n.getUILanguage). */
|
||||||
|
function t(key, ...subs) {
|
||||||
|
const msg = chrome.i18n.getMessage(key, subs)
|
||||||
|
return msg || key
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiLocaleTag() {
|
||||||
|
return (chrome.i18n.getUILanguage() || 'en').replace('_', '-').split('-')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUiRtl() {
|
||||||
|
return ['ar', 'fa', 'he', 'ur'].includes(uiLocaleTag())
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDocumentLocale() {
|
||||||
|
const lang = uiLocaleTag()
|
||||||
|
document.documentElement.lang = lang
|
||||||
|
document.documentElement.dir = isUiRtl() ? 'rtl' : 'ltr'
|
||||||
|
document.body?.classList.toggle('ui-rtl', isUiRtl())
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyShellI18n() {
|
||||||
|
document.title = t('extName')
|
||||||
|
const brandSub = document.querySelector('.brand-sub')
|
||||||
|
if (brandSub) brandSub.textContent = t('webClipper')
|
||||||
|
if (typeof els !== 'undefined' && els.connLabel) {
|
||||||
|
els.connLabel.textContent = t('connected')
|
||||||
|
}
|
||||||
|
if (typeof els !== 'undefined' && els.settingsBtn) {
|
||||||
|
els.settingsBtn.title = t('instanceSettings')
|
||||||
|
els.settingsBtn.setAttribute('aria-label', t('instanceSettings'))
|
||||||
|
}
|
||||||
|
const urlLabel = document.getElementById('instanceUrlLabel') || document.querySelector('#settingsPanel .field > span')
|
||||||
|
if (urlLabel) urlLabel.textContent = t('instanceUrlLabel')
|
||||||
|
const presetProd = document.querySelector('[data-url="https://memento-note.com"]')
|
||||||
|
if (presetProd) presetProd.textContent = t('presetProduction')
|
||||||
|
if (typeof els !== 'undefined' && els.applyInstanceBtn) {
|
||||||
|
els.applyInstanceBtn.textContent = t('applyReconnect')
|
||||||
|
}
|
||||||
|
if (typeof els !== 'undefined' && els.openLoginBtn) {
|
||||||
|
els.openLoginBtn.textContent = `${t('openMomento')} ↗`
|
||||||
|
}
|
||||||
|
const hint = document.querySelector('.settings-hint')
|
||||||
|
if (hint) hint.textContent = t('settingsHint')
|
||||||
|
const footer = document.querySelector('.footer-meta')
|
||||||
|
if (footer) footer.textContent = t('footerVersion')
|
||||||
|
}
|
||||||
214
memento-note/extension/i18n/generate-translations.cjs
Normal file
214
memento-note/extension/i18n/generate-translations.cjs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const zlib = require('zlib')
|
||||||
|
|
||||||
|
const VERSION = "0.3.1"
|
||||||
|
|
||||||
|
// Embedded Momento clipper/extension string table (gzip+base64).
|
||||||
|
// Source English + contextual French anchors; other locales are MT batch
|
||||||
|
// post-edited with placeholder hardening.
|
||||||
|
|
||||||
|
const PACK_B64 = `
|
||||||
|
H4sIAOZLE2oC/+V9W3MUR7buX6lwTIRnIhDEfjgvxI4dgTGzx2c84A3YjuO3UnchCrW6tKu7pZEmfEKtO0gYbCNuFgYZgS4WqCUk1IAQEVv2u3iTPC/E
|
||||||
|
CF1AIuYvnG+tzKxLX6qyNTY8nJgxSN1ZmSvX5VvrW1lV/O09K/3eQeNv71l/zR41myz+ucnKZMwG+vm9vzhNVjrrGJ9b9cbhlN3cbLnvfbnPoOEfWpmE
|
||||||
|
azdnbSddetVhszmbcy2jFVc147OMYaaTxmm74XQK/2WtpJHFBIZNM7c5OddQy6SdrFXvOI0Z4x8dl4yEk05biWzGUMOc1rQ3NGO5LZa7X0lzKEGCnLSz
|
||||||
|
qbI9kNw0hbySr4Bkajslo0t3KoWwkmXTel/QsKSdqTryqJM1EqHRdjqTNdMJ64SVzdrphkw1tX96/OPQ+E/d1MdmvZUqHf9/gkpUg72rm10rY2U/cZ1k
|
||||||
|
LlHJXP43hhhq/M+C0WTxbHVkkv0Jp4lnMpubU23HLbmZ0nkO0bdsatcbQlc5zVZaKb/kkmP4KmSYjNTIn+AcZXKamaxlZE9bxp9OnvzkhPF7xzU+PnT0
|
||||||
|
D7RRwzkVdiXpH8ZhuJMND7TTuNLOGPWu04rvjNOQM2UZGbshXWenhR+dcrBZ9zPLzVTQUoVQMP7WIsZ+KdzQdT+x3CY7Qx99aKXtcldQkyTM9PtZw0wk
|
||||||
|
8JWQK2vWQ9jTVqLRaLTa6h3TTR7I2NgvnNtKZ9g43uQiPrB9Ui1r5ISdtIxPzLSVEltRkfRpOo2wLpPj03SWQiXpRZy86KgKwHIfDgRnmyUM61pmEsY6
|
||||||
|
aTdZx9Jlcfd//81ostM5bIHGlV0Aqd0KHuQ6f91v/O7wsU+PnvwdXS8uJi9OmQnrtJNKQuV8HY/hn+Bs0BH5y3u/+7f3GBDMpmZGgvf+13tffikcK2Wx
|
||||||
|
j39oZStG6Qk1wEiqERx6DWnHLdub/DQ0cSWXPWk3H/SBT8CeIyxGwEiwlCB4MinyEnYGDumJYQI2WenCojTOk7EixnmXeuM/EV9XgENyOZJAbMFssT62
|
||||||
|
043H0qm2MrXgOyOFLyE3vmU8wVdwlEPsvXZ9BcRl7+aNwcgWO6u3olGfchJwIt+vRRjsVzH0sdNgp49b/52z3XIrfZKyzIwIWw5pJwB74QA/ZbuZbGBS
|
||||||
|
M1nVuQ87uRSHgpHCMN/R9xsn3TYfzOC54eDK0KwV8V4GSkp8H8gjmKNKIsE3/+i4G845Jx0gftXEQ7v/HbDvdxXDg9A/LjhOZ7PNmYMHDpSD/ZciXDNZ
|
||||||
|
16alKjnSce9bYVdO2WTzFttkD1eWyTpOqt50Ca9KYSqJOey0SU6rtFa6zIf+kDBamdBZi1VJskP8je/fiK0W22r9wDqFoCWHLt8KfW/U8wAD4UBmUpbm
|
||||||
|
oqJi2hXlhqhAEpbbnK046oj4zou0k06VZMihVlKo1JuJMo18QJ+xBtJmqq0dsp5A5kuUq0F9bWTE9yxB1szmMt5XVa9RvijGnxAqKRc4MBLQzYCQbgDE
|
||||||
|
mnaZHv7TSluuSX6OZNeQ2Qd0y1hNJjJRwsjkmppMt20fVw9WU72VpDyR8cONdFMx0MhaMsQy0NpJp5of8QwcMkePnTzywbFjf64cN+rb2OA5jsRkpMws
|
||||||
|
FYocLuRDH1WrdD4jB7PDxQ5FyyHsrkImZJQ2xXe+I5+CWnPl2cjHL8QupMuK0Q2kcDtxxHWdsvlPQAzAJWzRCnGMVtfBj0i1Cf6srVI9uV+m8KzbVh4/
|
||||||
|
9KGE2qNO1TQlvhBpkPF5H4FCQmQkyziVS6V4sx5sC3e0/oh9V6rA1bZFIFh+kvEmIKvHXU0eJLbsZ1vah5VtddwyP5IfG6jEchL0uGrjCRTKU1aDJwfq
|
||||||
|
+P0ynPG9+4mdaDwJHZRO/aeqhUJYT63wV0vtk/3C1SFxqOpr43FH0q7VYAPlrXYURRnJ5YjVgSMkcywlAjDnQuj08oSRNFGZtjgZlLdu2gJxUysH2ByG
|
||||||
|
Ld/CIEyKmsdpJrylUt13OF1ORwU41d+axO6kk05aOdQO0IEWs1ue0GB2aX9fWszuUBJJNWOFZI5jd0RvkDaFzjx2F5whnuEtT+D/DSnK080+3ds7z7P/
|
||||||
|
Owf1ww286kh6VBTZy7W4tqtN9w47qRQcL/U+7V8xvpxgfJ46Sn3H+BgempCcD6OgqDSyVANgGqMalsddgruUKcz2VyjhbVO/NCxg5bIIpgxVvssTyDwU
|
||||||
|
EgmLIr4B6L3f+Azmsk/ZtHtqnuSyjmtnuATKUNwlUtiS5R7wS+gklcRZgqNvDejIgab5YqyCbZo5ylOY09Rjh4c5fIGLWA48URKdCHYIZm5Y6QQVT6Rz
|
||||||
|
Ef0aFPEfZ/sNjyXiSsoOOblcHFekawM8ETNkglO8Dca4POFRxuWJLIOAFUsa3XjWeCiTzcFxDypoJfhVaAtPSf2j49ryRMI100YzJx3pkjm4VsaXqRkR
|
||||||
|
z5SStGKKGp26Bai4DERvLdSSZscU/uSxDJOugEdnLT2W6Wcal7w2ZUPQXJpAhoJGj3V+1NTsiG9ox7BIwsmRGHbC/p9bBwPSEAMlBaTep/gbpwD7q09H
|
||||||
|
NYnoZ1bOZnxKvm/WO24SeJTLeG0+jmgV8ZwWQ0CkR0vDG0qcNt0GVk+GKXSGIRsc9QguaOMMLWQIILIWXT0skzUvsDyR0aGrBJw1sVWR9t8hW6XPDB5i
|
||||||
|
IeIs9ghFVuHa4KcEXu87uayd8gsXYGlVENXgrhJIyR39wTHklQUVX+uRV5UsXMNsodhOvm/50aTHYl0dFpt1TVuDxQZDmT0/js5+6GS02ayHZjUyWpQJ
|
||||||
|
OoR2+Sz8z2myLW1OuzyRJpcQFT/ydBN+JGyhclmwXKAw2C38H+jJbJe+hlMkzFQil+KB2oT3OPiyy9crJauME819j/jDhU3eEQd27Ao+EcOCVWaDdl0r
|
||||||
|
lgqHMTNLNVlaRkEUHf4UCwCOqUZEoFLebbHSlCIcNyM9DkDThJSUkLZusRJVKvIongxzMVq7OmTZy7ZcACDpi0STsmQpAHDiFNceSO9Zm0pcPf4c1NX7
|
||||||
|
pgyTQJ7UINGhKQKwI1Wjw6fBWupTy+NNbDAKFiAtoXOLVwOLubyCnYJHkIKQwqOp9V9oV+1U/8C29S5SN5vM0yU+9bQYqVeB/L/+mekR95QJYpg2TthM
|
||||||
|
UTMW6nkqgpLUQ29xkPtPO/Vw4bRxUhyeGh+ddv3TszrEvt1ev7yYwPA0E4FM1kqlsgbynWWA2dTb6WQu3WC05/jKJnzRwPOpGU7wkRktp32sauZO/drn
|
||||||
|
qiQpdm2l49m3nTidpRZAYLzmyWqd7skqacpT0Ec8uL2upoPVRqZtMKBFa8EiZIM9Eu90q0U7Za+AlYlGtgjDyu1HsO/ly6dOpaV/6VLwPy4vNshLkrY8
|
||||||
|
dq0DCyfKCh5e9weKQlJRpsSJMuR1ocNXXJ+Bz30gT2cwxnTrhY/T1IfSTVaK3PNt0/FGwIaRZk8iZ2YxjeOMY5bbaLrAhvZcA4x3ysKGlofrUSG5y4un
|
||||||
|
Amo5aXJN4dYdOIH91B1xW2lfLjaTwWgLU9sN+MUSUcnKW37oG4MMeoI1QaMhiRZHF+GeS5w2nNOIb4QonDf2EBfDGxkPgmihd5b7F8HSG5CuMtLb4ij6
|
||||||
|
YTN0lLvfu/gtkPNDuUyreTplkBHTkkJG0HKUz3JTcee5zQeNv5huI1/A9mM4Zt+BYdmQ+4xck4BduMpoO3FwJY4JcsG6xxWZ9lwmAePZ8ABNIl4n54k/
|
||||||
|
4RWOzOLQUrEM/CgSIH1nZJrJB4Eueqz7T0Q8OIjIsTgeoXOZelotNyndPigPnwDjuiz7/hcIL/vUKRV+RiB+9Kj4BzZVLIQf0iIZbABRm14eTZzOZCug
|
||||||
|
D4DHo3hmWo+N+6e6jUBnwi2BGvBpkxYWe91PmTaDsBSi7OMdBZJvK6DIcqFe2J50pCwff6TshataMYanf6YSgx5P99IuIjX7Lon6EewFZdJpRE66Matc
|
||||||
|
hlwIvu/Fi7G8WE8E009mdSfamuqpZUzILVC2CrJqEPcvbCuVVggbd9rcyKfNvJAmY3dItibjhAo1sRuZV/Qou0T7KMp+iBCmIZ6xfxS4qy4U/JXY+hc5
|
||||||
|
SKl3/vxfOXJwcTiXCQCsDl/n4Vpn0M1mYKTWGbSA7hYwiZPM0AmpXXUWneHQ/SKXMZugEqrJKWyp4IJj1qNW4kwefzANhIcbS3VmNTj6f/qjsRy1EN8N
|
||||||
|
SQ94A5i6ddpKx7H0E15QEvhy3hMIXpp/qnD2PzOeqiKsnqM/5TAZEqAaS98/sOwm5A2OcUsQI7e0ajeACySc8UfrdIqaZblTDeDoKB/SEYz9c4HXTkpK
|
||||||
|
EU3ZP18exdxCD+05SoOMQbJCyAhUyoTURQiGHZtN2YC29Pj7h4GU2hjSoQohlYF1mDwRHeG3obkCTlw6WfUD8naMbGwmYg+UI+gO4htvm5cL5MWcf1pe
|
||||||
|
p3laHleF1aJt3lEmitUfdtysmXRck8+/gwRK717o5uURlDCmOD5v466DQ/1oM0XTisZExkjkkNndtOPxqYPAJWp9wN9MumEmx4fmtri/1YY8tZ6aowI1
|
||||||
|
mrQYe2jHsSfmJGDS0TgxV7tJOlqEXRx+h2WOI+0fm4Y8M8/mVHvONun32g7NLfMMVWl0oTg0T9jLD/Z+ap6yE6ZryBsKSQWxJ+YeENdTMGvcIW015LhZ
|
||||||
|
GTwy907M4TxlXmN8bPrn5XBCi/abBko0kOnJVlnXSZn4IgVN2gl4Hs1kZUgTe+Tq//7v//7ZkeMnPlr+9uh//Md/1Hx+7hjNOYAy3zZKEW6S0KbRTH8u
|
||||||
|
z5hcgdunuM2OLCLvmna4jSsP+ciI5FMZG3Y/AP6YohAkNcPZ8TnfoCgypu6JuYxbTJk2ssv3srmUE8fGDy0/pjsajdNmmx/3NdxX7fjH3abm3dV8czXU
|
||||||
|
f/jY0ZOHjkP56nqjafle2m4yjd9XuwX7D2/tHuyEtE+WUcKMY+6mxoE68CljnQGYCsC1yMgCgeHy1OQV4GygljQ5PIF8dOpginuqhUTiDm0+COCb4U0N
|
||||||
|
zh7aEB84ZiKp+3G1tnBpIVYsd//PnOkm6aI02cfIIGApViw9Bn+UkomMKW/v5n/nlu8dDIkhj805QviWbQQMKc8PKs1buF04qOsIOPGwxPAb0eU4pMvQ
|
||||||
|
xUaQ5F26Mct0G7ATwgAvvPYbH7Gb0lF5qoXBQ2FxxtI7LfcyNM+fdDTOy2n+dNLRPjAX6RGyvcvzcml0MSrdYCfNg9I9qNeStE3Sojw559wKxHHNJhtL
|
||||||
|
mZlArgWuWnRXZxmk6pyfK1j1TtCduNNzKTaPMPXv/s6Qz6flPUQNIp50iHgA6mNPzxMqg0eRcT+WQyVPJSp+KOsuj2ifndvt5IPGqZyHDBpEXFykw8MP
|
||||||
|
nXbAzGq7F9wDRotPxeE3+8jfck1UDVtNyyN0bzjcB1CRcFGJ4QIEh8Zd4aY0ocweMcRbDGXYeUfn4laZvSPotpcjHPhUKEdU4dceLjrq/nAz/kT8UKoB
|
||||||
|
l8EDlh8YTSY1ro1UyiJANatV1ZE3iLfonXmLfOmkAym6WeYLx4OfQMZWG9JjzAFNmMK7w5lWgycHppAwQerwy4GYg26riTWGtHiQzsBknZrJcRr6K8dD
|
||||||
|
myrgSzUbTYePl5Y2yZCifO1lnaQZ+EayXzv7WzwHnOXiEiWIaArVG3TenqEU30KcvN02+Y5YGzp08AfKihzKggD5tcW5PzX44YEYJR/srJn2QsWa94mb
|
||||||
|
DSkSi24Hs41TDt17S/dEqOf9IkgwB0jW0b9tPJP5DUnw+/Kq1Ps2N8BM2k+NBNim+zuyZjtHJC5nGix++xfuHUflBD81oFdpW1BHbSrcrMWEP0onYA6z
|
||||||
|
5O5xRYUrepLxkSTCdGCEqCQ3VQ8TNlDxQQZ1xF2jZLl/kf0eO3pkT+wXRCS3PKvYL8JCyGpSp8tKmnTozpydNm/Je8fb29mENpFhRBn2gux7gGpsvuXV
|
||||||
|
UmwYINvcTN0qE6Br2Clx52Mq5ajKzdJiwydFHJOGye2ymDieDh+1qNWuMIAAGnBl1saHbWw5q82HE+Hjab7WeVs0VwSRixTTYmY1SK7Gw8Yncg0NFiVL
|
||||||
|
hk4PYcmOAnQzuVRKorHJruDaWUY7WFyRXSFXgOtatXBdcTUNi6a5cl3lukIkjWeRUy106yf8EXtKCLwN4JnuHeJQenDroKkHw5LwCXXC9GMdySf1vh8v
|
||||||
|
ekSXBGHMpMaKGXxQOYwutd0NTsIDQAWI+jlzvwEgb6F1ArCatbU47UmVd8W8WVvnFnAGK6uGm8BZje/4FnCybcpugu2zRGeVG2aJuIbpLD43MBlxkawd
|
||||||
|
4LLVQVGDz3rACLeQwzli4m8JZ06brYHTYirP9/BDhkLHe5An5mzZ0WG0UI6Z1WG0MmpzsTeCf5RO2iAPjjajzdhc35io0mritExq9+/fH0tqj9uZZhOJ
|
||||||
|
uUZe65VLcC8wWttqtlMO0Sn5uLOktI7bDGRnB9OhtGzCrKn1qDONZKR5Vw87Z3JC0TU88uwlhVz6fTAZN5QWYm73ZlBU3Db+6ef/gnQJB/lteZye0yVd
|
||||||
|
AY9d5iIpwzUbGmw65ncZEbI501AVtAbTtZsJhzW5LnmKKaPUS9OOD0yo3elWAKWK2u7rtuXNF2QHTnOBDFfDvd00jYQPUkdtVNc26IT9oDpISpnes5ci
|
||||||
|
fxhW7fdyH4mpbCL1x1Gc/e3eeeWd8yZNj/AKNk79K24zNQEIcuIhLnEsRc/5m8tjy/NW8JHpb2Vr2qrLYAihBx3+Lj+At+z9+NcSZxx6VFhRFmzl81/x
|
||||||
|
CHj59p4OgZ2a6C9fQmnH5F7V8g+iWeXUyoCT1ilURjDObcc7Bxa//WvHwNaejoG1uO9hejuAY1R6bFp4Ufkp8DH/EDhd5Qi4CVOmqNz913jvctexvdBe
|
||||||
|
Unizw6e+9K1s2zXk7NB5r6mOezMcS96JrzSePOk9EHxUms98Mbdp13zmq458EdC6R742ko3YzOnlEZTbez30tWs99A09Ke1kgrO8JcKrQuhXPNb9EJF0
|
||||||
|
UCArWd+RUJuueqDbJDiuECV0nktf1cBzPTygkZbGka5V24kuV3AuNbsoldDrtzTPckms5QmDMvfyvRYqZgInujY7fYUzXZtCwBRMd/mWHzR6PPePyF2m
|
||||||
|
wAa6n8F/0UEYSLiPb7uO5mEu7eSUYwe2kqBndOkEAv4bfEcXn+f6aKr50LN/iuta/9+c48Ly5ALqKAD8AzmM7ukJnOOe4nNceYzrBB+FTlbGSa0Hofd6
|
||||||
|
jlvjMa7ln+KKmvVXPMQ9Sc/y6BJeNxAKVRnvZ05KpX8tvitOcGsiuxndA1yWOjA2nuq6pgeD8lFnOrvlhIhyq5TnivpWm+hq0lwup9/hqW2TNrv1Tm1z
|
||||||
|
use2FRDQSSdSwGC909skSj2KZYIgh95SRdgJeK9aE0fQWgJZ9umWwK1FGme5zPGc4Emu7TKWKAjyUxExNdvVPcktV44p3L32E93yqQR0sKL2cLIrasvg
|
||||||
|
6a64y+ivtJBlSDJTA+H9MKLCiVElR1tKk+4mtNnuxxa9OK5esOr3+c7RBno3QaNFt4hDysZM1mgxxcNXuVbv1m7/HbJ1RpNpNnrPr9Jb55qsLI3lx5K9
|
||||||
|
K2p8z3PaNF39J5ITOg8kHzLpgaQUJNd5JNkSW3J+syeSP/W1KYZmbauu1lc9Wy2O48rnke09P4980rGaxUPr+L/TDAIH82k/kOwVFs30FHr8vc0ps5EC
|
||||||
|
TFHaU/67n5G3gk5W4dXPSavd8g5z2+lhenoAyKGH3clFUbt6D/Xs+UB3L8e5py3rFD1jQe9lcoDP9G5MJ2sk7Sy9ELoebNU7z7X4gTaDnydW0uOaLNRP
|
||||||
|
r4WoY1ZbJ2/GtOQbok+JN0TTLtvtM3QTnuYLoo+aZlPKEYW2DX+pd6zGM1b8A8YNYjOhq2rgt7ksaGm75uPFxxCXLRZZOniKS88Yt7+dZ4xl6qNHSuQ7
|
||||||
|
o4F+EcQ2bTVoP2Z80JBwKsHUaeb2k+xwNhkWP1c7gP8zjyX/zihx6CFjaQGDng1Kk6PqPmKsZokktX9O0yPGtKiUKBd4NVA1UnsolSKpxcukmzMp09R8
|
||||||
|
xvjPiO/T9JxxmuBVbudgaH3w2EbSFr9sKRhKXkDocdm/WKmkgbKJn92CgEa9fSZ4chsEEj0iS8KHw0Fsg5/h5VPberJyCXzyW3It+EBjDU8IB5b4zZ4R
|
||||||
|
dsQzwtY7fUb4AwvA25j1rB969bR/21wdPYZWb6YaCQgrYKAGcf2AmhhNTZSUypEw+g3UVosVPMGK5a8BnCdwT5qohmScZH+1Z4I/tbNZF4Aih0by2GMi
|
||||||
|
RkuP8CoR2ePyOeMYFvuByy/GFW8aquVxYN2ngWGuZtOt4aXU8glgccOxev4X9T+222JyMUK1DSol0C76jQtdlOokkMZDwITATjOVj/gwrcFpj3mD393p
|
||||||
|
7QdWo32msYaTW84GaeR+0mNJQqj+zK+AwRYnlXUcW+Mh3yOuYaOYo1cVorZqAMRLeKbI5gf8G6kuCteCXokcwW2PSeilB1Y9t9R5kTWFqsgUMkef4uQk
|
||||||
|
3sSIZJe0G4IJUo/ZUr4IZjbWkh8zGoT2z0IFqgLgCWS+1Xm5tffortXkndvSTsPJKUDRanhet1o94yvutJUq0RmHcSSFPWq67T9dS7bTA750ku+2t/28
|
||||||
|
iLrbNJD4E3YNT+t+4rajvm5ty+bO0DxOuo25NL3F0Wml263azfa0mWinL8Q+aMFWh144Qp2CbNpuXH7QGnrx9c+dP/Uk2tsgy0/XjHbjJEY3Ga0/d5qZ
|
||||||
|
dBsdBbvQuN9H0mG6f+YXvOj+i0bH6hM2ySyUEc11P3GEtNifDtelI7nwBVrvv2bSBjXbKVuL8voXQXlnrDPq6YMztd3P/Hkm+9O15rRl0KMkrTY7CJ/n
|
||||||
|
Nibazuz5juYvzEzWycBfqIEklIEf0k5rWtbQUW/EzrYuP3Dbtd/H9XljCrs3PXVILpzK1QsynCH18I1G5FT+7SrGJym70Va3N7fCdc9g5+1WQ+qnnqSJ
|
||||||
|
RG049ZmfO3MNuTM/9dBJjtNqphVm7oUQfw5C/L8P7eWg16aiF87NtuIjBxKWXsTVtt840eyarcmfHxs5+iEtTOg67Zl2C7uhXxpTsCy9jKvtQKsNrEec
|
||||||
|
UgyShhypbPF0b72TaE+36b5qiwMbSabdyAIboKlYJiz2gVK8PQEs/3nRTKbbwiBRw6mvaQBA4PG23pnvsUY4ohOmxU6Spsi9DVr8eVuj2wZbtrbVQ+Fx
|
||||||
|
B71wtZ/64inx55lGsx1aazQPKhCWCEw3LhNU09uWzPo2LJuwf+r5qQ++0wiUTSKy5QXkecYZs1E8vptt/OmaBif+nHcBx7Kbc5GcGLmjDaY9Y8BzhUQ/
|
||||||
|
XYvlxF+YzXaGvCoFqWo45yX3cn5eTNPtaVmTo1ltm/+1JakTQYxzZywVURRQwYDRY8VHzTPN4N+tUCTAgXIj5bLA4WAFSNE85sVGckkT7irzo0kmY58I
|
||||||
|
xgqH/vKDeiytQLYNI/gSBba6NJnntP2V0lYMS/7CS3F6NNlPog7y/TukySeEC7QbToML8OAAMJts9hDyZ+gA2mtyEgD9ZhMBhliSlZRnWUJOfng3JyAT
|
||||||
|
6UWbPHvAmQQJSzmtbfFv0mprVeGsyZqpYjsD5xfel8SWmunM07aadDgzg3ksaf6jazZ476GPpMwynFtjCfMXUHyrC+Ga9J/fFUnZ+PkxIgFll6l/wzPS
|
||||||
|
+jUd+nyMshXsn3Z+/u6nvppuepbCZc0GhOs+KgzpLLjV/LlbMus2yrfIgLbhZMxku+mBT1UC/Wmr2WAqg+ocCH8hhhIcvRvm/IVTT3mpVZc4fy4SRiMI
|
||||||
|
1pk0gjCYNaqdCpcAZg65vpvRMJZAH4ZV6ZCznS4WxmokQyXa+U6PBD0UlhYHwtXq7AgWLQBaIHeo/o1m0oHUEkzqBDutQjsJk+BU6UbzeNjPjwQLpoof
|
||||||
|
yEaJSlA7nbNhfx7hhyotNdqah8KG5F3YAcFuRlWwXKp7yYwrEztQ2ddwOvxFlWpIQLcqSsq0yIbMRdHqlW9W5ldm6lYerzxZKa48W5l/3lEDlV4ZWVl6
|
||||||
|
3vO8Y6Ww8vT5hZVHzztX5o2VWZ7yef55p/hmpfi89/mAsVLEV88HVubw9RP89xTXDKw8Muiilcc0HN8bz/O4fonGzmDUEgR7ijU6cX2Qb0PSJUwEqZ9/
|
||||||
|
9bwPi/DSmGPQWHlMEhSe92PWxeddNOHSygxNz4I9pfnUF9gtffi8A7/WwM1XrgttGZiroMnQKyg6mqav3AxtkSRfiuXrKzfIAM+qXKlxC3YddDkHrUCX
|
||||||
|
xsoiaYquh+4Kundkh2ZQhniwsmQ8Pw+RFsgqEPDJ8wvkHDVxe2yuIL1qaeURJoOTPONfipiXvGZOmrmwR54vdV7AGnCkBfxwAbM9hnfwWvhyFvMvYU2o
|
||||||
|
tFTPRboKAlyI6wisfI9JHsPpBsQ6wwE1L8X2B1a+4U0WIMogx1uJzmW/ADqBo4mWQdgMAbf3DbDfWBmlMMJeBlQPgaLxPMm0sohopNHPu8iAtJDBQUXh
|
||||||
|
PcPqGuA1vuIYft5DepHBXGRpORT33mpA8Ayt3ALaXF2Z3kPDAdaCwEVW8jyJOAcbkiN1wYQFQgy5z0ekKorQAtt1Hlq5yQ4mQELom3e9wNru5+AqPr9A
|
||||||
|
U8Kl+TvM3I/POvzvxIyzkIBU1UWWPwDVCNQqGNJUBHPkFiSHANIZxj9yuq7nX5G7EZgCPJ8PavU0Vr4OIihNN7+ywJjF4SKQ+UJclyM8C0kjHP8xSf6U
|
||||||
|
9KnX5ID2i3DxLt7y8z52Cl+AuHYH26FIOcFveYgZw3O9hdYHAnCATINMSUFA+qQEsvKQhIjohEDYB6zEDvaOJY4YAEB8X4TSLLtg58FgCi0KdwykT0pH
|
||||||
|
4by7Mr9PKIjCdaDkagIfAQpkVPLPOXI74B6FCnnjSlGjexJUiEpvhcg2Cl3BAbIgsZbij1NyqGboim2tBAsQuSPeK0KEIocmzEO4J4Thel0XyqCcSxY4
|
||||||
|
/T0USC/AzpcXP8zJeoPKEYaPMtUX/BqmyB5Sij2MPOWI8ZVewwZ7J9ej6ucJrTXLWXFOugUs6sFfBI7rdXC4rMBFc7zWkqizyE0e8L4WlDOVlGwEn/hd
|
||||||
|
5OgZDJS49kysXS1/ajV5INISgwnNy6WALw9H4vOBmJZPhQKruDKv1/6pVJyRNd9hI2hlpMT5qP5aIo3Iz/q8mnsxnAHJL7ionvWjUpqK5uABGH4W1glm
|
||||||
|
IdI5ITo7PSbySpig68nU5mUykWQDc2g3mkqzmUhjIgKCmaR662nlImCgEyLN8mUXyuJVrxtVoSTwPHqO6YaHSNKnVhY1+lQrV8h/Cbuknh7H9qyokKRi
|
||||||
|
IjA8snNVAS0J+Mvrz0rdLKq+2TPmdLpZ0PVTBosilRyiCFyCpUgfj6WmY9pawSl0Gluh3ZVEc1x7a+VbvoJLYngCVW2cV8kes/u4ZoaKCqwijiR8Qv7M
|
||||||
|
BTWHzPOvSIeC5LIJH0oZHsV0wNjuKt0Wyv2nsF+jK1a2d07p76ZBRtiIXSxyDqAqubqfRbTNykoEIjhAmQdcC1csFKp006omLxHB/SISYjtrK2Mkdh39
|
||||||
|
wZiIC5/Qj09FDVagvgNlOkkUn0BOlVSK+kQ4ovnGOUfxTxm9zzi9DjBx6NLoxJXVDBHFpIRulRQeKqybxdflFtDr2VW0hagPCn60h+vjasWhRmuv0nKh
|
||||||
|
ACt6TF+FYGDeKk0/if8z3MRaFImWSTWVL19VpIwVOjJFARYl5L2GliBX3bUQgT1YlKRoP63zZMnW069fjcxvnJ3cXvp649q4Ru9wo9ArL/pmcGsyv9H3
|
||||||
|
ePPq0tbo463r3ZuX+zaHpzbPD21/tbDRX9jsHMeHXk2xNXVp+34BA14W73Hdsv3s+82v7oiB232TGwszweGbw+c3zo1ApvWOTo3entwCJrvycKP/SUxX
|
||||||
|
r9K2qzbnNhZmhaixbbzN4cnAyLi2nZAUomzc6NDq0pUodOP+9y+XBgzd50s2+h++fDr86ofu7ftLe+yzbTy+tHVpfOPRw1d95zcvFwJ7jWibbZ79dmOx
|
||||||
|
I2CWqC7Z1uzV7bm5ciegfXOH7J+L/Zv9l6k/9s/Fs9g6vGPz3ujm/IXtsX6Mg2/RUPF4ibEx2r11sXfr2pONp0PKjfbSx9o62w+/3UMHa/PKrc0HQ9v3
|
||||||
|
n726ch9ivuo4+2rk0cb5EZL6dsfmzTuvLt3f+u7qga0fr211Pto8O7ExM7R5o+vVtYtGnYF9Ct29XBp7deOHzRvP1B5iGki06q2+Vz9c9UIurlm0/ezq
|
||||||
|
5uzI5vDZ8BXRzaFXV3u2p59sPb5r/Jux0d/76pubWi2hjdExXIOteA2hzeGOjcLXYrrNqZHNocLb6AeRAeYHgBiwyubAREQHaOPZ062hO/H9ns0LFwGE
|
||||||
|
/1y8HsRF4AyM97J4zgPIl0/uEIjOLm2N3MeHYnmBRS+LjzcfjL/qO6csHdnAEdcExK/WtxEDyQFH5mN7My+f9Lx8dmPj3tVX3y6p8I5tv2BuBJtwd7Ea
|
||||||
|
Q7xYEvt/dfXJ5r0fhINvjQ9sPL4ggsID95imyfb0wkZP/8bweGmwc2wrvAjNFtEXkXKeu7X99Knn8bh4o3Bje3ooCG6lEVelq+FNglwhpo1pYmzeu43N
|
||||||
|
iDX0WhdetnyXzYqNC1eATcKkbOBXHde3n/X5KWnhzkbPwuatC8AuD7W2n323PTIovELpU6NxsPUdRcfGcCGMSdX7BJtzTzbOjQvh9FoCws83zp4HEm8N
|
||||||
|
D2iwfYGrsQx/8+urm+e645m9FCBUtVQi8RsXv3rV0aFD4AHEm99f3LxxZ/PxRR22LhxRXAVH1CHsQmhdir516eZm/0Xobeve0npHfnv63stHZ6Gf7bt5
|
||||||
|
VJAb84MbPaEoq0i2BSgiuMTaGsxaDNzue/BuyDR06gUFYCrgXxHcWdajF+6+LHYopK5CjiWC3R+EbmMpcMW6EQlq66sCIBj+rPRfkb8CDgGKGiT11d3L
|
||||||
|
wGiRjkSm++ci5Lsss8/Q3MvipIjOAE5H0k65S+HSKpUELq7OIMWVwgnE9gWKBC6uQhNRj289GRZ6ETmMCzUxiQThbwY9ZYoSVIP3RdcEkZqiyc+YUXRu
|
||||||
|
veuH9a7F9a6R9a4H613965131zvH1rsur3dOr3dNrnd1rXd9jQEa9I4q3/Wu6zRbZ3E9P77edWG9c3S9a4L/7F/PD613Dq7nb6539a533lvvfMTLfePp
|
||||||
|
Y73rK5akn1bHuhAg/yON7Lq93nVlvStPP+SvrOeX1vPXsLutgSlwv/X8fX+Gznme4SL9mf8R+t56eD14iQYbLNUHieBp4koMOYzUXnXux3JuFrHYs3ii
|
||||||
|
KDcllHl3Pd/Nu/tuvfNbLepYtr/7uq8o8LU8StfCfp3P5A+wgS6XZFPCph2kos6F9a4xFuQKqavzCWsMDvBkDxTzVX4CFJPNfXej97xS1LX1zoE4olkW
|
||||||
|
BN+8ujywnr8QSzore959dUsGm+bmen7af6sB5t6eRYSce/nk6nr+a8811/Pf4kJ2/Ql2o4c0j2Sj6/lBWkEqXvz5zUbfXVDUMvfWZqklPkpsVW4A4ctm
|
||||||
|
2QNtxV7lTsg5LnP4/MCmfsKOMraeP+85LG2b4huKG+Y/zx5Q2+zfHBjZWJzfnLi53fUU022OT6B8xK63fni8PXlemHU9P4C6QF1y1uCNnF/v+lEZ8HxY
|
||||||
|
NTHkd6t75NXIIFtBIlEc+eXpb/nARZvHls6FQzKaC2/NdQsWDOVsTwIX80JoLUaMixUVxgx0ef7ZxgXAz7m3wYNf5Yub576Hc26ODiNDKVBaYp+8GUGL
|
||||||
|
oentu5cDsRlJjte7vhFxedDPL8iAbKhQKinJONsj48iYAhDgJ6hX4Tzr+Ukh9tZ098Z3s3zVCMV/XkJHEO7LgiuSVIevvC+WiWTXKuhV0oQwlVaPJd58
|
||||||
|
yQPOmPfhAZgnUO3GMnCW4luOzenw+qFo5ZKmVOL8dDhQixz6gYgn/V5mxBcZ/UogaV3T5fA8+noliBzzkz9BTRgj5WIXKEbhm1hVk+VXKEVoHwKqzpai
|
||||||
|
GP1KDk9tAD/viLWfVZEgpitQqRYaDAhQHmgRDQMWCFRCr1vAPQKjrHzihd5N56D/IUG/Xz5K32N/9IvHbtbNjyoHy8RbOTm8fDKgEewanYaN+9+BuJRb
|
||||||
|
K6bfICOE0Os2Z8lJb1e1tB8EgG6chS/+qPJiqctrdCYoVYsSvevH+P7EueGt2YH4/kR5HR2ApEqNis3+JyobxDQq1jufsq4IXATDg3PrdCy8wfq9isDg
|
||||||
|
uHYFq7FAiab7wsbXD6houdu5fTdPJUr+3HondPBg4+YAEvz20qJAadHhKHO8qs0MlaoGlf3LcSC6t+G3NAzPIqWzvMVeBzuJ5yFj23cHlAtE9Tr670Qm
|
||||||
|
Ta+oqNIA2bg/+PJxbzUQj22JRNGgH+lMIH9JyPDq+nf0ydAFLioHt649hrmDcBrZO+EN9YtaRqODQgch/TcoB4ZLIlWiBcplpbiNnvGXT7+tVnPoNlrK
|
||||||
|
SxgRZRFJMr4HI/1clTEac1VpyTDiC0o5zQJShSRMIjo0MseNB7syRoBmVKsgojs20WXqxuLC9uQzVZ3uzTIkQ6MT1djZmRzfmbj6pveWsfvdI+NNfm7n
|
||||||
|
7tSb8zqNHB7/1dXdG3O7Yx271zqM14Wh3ZGC8ebr4d2B4Z2Lw8abnvO750bfnCvu3Fn0NLfTU6RPHuZ3r1w0dh+P7M5efTN0dffKlLH7/cXdWf9odrd7
|
||||||
|
eGe2m0ddKbyeLbwZmtgZ6N8ZGNV5Eme3f/h1gS/mLT2N6ciU771qZ0UIs3NxPLYHo0ZehX6M3aFzuzcGtXovvkl0ey6vFzpIhzeu+sq7USTVd8/hT+2m
|
||||||
|
y5tL3Tsjwzvnh3Z7rhr0y92p3Z6BPR7l797K716fgFivH9wyYDX4hGfIq7vdxd3rl+L6LQHfvDL3uljQ7rNI14E+uMPy+52rF3fOXfK6K7s3uo2dh9fg
|
||||||
|
czvdt3fPzUu3gtLmjJ3i4M6Nxd3rhd1bHTTB7rNbbzpH6Wqo5vVCkRSLy+G15Cxhl9zLyT/Jeau79hYKSUTyvum6xyFy6+vXC4tvhqCqfgh9pTewLZL/
|
||||||
|
wdzOt8MHdjuncA3Cz3hzbWj3+zvG64eDb4aGaUP0wY0imWumsNOJKYojOxOYfnB8p/si7/jK0+BeY5oku7eGdyYnSBBWHcd8XJtkd6h7dywvB78udIS3
|
||||||
|
ofUwyc5DCHrjqfKVuL7IzmDHzvfz3oMj8mqK07fQFtntvvWm64bxZugeNEWmfF24CKQgvLjSX7Lvyv2Rnak5RBVshnHx/ZE3g/mDAcCGb4fwOYTeCpFv
|
||||||
|
DcEv4CFGibCIn4lLFJ4jwwTbO3d7gtJGtj8EHssJI9se7N5K3iCOR/U4dsbyb/JTO2ODBgXw95p3F2Cz8BnAhlymciCFBCK/llEkcBPgsPP9OIB+hNDt
|
||||||
|
3CgpdbcwvjMwXpq5YtoYO+OLkL0cja5cJAFl/FPQe4jkIareUxt+CiZDAvG/Ha684RLYhjwP5oILxbQnAuvMyXUqenf1poRY2dgd/bqGroRfMlCWfifN
|
||||||
|
CMAfooZrIM+Bg32InQvdr+enDEQQwMaQ6akMct/0zpPSlUuGnEij6wB02+3K+wVXTLfhzbXh3e4ZX2C9FoOIMsobRdjpFpdcr2c7kLTDaT66tyDSRWxX
|
||||||
|
YacwvLswGN9V8AoHkRo9HKjUUNj5ZnR38ikG6vQUdnsHqaBCotjtvkFuqdNP8EfrNBR2b/XvDj3V7Sa86SII2AewuLozXTSgbVxs7BQuAJa7d2Zv7Vxi
|
||||||
|
tN7tugHblrhQ1f6BwHbOwKy7ikGr20HwTVAtGn+jroEPlASbQC5VFUQ0DbC7nTvFQNBScrvjXVmlVbB7rXvn9mBFCNXuEwQL9iDnucr10/mCsTNVRJSQ
|
||||||
|
USgKum68udxfVhxVaBBICJfgrQqF6C6BTECh8kDk60B9iFCfDaZDGlNWCcS1BcIZFVOIWKmuysh+gKxJSBKJShHTVGkF7HTTjne/G0QVIXVu1ElrcFwF
|
||||||
|
OgChujmYF6Npv34Npq9rxi43iuiv9az147/e1cm1fgM/nlsdN9a61rrph9UZDb6/OrbWjYs7VxeM1fm1/Oq91bHVSYM+VbP18y+9q/Pi0x7879zqrLGW
|
||||||
|
XztnrE7jElywOmPg4278eG+1yDMohfL89/HR/OqEsTq+1gW988yTWK4bn/CX02s9Fa7g8Tp9AayNDaw+MlbvQMKzRlArMT0CvnQ+sOHoPsFajxA8tk2w
|
||||||
|
+hiTkVYC4+NaBKuP1npZ3b3ciWJlh/cR1zKoMMPqj2vn1rqraVerg7B6Z/XR6jSZifbzANd10tTd0DZEw+dTe+wm4OIFzHaO5uvnVbAG+ckY+xI0Rz/F
|
||||||
|
NRQwJo+x98rNHtVV4DXmsXKJzsIK6lB3c6zeRoBxq0F+HeWyBsSZpN1QUHBkiDAx+NMZbDiv9jiDj8bx94KKrv7VKfxHWuAIW+tbLayOyS/Zl/K8U3L2
|
||||||
|
Sdhiiq3Lk02v3sf+u/feteBZ7kDpZOuZGroXtBMD/v6ADIq/H4U0g+3Mk2AqOqHwblKNMjQUoLAGm13rY/XdW+skr4A1jdXb+LQfZiUNMszcEw7YD7SZ
|
||||||
|
UHpjBWOWeweUIhHYj7g+Z9t5XsJfBy9enSLjkx+uTmi1QRj1CPNwDe3c96C4ZogYPklGRjBVR0/8MEmaIul+kNPGvnID03VSLJHrdMKxxlZHVie0Oiac
|
||||||
|
AGY4Z4z5L9yoPN9b6KFAnB5hpq7VOQqVR+QFwlXuQapzq9MR/RPOLWTSPgkdkf0TSmzkNasTB7FLWneclse+KcmVpj4ZnOzCyj4ia06QQ6pM4sspI1Mo
|
||||||
|
kjKzb+cJjbYKht5nrx4jbxMycKaLbLGwHCKWQiLGtlkQWPnVoriG7D0OWAIKYUW9hovEAQjZJS0mRaHg5VCsIBVHO/QxUxksBCY95PBFaOg1Wyj3wlRn
|
||||||
|
q2Gkml3BVATUEngwXg3otWEY4gu0GwKxHrG0H+jSiIFg30/RPialqpICtRozImw0V41p0VAAARHOl8ih169RRY9IoO/wuRM/NnsoAlmxdV6YK/eEzsdJ
|
||||||
|
R3MMgQuUb6YJzoNVrMwgcYlDo3/jJw/h7VNwu4mYJo6/j15IuSDHx3ZxpCdTJE9hkwzj4yoSONQ1mjglyS2qkcPgRBVA3vOwyIaOgpsqRXulvg7CF7WU
|
||||||
|
TlOHA6Hbi3zogeqaGZ3WTtj5vXl0+jwkPIHJ6oxur4eL6DlKrmI5VRxxsdPvBe59lVqm+ftuwqt+Kd9DDvheHx6r9oAERPgoH0xHGv0fLzHLKRg2382T
|
||||||
|
MlRHrz6UwF3qORG9IBHxXuq5DcXOrJ2N6gOVJLU7lMRZd/GvZ6UqYPVHA2ssrN6mon2MVvyRTTVSMd2V8rWIHhBmeiQLDFwlU4g0Y3QniMWaDhQ3Xopb
|
||||||
|
HRRQJyHSL2g8hY3je65iAFtjeu2gEgX6cVmhGNDoB5VMJz3Rd+TJtdh3OkBfc9QlUSSLJZgj6sYJooR+eNlvrYtiLpou1vBKh3+p3KzdUsLFNf/dUEUL
|
||||||
|
dTpIftsIwlLDiLse0A8t/PdBMszfB4nUzAG8YO4ZCoNpIj2kyEmSvv+XCTnaT7n3xdYfchX6y8RaLyaZFnxunKZ5gMDtxyzhC6gfQUurEmStB3PC2vhK
|
||||||
|
p5OEdboh8pJYg4ButSgDPfKfHQ3pK7Z7xKqIbSH5e6CtP5AZNbaJ9APpdvWBJ7xBatV9nSvZhNC0d81/OhfaA3JpdYrWliDpHDHc3tUi9NjDZJdUCtPt
|
||||||
|
sUdEu7nP9RhU4DsCFWRUpo1zru4zGEm72dT0ve4/VSq8kSaZ9eaO7R0F/qEWjB8TfaEqDilXmCIr/DJNDGN1Bi6p1tpPP8H9/z74j45BGQIcNezDPOEv
|
||||||
|
HCv9/Mu0cn7hEaRgmp5mo6JSOjymok/w2fQeu0F/axFjv6z1GSDyH9rCpMBIT2CxJSq0f2EQmOZm0QxtxGCvmVbbgEqwcwK+WU8fQJhZ6ZUCbvCDCNXV
|
||||||
|
6QOYSQKMtOWMnNfzlTqDzbS2RCKRk8jCneauYP+amkC/sFDj5Iu0Qf1WUB+PnC1FwWCsx7R8DNHzgWY7KQB6UGWICrIv7Mp67R8MhxpC3R85swg/Mg22
|
||||||
|
9Tb6P2PszZRVxymgezn2YWOG8ujmD42dpp2TEhA4ZCRpjJg+0C8TGNl3UCUvP015HseuNcVx52uYqoUx+NcvzAkDckv9Sf/3qwZyRaq/8Occ251Vz0ER
|
||||||
|
SFMxbaHAQiplRbaEVPx5W5GoIZJdGPgiGkSiGWTwZb20Y1ZSYfU+rp4pdbvYdpHQLktGEVmKHUH5sBCFcdk2gjBCmiT7I4H1GSXQQa6rop3PMKYZTHk9
|
||||||
|
iTp6jaVuQW/XBgzuj00ybnHgVEHtMRY9kEv7fYiJ6yUJaACaC3CQGULAFRYgpRN8En8EamJXj4KoF5UetRpK5fVZ6dJc0vVKe0d0k9jQRFG6jVqbSRw+
|
||||||
|
4rmkcPn0DjtKAm3vSScit6TifYFUQqyhV5hdeMgs/6jsXzUPlcVizQ0lTkQl4CJON1entfpLtKs8n1p16/WXvHYS7d4HgQoZ+NftMyG0ZgVkavSYAnJN
|
||||||
|
+4ao3mDiCoWUp9lkmiJZOMAktSXGw8wRUz3SajcFo6PKhFq9p+A83ra1+1B+oc466KWDXg56opr3mV7PC8d/RChL2RjfkJRTIj+T3IbfFqjahCpNgAEL
|
||||||
|
zXnFd2QbijYaeJqpdIa32IaiTgIdNa1OV3KwiD6UKBj8PMbkYCYqL1dpT61OYlSPcLpeT4FRXSmqPLHYmCAPDziSeiXhrcb+yOxzQi5Dkbwf8Nl0VIMq
|
||||||
|
mH+4pznJ3tkb3ldMr8rPsqHCrKQC4ungjoyvAYogj+Arq1WzfVVSlVQspaoCgShdNNpapVEh1B5wbZnmNVpbtFGKhHH80CfSk0y5JFzg3yIKU9Ry2Nbq
|
||||||
|
YtVYLrOFhIRhDdLoOSpa+uTStPZpO6ph9WL0/os7A/xn34vRzhejN/Gr8WJ0nn+fwk9DL+6cfzE6+2L02YvRyRejBY1+1ovRHh47jkmMaiuM86c3sciL
|
||||||
|
O3ksY3hDQstP8k/D+OkSFsdfj16MLr0Y7WORxA83WUqS7wJ+uiL+usnXCeGL/KcYxmvjr36eGh9fF9NigoJcvg6/TmMh/NXL+xgS+9DYVJFnOU8b4FmL
|
||||||
|
YvAwb3Hkxejii9FRiM3bgAh3RjUaatXWmlSCB+wT2khMzy3WyFXbaqEN8QYf8e8XYhty5VeO88UdwuysFCl4XJeumlqmX9zpZMMVIBD2pNW38y3Nxqk8
|
||||||
|
s/hFOpP6rlhtybhGn/K/Aq81omzwECoxAt91CCci991b948FE5a+K+YOuLoXVZNsFhijSwwYD4TPzXKvimgLVtPeZf50NjBJVH9wT9HG1pNdRQ7hJf/F
|
||||||
|
QRzozzwjs6HyCEBhV7LilFQ5fXmWHJWitRCCpSEBVWwV+l7IIFBovE785aNMkWW9xwNmKfDvdPg+rmI/sq9YOzrTTWhqZeGpbLnR2zXcixbh/0WBrP1i
|
||||||
|
8VEB26EQnlRggOuuCyUMCdDbbyjVTfH0wnQjBwI6JE+74KO2H2UyKnrYIfIkGk35TBhZLDwsfiI7h2B8KWikEeHq/Sz0bKlXKpvE9C1JHNqG2MCCUEKB
|
||||||
|
MxMkGq2Q2+I6mbyze8I/pFKHeIr+yonS17jefW2ssGd83U1hox8EAFeaoVKtzS52hZ35CgWNanFWmXh06W00OTmkpz1MWxICyPx6XRQIEY1O6U5ARdLD
|
||||||
|
ZXGZiJZxdWVkw5OBP6/clfzxoFeJFIUxhQ93CKsFpQ2WQIZXYRQ4kUyWYk55fle4KK+b9aofOHcX+SRdc8svEiKroaoVVSEcFpEN1XJB5XYjm6oeqJTK
|
||||||
|
7RdqUcVNVKNVKIdzw6ynok5hiSLHTx9XI95ksc1WURV6gFMqWwgGhzx0ktAUgsJgiekXuFHIF/jujlSZVNGUFOPOWQUc5B+VqsyYxizP2MUuM+1F0yMB
|
||||||
|
jjVlycrZw//eT5lGIGWWeltcZ7ciMM6KYqpCXTmgDMCf7dctefxAkxqRuxd5pxiSXKsjHCN2mMfENIUrii33KvKR8oH9+/drv7vKYy2VdeK79RXx07tp
|
||||||
|
IwdsISqBKXa2MS8HhxGlrqpf3mTMneUJloQrx1YNQ6oqpQFjvASuuR9UXTXcUu6t0ZZmDXfy/ubFGtVKi+p9aeX0QlPPxBwh1eg1qkOYqYJCbjYEE0XW
|
||||||
|
cIdadCGQVn2oje5jl9dXsf1szuWElvHtbC18Ks8OlTrdrEkI+h3G6fS6Ve4VlG/Uh5V5Ns1DheR9vOvbOp3vatdKnZcCAbabV1gQ1QgPmzuQwyrDSlxn
|
||||||
|
XJGGK/uEep+F1E+/DnnxVlB1/EOfnV7g8efZ6fuYLItcfoXrZZp52gtev4ILlvMV2+jBCqx0vwrcanopWBUvikXL36i1Hu/qqvb1/Tyi4c7DOxXt7gvV
|
||||||
|
Lnr1WpUGPE/UKUwflbqlOaI68yUtuxobOZJ7eRw2z1Ws34MItQG+E0YVXb0p1dPq8LphXuFVsbXv1R8H48oKjRZ/CQZXrvavy3qHJ91nBKq8gO5DFo1J
|
||||||
|
YXFt/4jiPgL19CvpJY0zgZLGXvV4D61ZZZkqBwZKqTdFI0p0XVQqlAaYVlW/amuptBissGStf8krqDXamxU6H8o+0acOvw1XrOBXCilq43e8iS+//H/O
|
||||||
|
wOW7jPkAAA==
|
||||||
|
`
|
||||||
|
|
||||||
|
function loadStrings () {
|
||||||
|
const buf = Buffer.from(PACK_B64.replace(/\s+/g, ''), 'base64')
|
||||||
|
return JSON.parse(zlib.gunzipSync(buf).toString('utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function main () {
|
||||||
|
const STRINGS = loadStrings()
|
||||||
|
const payload = {
|
||||||
|
version: VERSION,
|
||||||
|
strings: STRINGS,
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(__dirname, 'translations.json'),
|
||||||
|
JSON.stringify(payload, null, 2) + "\n",
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
console.log('Wrote', path.join(__dirname, 'translations.json'))
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
2735
memento-note/extension/i18n/translations.json
Normal file
2735
memento-note/extension/i18n/translations.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Memento Web Clipper",
|
"name": "__MSG_extName__",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "Enregistrez des pages et des sélections dans Momento avec résumé IA.",
|
"description": "__MSG_extDescription__",
|
||||||
|
"default_locale": "en",
|
||||||
"permissions": ["activeTab", "scripting", "storage", "sidePanel", "tabs"],
|
"permissions": ["activeTab", "scripting", "storage", "sidePanel", "tabs"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"http://localhost:3000/*",
|
"http://localhost:3000/*",
|
||||||
@@ -26,6 +27,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "Momento Web Clipper"
|
"default_title": "__MSG_extActionTitle__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,3 +488,20 @@ input[type="text"]:focus,
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
.preview-tags { justify-content: flex-start; border-top: none; margin-top: 0; padding-top: 0; }
|
.preview-tags { justify-content: flex-start; border-top: none; margin-top: 0; padding-top: 0; }
|
||||||
|
|
||||||
|
html[dir="rtl"] .header,
|
||||||
|
html[dir="rtl"] .header-right,
|
||||||
|
html[dir="rtl"] .selection-head,
|
||||||
|
html[dir="rtl"] .page-row {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
html[dir="rtl"] .page-row .url {
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
html[dir="rtl"] .notebook-select,
|
||||||
|
html[dir="rtl"] .dropdown-item,
|
||||||
|
html[dir="rtl"] .label,
|
||||||
|
html[dir="rtl"] .sub {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div id="connBadge" class="conn-badge" hidden>
|
<div id="connBadge" class="conn-badge" hidden>
|
||||||
<span class="conn-dot"></span>
|
<span class="conn-dot"></span>
|
||||||
<span id="connLabel">Connecté</span>
|
<span id="connLabel"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="settingsBtn" class="icon-btn" title="Instance Momento" aria-label="Instance Momento">
|
<button type="button" id="settingsBtn" class="icon-btn" title="" aria-label="">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,31 +32,29 @@
|
|||||||
|
|
||||||
<div id="settingsPanel" class="settings-panel" hidden>
|
<div id="settingsPanel" class="settings-panel" hidden>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>URL de votre instance Momento</span>
|
<span id="instanceUrlLabel"></span>
|
||||||
<input id="baseUrl" type="text" spellcheck="false" placeholder="http://localhost:3000" />
|
<input id="baseUrl" type="text" spellcheck="false" placeholder="http://localhost:3000" />
|
||||||
</label>
|
</label>
|
||||||
<div class="preset-row">
|
<div class="preset-row">
|
||||||
<button type="button" class="preset-btn" data-url="https://memento-note.com">Production</button>
|
<button type="button" class="preset-btn" data-url="https://memento-note.com"></button>
|
||||||
<button type="button" class="preset-btn" data-url="http://localhost:3000">localhost:3000</button>
|
<button type="button" class="preset-btn" data-url="http://localhost:3000">localhost:3000</button>
|
||||||
<button type="button" class="preset-btn" data-url="http://127.0.0.1:3000">127.0.0.1:3000</button>
|
<button type="button" class="preset-btn" data-url="http://127.0.0.1:3000">127.0.0.1:3000</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
<button type="button" id="applyInstanceBtn" class="btn btn-primary btn-sm">Appliquer & reconnecter</button>
|
<button type="button" id="applyInstanceBtn" class="btn btn-primary btn-sm"></button>
|
||||||
<button type="button" id="openLoginBtn" class="btn btn-secondary btn-sm">Ouvrir Momento ↗</button>
|
<button type="button" id="openLoginBtn" class="btn btn-secondary btn-sm"></button>
|
||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint"></p>
|
||||||
Connectez-vous sur <strong>la même URL</strong> dans Chrome (Google OAuth). En dev, utilisez exactement
|
|
||||||
<code>http://localhost:3000</code> ou <code>http://127.0.0.1:3000</code> — pas un mélange des deux.
|
|
||||||
</p>
|
|
||||||
<p id="settingsStatus" class="settings-status" hidden></p>
|
<p id="settingsStatus" class="settings-status" hidden></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main id="screen" class="main"></main>
|
<main id="screen" class="main"></main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<span class="footer-meta">Momento Web Clipper v0.3.0</span>
|
<span class="footer-meta"></span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="i18n.js"></script>
|
||||||
<script src="sidepanel.js"></script>
|
<script src="sidepanel.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async function ensureApiPermission() {
|
|||||||
const has = await chrome.permissions.contains({ origins: [origin] })
|
const has = await chrome.permissions.contains({ origins: [origin] })
|
||||||
if (!has) {
|
if (!has) {
|
||||||
const granted = await chrome.permissions.request({ origins: [origin] })
|
const granted = await chrome.permissions.request({ origins: [origin] })
|
||||||
if (!granted) throw new Error('Autorisez l’accès à votre instance Momento dans Chrome.')
|
if (!granted) throw new Error(t('errPermissionDenied'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ function sortNotebooksHierarchy(list) {
|
|||||||
byParent.get(pid).push(n)
|
byParent.get(pid).push(n)
|
||||||
}
|
}
|
||||||
for (const items of byParent.values()) {
|
for (const items of byParent.values()) {
|
||||||
items.sort((a, b) => (a.name || '').localeCompare(b.name || '', 'fr'))
|
items.sort((a, b) => (a.name || '').localeCompare(b.name || '', uiLocaleTag()))
|
||||||
}
|
}
|
||||||
const out = []
|
const out = []
|
||||||
const seen = new Set()
|
const seen = new Set()
|
||||||
@@ -133,19 +133,19 @@ function notebookSelectHtml() {
|
|||||||
.map((n) => {
|
.map((n) => {
|
||||||
const indent = n.depth > 0 ? '\u00A0\u00A0'.repeat(n.depth) + '↳ ' : ''
|
const indent = n.depth > 0 ? '\u00A0\u00A0'.repeat(n.depth) + '↳ ' : ''
|
||||||
const sel = n.id === selectedNotebookId ? ' selected' : ''
|
const sel = n.id === selectedNotebookId ? ' selected' : ''
|
||||||
return `<option value="${escapeHtml(n.id)}"${sel}>${escapeHtml(indent + (n.name || 'Sans nom'))}</option>`
|
return `<option value="${escapeHtml(n.id)}"${sel}>${escapeHtml(indent + (n.name || t('notebookUnnamed')))}</option>`
|
||||||
})
|
})
|
||||||
.join('')
|
.join('')
|
||||||
return `<select id="notebookSelect" class="notebook-select" aria-label="Carnet de destination">
|
return `<select id="notebookSelect" class="notebook-select" aria-label="${escapeHtml(t('destinationNotebook'))}">
|
||||||
${notebooks.length ? opts : '<option value="">Aucun carnet</option>'}
|
${notebooks.length ? opts : `<option value="">${escapeHtml(t('noNotebooks'))}</option>`}
|
||||||
</select>`
|
</select>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatReadingTime(minutes) {
|
function formatReadingTime(minutes) {
|
||||||
const m = Number(minutes) || 0
|
const m = Number(minutes) || 0
|
||||||
if (m <= 0) return ''
|
if (m <= 0) return ''
|
||||||
if (m === 1) return '1 min de lecture'
|
if (m === 1) return t('readingTimeOne')
|
||||||
return `${m} min de lecture`
|
return t('readingTimeOther', String(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getActiveTab() {
|
async function getActiveTab() {
|
||||||
@@ -186,7 +186,7 @@ async function syncPickMode() {
|
|||||||
function updateConnBadge() {
|
function updateConnBadge() {
|
||||||
if (!els.connBadge) return
|
if (!els.connBadge) return
|
||||||
els.connBadge.hidden = !connected
|
els.connBadge.hidden = !connected
|
||||||
if (els.connLabel) els.connLabel.textContent = connected ? 'Connecté' : 'Déconnecté'
|
if (els.connLabel) els.connLabel.textContent = connected ? t('connected') : t('disconnected')
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSettingsStatus(msg, isError) {
|
function setSettingsStatus(msg, isError) {
|
||||||
@@ -207,14 +207,14 @@ function selectionBlockHtml() {
|
|||||||
if (selectionText) {
|
if (selectionText) {
|
||||||
return `<div class="selection-panel has-text" id="selectionSlot">
|
return `<div class="selection-panel has-text" id="selectionSlot">
|
||||||
<div class="selection-head">
|
<div class="selection-head">
|
||||||
<span class="status live"><span class="pulse-dot sky"></span> Sélection détectée</span>
|
<span class="status live"><span class="pulse-dot sky"></span> ${escapeHtml(t('selectionDetected'))}</span>
|
||||||
<button type="button" class="clear-btn" id="clearSel">Ignorer</button>
|
<button type="button" class="clear-btn" id="clearSel">${escapeHtml(t('ignore'))}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-body"${rtlAttrs(selectionText)}>「 ${escapeHtml(selectionText)} 」</div>
|
<div class="selection-body"${rtlAttrs(selectionText)}>「 ${escapeHtml(selectionText)} 」</div>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
return `<div class="selection-hint" id="selectionSlot">
|
return `<div class="selection-hint" id="selectionSlot">
|
||||||
<p>Astuce : surlignez du texte sur la page pour clipper une sélection précise. Le panneau reste ouvert pendant la sélection.</p>
|
<p>${escapeHtml(t('selectionHint'))}</p>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,15 +224,15 @@ function actionsBlockHtml() {
|
|||||||
${
|
${
|
||||||
hasSel
|
hasSel
|
||||||
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
? `<button type="button" class="btn btn-sky" id="clipSelBtn">
|
||||||
${ICON_SELECT} Clipper la sélection
|
${ICON_SELECT} ${escapeHtml(t('clipSelection'))}
|
||||||
</button>`
|
</button>`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
<button type="button" class="btn ${hasSel ? 'btn-secondary' : 'btn-primary'}" id="clipPageBtn" ${pageRestricted ? 'disabled' : ''}>
|
<button type="button" class="btn ${hasSel ? 'btn-secondary' : 'btn-primary'}" id="clipPageBtn" ${pageRestricted ? 'disabled' : ''}>
|
||||||
${ICON_CLIP} Clipper cette page
|
${ICON_CLIP} ${escapeHtml(t('clipPage'))}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-link link-only" id="clipLinkBtn" ${pageRestricted ? 'disabled' : ''}>
|
<button type="button" class="btn-link link-only" id="clipLinkBtn" ${pageRestricted ? 'disabled' : ''}>
|
||||||
${ICON_LINK} Enregistrer le lien seul
|
${ICON_LINK} ${escapeHtml(t('saveLinkOnly'))}
|
||||||
</button>
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
@@ -290,7 +290,7 @@ async function refreshPageContext() {
|
|||||||
|
|
||||||
if (!tab?.id || pageRestricted) {
|
if (!tab?.id || pageRestricted) {
|
||||||
pageUrl = tab?.url || ''
|
pageUrl = tab?.url || ''
|
||||||
pageTitle = tab?.title || 'Page non accessible'
|
pageTitle = tab?.title || t('pageNotAccessible')
|
||||||
selectionText = ''
|
selectionText = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -354,9 +354,9 @@ async function loadNotebooks(preferredId) {
|
|||||||
connected = false
|
connected = false
|
||||||
updateConnBadge()
|
updateConnBadge()
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
throw new Error('Connectez-vous à Momento sur la même URL (bouton « Ouvrir Momento »).')
|
throw new Error(t('errLoginRequired'))
|
||||||
}
|
}
|
||||||
throw new Error('Impossible de charger les carnets.')
|
throw new Error(t('errLoadNotebooks'))
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
notebooks = data.notebooks || []
|
notebooks = data.notebooks || []
|
||||||
@@ -367,7 +367,7 @@ async function loadNotebooks(preferredId) {
|
|||||||
connected = true
|
connected = true
|
||||||
updateConnBadge()
|
updateConnBadge()
|
||||||
errorMessage = ''
|
errorMessage = ''
|
||||||
setSettingsStatus('Carnets chargés.', false)
|
setSettingsStatus(t('notebooksLoaded'), false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notebooks = []
|
notebooks = []
|
||||||
connected = false
|
connected = false
|
||||||
@@ -381,16 +381,16 @@ async function applyInstance() {
|
|||||||
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
const url = (els.baseUrl?.value || DEFAULT_BASE).replace(/\/$/, '')
|
||||||
if (els.baseUrl) els.baseUrl.value = url
|
if (els.baseUrl) els.baseUrl.value = url
|
||||||
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
await chrome.storage.sync.set({ [STORAGE_KEYS.baseUrl]: url })
|
||||||
setSettingsStatus('Connexion en cours…', false)
|
setSettingsStatus(t('connecting'), false)
|
||||||
await loadNotebooks(selectedNotebookId)
|
await loadNotebooks(selectedNotebookId)
|
||||||
if (connected) {
|
if (connected) {
|
||||||
setSettingsStatus(`Connecté à ${url}`, false)
|
setSettingsStatus(t('connectedToUrl', url), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIdle() {
|
function renderIdle() {
|
||||||
const restrictedBlock = pageRestricted
|
const restrictedBlock = pageRestricted
|
||||||
? `<div class="restricted-note">Cette page ne peut pas être clippée (page système Chrome). Ouvrez un site web normal.</div>`
|
? `<div class="restricted-note">${escapeHtml(t('restrictedPage'))}</div>`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const authHint =
|
const authHint =
|
||||||
@@ -403,12 +403,12 @@ function renderIdle() {
|
|||||||
${authHint}
|
${authHint}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="label">Carnet de destination</span>
|
<span class="label">${escapeHtml(t('destinationNotebook'))}</span>
|
||||||
${notebookSelectHtml()}
|
${notebookSelectHtml()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-card">
|
<div class="page-card">
|
||||||
<span class="sub">Page active</span>
|
<span class="sub">${escapeHtml(t('activePage'))}</span>
|
||||||
<div class="page-row">
|
<div class="page-row">
|
||||||
<img src="${escapeHtml(pageFavicon)}" alt="" onerror="this.src='https://www.google.com/s2/favicons?domain=google.com&sz=32'" />
|
<img src="${escapeHtml(pageFavicon)}" alt="" onerror="this.src='https://www.google.com/s2/favicons?domain=google.com&sz=32'" />
|
||||||
<div class="page-text">
|
<div class="page-text">
|
||||||
@@ -432,9 +432,9 @@ function renderLoading(label) {
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="state-title">Analyse de la source</div>
|
<div class="state-title">${escapeHtml(t('analyzingSource'))}</div>
|
||||||
<div class="state-sub">${escapeHtml(label || 'Traitement en cours…')}</div>
|
<div class="state-sub">${escapeHtml(label || t('statusAnalyzing'))}</div>
|
||||||
<div class="state-detail">Résumé, tags et préparation de la note Momento.</div>
|
<div class="state-detail">${escapeHtml(t('processingDetail'))}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -448,9 +448,9 @@ function renderConfirm() {
|
|||||||
|
|
||||||
els.screen.innerHTML = `
|
els.screen.innerHTML = `
|
||||||
<div class="confirm-panel">
|
<div class="confirm-panel">
|
||||||
<span class="label">Aperçu avant enregistrement</span>
|
<span class="label">${escapeHtml(t('previewBeforeSave'))}</span>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Titre de la note</span>
|
<span>${escapeHtml(t('noteTitleLabel'))}</span>
|
||||||
<input id="titleInput" type="text" value="${escapeHtml(editableTitle)}" maxlength="300" />
|
<input id="titleInput" type="text" value="${escapeHtml(editableTitle)}" maxlength="300" />
|
||||||
</label>
|
</label>
|
||||||
${
|
${
|
||||||
@@ -466,7 +466,7 @@ function renderConfirm() {
|
|||||||
${
|
${
|
||||||
excerpt && pendingClipType !== 'link'
|
excerpt && pendingClipType !== 'link'
|
||||||
? `<div class="excerpt-preview"${rtlAttrs(excerpt)}>
|
? `<div class="excerpt-preview"${rtlAttrs(excerpt)}>
|
||||||
<span class="excerpt-label">Extrait</span>
|
<span class="excerpt-label">${escapeHtml(t('excerptLabel'))}</span>
|
||||||
${escapeHtml(excerpt)}
|
${escapeHtml(excerpt)}
|
||||||
</div>`
|
</div>`
|
||||||
: ''
|
: ''
|
||||||
@@ -474,8 +474,8 @@ function renderConfirm() {
|
|||||||
${tagsHtml ? `<div class="tags preview-tags">${tagsHtml}</div>` : ''}
|
${tagsHtml ? `<div class="tags preview-tags">${tagsHtml}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn btn-primary" id="saveBtn">Enregistrer dans Momento</button>
|
<button type="button" class="btn btn-primary" id="saveBtn">${escapeHtml(t('saveToMomento'))}</button>
|
||||||
<button type="button" class="btn-link" id="cancelConfirmBtn">Retour</button>
|
<button type="button" class="btn-link" id="cancelConfirmBtn">${escapeHtml(t('back'))}</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -500,16 +500,16 @@ function renderSuccess() {
|
|||||||
<div class="center-state" style="justify-content:flex-start;padding-top:12px">
|
<div class="center-state" style="justify-content:flex-start;padding-top:12px">
|
||||||
<div class="success-icon">✓</div>
|
<div class="success-icon">✓</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="badge-ok">Note enregistrée</span>
|
<span class="badge-ok">${escapeHtml(t('noteSaved'))}</span>
|
||||||
<div class="note-title"${rtlAttrs(successTitle)}>${escapeHtml(successTitle)}</div>
|
<div class="note-title"${rtlAttrs(successTitle)}>${escapeHtml(successTitle)}</div>
|
||||||
<div class="state-detail">Carnet « ${escapeHtml(nb?.name || '')} »</div>
|
<div class="state-detail">${escapeHtml(t('sentToNotebook', nb?.name || ''))}</div>
|
||||||
${reading ? `<div class="state-detail">${escapeHtml(reading)}</div>` : ''}
|
${reading ? `<div class="state-detail">${escapeHtml(reading)}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${tagsHtml ? `<div class="tags">${tagsHtml}</div>` : ''}
|
${tagsHtml ? `<div class="tags">${tagsHtml}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn btn-primary" id="viewBtn">Voir dans Momento ↗</button>
|
<button type="button" class="btn btn-primary" id="viewBtn">${escapeHtml(t('viewInMomento'))} ↗</button>
|
||||||
<button type="button" class="btn-link" id="againBtn">Clipper autre chose</button>
|
<button type="button" class="btn-link" id="againBtn">${escapeHtml(t('clipAnother'))}</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
document.getElementById('viewBtn')?.addEventListener('click', () => {
|
document.getElementById('viewBtn')?.addEventListener('click', () => {
|
||||||
@@ -529,13 +529,13 @@ function renderError() {
|
|||||||
<div class="center-state">
|
<div class="center-state">
|
||||||
<div class="error-icon">!</div>
|
<div class="error-icon">!</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="state-title" style="color:#ef4444">Échec</div>
|
<div class="state-title" style="color:#ef4444">${escapeHtml(t('failure'))}</div>
|
||||||
<div class="state-detail">${escapeHtml(errorMessage || 'Une erreur s\'est produite.')}</div>
|
<div class="state-detail">${escapeHtml(errorMessage || t('genericError'))}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn btn-danger" id="retryBtn">Réessayer</button>
|
<button type="button" class="btn btn-danger" id="retryBtn">${escapeHtml(t('retry'))}</button>
|
||||||
<button type="button" class="btn-link" id="backIdleBtn">Retour</button>
|
<button type="button" class="btn-link" id="backIdleBtn">${escapeHtml(t('back'))}</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
document.getElementById('retryBtn')?.addEventListener('click', () => {
|
document.getElementById('retryBtn')?.addEventListener('click', () => {
|
||||||
@@ -553,7 +553,9 @@ function renderError() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
if (state === 'loading' || state === 'saving') return renderLoading(state === 'saving' ? 'Enregistrement…' : 'Analyse…')
|
if (state === 'loading' || state === 'saving') {
|
||||||
|
return renderLoading(state === 'saving' ? t('statusSaving') : t('statusAnalyzing'))
|
||||||
|
}
|
||||||
if (state === 'confirm') return renderConfirm()
|
if (state === 'confirm') return renderConfirm()
|
||||||
if (state === 'success') return renderSuccess()
|
if (state === 'success') return renderSuccess()
|
||||||
if (state === 'error') return renderError()
|
if (state === 'error') return renderError()
|
||||||
@@ -573,7 +575,7 @@ async function runAnalyze(type) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (type === 'selection') {
|
if (type === 'selection') {
|
||||||
if (!selectionText) throw new Error('Aucune sélection active.')
|
if (!selectionText) throw new Error(t('errNoSelection'))
|
||||||
await refreshPageContext()
|
await refreshPageContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,14 +595,14 @@ async function runAnalyze(type) {
|
|||||||
body: JSON.stringify(analyzeBody),
|
body: JSON.stringify(analyzeBody),
|
||||||
})
|
})
|
||||||
const analysis = await analyzeRes.json()
|
const analysis = await analyzeRes.json()
|
||||||
if (!analyzeRes.ok) throw new Error(analysis.error || 'Analyse impossible')
|
if (!analyzeRes.ok) throw new Error(analysis.error || t('errAnalyzeFailed'))
|
||||||
|
|
||||||
analyzeResult = analysis
|
analyzeResult = analysis
|
||||||
editableTitle = analysis.title || pageTitle || pageDomain
|
editableTitle = analysis.title || pageTitle || pageDomain
|
||||||
state = 'confirm'
|
state = 'confirm'
|
||||||
render()
|
render()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage = e.message || 'Erreur réseau'
|
errorMessage = e.message || t('errNetwork')
|
||||||
state = 'error'
|
state = 'error'
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
@@ -626,7 +628,7 @@ async function runSave() {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const saved = await saveRes.json()
|
const saved = await saveRes.json()
|
||||||
if (!saveRes.ok) throw new Error(saved.error || 'Enregistrement impossible')
|
if (!saveRes.ok) throw new Error(saved.error || t('errSaveFailed'))
|
||||||
|
|
||||||
successTitle = title
|
successTitle = title
|
||||||
successTags = analyzeResult.tags || []
|
successTags = analyzeResult.tags || []
|
||||||
@@ -635,7 +637,7 @@ async function runSave() {
|
|||||||
state = 'success'
|
state = 'success'
|
||||||
render()
|
render()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessage = e.message || 'Erreur réseau'
|
errorMessage = e.message || t('errNetwork')
|
||||||
state = 'error'
|
state = 'error'
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
@@ -688,7 +690,9 @@ document.addEventListener('visibilitychange', async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
applyDocumentLocale()
|
||||||
applyInstanceConfigVisibility()
|
applyInstanceConfigVisibility()
|
||||||
|
applyShellI18n()
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
try {
|
try {
|
||||||
await ensureApiPermission()
|
await ensureApiPermission()
|
||||||
|
|||||||
90
memento-note/lib/flashcards/deck-queries.ts
Normal file
90
memento-note/lib/flashcards/deck-queries.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { isCardMastered } from '@/lib/flashcards/sm2'
|
||||||
|
|
||||||
|
export interface DeckSummary {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
notebookId: string | null
|
||||||
|
totalCards: number
|
||||||
|
dueCount: number
|
||||||
|
masteredCount: number
|
||||||
|
lastReviewedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listDeckSummaries(userId: string): Promise<DeckSummary[]> {
|
||||||
|
const now = new Date()
|
||||||
|
const decks = await prisma.flashcardDeck.findMany({
|
||||||
|
where: { userId },
|
||||||
|
include: {
|
||||||
|
flashcards: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
interval: true,
|
||||||
|
nextReviewAt: true,
|
||||||
|
reviews: {
|
||||||
|
orderBy: { reviewedAt: 'desc' },
|
||||||
|
take: 1,
|
||||||
|
select: { reviewedAt: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
})
|
||||||
|
|
||||||
|
return decks.map((deck) => {
|
||||||
|
const totalCards = deck.flashcards.length
|
||||||
|
const dueCount = deck.flashcards.filter((c) => c.nextReviewAt <= now).length
|
||||||
|
const masteredCount = deck.flashcards.filter((c) => isCardMastered(c.interval)).length
|
||||||
|
const lastReview = deck.flashcards
|
||||||
|
.flatMap((c) => c.reviews.map((r) => r.reviewedAt))
|
||||||
|
.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: deck.id,
|
||||||
|
name: deck.name,
|
||||||
|
notebookId: deck.notebookId,
|
||||||
|
totalCards,
|
||||||
|
dueCount,
|
||||||
|
masteredCount,
|
||||||
|
lastReviewedAt: lastReview ? lastReview.toISOString() : null,
|
||||||
|
createdAt: deck.createdAt.toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDeckDetail(userId: string, deckId: string) {
|
||||||
|
const deck = await prisma.flashcardDeck.findFirst({
|
||||||
|
where: { id: deckId, userId },
|
||||||
|
include: {
|
||||||
|
flashcards: {
|
||||||
|
orderBy: { nextReviewAt: 'asc' },
|
||||||
|
include: {
|
||||||
|
note: { select: { id: true, title: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!deck) return null
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
return {
|
||||||
|
id: deck.id,
|
||||||
|
name: deck.name,
|
||||||
|
notebookId: deck.notebookId,
|
||||||
|
cards: deck.flashcards.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
front: c.front,
|
||||||
|
back: c.back,
|
||||||
|
type: c.type,
|
||||||
|
interval: c.interval,
|
||||||
|
easinessFactor: c.easinessFactor,
|
||||||
|
nextReviewAt: c.nextReviewAt.toISOString(),
|
||||||
|
noteId: c.noteId,
|
||||||
|
noteTitle: c.note?.title ?? null,
|
||||||
|
due: c.nextReviewAt <= now,
|
||||||
|
mastered: isCardMastered(c.interval),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
55
memento-note/lib/flashcards/deck-utils.ts
Normal file
55
memento-note/lib/flashcards/deck-utils.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
|
||||||
|
export async function getOrCreateDeckForNotebook(params: {
|
||||||
|
userId: string
|
||||||
|
notebookId: string | null
|
||||||
|
notebookName?: string | null
|
||||||
|
manualName?: string
|
||||||
|
}) {
|
||||||
|
const { userId, notebookId, notebookName, manualName } = params
|
||||||
|
|
||||||
|
if (notebookId) {
|
||||||
|
const existing = await prisma.flashcardDeck.findFirst({
|
||||||
|
where: { userId, notebookId },
|
||||||
|
})
|
||||||
|
if (existing) return existing
|
||||||
|
|
||||||
|
const notebook = await prisma.notebook.findFirst({
|
||||||
|
where: { id: notebookId, userId },
|
||||||
|
select: { name: true },
|
||||||
|
})
|
||||||
|
if (!notebook) {
|
||||||
|
throw new Error('Notebook not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.flashcardDeck.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
notebookId,
|
||||||
|
name: notebook.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = (manualName || notebookName || 'Deck').trim().slice(0, 120)
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('Deck name required')
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.flashcardDeck.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripHtmlToText(html: string): string {
|
||||||
|
return html
|
||||||
|
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||||
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
86
memento-note/lib/flashcards/generate-flashcards.ts
Normal file
86
memento-note/lib/flashcards/generate-flashcards.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { getSystemConfig } from '@/lib/config'
|
||||||
|
import { getChatProvider } from '@/lib/ai/factory'
|
||||||
|
|
||||||
|
export type FlashcardStyle = 'qa' | 'cloze' | 'concept'
|
||||||
|
|
||||||
|
export interface GeneratedFlashcard {
|
||||||
|
front: string
|
||||||
|
back: string
|
||||||
|
type: FlashcardStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLE_HINTS: Record<FlashcardStyle, string> = {
|
||||||
|
qa: 'question/answer pairs — front is a clear question, back is a concise answer',
|
||||||
|
cloze: 'fill-in-the-blank — front uses ___ for the missing word(s), back is the complete sentence',
|
||||||
|
concept: 'term/definition — front is a term or concept name, back is its definition',
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFlashcardsJson(raw: string, style: FlashcardStyle): GeneratedFlashcard[] {
|
||||||
|
const trimmed = raw.trim()
|
||||||
|
const arrayMatch = trimmed.match(/\[[\s\S]*\]/)
|
||||||
|
if (!arrayMatch) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(arrayMatch[0]) as unknown
|
||||||
|
if (!Array.isArray(parsed)) return []
|
||||||
|
return parsed
|
||||||
|
.map((item) => {
|
||||||
|
if (!item || typeof item !== 'object') return null
|
||||||
|
const obj = item as Record<string, unknown>
|
||||||
|
const front = typeof obj.front === 'string' ? obj.front.trim() : ''
|
||||||
|
const back = typeof obj.back === 'string' ? obj.back.trim() : ''
|
||||||
|
const type = (typeof obj.type === 'string' ? obj.type : style) as FlashcardStyle
|
||||||
|
if (!front || !back) return null
|
||||||
|
return {
|
||||||
|
front: front.slice(0, 500),
|
||||||
|
back: back.slice(0, 800),
|
||||||
|
type: ['qa', 'cloze', 'concept'].includes(type) ? type : style,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((c): c is GeneratedFlashcard => c !== null)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateFlashcardsFromNote(params: {
|
||||||
|
title: string
|
||||||
|
textContent: string
|
||||||
|
count: number
|
||||||
|
style: FlashcardStyle
|
||||||
|
language?: string
|
||||||
|
}): Promise<GeneratedFlashcard[]> {
|
||||||
|
const count = Math.min(20, Math.max(5, params.count))
|
||||||
|
const excerpt = params.textContent.slice(0, 8000)
|
||||||
|
const lang = params.language && params.language !== 'auto' ? params.language : 'same as source'
|
||||||
|
|
||||||
|
const config = await getSystemConfig()
|
||||||
|
const provider = getChatProvider(config)
|
||||||
|
|
||||||
|
const prompt = `You create study flashcards from personal notes for spaced repetition.
|
||||||
|
|
||||||
|
Note title: ${params.title || 'Untitled'}
|
||||||
|
Language: ${lang}
|
||||||
|
Style: ${params.style} — ${STYLE_HINTS[params.style]}
|
||||||
|
|
||||||
|
Source content:
|
||||||
|
${excerpt}
|
||||||
|
|
||||||
|
Generate exactly ${count} flashcards. Use the same language as the source.
|
||||||
|
Respond with ONLY a JSON array (no markdown):
|
||||||
|
[
|
||||||
|
{ "front": "...", "back": "...", "type": "${params.style}" }
|
||||||
|
]
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Each card tests one distinct fact from the note
|
||||||
|
- Front and back must be self-contained
|
||||||
|
- No duplicate cards
|
||||||
|
- Cloze cards must include ___ on the front`
|
||||||
|
|
||||||
|
const raw = await provider.generateText(prompt)
|
||||||
|
const cards = parseFlashcardsJson(raw, params.style)
|
||||||
|
if (cards.length === 0) {
|
||||||
|
throw new Error('Could not parse flashcards from AI response')
|
||||||
|
}
|
||||||
|
return cards.slice(0, count)
|
||||||
|
}
|
||||||
39
memento-note/lib/flashcards/sm2.ts
Normal file
39
memento-note/lib/flashcards/sm2.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export type Sm2Grade = 1 | 2 | 3 | 4
|
||||||
|
|
||||||
|
export interface Sm2State {
|
||||||
|
easinessFactor: number
|
||||||
|
interval: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sm2UpdateResult extends Sm2State {
|
||||||
|
nextReviewAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
/** SM-2 update per US-FLASHCARDS spec (grades 1–4). */
|
||||||
|
export function computeSm2Update(
|
||||||
|
grade: number,
|
||||||
|
previous: Sm2State,
|
||||||
|
): Sm2UpdateResult {
|
||||||
|
const g = Math.min(4, Math.max(1, Math.round(grade))) as Sm2Grade
|
||||||
|
const ef = previous.easinessFactor
|
||||||
|
const newEF = Math.max(
|
||||||
|
1.3,
|
||||||
|
ef + 0.1 - (5 - g) * (0.08 + (5 - g) * 0.02),
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextInterval =
|
||||||
|
g <= 2 ? 1 : Math.max(1, Math.round(previous.interval * newEF))
|
||||||
|
|
||||||
|
const nextReviewAt = new Date()
|
||||||
|
nextReviewAt.setDate(nextReviewAt.getDate() + nextInterval)
|
||||||
|
|
||||||
|
return {
|
||||||
|
easinessFactor: newEF,
|
||||||
|
interval: nextInterval,
|
||||||
|
nextReviewAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCardMastered(interval: number): boolean {
|
||||||
|
return interval >= 21
|
||||||
|
}
|
||||||
@@ -2404,6 +2404,64 @@
|
|||||||
"Japonais": "Japanese"
|
"Japonais": "Japanese"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flashcards": {
|
||||||
|
"generateTitle": "Generate flashcards",
|
||||||
|
"generateAction": "Generate with AI",
|
||||||
|
"confirmSave": "Save to deck",
|
||||||
|
"generateFailed": "Could not generate flashcards",
|
||||||
|
"saveFailed": "Could not save flashcards",
|
||||||
|
"savedCount": "{count} flashcards saved",
|
||||||
|
"cardCount": "Number of cards",
|
||||||
|
"styleLabel": "Card style",
|
||||||
|
"style": {
|
||||||
|
"qa": "Q&A",
|
||||||
|
"cloze": "Cloze",
|
||||||
|
"concept": "Concept"
|
||||||
|
},
|
||||||
|
"previewHint": "Edit cards before saving",
|
||||||
|
"frontPlaceholder": "Question / front",
|
||||||
|
"backPlaceholder": "Answer / back",
|
||||||
|
"toolbarGenerate": "Generate flashcards",
|
||||||
|
"tabDecks": "Decks",
|
||||||
|
"tabProgress": "Progress",
|
||||||
|
"emptyDecks": "No flashcard decks yet",
|
||||||
|
"emptyDecksHint": "Open a note and use the graduation cap in the toolbar to generate cards with AI.",
|
||||||
|
"createDeck": "Create deck",
|
||||||
|
"newDeckPlaceholder": "Thematic deck name…",
|
||||||
|
"deckCreated": "Deck created",
|
||||||
|
"dueCount": "{count} due today",
|
||||||
|
"upToDate": "Up to date",
|
||||||
|
"cardCountLabel": "{count} cards",
|
||||||
|
"masteredShort": "mastered",
|
||||||
|
"viewDeck": "Details",
|
||||||
|
"review": "Review",
|
||||||
|
"startReview": "Start review",
|
||||||
|
"activeDeck": "Active deck",
|
||||||
|
"statTotal": "Total: {count}",
|
||||||
|
"statDue": "Due: {count}",
|
||||||
|
"statMastered": "Mastered: {count}",
|
||||||
|
"exitSession": "Exit session",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"front": "Front",
|
||||||
|
"back": "Back",
|
||||||
|
"tapToFlip": "Space or tap to flip",
|
||||||
|
"grade": {
|
||||||
|
"hard": "Hard (1)",
|
||||||
|
"difficult": "Difficult (2)",
|
||||||
|
"good": "Good (3)",
|
||||||
|
"easy": "Easy (4)"
|
||||||
|
},
|
||||||
|
"sessionComplete": "Session complete",
|
||||||
|
"backToDecks": "Back to decks",
|
||||||
|
"loadDeckFailed": "Could not load deck",
|
||||||
|
"reviewFailed": "Could not save review",
|
||||||
|
"heatmapTitle": "Review activity",
|
||||||
|
"heatmapLast90": "Last 90 days",
|
||||||
|
"retentionRate": "Retention rate",
|
||||||
|
"retentionCurve": "Weekly retention",
|
||||||
|
"difficultCards": "Hardest cards"
|
||||||
|
},
|
||||||
"brainstorm": {
|
"brainstorm": {
|
||||||
"title": "Waves of Thought",
|
"title": "Waves of Thought",
|
||||||
"subtitle": "Unfold dimensions of potentiality",
|
"subtitle": "Unfold dimensions of potentiality",
|
||||||
|
|||||||
@@ -2408,6 +2408,64 @@
|
|||||||
"Japonais": "Japonais"
|
"Japonais": "Japonais"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flashcards": {
|
||||||
|
"generateTitle": "Générer des flashcards",
|
||||||
|
"generateAction": "Générer avec l'IA",
|
||||||
|
"confirmSave": "Enregistrer dans le deck",
|
||||||
|
"generateFailed": "Impossible de générer les flashcards",
|
||||||
|
"saveFailed": "Impossible d'enregistrer les flashcards",
|
||||||
|
"savedCount": "{count} flashcards enregistrées",
|
||||||
|
"cardCount": "Nombre de cartes",
|
||||||
|
"styleLabel": "Style de cartes",
|
||||||
|
"style": {
|
||||||
|
"qa": "Question / réponse",
|
||||||
|
"cloze": "Texte à trous",
|
||||||
|
"concept": "Terme / définition"
|
||||||
|
},
|
||||||
|
"previewHint": "Modifiez les cartes avant l'enregistrement",
|
||||||
|
"frontPlaceholder": "Question / recto",
|
||||||
|
"backPlaceholder": "Réponse / verso",
|
||||||
|
"toolbarGenerate": "Générer des flashcards",
|
||||||
|
"tabDecks": "Decks",
|
||||||
|
"tabProgress": "Progression",
|
||||||
|
"emptyDecks": "Aucun deck de flashcards",
|
||||||
|
"emptyDecksHint": "Ouvrez une note et utilisez l'icône casquette dans la barre d'outils pour générer des cartes avec l'IA.",
|
||||||
|
"createDeck": "Créer un deck",
|
||||||
|
"newDeckPlaceholder": "Nom du deck thématique…",
|
||||||
|
"deckCreated": "Deck créé",
|
||||||
|
"dueCount": "{count} à réviser",
|
||||||
|
"upToDate": "À jour",
|
||||||
|
"cardCountLabel": "{count} cartes",
|
||||||
|
"masteredShort": "maîtrisées",
|
||||||
|
"viewDeck": "Détails",
|
||||||
|
"review": "Réviser",
|
||||||
|
"startReview": "Lancer la révision",
|
||||||
|
"activeDeck": "Deck actif",
|
||||||
|
"statTotal": "Total : {count}",
|
||||||
|
"statDue": "À réviser : {count}",
|
||||||
|
"statMastered": "Maîtrisées : {count}",
|
||||||
|
"exitSession": "Quitter la session",
|
||||||
|
"previous": "Précédent",
|
||||||
|
"next": "Suivant",
|
||||||
|
"front": "Recto",
|
||||||
|
"back": "Verso",
|
||||||
|
"tapToFlip": "Espace ou clic pour retourner",
|
||||||
|
"grade": {
|
||||||
|
"hard": "Difficile (1)",
|
||||||
|
"difficult": "Dur (2)",
|
||||||
|
"good": "Bien (3)",
|
||||||
|
"easy": "Facile (4)"
|
||||||
|
},
|
||||||
|
"sessionComplete": "Session terminée",
|
||||||
|
"backToDecks": "Retour aux decks",
|
||||||
|
"loadDeckFailed": "Impossible de charger le deck",
|
||||||
|
"reviewFailed": "Impossible d'enregistrer la révision",
|
||||||
|
"heatmapTitle": "Activité de révision",
|
||||||
|
"heatmapLast90": "90 derniers jours",
|
||||||
|
"retentionRate": "Taux de rétention",
|
||||||
|
"retentionCurve": "Rétention hebdomadaire",
|
||||||
|
"difficultCards": "Cartes difficiles"
|
||||||
|
},
|
||||||
"brainstorm": {
|
"brainstorm": {
|
||||||
"title": "Vagues de pensée",
|
"title": "Vagues de pensée",
|
||||||
"subtitle": "Déployer les dimensions du potentiel",
|
"subtitle": "Déployer les dimensions du potentiel",
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FlashcardDeck" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"notebookId" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FlashcardDeck_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Flashcard" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"deckId" TEXT NOT NULL,
|
||||||
|
"noteId" TEXT,
|
||||||
|
"front" TEXT NOT NULL,
|
||||||
|
"back" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL DEFAULT 'qa',
|
||||||
|
"interval" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"easinessFactor" DOUBLE PRECISION NOT NULL DEFAULT 2.5,
|
||||||
|
"nextReviewAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Flashcard_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FlashcardReview" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cardId" TEXT NOT NULL,
|
||||||
|
"grade" INTEGER NOT NULL,
|
||||||
|
"reviewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "FlashcardReview_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FlashcardDeck_notebookId_key" ON "FlashcardDeck"("notebookId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "FlashcardDeck_userId_idx" ON "FlashcardDeck"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Flashcard_deckId_idx" ON "Flashcard"("deckId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Flashcard_noteId_idx" ON "Flashcard"("noteId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Flashcard_nextReviewAt_idx" ON "Flashcard"("nextReviewAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "FlashcardReview_cardId_idx" ON "FlashcardReview"("cardId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "FlashcardReview_reviewedAt_idx" ON "FlashcardReview"("reviewedAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FlashcardDeck" ADD CONSTRAINT "FlashcardDeck_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FlashcardDeck" ADD CONSTRAINT "FlashcardDeck_notebookId_fkey" FOREIGN KEY ("notebookId") REFERENCES "Notebook"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Flashcard" ADD CONSTRAINT "Flashcard_deckId_fkey" FOREIGN KEY ("deckId") REFERENCES "FlashcardDeck"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Flashcard" ADD CONSTRAINT "Flashcard_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FlashcardReview" ADD CONSTRAINT "FlashcardReview_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "Flashcard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -54,6 +54,7 @@ model User {
|
|||||||
noteClusters NoteCluster[]
|
noteClusters NoteCluster[]
|
||||||
bridgeNotes BridgeNote[]
|
bridgeNotes BridgeNote[]
|
||||||
bridgeSuggestions BridgeSuggestion[]
|
bridgeSuggestions BridgeSuggestion[]
|
||||||
|
flashcardDecks FlashcardDeck[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
@@ -111,6 +112,7 @@ model Notebook {
|
|||||||
children Notebook[] @relation("NotebookTree")
|
children Notebook[] @relation("NotebookTree")
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
workflows Workflow[]
|
workflows Workflow[]
|
||||||
|
flashcardDeck FlashcardDeck?
|
||||||
|
|
||||||
@@index([userId, order])
|
@@index([userId, order])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@ -195,6 +197,7 @@ model Note {
|
|||||||
bridgeNote BridgeNote?
|
bridgeNote BridgeNote?
|
||||||
sourceLiveBlocks LiveBlockRef[] @relation("SourceLiveBlocks")
|
sourceLiveBlocks LiveBlockRef[] @relation("SourceLiveBlocks")
|
||||||
targetLiveBlocks LiveBlockRef[] @relation("TargetLiveBlocks")
|
targetLiveBlocks LiveBlockRef[] @relation("TargetLiveBlocks")
|
||||||
|
flashcards Flashcard[]
|
||||||
|
|
||||||
@@index([isPinned])
|
@@index([isPinned])
|
||||||
@@index([isArchived])
|
@@index([isArchived])
|
||||||
@@ -855,3 +858,49 @@ model BridgeSuggestion {
|
|||||||
@@index([userId, isDismissed])
|
@@index([userId, isDismissed])
|
||||||
@@index([clusterAId, clusterBId])
|
@@index([clusterAId, clusterBId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model FlashcardDeck {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
notebookId String? @unique
|
||||||
|
name String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
flashcards Flashcard[]
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Flashcard {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
deckId String
|
||||||
|
noteId String?
|
||||||
|
front String
|
||||||
|
back String
|
||||||
|
type String @default("qa")
|
||||||
|
interval Int @default(1)
|
||||||
|
easinessFactor Float @default(2.5)
|
||||||
|
nextReviewAt DateTime @default(now())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deck FlashcardDeck @relation(fields: [deckId], references: [id], onDelete: Cascade)
|
||||||
|
note Note? @relation(fields: [noteId], references: [id], onDelete: SetNull)
|
||||||
|
reviews FlashcardReview[]
|
||||||
|
|
||||||
|
@@index([deckId])
|
||||||
|
@@index([noteId])
|
||||||
|
@@index([nextReviewAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model FlashcardReview {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
cardId String
|
||||||
|
grade Int
|
||||||
|
reviewedAt DateTime @default(now())
|
||||||
|
card Flashcard @relation(fields: [cardId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([cardId])
|
||||||
|
@@index([reviewedAt])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user