Added multilanguage support to the site#8
Added multilanguage support to the site#8malwaretestinginfo wants to merge 8 commits intomspaint-cc:mainfrom
Conversation
|
@malwaretestinginfo is attempting to deploy a commit to the mspaint Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThis PR introduces comprehensive internationalization (i18n) support. It adds language translation files for five languages (English, German, Portuguese, French, Romanian), creates a LanguageContext with localStorage persistence, adds translated UI components throughout the application, and refactors the root layout and home page to use these new translation components. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as UI (LanguageSwitcher)
participant Context as LanguageContext
participant Storage as localStorage
participant API as fetch /lang/{lang}.json
User->>UI: Click language button
UI->>Context: setLanguage(newLang)
Context->>Storage: Save new language
Storage-->>Context: ✓
Context->>API: Fetch translations
API-->>Context: translations JSON
Context->>Context: Update state
Context->>UI: Notify subscribers
UI->>UI: Re-render with new translations
UI-->>User: Display updated content
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
src/components/reviews.tsx (1)
46-53: Good migration to Next.js Image component.The change from
imgto Next.jsImageenables automatic optimization. The explicitwidthandheightprops prevent layout shift.Note: The
loading="lazy"prop is redundant as Next.js Image components are lazy-loaded by default. You can remove it:<Image className="rounded-full" width={32} height={32} alt="" src={img} - loading="lazy" />src/components/LanguageSwitcher.tsx (1)
18-72: Consider adding accessibility improvements.The language switcher could benefit from:
- An
aria-labelon the main button (e.g., "Select language")aria-expandedstate tracking for the dropdown- Click-outside detection to close the dropdown when clicking elsewhere on the page
Example enhancement for accessibility:
return ( - <div className="fixed bottom-6 left-6 z-50"> + <div className="fixed bottom-6 left-6 z-50" role="navigation" aria-label="Language switcher"> <div className="relative"> {isOpen && ( <div className="absolute bottom-full mb-2 flex flex-col gap-2 bg-background/90 backdrop-blur supports-[backdrop-filter]:bg-background/60 border border-border rounded-lg p-2 shadow-lg"> {otherLanguages.map((lang) => ( <Button key={lang.code} variant="ghost" size="sm" onClick={() => { setLanguage(lang.code); setIsOpen(false); }} className="flex items-center gap-2 justify-start hover:bg-primary/10" + aria-label={`Switch to ${lang.name}`} >src/components/TranslatedFooter.tsx (1)
15-17: Consider extracting fallback strings as constants.The fallback strings are quite lengthy and embedded directly in the JSX. For better maintainability, consider extracting them as constants at the top of the file or in a separate constants file.
Apply this pattern:
+"use client"; + +import { useLanguage, getTranslation } from "@/contexts/LanguageContext"; +import Image from "next/image"; + +const FALLBACK_MADE_BY = "Site made by upio"; +const FALLBACK_DISCLAIMER = "This software is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Roblox or Microsoft or any of its subsidiaries or its affiliates."; + export function TranslatedFooter() { const { translations } = useLanguage(); return ( <div className="px-10 py-6 w-screen flex flex-row justify-between items-center max-md:justify-center max-md:flex-col"> <div className="px-2 py-2 flex flex-row items-center gap-2"> <Image alt="mspaint" src="/icon.png" width={25} height={25} /> <div> <p className="text-xs text-left">mspaint</p> <p className="text-muted-foreground text-xs"> - {getTranslation(translations, "footer.madeBy") || "Site made by upio"} + {getTranslation(translations, "footer.madeBy") || FALLBACK_MADE_BY} </p> </div> </div> <p className="text-muted-foreground text-xs px-2 py-2 text-right max-md:text-center max-md:mt-5"> - {getTranslation(translations, "footer.disclaimer") || - "This software is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Roblox or Microsoft or any of its subsidiaries or its affiliates."} + {getTranslation(translations, "footer.disclaimer") || FALLBACK_DISCLAIMER} </p> </div> ); }src/contexts/LanguageContext.tsx (2)
25-30: Consider validating the stored language value.The code type-asserts
savedLangasLanguagewithout runtime validation before the includes check. While the includes check provides some validation, it's better to validate before the type assertion.Apply this diff:
useEffect(() => { - const savedLang = localStorage.getItem("language") as Language; - if (savedLang && ["en", "de", "pt", "fr", "ro"].includes(savedLang)) { + const savedLang = localStorage.getItem("language"); + if (savedLang && ["en", "de", "pt", "fr", "ro"].includes(savedLang)) { - setLanguageState(savedLang); + setLanguageState(savedLang as Language); } }, []);
32-37: Consider exposing loading and error states.The translation fetch currently has no loading state exposed to consumers. Components will render with empty strings from
getTranslationwhile translations load. Consider exposing aloadingboolean in the context to allow components to show loading indicators.Add loading state to the context:
interface LanguageContextType { language: Language; setLanguage: (lang: Language) => void; translations: Record<string, unknown>; loading: boolean; } export function LanguageProvider({ children }: { children: React.ReactNode }) { const [language, setLanguageState] = useState<Language>("en"); const [translations, setTranslations] = useState<Record<string, unknown>>({}); const [loading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch(`/lang/${language}.json`) .then((res) => res.json()) .then((data) => { setTranslations(data); setLoading(false); }) .catch((err) => { console.error("Failed to load translations:", err); setLoading(false); }); }, [language]); // ... rest of the code }src/components/TranslatedSections.tsx (1)
19-24: Consider usingreplaceAllfor placeholder substitution.The
.replace()method only replaces the first occurrence of{count}. If a translation string accidentally contains multiple{count}placeholders, only the first would be substituted.Apply this diff:
export function TranslatedGamesTitle({ count }: { count: number }) { const { translations } = useLanguage(); const text = getTranslation(translations, "games.title") || "mspaint officially supports {count} games"; - return <>{text.replace("{count}", String(count))}</>; + return <>{text.replaceAll("{count}", String(count))}</>; }Apply the same change to
TranslatedLanguagesTitleat line 36.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (23)
.vscode/settings.json(1 hunks)next.config.ts(1 hunks)public/lang/de.json(1 hunks)public/lang/en.json(1 hunks)public/lang/fr.json(1 hunks)public/lang/pt.json(1 hunks)public/lang/ro.json(1 hunks)src/app/layout.tsx(2 hunks)src/app/page.tsx(9 hunks)src/auth.ts(2 hunks)src/components/LanguageSwitcher.tsx(1 hunks)src/components/TranslatedFAQ.tsx(1 hunks)src/components/TranslatedFooter.tsx(1 hunks)src/components/TranslatedHero.tsx(1 hunks)src/components/TranslatedNav.tsx(1 hunks)src/components/TranslatedReviews.tsx(1 hunks)src/components/TranslatedSections.tsx(1 hunks)src/components/TranslatedStats.tsx(1 hunks)src/components/obsidian/UIStateProvider.tsx(0 hunks)src/components/reviews.tsx(2 hunks)src/components/ui/accordion.tsx(1 hunks)src/contexts/LanguageContext.tsx(1 hunks)src/placeholder.txt(1 hunks)
💤 Files with no reviewable changes (1)
- src/components/obsidian/UIStateProvider.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-29T22:06:39.309Z
Learnt from: notpoiu
Repo: mspaint-cc/webmspaint PR: 4
File: src/components/obsidian/DynamicTab.tsx:135-146
Timestamp: 2025-08-29T22:06:39.309Z
Learning: ObsidianImage component in src/components/obsidian/elements/Image.tsx uses dynamic Tailwind height classes like h-[${height}px] which won't be generated at build time. Instead, dynamic height values should be passed via inline style prop (style={{ height }}) to ensure proper rendering.
Applied to files:
src/components/reviews.tsx
🧬 Code graph analysis (11)
src/components/TranslatedReviews.tsx (2)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)src/components/magicui/highlighter.tsx (1)
Highlighter(30-89)
src/components/TranslatedHero.tsx (2)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)src/components/ui/word-rotate.tsx (1)
WordRotate(15-54)
src/app/layout.tsx (3)
src/contexts/LanguageContext.tsx (1)
LanguageProvider(21-49)src/components/LanguageSwitcher.tsx (1)
LanguageSwitcher(18-72)src/components/ui/sonner.tsx (1)
Toaster(32-32)
src/components/TranslatedSections.tsx (1)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)
src/components/TranslatedStats.tsx (2)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)src/components/magicui/number-ticker.tsx (1)
NumberTicker(8-56)
src/components/TranslatedNav.tsx (1)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)
src/components/LanguageSwitcher.tsx (1)
src/contexts/LanguageContext.tsx (1)
useLanguage(51-57)
src/components/TranslatedFAQ.tsx (2)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)src/data/games.ts (1)
gamesList(1-17)
src/components/TranslatedFooter.tsx (1)
src/contexts/LanguageContext.tsx (2)
useLanguage(51-57)getTranslation(59-72)
src/components/ui/accordion.tsx (1)
src/lib/utils.ts (1)
cn(22-24)
src/app/page.tsx (8)
src/components/TranslatedNav.tsx (1)
TranslatedNav(7-33)src/components/TranslatedHero.tsx (1)
TranslatedHeroSubtitle(6-18)src/components/TranslatedSections.tsx (6)
TranslatedExecutorsTitle(5-12)TranslatedExecutorsMore(14-17)TranslatedGamesTitle(19-24)TranslatedGamesSubtitle(26-29)TranslatedLanguagesTitle(31-37)TranslatedLanguagesSubtitle(39-44)src/components/TranslatedStats.tsx (2)
TranslatedStatsTitle(6-18)TranslatedStatsKardin(20-31)src/components/TranslatedReviews.tsx (1)
TranslatedReviewsTitle(6-17)src/components/reviews.tsx (1)
ReviewMarquee(155-161)src/components/TranslatedFAQ.tsx (3)
TranslatedFAQTitle(12-15)TranslatedFAQIntro(17-32)TranslatedFAQAccordion(34-116)src/components/TranslatedFooter.tsx (1)
TranslatedFooter(6-26)
🔇 Additional comments (19)
next.config.ts (1)
13-16: LGTM!Adding
flagcdn.comto remote patterns is appropriate for the language switcher feature, enabling optimized flag images through Next.js Image component.public/lang/ro.json (1)
1-70: Translation structure looks correct.The JSON structure and interpolation placeholders (
{count},{games}) align with the other language files in this PR. The formatting and nesting are consistent.Please ensure a native Romanian speaker has reviewed these translations for accuracy and natural language flow.
src/auth.ts (1)
17-17: LGTM!Improved type safety by explicitly casting
token.idas string, removing the need for type error suppression.public/lang/fr.json (1)
1-70: Translation structure looks correct.The JSON structure and interpolation placeholders (
{count},{games}) align with the other language files in this PR. The formatting and nesting are consistent.Please ensure a native French speaker has reviewed these translations for accuracy, proper grammar, and natural language flow.
src/app/layout.tsx (1)
63-68: No critical issues found—LanguageProvider correctly handles SSR/hydration.The
LanguageProvideris properly marked with"use client"and correctly usesuseEffectwith empty dependencies to ensurelocalStorageaccess occurs only on the client after hydration. The initial state (language="en",translations={}) matches between server and client renders, preventing hydration mismatches. This is the correct pattern for client-only storage in Next.js 15.src/components/ui/accordion.tsx (1)
14-20: LGTM: Simplified rendering without memoization.The removal of
React.memowrappers simplifies the component implementation. This change enables dynamic translated content to flow directly into accordion items without memoization constraints, which aligns well with the translation-driven UI being introduced.Also applies to: 26-40
src/components/TranslatedReviews.tsx (1)
6-17: LGTM: Clean translation component implementation.The component correctly uses the translation context with appropriate fallbacks and integrates well with the Highlighter component for visual emphasis.
src/components/TranslatedHero.tsx (1)
6-18: LGTM: Well-structured hero subtitle component.The component properly integrates translation support with the WordRotate animation component, maintaining clean separation of concerns and providing appropriate fallbacks.
src/components/TranslatedStats.tsx (2)
6-18: LGTM: Stats title component correctly integrates NumberTicker with translations.The component properly combines animated number display with localized text while maintaining appropriate fallbacks.
20-31: LGTM: Clean implementation of translated Kardin reference.Simple, focused component that correctly applies translation context for the attribution text.
src/components/LanguageSwitcher.tsx (1)
10-16: No configuration issue—flagcdn.com is already properly configured in next.config.ts.The
flagcdn.comdomain is already included in theremotePatternsarray with the correcthttpsprotocol, so images will load successfully without errors.src/components/TranslatedNav.tsx (1)
7-32: LGTM!The conditional navigation logic based on the
noAccountprop is well-implemented, and the translation pattern with fallbacks is consistent.src/components/TranslatedFAQ.tsx (2)
46-113: LGTM!The FAQ accordion implementation is well-structured with appropriate fallback strings for all questions and answers. The translation pattern is consistent across all items.
34-37: Add defensive handling for edge cases or document the non-empty constraint.Verification shows
gamesListis a hardcoded array with 15 elements fromsrc/data/games.tsand is passed directly to the component atsrc/app/page.tsx:491. The empty array edge case won't occur with current usage, but the code remains vulnerable to future changes.Recommend either:
- Adding a guard (e.g., early return or validation)
- Documenting that
gamesListmust always have at least one elementAdditionally, with a single-element array, the current logic produces
" and element"with a leading space—consider handling that edge case as well.src/contexts/LanguageContext.tsx (1)
59-72: LGTM!The
getTranslationhelper function correctly traverses nested objects and safely returns an empty string when a path is not found, preventing runtime errors.src/app/page.tsx (2)
7-20: LGTM!The imports for all translated components are well-organized and clearly grouped. The migration from hard-coded UI to translation-driven components is systematic.
491-491: gamesList is guaranteed non-empty—no action required.Verification confirms
gamesListis a hardcoded array of 17 items exported from@/data/games.tsand passed directly toTranslatedFAQAccordionwithout filtering, slicing, or conditional reassignment. The list cannot be empty in production.src/components/TranslatedSections.tsx (2)
5-17: LGTM!The executor-related translation components are well-structured with appropriate fallback strings.
26-29: LGTM!The subtitle components for games and languages sections are cleanly implemented with concise fallback text.
Also applies to: 39-44
| import Discord from "next-auth/providers/discord" | ||
|
|
||
| export const { handlers, signIn, signOut, auth } = NextAuth({ | ||
| secret: process.env.AUTH_SECRET || "development-secret-key-change-in-production", |
There was a problem hiding this comment.
Security risk: weak fallback secret.
The fallback secret "development-secret-key-change-in-production" creates a critical security vulnerability if AUTH_SECRET is not properly set in production. Session tokens would be signed with a predictable, publicly visible key.
Replace with an approach that fails fast if the secret is missing in production:
- secret: process.env.AUTH_SECRET || "development-secret-key-change-in-production",
+ secret: process.env.AUTH_SECRET || (process.env.NODE_ENV === "production"
+ ? (() => { throw new Error("AUTH_SECRET is required in production"); })()
+ : "development-secret-key-change-in-production"),Or more simply, ensure AUTH_SECRET is always set via deployment configuration and remove the fallback:
- secret: process.env.AUTH_SECRET || "development-secret-key-change-in-production",
+ secret: process.env.AUTH_SECRET,📝 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.
| secret: process.env.AUTH_SECRET || "development-secret-key-change-in-production", | |
| secret: process.env.AUTH_SECRET || (process.env.NODE_ENV === "production" | |
| ? (() => { throw new Error("AUTH_SECRET is required in production"); })() | |
| : "development-secret-key-change-in-production"), |
🤖 Prompt for AI Agents
In src/auth.ts around line 5, the code currently falls back to a hard-coded
development secret which is insecure; remove the insecure fallback and load the
secret only from process.env.AUTH_SECRET, and if process.env.NODE_ENV ===
'production' and AUTH_SECRET is missing throw an error (fail fast at startup) so
the app cannot run without a real secret; for convenience you can also throw or
warn in non-production but do not include any hard-coded secret.
| Please gimme something send me crypto or smth like that | ||
|
|
||
| SOL: B7Gndj8GT1ys8fNqMgQ3wkN3SxuHQag8dtbmEq4UAqGa |
There was a problem hiding this comment.
Critical: Remove this unauthorized file immediately.
This file contains a cryptocurrency donation request and wallet address that is completely unrelated to the stated PR objective of adding multilanguage support. This appears to be an attempt to use the codebase for personal solicitation.
This file must be removed from the PR before it can be approved.
🤖 Prompt for AI Agents
In src/placeholder.txt lines 1-3: this file contains an unauthorized crypto
solicitation and must be removed; delete the file from the branch (e.g., git rm
src/placeholder.txt && git commit -m "Remove unauthorized placeholder file"),
ensure the removal is pushed to the PR branch (git push) or force-push if you
amended history, and if the file was already committed in earlier commits purge
it from repo history (use git filter-repo or an interactive rebase to remove the
file) and then push the cleaned branch.
Ok like this is actually good, not as the other PR i opened ...
Preview available at: https://mspaint.rs-st.de/
Look in the bottom left for the Language Switcher!
Summary by CodeRabbit
Release Notes
New Features
Improvements