Drift is a TikTok-style language learning app that immerses you in short, swipeable content. Japanese-first excellence with graceful degradation for other languages.
- Vertical Swipe Feed: Smooth, addictive swiping through bite-sized language content
- Microactions: Like π, Dislike π, Save β, and Zap β‘ snippets
- Word Tapping: Tap any word for instant meaning/reading lookup
- Delayed Meaning Reveal: 800ms delay before showing translation (reduces instant-translation addiction)
- Furigana Support: Show readings above kanji (configurable: off/on/unknown-only)
- Dialect Tags: Standard (ζ¨ζΊθͺ), Kansai (ι’θ₯ΏεΌ), Hakata (εε€εΌ)
- J-J or J-E Dictionary: Choose monolingual or bilingual definitions
- All user signals stored locally in IndexedDB
- No tracking, no analytics, no behavior linked to identity by default
- Local preference profile built from:
- Dwell time
- Likes/dislikes
- Saves
- Word taps
- Meaning reveals
- Zaps
- Optional Login: Browse without an account, or login with Nostr
- Content Source: Snippets fetched from Nostr relays
- NIP-07: Browser extension signer support
- NIP-46: Nostr bunker support
- NIP-57: Lightning zaps with split payments
When zapping a snippet:
- 45% to curator
- 45% to original author
- 10% to Drift platform
If a recipient lacks a Lightning wallet, their share is redistributed.
- Set daily goals (5/10/15 snippets)
- Track progress with completion stats
- Build consistency without punitive streaks
- Installable on mobile and desktop
- Offline caching of snippets
- Service worker with background sync
- Node.js 18+
- npm or pnpm
# Clone the repository
git clone <repository-url>
cd drift
# Install dependencies
npm install
# Start development server
npm run devThe app will be available at http://localhost:8080.
npm run buildThe built files will be in the dist/ directory.
By default, Drift connects to these relays:
wss://relay.ditto.pubwss://relay.nostr.bandwss://relay.damus.io
Users can customize relays in Settings.
Drift uses kind 38859 for addressable snippet events. See NIP.md for full documentation.
Example snippet event:
{
"kind": 38859,
"content": "δ»ζ₯γ―倩ζ°γγγγ§γγγ",
"tags": [
["d", "unique-id"],
["L", "drift"],
["l", "ja", "drift"],
["alt", "Drift snippet: δ»ζ₯γ―倩ζ°γγγγ§γγγ"],
["length", "sentence"],
["difficulty", "2"],
["t", "daily-life"],
["dialect", "ζ¨ζΊθͺ"]
]
}| Setting | Options | Default |
|---|---|---|
| Target Language | ja, en, es, fr, de, zh, ko, pt, it, ru | ja |
| Dictionary Mode | L-L (monolingual), L-E (bilingual) | L-E |
| Furigana Display | Off, On, Unknown Only | Unknown Only |
| Max Snippet Length | word, phrase, sentence, paragraph | sentence |
| Hide Sensitive Content | On/Off | On |
| Daily Drift Target | 5, 10, 15 | 10 |
| Auto-advance | On/Off | Off |
| Offline Cache Size | 50, 100 snippets | 50 |
| Publish Reactions | On/Off | Off |
- React 18 with TypeScript
- Vite for fast builds
- TailwindCSS with shadcn/ui components
- Nostrify for Nostr protocol
- TanStack Query for data fetching
- IndexedDB for local storage
- PWA with Workbox
src/
βββ components/
β βββ drift/ # Drift-specific components
β β βββ SwipeFeed.tsx
β β βββ SnippetCard.tsx
β β βββ JapaneseText.tsx
β β βββ WordLookupSheet.tsx
β β βββ ZapSplitDialog.tsx
β βββ auth/ # Authentication components
β βββ ui/ # shadcn/ui components
βββ hooks/
β βββ useSnippets.ts # Snippet fetching
β βββ usePreferences.ts # User preferences
β βββ useUnknownWords.ts # Vocabulary tracking
β βββ useDailyDrift.ts # Daily session
β βββ useZapSplit.ts # Zap splitting
βββ lib/
β βββ storage.ts # IndexedDB layer
β βββ personalization.ts # Ranking algorithm
β βββ japanese.ts # Japanese utilities
β βββ snippetGenerator.ts # AI generation stub
βββ pages/
β βββ Feed.tsx
β βββ SettingsPage.tsx
β βββ SavedPage.tsx
β βββ WordsPage.tsx
β βββ StatsPage.tsx
β βββ DailyDrift.tsx
β βββ Onboarding.tsx
βββ types/
βββ snippet.ts # Core data types
βββ nostr.ts # Nostr event types
The snippetGenerator.ts provides a stub interface for AI-powered snippet generation:
interface AIGeneratorBackend {
generateSnippet(options: GenerationOptions): Promise<string>;
translateText(text: string, from: LanguageCode, to: LanguageCode): Promise<string>;
getReading(text: string, language: LanguageCode): Promise<string>;
getDefinition(word: string, language: LanguageCode, targetLanguage: LanguageCode): Promise<string>;
}Replace the stubAIBackend with your preferred AI service.
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests:
npm test - Submit a pull request
MIT
- Built with MKStack
- Vibed with Shakespeare