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
17 changes: 17 additions & 0 deletions .changeset/fair-shirts-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@compiler/demo-next": patch
"@replexica/integration-directus": patch
"replexica": patch
"@replexica/sdk": patch
"lingo.dev": patch
"@lingo.dev/_compiler": patch
"@lingo.dev/_locales": patch
"@lingo.dev/_logging": patch
"@lingo.dev/compiler": patch
"@lingo.dev/_react": patch
"@lingo.dev/_sdk": patch
"@lingo.dev/_spec": patch
"docs": patch
---

added missing changeset
41 changes: 41 additions & 0 deletions community/auto-localized-notes/.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
78 changes: 78 additions & 0 deletions community/auto-localized-notes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Auto-Localized Notes

A demo Next.js application showcasing **lingo.dev SDK** usage for automatic language detection and translation.
Users can write notes in any language, and the app automatically detects the source language and translates the note into a target language.

This project is intentionally minimal to focus on demonstrating **SDK integration, server/client separation, and real-world usage**, rather than application complexity.

---

## What this project does

- Accepts user input in **any language**
- Detects the input language automatically
- Translates the text into a selected target language
- Displays both:
- Original text
- Translated text
- Allows searching notes by translated content

The lingo.dev SDK is used **server-side only** via Next.js Server Actions to ensure correct handling of Node-only dependencies and API keys.

---

## Tech Stack

- **Next.js (App Router)**
- **TypeScript**
- **Tailwind CSS**
- **lingo.dev SDK**

---

## Prerequisites

Before running this project locally, ensure you have:

- **Node.js** `>= 18`
- **pnpm / npm / yarn**
- A **lingo.dev API key**

### Environment variables

Create a `.env.local` file in the project root:

```env
LINGODOTDEV_API_KEY=your_api_key_here
```

---

## How to run locally

1. Clone the repository

```
git clone <repo-url>
cd auto-localized-notes
```

2. Install dependencies

```
npm install
# or
pnpm install
```

3. Start the development server

```
npm run dev
```

4. Open in browser

```
http://localhost:3000
```
36 changes: 36 additions & 0 deletions community/auto-localized-notes/app/actions/translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use server";

import { LingoDotDevEngine } from "lingo.dev/sdk";

const lingoDotDev = new LingoDotDevEngine({
apiKey: process.env.LINGODOTDEV_API_KEY,
});
Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

LingoDotDevEngine apiKey requirement undefined behavior

💡 Result:

LingoDotDevEngine requires an apiKey in its constructor config (it’s documented as “Required” / “The SDK requires an API key”). If you pass apiKey: undefined (or omit it), that’s not a supported/defined configuration—the SDK will still attempt to call the Lingo.dev Engine, but requests will fail authentication because there’s no valid key to send. Use a real key (commonly via process.env.LINGODOTDEV_API_KEY) and consider adding your own upfront validation to throw a clear error when it’s missing. [1], [2]

Sources:
[1] https://docs.lingo.dev/setup/sdk
[2] https://lingo.dev/en/sdk/javascript


🏁 Script executed:

find . -type f -name "translate.ts" | head -20

Repository: lingodotdev/lingo.dev

Length of output: 122


🏁 Script executed:

cat -n community/auto-localized-notes/app/actions/translate.ts

Repository: lingodotdev/lingo.dev

Length of output: 1063


Fail fast if LINGODOTDEV_API_KEY is missing.

The LingoDotDevEngine requires a valid API key (documented as required by Lingo.dev SDK). Passing undefined is not a supported configuration—the SDK will initialize but requests will fail at runtime with authentication errors, making misconfiguration hard to diagnose. Add an explicit guard with a clear error before SDK initialization.

✅ Suggested change
-const lingoDotDev = new LingoDotDevEngine({
-  apiKey: process.env.LINGODOTDEV_API_KEY,
-});
+const apiKey = process.env.LINGODOTDEV_API_KEY;
+if (!apiKey) {
+  throw new Error("Missing LINGODOTDEV_API_KEY");
+}
+const lingoDotDev = new LingoDotDevEngine({
+  apiKey,
+});
📝 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 lingoDotDev = new LingoDotDevEngine({
apiKey: process.env.LINGODOTDEV_API_KEY,
});
const apiKey = process.env.LINGODOTDEV_API_KEY;
if (!apiKey) {
throw new Error("Missing LINGODOTDEV_API_KEY");
}
const lingoDotDev = new LingoDotDevEngine({
apiKey,
});
🤖 Prompt for AI Agents
In `@community/auto-localized-notes/app/actions/translate.ts` around lines 5 - 7,
Check for process.env.LINGODOTDEV_API_KEY and throw a clear error before
initializing LingoDotDevEngine to fail fast; if the env var is missing,
log/throw a descriptive message like "Missing LINGODOTDEV_API_KEY" and abort
initialization so you don't create lingoDotDev with undefined. Update the
initialization site where new LingoDotDevEngine({ apiKey:
process.env.LINGODOTDEV_API_KEY }) is called to validate the env var, then
create lingoDotDev only after the check passes.


export async function translateText(
text: string,
fromLang: string,
toLang: string,
) {
try {
const result = await lingoDotDev.localizeText(text, {
sourceLocale: fromLang,
targetLocale: toLang,
});

return result;
} catch (error) {
console.error(error);
return "error translating the text";
}
}

export async function recognizeText(text: string) {
try {
const detectedLang = await lingoDotDev.recognizeLocale(text);

return detectedLang;
} catch (error) {
console.error(error);
return "error detecting the language";
}
Comment on lines +9 to +35
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid returning sentinel error strings.

Returning "error …" makes failures indistinguishable from valid translations/locales. Prefer null (or a structured result) so callers can handle errors explicitly.

✅ Suggested change
-export async function translateText(
+export async function translateText(
   text: string,
   fromLang: string,
   toLang: string,
-) {
+): Promise<string | null> {
   try {
     const result = await lingoDotDev.localizeText(text, {
       sourceLocale: fromLang,
       targetLocale: toLang,
     });

     return result;
   } catch (error) {
     console.error(error);
-    return "error translating the text";
+    return null;
   }
 }

-export async function recognizeText(text: string) {
+export async function recognizeText(text: string): Promise<string | null> {
   try {
     const detectedLang = await lingoDotDev.recognizeLocale(text);

     return detectedLang;
   } catch (error) {
     console.error(error);
-    return "error detecting the language";
+    return null;
   }
 }
🤖 Prompt for AI Agents
In `@community/auto-localized-notes/app/actions/translate.ts` around lines 9 - 35,
Both translateText and recognizeText currently return sentinel error strings
("error translating the text"/"error detecting the language") which can collide
with valid outputs; change their error handling to return null (or a structured
error object) instead of strings and preserve logging. Specifically, in
translateText and recognizeText catch blocks (functions named translateText and
recognizeText) replace the string returns with return null (or an Error-style
result) and keep console.error(error) so callers can detect failures explicitly
and handle null results appropriately.

}
Binary file added community/auto-localized-notes/app/favicon.ico
Binary file not shown.
26 changes: 26 additions & 0 deletions community/auto-localized-notes/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "tailwindcss";

:root {
--background: #ffffff;
--foreground: #171717;
}

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
Comment on lines +22 to +26
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use the declared font variables instead of hardcoded Arial.
Right now the Geist font variables are set but never used, so the app renders with Arial/Helvetica. Switching to the variable makes the font import effective.

♻️ Suggested change
 body {
   background: var(--background);
   color: var(--foreground);
-  font-family: Arial, Helvetica, sans-serif;
+  font-family: var(--font-sans, Arial, Helvetica, sans-serif);
 }
🤖 Prompt for AI Agents
In `@community/auto-localized-notes/app/globals.css` around lines 22 - 26, The
body CSS currently hardcodes "Arial, Helvetica, sans-serif" so the declared
Geist font variables are ignored; update the body selector's font-family to use
the declared CSS variable (e.g., replace font-family: Arial, Helvetica,
sans-serif; with font-family: var(--font-sans, Arial, Helvetica, sans-serif);)
so the Geist font variable is applied while keeping a fallback.

34 changes: 34 additions & 0 deletions community/auto-localized-notes/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
159 changes: 159 additions & 0 deletions community/auto-localized-notes/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use client";

import { useState, useTransition } from "react";
import { recognizeText, translateText } from "./actions/translate";

type Note = {
id: string;
original: string;
translated: string;
detectedLang: string;
};

export default function HomePage() {
const [text, setText] = useState("");
const [query, setQuery] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [lang, setLang] = useState("en");
const [notes, setNotes] = useState<Note[]>([]);
const [isPending, startTransition] = useTransition();

const languages = ["en", "hi", "fr", "es"];

const handleChange = (e: any) => {
setLang(e.target.value);
};

const handleSubmit = () => {
if (!text.trim()) return;

startTransition(async () => {
const detectedLang = await recognizeText(text);
const result = await translateText(text, detectedLang, lang);

setNotes((prev) => [
{
id: crypto.randomUUID(),
original: text,
translated: result,
detectedLang: detectedLang,
},
...prev,
]);

setText("");
});
};
Comment on lines +27 to +46
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for server action calls.

If recognizeText or translateText fails (e.g., network error, service unavailable), the error will be silently ignored and the user receives no feedback. Consider wrapping in try/catch and displaying an error state.

🛡️ Suggested fix with error handling
+  const [error, setError] = useState<string | null>(null);
+
   const handleSubmit = () => {
     if (!text.trim()) return;
+    setError(null);
 
     startTransition(async () => {
-      const detectedLang = await recognizeText(text);
-      const result = await translateText(text, detectedLang, lang);
-
-      setNotes((prev) => [
-        {
-          id: crypto.randomUUID(),
-          original: text,
-          translated: result,
-          detectedLang: detectedLang,
-        },
-        ...prev,
-      ]);
-
-      setText("");
+      try {
+        const detectedLang = await recognizeText(text);
+        const result = await translateText(text, detectedLang, lang);
+
+        setNotes((prev) => [
+          {
+            id: crypto.randomUUID(),
+            original: text,
+            translated: result,
+            detectedLang: detectedLang,
+          },
+          ...prev,
+        ]);
+
+        setText("");
+      } catch (err) {
+        setError("Translation failed. Please try again.");
+      }
     });
   };
🤖 Prompt for AI Agents
In `@community/auto-localized-notes/app/page.tsx` around lines 27 - 46, The
handleSubmit function lacks error handling for the async server actions
recognizeText and translateText, so failures are swallowed and users get no
feedback; wrap the startTransition callback in a try/catch (or place try/catch
inside the async callback) to catch errors from recognizeText/translateText, set
an error state (e.g., setError or a local error flag) and avoid mutating state
on failure, and optionally call setText("") only on success; update UI to render
the error state. Reference handleSubmit, recognizeText, translateText, setNotes,
and setText when making the changes.


const filteredNotes =
query === ""
? notes
: notes.filter(
(note) =>
note.translated.toLowerCase().includes(query.toLowerCase()) ||
note.original.toLowerCase().includes(query.toLowerCase()),
);

const querySearch = () => {
setQuery(searchQuery.trim());
};

return (
<main className="min-h-screen bg-slate-100 dark:bg-slate-950">
<div className="max-w-2xl mx-auto p-6 space-y-8 text-slate-900 dark:text-slate-100">
{/* Header */}
<header className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight">
Auto-Localized Notes
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Write in any language. Read in your own.
</p>
</header>

{/* Search */}
<div className="rounded-xl border bg-white dark:bg-slate-900 dark:border-slate-700 p-4 shadow-sm space-y-3">
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
Search notes
</label>
<div className="flex gap-2">
<input
type="text"
className="flex-1 rounded-md border bg-white dark:bg-slate-800 dark:border-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search translated text…"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button
onClick={querySearch}
className="rounded-md border dark:border-slate-700 px-4 py-2 text-sm font-medium hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50"
>
Search
</button>
</div>
</div>

{/* Input */}
<div className="rounded-xl border bg-white dark:bg-slate-900 dark:border-slate-700 p-4 shadow-sm space-y-4">
<label className="text-sm font-medium text-gray-600 dark:text-gray-300">
New note
</label>

<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Write in any language…"
className="w-full rounded-md border bg-white dark:bg-slate-800 dark:border-slate-700 px-3 py-2 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={4}
/>

<div className="flex items-center justify-between gap-3">
<select
value={lang}
onChange={handleChange}
className="rounded-md border bg-white dark:bg-slate-800 dark:border-slate-700 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{languages.map((l) => (
<option key={l} value={l}>
{l}
</option>
))}
</select>

<button
onClick={handleSubmit}
disabled={isPending}
className="rounded-md bg-black dark:bg-white px-5 py-2 text-sm font-medium text-white dark:text-black hover:bg-gray-900 dark:hover:bg-gray-200 disabled:opacity-60"
>
{isPending ? "Translating…" : "Add note"}
</button>
</div>
</div>

{/* Notes */}
<section className="space-y-4">
{filteredNotes.map((note) => (
<div
key={note.id}
className="rounded-xl border bg-white dark:bg-slate-900 dark:border-slate-700 p-5 shadow-sm space-y-3"
>
<div className="space-y-1">
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400">
Original ({note.detectedLang})
</p>
<p className="text-sm whitespace-pre-wrap">{note.original}</p>
</div>

<div className="space-y-1">
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400">
Translated ({lang})
</p>
<p className="text-sm whitespace-pre-wrap">{note.translated}</p>
</div>
Comment on lines +147 to +152
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: Translated language label shows current selection, not the actual target language.

Line 149 displays {lang} which is the currently selected language in the dropdown, not the language the note was actually translated to. If a user creates a note translated to French, then changes the dropdown to Spanish, the existing note will incorrectly show "Translated (es)" even though the text is in French.

🐛 Suggested fix: store target language per note

Update the Note type to include targetLang:

 type Note = {
   id: string;
   original: string;
   translated: string;
   detectedLang: string;
+  targetLang: string;
 };

Store it when creating the note in handleSubmit:

       setNotes((prev) => [
         {
           id: crypto.randomUUID(),
           original: text,
           translated: result,
           detectedLang: detectedLang,
+          targetLang: lang,
         },
         ...prev,
       ]);

Display the stored value in the JSX:

               <p className="text-xs font-semibold text-gray-500 dark:text-gray-400">
-                Translated ({lang})
+                Translated ({note.targetLang})
               </p>
🤖 Prompt for AI Agents
In `@community/auto-localized-notes/app/page.tsx` around lines 147 - 152, The
Translated label is using the current dropdown state `{lang}` instead of the
note's actual target language; update the Note type (or whatever model holds
notes) to include a targetLang field, set that field when creating a note in
handleSubmit (use the selected lang value at submit time), and change the JSX to
render the stored target language (e.g., use note.targetLang instead of lang) so
existing notes keep their original translation language regardless of later
dropdown changes.

</div>
))}
</section>
</div>
</main>
);
}
Loading
Loading