diff --git a/community/lingo-launch/.gitignore b/community/lingo-launch/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/community/lingo-launch/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/community/lingo-launch/README.md b/community/lingo-launch/README.md new file mode 100644 index 000000000..f7b27f2d1 --- /dev/null +++ b/community/lingo-launch/README.md @@ -0,0 +1,201 @@ +# 🌍 LingoLaunch + +**Build once. Launch everywhere.** + +LingoLaunch is a multilingual page builder that allows users to create landing, pricing, and about pages in a single source language and automatically generate translations using **Lingo.dev**. + +This project demonstrates how localization can be automated in modern web applications using Next.js and Lingo CLI. + +--- + +## 🚀 Features + +* 📝 Create page content in English +* ⚡ Automatically generate translations (FR, HI, etc.) +* 🌎 Switch languages instantly in preview +* 🔄 Automated translation pipeline via Lingo CLI +* 🧱 Built with Next.js App Router + +--- + +## 🛠 Tech Stack + +* **Next.js (App Router)** +* **TypeScript** +* **Lingo.dev CLI** +* **Tailwind CSS** +* Node.js File System API + +--- + +## 📂 Project Structure + +``` +lingo-launch/ +│ +├── app/ +│ ├── dashboard/ # Dashboard UI +│ ├── editor/[pageId]/ # Page editor +│ ├── preview/[pageId]/ # Localized preview +│ └── api/save-and-compile/ # Save + run Lingo +│ +├── public/ +│ └── locales/ # Generated translation files +│ +├── lingo.config.ts # Lingo configuration +└── README.md +``` + +--- + +## ⚙️ How It Works + +### 1️⃣ User Writes Content + +User creates or edits a page at: + +``` +/editor/{pageId} +``` + +Example: + +``` +/editor/landing +``` + +They write content in English only. + +--- + +### 2️⃣ Save & Generate + +When the user clicks **Save & Generate**: + +* The app creates: + +``` +public/locales/{pageId}/en.json +``` + +* Then runs: + +``` +lingo compile +``` + +* Lingo automatically generates: + +``` +fr.json +hi.json +``` + +--- + +### 3️⃣ Preview + +The localized page can be viewed at: + +``` +/preview/{pageId} +``` + +Users can switch languages instantly. + +--- + +## 🧠 Why This Project Matters + +Traditional localization requires: + +* Manual translation +* Large JSON management +* Developer overhead + +LingoLaunch automates the entire pipeline. + +This demonstrates: + +* Automated i18n workflows +* Dynamic content localization +* CLI-based translation integration +* Clean separation between content and presentation + +--- + +## 🏃 Running the Project + +### 1. Install dependencies + +```bash +pnpm install +``` + +or + +```bash +npm install +``` + +--- + +### 2. Install Lingo CLI (if not already) + +```bash +pnpm dlx lingo compile +``` + +or + +```bash +npx lingo compile +``` + +--- + +### 3. Start development server + +```bash +pnpm dev +``` + +--- + +### 4. Test Flow + +1. Go to `/dashboard` +2. Open `/editor/landing` +3. Enter title + description +4. Click **Save & Generate** +5. Open `/preview/landing` +6. Switch languages + +--- + +## 🌟 Demo Flow for Judges + +> “We allow users to create content in one language and automatically generate localized versions using Lingo.dev. This eliminates manual translation overhead and makes global launch instant.” + +--- + +## 📌 Future Improvements + +* 🔐 Authentication +* 🗄 Database integration (Supabase) +* 📦 Page templates +* 🧩 Component-based page builder +* 🌍 Auto language detection +* ☁️ Cloud-based compile pipeline + +--- + +## 🏆 Hackathon Focus + +This MVP focuses on: + +* Working automation +* Clean architecture +* Clear user flow +* Practical use of Lingo CLI +* Real-world localization pipeline diff --git a/community/lingo-launch/app/api/lingo-sync/route.ts b/community/lingo-launch/app/api/lingo-sync/route.ts new file mode 100644 index 000000000..1cfd820eb --- /dev/null +++ b/community/lingo-launch/app/api/lingo-sync/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export async function POST(req: Request) { + try { + const { texts } = await req.json(); + + if (!Array.isArray(texts) || texts.length === 0) { + return NextResponse.json({ message: 'No texts provided' }, { status: 400 }); + } + + // Generate the dummy source file content + // We import useLingo but don't use the hook or component logic, just static t() calls + // so the compiler picks them up. + // Actually, lingo might need a valid react component structure. + const fileContent = ` +// This file is auto-generated by LingoLaunch to enable dynamic content translation. +// Do not edit manually. +import { useLingo } from '@lingo.dev/compiler/react'; + +export default function LingoDynamicSource() { + const { t } = useLingo(); + + return ( + <> + ${texts.map(text => `{/* @ts-ignore */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')} + + ); +} +`; + + const filePath = path.join(process.cwd(), 'app', 'lingo-dynamic-source.tsx'); + + // Write the file + fs.writeFileSync(filePath, fileContent, 'utf-8'); + + // Run lingo run + // Using npx lingo run. Assuming it's available in the environment. + // We might need to handle the output/error. + // CWD should be project root. + const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); + + console.log('Lingo Run Output:', stdout); + if (stderr) console.error('Lingo Run Error:', stderr); + + return NextResponse.json({ success: true, message: 'Translations updated' }); + } catch (error) { + console.error('Error in lingo-sync:', error); + return NextResponse.json({ error: 'Failed to sync translations' }, { status: 500 }); + } +} diff --git a/community/lingo-launch/app/components/LanguageSwitcher.tsx b/community/lingo-launch/app/components/LanguageSwitcher.tsx new file mode 100644 index 000000000..748cefb0b --- /dev/null +++ b/community/lingo-launch/app/components/LanguageSwitcher.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useLingoContext } from '@lingo.dev/compiler/react'; +import { Check, ChevronDown, Globe } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +const languages = [ + { code: 'en', label: 'English' }, + { code: 'es', label: 'Español' }, + { code: 'de', label: 'Deutsch' }, + { code: 'fr', label: 'Français' }, + { code: 'hi', label: 'हिंदी' }, +]; + +export default function LanguageSwitcher() { + const { locale, setLocale } = useLingoContext(); + const [isOpen, setIsOpen] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + const currentLanguage = languages.find((l) => l.code === locale) || languages[0]; + + return ( +
+ + + {isOpen && ( + <> +
setIsOpen(false)} + /> +
+ {languages.map((language) => ( + + ))} +
+ + )} +
+ ); +} diff --git a/community/lingo-launch/app/components/Navbar.tsx b/community/lingo-launch/app/components/Navbar.tsx new file mode 100644 index 000000000..9936ab32f --- /dev/null +++ b/community/lingo-launch/app/components/Navbar.tsx @@ -0,0 +1,98 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter, usePathname } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { Globe, Moon, Sun, LogOut, LogIn, LayoutDashboard } from 'lucide-react'; +import { useTheme } from '@/theme/ThemeProvider'; +import { getCurrentUser, logout } from '@/app/lib/storage'; +import type { User } from '@/app/lib/storage'; +import LanguageSwitcher from './LanguageSwitcher'; + +export default function Navbar() { + const router = useRouter(); + const pathname = usePathname(); + const { theme, toggleTheme } = useTheme(); + const [user, setUser] = useState(null); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + setUser(getCurrentUser()); + }, [pathname]); + + const handleLogout = () => { + logout(); + setUser(null); + router.push('/'); + }; + + return ( + + ); +} diff --git a/community/lingo-launch/app/components/Providers.tsx b/community/lingo-launch/app/components/Providers.tsx new file mode 100644 index 000000000..8d6bdf1cc --- /dev/null +++ b/community/lingo-launch/app/components/Providers.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { LingoProvider, useLingoContext } from '@lingo.dev/compiler/react'; +import { ThemeProvider } from '@/theme/ThemeProvider'; +import { useEffect, useState } from 'react'; + +function LocaleSync({ children }: { children: React.ReactNode }) { + const { locale, setLocale } = useLingoContext(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + const savedLocale = localStorage.getItem('lingolaunch_locale'); + const validLocales = ['en', 'es', 'de', 'fr', 'hi']; + if (savedLocale && validLocales.includes(savedLocale) && savedLocale !== locale) { + setLocale(savedLocale as any); + } + }, []); + + useEffect(() => { + if (mounted && locale) { + localStorage.setItem('lingolaunch_locale', locale); + } + }, [locale, mounted]); + + if (!mounted) { + return null; // or a loading spinner to prevent hydration mismatch/flash + } + + return <>{children}; +} + +export default function Providers({ children }: { children: React.ReactNode }) { + const router = useRouter(); + + return ( + + + + {children} + + + + ); +} diff --git a/community/lingo-launch/app/components/SectionRenderer.tsx b/community/lingo-launch/app/components/SectionRenderer.tsx new file mode 100644 index 000000000..924b3fc78 --- /dev/null +++ b/community/lingo-launch/app/components/SectionRenderer.tsx @@ -0,0 +1,129 @@ +import type { PageSection } from '@/app/lib/storage'; +import { Zap, Shield, Sparkles, Star } from 'lucide-react'; +import { useLingoContext } from '@lingo.dev/compiler/react'; +import { useLingo, useLingoLocale } from 'lingo.dev/react-client'; + +const featureIcons = [Zap, Shield, Sparkles]; + +export default function SectionRenderer({ section }: { section: PageSection }) { + // @ts-ignore - Assuming t exists on context or casting to any if mostly checking runtime + const { t: translate } = useLingoContext() + const { type, content } = section; + + switch (type) { + case 'hero': + return ( +
+
+
+

+ {content.heading} +

+

+ {content.subheading} +

+ {content.buttonText && ( + + )} +
+
+ ); + + case 'features': { + const features = [ + { title: content.feature1Title, desc: content.feature1Desc }, + { title: content.feature2Title, desc: content.feature2Desc }, + { title: content.feature3Title, desc: content.feature3Desc }, + ].filter((f) => f.title); + + return ( +
+
+ {content.heading && ( +

+ {content.heading} +

+ )} +
+ {features.map((feature, i) => { + const Icon = featureIcons[i % featureIcons.length]; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.desc} +

+
+ ); + })} +
+
+
+ ); + } + + case 'text': + return ( +
+
+ {content.heading && ( +

+ {content.heading} +

+ )} +

+ {content.body} +

+
+
+ ); + + case 'cta': + return ( +
+
+

+ {translate(content.heading)} +

+

+ {translate(content.description)} +

+ {content.buttonText && ( + + )} +
+
+ ); + + case 'testimonial': + return ( +
+
+ +
+ “{translate(content.quote)}” +
+
+

{translate(content.author)}

+

{translate(content.role)}

+
+
+
+ ); + + default: + return null; + } +} diff --git a/community/lingo-launch/app/dashboard/page.tsx b/community/lingo-launch/app/dashboard/page.tsx new file mode 100644 index 000000000..b95367c4c --- /dev/null +++ b/community/lingo-launch/app/dashboard/page.tsx @@ -0,0 +1,229 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { + Plus, + Pencil, + Eye, + Trash2, + FileText, + X, + Globe, +} from "lucide-react"; +import { + getCurrentUser, + getPages, + createPage, + deletePage, +} from "@/app/lib/storage"; +import type { User, SitePage } from "@/app/lib/storage"; + +export default function DashboardPage() { + const router = useRouter(); + const [user, setUser] = useState(null); + const [pages, setPages] = useState([]); + const [showCreate, setShowCreate] = useState(false); + const [newTitle, setNewTitle] = useState(""); + const [newDesc, setNewDesc] = useState(""); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + const currentUser = getCurrentUser(); + if (!currentUser) { + router.push("/login"); + return; + } + setUser(currentUser); + setPages(getPages(currentUser.id)); + }, [router]); + + const handleCreate = () => { + if (!user || !newTitle.trim()) return; + const page = createPage(user.id, newTitle.trim(), newDesc.trim()); + setPages((prev) => [...prev, page]); + setNewTitle(""); + setNewDesc(""); + setShowCreate(false); + router.push(`/editor/${page.id}`); + }; + + const handleDelete = (pageId: string) => { + deletePage(pageId); + setPages((prev) => prev.filter((p) => p.id !== pageId)); + }; + + if (!mounted || !user) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Your Pages

+

+ Create and manage your multilingual pages +

+
+ +
+ + {/* Create Page Modal */} + {showCreate && ( +
+
+
+

+ Create New Page +

+ +
+ +
+
+ + setNewTitle(e.target.value)} + placeholder="My Landing Page" + className="w-full px-4 py-2.5 bg-background border border-input rounded-lg text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" + autoFocus + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + /> +
+
+ +