Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions community/lingo-launch/.gitignore
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
201 changes: 201 additions & 0 deletions community/lingo-launch/README.md
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
56 changes: 56 additions & 0 deletions community/lingo-launch/app/api/lingo-sync/route.ts
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 ')}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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:

  • Backslash sequences (e.g., \" to un-escape the quote)
  • Template literal expressions if the outer template uses backticks
  • Newlines to break out of the t() call

At a minimum, sanitize backslashes before quotes, and also handle ${}, backticks, and newlines:

Proposed safer escaping
-      ${texts.map(text => `{/* `@ts-ignore` */}\n      {t("${text.replace(/"/g, '\\"')}")}`).join('\n      ')}
+      ${texts.map(text => {
+        const safe = text
+          .replace(/\\/g, '\\\\')
+          .replace(/"/g, '\\"')
+          .replace(/\n/g, '\\n')
+          .replace(/\r/g, '\\r')
+          .replace(/`/g, '\\`')
+          .replace(/\$\{/g, '\\${');
+        return `{/* `@ts-ignore` */}\n      {t("${safe}")}`;
+      }).join('\n      ')}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
${texts.map(text => `{/* @ts-ignore */}\n {t("${text.replace(/"/g, '\\"')}")}`).join('\n ')}
${texts.map(text => {
const safe = text
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${');
return `{/* `@ts-ignore` */}\n {t("${safe}")}`;
}).join('\n ')}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 31, The generated
TypeScript interpolation in texts.map that emits {t("${text.replace(/"/g,
'\\"')}")} is vulnerable to code injection because only double quotes are
escaped; update the sanitizer used before interpolation (the texts.map arrow
callback) to first escape backslashes, then escape double quotes, backticks,
sequence `${` (or replace/escape both '$' and '{'), and convert newlines to safe
escape sequences so user input cannot break out of the t() call or inject
template expressions; ensure this robust escaping is applied wherever the same
pattern is used and keep the sanitized value passed into t() (reference the
texts.map(...) expression and the t(...) call).

</>
);
}
`;

const filePath = path.join(process.cwd(), 'app', 'lingo-dynamic-source.tsx');

// Write the file
fs.writeFileSync(filePath, fileContent, 'utf-8');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Synchronous file write blocks the event loop.

fs.writeFileSync on an API request path blocks all other requests. Use fs.promises.writeFile instead.

Proposed fix
-        fs.writeFileSync(filePath, fileContent, 'utf-8');
+        await fs.promises.writeFile(filePath, fileContent, 'utf-8');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs.writeFileSync(filePath, fileContent, 'utf-8');
await fs.promises.writeFile(filePath, fileContent, 'utf-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 40, The sync call
fs.writeFileSync(filePath, fileContent, 'utf-8') blocks the event loop; replace
it with the async promise-based API (fs.promises.writeFile or imported
fs.promises) and await it inside the request handler (make the handler async if
not already), and wrap the await in try/catch to surface and log errors before
returning the response. Target the fs.writeFileSync(...) invocation in route.ts
(inside the API request handler function) and change to an awaited
fs.promises.writeFile(...) call with proper error handling.


// 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() });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

exec has no timeout — can hang indefinitely.

If npx lingo run stalls, this request will never resolve and the connection stays open. Add a timeout.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@community/lingo-launch/app/api/lingo-sync/route.ts` at line 46, The execAsync
call that runs 'npx lingo run' can hang indefinitely; update the call site in
the route handler where execAsync is used to pass a timeout (or an AbortSignal)
to the spawn/exec options so the child is killed after a reasonable period
(e.g., 30s), and add explicit error handling for the timeout case to log/return
a clear error response; locate the execAsync invocation (the const { stdout,
stderr } = await execAsync('npx lingo run', { cwd: process.cwd() }); line) and
modify its options to include timeout/abort and wrap the await in a try/catch
that handles and logs timeout errors and ensures the child process is cleaned
up.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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 (npx lingo run). In a deployed environment, this is a remote code execution vector. At minimum, gate this behind an auth check or an API key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@community/lingo-launch/app/api/lingo-sync/route.ts` around lines 9 - 55, The
POST handler (exported POST) currently writes files (filePath /
lingo-dynamic-source.tsx) and executes a shell command via execAsync('npx lingo
run') with no auth; add an authentication/authorization gate at the top of POST
that validates a secret/API key from headers or session against a server-side
env var (e.g., process.env.LINGO_SYNC_KEY) and immediately return a 401/403
NextResponse when missing/invalid; additionally validate and sanitize the
incoming texts array (ensure strings, length limits, escape content) before
building fileContent, restrict filePath to a known safe location and prevent
path traversal, and only run execAsync when auth succeeds—log and return errors
safely using NextResponse with appropriate status codes.

}
85 changes: 85 additions & 0 deletions community/lingo-launch/app/components/LanguageSwitcher.tsx
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>
);
}
Loading
Loading