-
Notifications
You must be signed in to change notification settings - Fork 837
Lingo-launch@pr1 #2001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Lingo-launch@pr1 #2001
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'); | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Synchronous file write blocks the event loop.
Proposed fix- fs.writeFileSync(filePath, fileContent, 'utf-8');
+ await fs.promises.writeFile(filePath, fileContent, 'utf-8');📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| // 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() }); | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Proposed fix- const { stdout, stderr } = await execAsync('npx lingo run', { cwd: process.cwd() });
+ const { stdout, stderr } = await execAsync('npx lingo run', {
+ cwd: process.cwd(),
+ timeout: 60_000, // 60 seconds
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| 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 }); | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+9
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: Unauthenticated endpoint writes arbitrary files and executes shell commands. This POST handler has no authentication or authorization. Any client can trigger file writes to disk and shell command execution ( 🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div className="relative"> | ||
| <button | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| className="flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors rounded-md hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" | ||
| aria-label="Select language" | ||
| > | ||
| <Globe className="w-4 h-4" /> | ||
| <span className="hidden sm:inline">{currentLanguage.label}</span> | ||
| <ChevronDown className={`w-3 h-3 transition-transform ${isOpen ? 'rotate-180' : ''}`} /> | ||
| </button> | ||
|
|
||
| {isOpen && ( | ||
| <> | ||
| <div | ||
| className="fixed inset-0 z-40" | ||
| onClick={() => setIsOpen(false)} | ||
| /> | ||
| <div className="absolute right-0 z-50 w-48 py-1 mt-2 origin-top-right bg-popover text-popover-foreground border rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none animate-in fade-in zoom-in-95 duration-200"> | ||
| {languages.map((language) => ( | ||
| <button | ||
| key={language.code} | ||
| onClick={async () => { | ||
| setIsOpen(false); | ||
|
|
||
| // Sync translations if switching to a non-default language (or always) | ||
| // We'll do it always to be safe and ensure latest content is captured | ||
| try { | ||
| // Dynamically import to avoid server-side issues if any, though this is a client component | ||
| const { getAllContentStrings } = await import('@/app/lib/storage'); | ||
| const texts = getAllContentStrings(); | ||
|
|
||
| if (texts.length > 0) { | ||
| await fetch('/api/lingo-sync', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ texts }), | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| console.error('Failed to sync translations:', e); | ||
| } | ||
|
|
||
| setLocale(language.code as any); | ||
| }} | ||
| className={`flex items-center w-full px-4 py-2 text-sm text-left hover:bg-accent hover:text-accent-foreground ${locale === language.code ? 'bg-accent/50 font-medium' : '' | ||
| }`} | ||
| > | ||
| <span className="flex-1">{language.label}</span> | ||
| {locale === language.code && <Check className="w-4 h-4 ml-2" />} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Code injection via insufficient input sanitization.
Only double quotes are escaped (
text.replace(/"/g, '\\"')), but the user-supplied text is interpolated into a generated TypeScript source file. An attacker can inject arbitrary code through:\"to un-escape the quote)t()callAt a minimum, sanitize backslashes before quotes, and also handle
${}, backticks, and newlines:Proposed safer escaping
📝 Committable suggestion
🤖 Prompt for AI Agents