-
Notifications
You must be signed in to change notification settings - Fork 827
feat(community): add auto-localized notes demo (NextJS) #1857
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?
Changes from all commits
58b72b1
5d0e451
4de3ff0
e62a7b3
fc4861e
6edb11b
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,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 |
| 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,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 | ||
| ``` |
| 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, | ||
| }); | ||
|
|
||
| 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
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. Avoid returning sentinel error strings. Returning ✅ 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 |
||
| } | ||
| 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
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. Use the declared font variables instead of hardcoded Arial. ♻️ 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 |
||
| 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> | ||
| ); | ||
| } |
| 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
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. Add error handling for server action calls. If 🛡️ 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 |
||
|
|
||
| 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
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. Bug: Translated language label shows current selection, not the actual target language. Line 149 displays 🐛 Suggested fix: store target language per noteUpdate the type Note = {
id: string;
original: string;
translated: string;
detectedLang: string;
+ targetLang: string;
};Store it when creating the note in 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 |
||
| </div> | ||
| ))} | ||
| </section> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
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.
🧩 Analysis chain
🌐 Web query:
LingoDotDevEngine apiKey requirement undefined behavior💡 Result:
LingoDotDevEnginerequires anapiKeyin its constructor config (it’s documented as “Required” / “The SDK requires an API key”). If you passapiKey: 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 viaprocess.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:
Repository: lingodotdev/lingo.dev
Length of output: 122
🏁 Script executed:
Repository: lingodotdev/lingo.dev
Length of output: 1063
Fail fast if
LINGODOTDEV_API_KEYis missing.The LingoDotDevEngine requires a valid API key (documented as required by Lingo.dev SDK). Passing
undefinedis 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
📝 Committable suggestion
🤖 Prompt for AI Agents