Skip to content

(SP:3) feat(i18n): translate about page and auth form validation mess…#227

Merged
ViktorSvertoka merged 3 commits into
developfrom
feat/translate-about
Jan 31, 2026
Merged

(SP:3) feat(i18n): translate about page and auth form validation mess…#227
ViktorSvertoka merged 3 commits into
developfrom
feat/translate-about

Conversation

@TiZorii
Copy link
Copy Markdown
Collaborator

@TiZorii TiZorii commented Jan 31, 2026

…ages

  • Add about page translations (EN, UK, PL)
  • Add auth.fields.validation translations for form errors

Summary by CodeRabbit

  • New Features

    • About page now generates metadata (title & description).
    • AI endpoint: improved diagnostics, enhanced error handling, rate‑limit behavior, and a new service status endpoint.
  • Internationalization

    • Full i18n support across About components.
    • Large set of About translations added for English, Polish, and Ukrainian.
  • Usability

    • Client-side form validation with localized error messages.

✏️ Tip: You can customize this high-level summary in your review settings.

…ages

  - Add about page translations (EN, UK, PL)
  - Add auth.fields.validation translations for form errors
@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 31, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit a3f28b4
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/697dc2661401120008470ebc
😎 Deploy Preview https://deploy-preview-227--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

Adds i18n to about pages and components, new client-side validation messages for auth fields, large locale translation bundles (en/pl/uk), and hardens the AI explain API with dynamic Groq import, diagnostic logging, rate limiting, standardized error responses, and a GET status endpoint.

Changes

Cohort / File(s) Summary
About Page Metadata
frontend/app/[locale]/about/page.tsx
Added exported generateMetadata that loads about translations server-side and returns localized title/description meta.
About UI Components
frontend/components/about/CommunitySection.tsx, frontend/components/about/HeroSection.tsx, frontend/components/about/TopicsSection.tsx, frontend/components/about/SponsorsWall.tsx, frontend/components/about/PricingSection.tsx, frontend/components/about/InteractiveGame.tsx
Replaced hard-coded strings with useTranslations lookups; InteractiveGame refactored to use translation keys for level names and renamed helper to getCurrentLevelKey. Minor JSX formatting tweaks.
Features Section
frontend/components/about/FeaturesSection.tsx
Feature model renamed to labelKey/descKey; FeatureBubble and TabButton signatures updated to accept t translation function; callers/pages updated to pass t.
Auth Field Validation
frontend/components/auth/fields/EmailField.tsx, frontend/components/auth/fields/NameField.tsx, frontend/components/auth/fields/PasswordField.tsx
Added onInvalid/onInput handlers to set/clear localized custom validity messages for required/format/length validations on inputs.
AI Explain API
frontend/app/api/ai/explain/route.ts
Replaced static Groq import with dynamic import/init and added detailed diagnostic logging helpers, client IP extraction, enhanced rate-limit checks (including skipped handling), safe body parsing, pre/post Groq call logging, expanded standardized error handling, and added a GET status endpoint.
Translation Bundles
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added a comprehensive about section to each locale and introduced auth.fields.validation messages (required, invalidEmail, passwordTooShort); minor dashboard label updates in some locales.
Small/Formatting
frontend/app/[locale]/about/page.tsx (whitespace), assorted components
Minor whitespace/formatting adjustments accompanying i18n changes.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Server as ExplainAPI
  participant RateLimiter
  participant GroqAPI

  Client->>Server: POST /api/ai/explain (body)
  Server->>Server: logRequestDiagnostics()
  Server->>Server: extract client IP
  Server->>RateLimiter: check(ip)
  alt rate limit exceeded
    RateLimiter-->>Server: limited (metadata)
    Server-->>Client: 429 + rate-limit metadata
  else proceed
    Server->>Server: parse body safely (request.text())
    Server->>Server: dynamic import('groq-sdk') / init
    alt SDK import/init fails
      Server-->>Client: 503 + SDK_IMPORT_ERROR / SDK_INIT_ERROR
    else SDK ready
      Server->>GroqAPI: chatCompletions request
      GroqAPI-->>Server: response
      Server->>Server: validate response content
      alt invalid/empty
        Server-->>Client: 502/500 + standardized AI error
      else success
        Server-->>Client: 200 + explanation payload
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 Hoppity-hop, keys take their place,

Translated titles brighten every space,
Validation nudges when inputs stray,
Groq boots carefully and logs the way,
About pages localize — hooray! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding i18n translations for the about page and auth form validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/translate-about

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/app/api/ai/explain/route.ts (1)

364-376: ⚠️ Potential issue | 🟠 Major

Security: Debug information in generic error response.

The generic error response includes errorName, nodeEnv, and isNetlify which exposes internal implementation details to clients. Error messages from the Groq SDK may also contain sensitive information.

🛡️ Proposed fix: Remove debug payload from error response
     return NextResponse.json(
       {
         error: 'Failed to generate explanation',
         code: 'AI_ERROR',
-        details: errorMessage, // Always include for debugging (remove in production)
-        debug: {
-          errorName,
-          nodeEnv: process.env.NODE_ENV,
-          isNetlify: !!process.env.NETLIFY,
-        }
       },
       { status: 500 }
     );
🤖 Fix all issues with AI agents
In `@frontend/app/api/ai/explain/route.ts`:
- Around line 258-269: The response in the SDK initialization error (in
frontend/app/api/ai/explain/route.ts) is leaking internal details via the debug
object (errorName/errorMessage); remove the debug payload from the
NextResponse.json return so the client only receives the generic error and code
(e.g., { error: 'Failed to initialize AI client', code: 'SDK_INIT_ERROR' }) and
instead log the full initError server-side (e.g., inside the catch block) for
debugging; update the return in the catch handling for initError to omit
errorName/errorMessage.
- Around line 335-341: Remove the exposure of the API key prefix from the client
response: in the error return block that builds the response object (where
apiKey and debug are referenced), delete the debug.keyPrefix property and ensure
no part of apiKey is included in the JSON sent to clients; if you need the
prefix for debugging, log it server-side via processLogger or a secure logger
inside the same scope (e.g., in the route handler) instead of returning it in
the response.
- Around line 182-197: Remove sensitive environment metadata from the HTTP error
response and stop printing all env keys to the console: in the GROQ_API_KEY
missing branch (the block using console.error and returning NextResponse.json)
delete the debug object fields (hasKey, nodeEnv, isNetlify, context) from the
JSON response and return only a generic error/code message with 503, and replace
the console.error([...Object.keys(process.env)...]) call with a minimal
server-side log that does not enumerate environment variable names (e.g., log a
short message or an internal error id) so debug info remains server-only; locate
the change where NextResponse.json is called and where console.error is used to
apply this fix.
- Around line 380-414: The public GET diagnostic handler (export async function
GET) in frontend/app/api/ai/explain/route.ts exposes sensitive environment and
API key details (it calls logEnvironmentDiagnostics and logRequestDiagnostics);
remove this handler entirely before merging or restrict it to non-production and
authenticated use only (e.g., run only when process.env.NODE_ENV ===
'development' and verify an admin auth token), and ensure any remaining
diagnostic logs do not include API key prefixes/lengths, deploy URLs, or
environment variable names.

In `@frontend/components/about/PricingSection.tsx`:
- Around line 45-50: The sr-only heading in PricingSection.tsx is hard-coded as
English; update it to use the i18n translator by replacing the literal "Pricing"
in the <h2 id="pricing-heading" className="sr-only"> with
t("about.pricing.heading") (or the chosen key in the about.pricing namespace),
and add that dedicated "heading" key to your translation files so the
screen-reader label is localized across locales; ensure the element still uses
id="pricing-heading" and className="sr-only" unchanged for accessibility.

In `@frontend/components/about/SponsorsWall.tsx`:
- Around line 48-53: The hard-coded aria-label in SponsorsWall.tsx should use
localization; update the Link's aria-label from the literal string to
aria-label={t("ctaAriaLabel")} using the existing
useTranslations("about.sponsors") call, and add the ctaAriaLabel key to the
"about.sponsors" section in all locale files (frontend/messages/en.json,
pl.json, uk.json) with appropriate translations so t("ctaAriaLabel") resolves
for each locale.
🧹 Nitpick comments (2)
frontend/app/api/ai/explain/route.ts (1)

12-92: Ensure debug logging is removed before production deployment.

The comment indicates this is temporary debug code. These logging functions expose diagnostic information including API key prefixes, request headers, and full request bodies in server logs. While useful for debugging, they should be removed or gated behind a debug flag before production to:

  • Reduce log verbosity and cost
  • Minimize exposure of potentially sensitive user input (term/context)
  • Clean up the codebase

Consider using a feature flag or environment check (e.g., process.env.DEBUG_AI_ENDPOINT) to conditionally enable this logging.

frontend/components/about/HeroSection.tsx (1)

65-68: Consider unifying mobile and desktop stat translation keys for consistency.

Mobile stats use short keys (stats.quizzes, stats.stars, stats.users, stats.followers) while desktop uses longer variants (stats.quizzesPassed, stats.githubStars, stats.activeUsers, stats.linkedinFollowers). All keys exist in translation files, but unifying on a single set would simplify maintenance. Consider reusing the desktop keys across both unless the shorter variants are intentionally maintained for localization purposes.

Comment thread frontend/app/api/ai/explain/route.ts Outdated
Comment thread frontend/app/api/ai/explain/route.ts
Comment thread frontend/app/api/ai/explain/route.ts
Comment thread frontend/app/api/ai/explain/route.ts Outdated
Comment on lines +45 to +50
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
>
<h2 id="pricing-heading" className="sr-only">Pricing</h2>
<GradientBadge icon={Sparkles} text="No Hidden Fees" className="mb-4" />
<GradientBadge icon={Sparkles} text={t("badge")} className="mb-4" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

cat -n frontend/components/about/PricingSection.tsx

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

     1	"use client"
     2	import { useState } from "react"
     3	import { motion } from "framer-motion"
     4	import { Check, Heart, X, Sparkles, Server, ArrowRight } from "lucide-react"
     5	import Link from "next/link"
     6	import { useTranslations } from "next-intl"
     7	import type { Sponsor } from "@/lib/about/github-sponsors"
     8	import { SponsorsWall } from "./SponsorsWall"
     9	import { GradientBadge } from "@/components/ui/gradient-badge"
    10	import { SectionHeading } from "@/components/ui/section-heading"
    11	import { ParticleCanvas } from "@/components/ui/particle-canvas"
    12	
    13	interface PricingSectionProps {
    14	  sponsors?: Sponsor[]
    15	}
    16	
    17	export function PricingSection({ sponsors = [] }: PricingSectionProps) {
    18	    const t = useTranslations("about.pricing")
    19	    const [activeShape, setActiveShape] = useState<"brackets" | "heart" | null>(null)
    20	
    21	    const juniorFeatures = [
    22	        t("junior.features.unlimited"),
    23	        t("junior.features.fullAccess"),
    24	        t("junior.features.noCard"),
    25	        t("junior.features.noGuilt"),
    26	    ]
    27	
    28	    const heroFeatures = [
    29	        t("hero.features.servers"),
    30	        t("hero.features.coffee"),
    31	        t("hero.features.badge"),
    32	        t("hero.features.feeling"),
    33	    ]
    34	
    35	    return (
    36	        <section className="w-full py-20 lg:py-28 relative overflow-hidden bg-gray-50 dark:bg-transparent" aria-labelledby="pricing-heading">
    37	            <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-[`#1e5eff`]/5 dark:bg-[`#ff2d55`]/5 blur-[100px] rounded-full pointer-events-none" aria-hidden="true" />
    38	
    39	            <div className="absolute inset-0 pointer-events-none z-0">
    40	                <ParticleCanvas activeShape={activeShape} className="w-full h-full" />
    41	            </div>
    42	
    43	            <div className="container-main relative z-10">
    44	                <div className="text-center mb-16">
    45	                    <motion.div
    46	                        initial={{ opacity: 0, y: 10 }}
    47	                        whileInView={{ opacity: 1, y: 0 }}
    48	                    >
    49	                        <h2 id="pricing-heading" className="sr-only">Pricing</h2>
    50	                        <GradientBadge icon={Sparkles} text={t("badge")} className="mb-4" />
    51	                    </motion.div>
    52	
    53	                    <SectionHeading
    54	                        title={t("title")}
    55	                        highlight={t("titleHighlight")}
    56	                        subtitle={t("subtitle")}
    57	                    />
    58	                </div>
    59	
    60	                <div className="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-12 max-w-4xl mx-auto mb-6">
    61	
    62	                    <motion.div
    63	                        whileHover={{ y: -5 }}
    64	                        onMouseEnter={() => setActiveShape("brackets")}
    65	                        onMouseLeave={() => setActiveShape(null)}
    66	                        onFocus={() => setActiveShape("brackets")}
    67	                        onBlur={() => setActiveShape(null)}
    68	                        className="flex flex-col p-8 lg:p-10 rounded-3xl border border-gray-200 dark:border-neutral-800 bg-white/10 dark:bg-neutral-900/10 backdrop-blur-md shadow-sm relative z-10"
    69	                    >
    70	                        <div className="mb-6">
    71	                            <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">{t("junior.title")}</h3>
    72	                            <p className="text-sm text-gray-600 dark:text-neutral-400">{t("junior.description")}</p>
    73	                        </div>
    74	                        <div className="mb-8">
    75	                            <span className="text-4xl lg:text-5xl font-black text-gray-900 dark:text-white">{t("junior.price")}</span>
    76	                            <span className="text-gray-500 dark:text-neutral-500 font-mono text-sm ml-2">{t("junior.period")}</span>
    77	                        </div>
    78	
    79	                        <ul className="space-y-4 mb-8 flex-1">
    80	                            {juniorFeatures.map((item) => (
    81	                                <li key={item} className="flex items-center gap-3 text-sm text-gray-700 dark:text-neutral-300">
    82	                                    <div className="p-1 rounded-full bg-green-500/10 text-green-500" aria-hidden="true">
    83	                                        <Check size={12} />
    84	                                    </div>
    85	                                    {item}
    86	                                </li>
    87	                            ))}
    88	                             <li className="flex items-center gap-3 text-sm text-gray-400 dark:text-neutral-500 line-through decoration-gray-300 dark:decoration-neutral-700">
    89	                                    <div className="p-1 rounded-full bg-gray-100 dark:bg-neutral-800 text-gray-400 dark:text-neutral-600" aria-hidden="true">
    90	                                        <X size={12} />
    91	                                    </div>
    92	                                    {t("junior.noYacht")}
    93	                                </li>
    94	                        </ul>
    95	
    96	                        <Link href="/" className="w-full py-4 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 text-gray-900 dark:text-white font-bold text-center transition-all uppercase tracking-widest text-xs">
    97	                            {t("junior.cta")}
    98	                        </Link>
    99	                    </motion.div>
   100	
   101	                    <motion.div
   102	                        whileHover={{ y: -5 }}
   103	                        onMouseEnter={() => setActiveShape("heart")}
   104	                        onMouseLeave={() => setActiveShape(null)}
   105	                        onFocus={() => setActiveShape("heart")}
   106	                        onBlur={() => setActiveShape(null)}
   107	                        className="relative flex flex-col p-8 lg:p-10 rounded-3xl overflow-hidden backdrop-blur-md z-10
   108	                            border border-[`#1e5eff`]/30 dark:border-[`#ff2d55`]/30
   109	                            bg-gradient-to-b from-[`#1e5eff`]/5 to-white/10 dark:from-[`#ff2d55`]/10 dark:to-neutral-900/10"
   110	                    >
   111	                        <div className="absolute top-0 right-0 px-3 py-1 rounded-bl-xl uppercase tracking-widest text-[10px] font-bold text-white
   112	                            bg-[`#1e5eff`] dark:bg-[`#ff2d55`]"
   113	                        >
   114	                            {t("hero.badge")}
   115	                        </div>
   116	
   117	                        <div className="mb-6">
   118	                            <h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
   119	                                {t("hero.title")}
   120	                                <Heart size={18} className="fill-[`#1e5eff`] text-[`#1e5eff`] dark:fill-[`#ff2d55`] dark:text-[`#ff2d55`]" aria-label="Supporter" />
   121	                            </h3>
   122	                            <p className="text-sm text-neutral-500 dark:text-neutral-400">{t("hero.description")}</p>
   123	                        </div>
   124	                        <div className="mb-8">
   125	                            <span className="text-4xl lg:text-5xl font-black text-[`#1e5eff`] dark:text-[`#ff2d55`]">{t("hero.price")}</span>
   126	                            <span className="text-neutral-500 font-mono text-sm ml-2">{t("hero.period")}</span>
   127	                        </div>
   128	
   129	                        <ul className="space-y-4 mb-8 flex-1">
   130	                            {heroFeatures.map((item) => (
   131	                                <li key={item} className="flex items-center gap-3 text-sm text-gray-900 dark:text-white font-medium">
   132	                                    <div className="p-1 rounded-full bg-[`#1e5eff`]/20 text-[`#1e5eff`] dark:bg-[`#ff2d55`]/20 dark:text-[`#ff2d55`]" aria-hidden="true">
   133	                                        <Sparkles size={12} />
   134	                                    </div>
   135	                                    {item}
   136	                                </li>
   137	                            ))}
   138	                            <li className="flex items-center gap-3 text-sm text-gray-600 dark:text-neutral-400 italic">
   139	                                    <div className="p-1 rounded-full bg-gray-200 dark:bg-neutral-800 text-gray-500 dark:text-neutral-500" aria-hidden="true">
   140	                                        <Server size={12} />
   141	                                    </div>
   142	                                    {t("hero.drizzle")}
   143	                                </li>
   144	                        </ul>
   145	
   146	                        <Link
   147	                            href="https://github.com/sponsors/DevLoversTeam"
   148	                            target="_blank"
   149	                            rel="noopener noreferrer"
   150	                            className="group w-full py-4 rounded-xl text-white font-bold flex items-center justify-center gap-2 transition-all uppercase tracking-widest text-xs
   151	                                bg-[`#1e5eff`] hover:bg-[`#1e5eff`]/90 shadow-[0_0_20px_rgba(30,94,255,0.3)] hover:shadow-[0_0_30px_rgba(30,94,255,0.5)]
   152	                                dark:bg-[`#ff2d55`] dark:hover:bg-[`#ff2d55`]/90 dark:shadow-[0_0_20px_rgba(255,45,85,0.3)] dark:hover:shadow-[0_0_30px_rgba(255,45,85,0.5)]"
   153	                        >
   154	                            {t("hero.cta")} <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" aria-hidden="true" />
   155	                        </Link>
   156	                    </motion.div>
   157	
   158	                </div>
   159	
   160	                <p className="text-center text-gray-400 dark:text-neutral-600 text-[10px] mb-16 font-mono max-w-lg mx-auto leading-relaxed">
   161	                    {t("disclaimer")}
   162	                </p>
   163	
   164	                <SponsorsWall sponsors={sponsors} />
   165	
   166	            </div>
   167	        </section>
   168	    )
   169	}

Script executed:

# Find translation files for the "about.pricing" namespace
find . -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) | xargs rg "about\.pricing|pricing.*title" --max-count=5 | head -30

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0


Script executed:

# Search for translation files and directories
find . -type d -name "locales" -o -name "translations" -o -name "i18n" | head -10

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

./frontend/i18n

Script executed:

# Explore the i18n directory structure
find ./frontend/i18n -type f | head -20

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

./frontend/i18n/config.ts
./frontend/i18n/request.ts
./frontend/i18n/routing.ts

Script executed:

# Search for translation JSON files
find . -type f -name "*.json" | rg -i "(en|locale|translation|message)" | head -20

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

./frontend/checkout.json
./frontend/components.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0000_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0001_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0002_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0003_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0004_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0005_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0006_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0007_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0008_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0009_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0010_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0012_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0013_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0014_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/0015_snapshot.json
./frontend/db/legacy-migrations/drizzle_legacy/meta/_journal.json
./frontend/drizzle/meta/0000_snapshot.json
./frontend/drizzle/meta/0001_snapshot.json

Script executed:

# Search for files containing "about.pricing" or pricing translations
rg "about\\.pricing|pricing.*title|sr-only" --type json --type ts --type tsx | head -30

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stderr:

unrecognized file type: tsx

Script executed:

# Search for pricing translations in all JSON files
rg "pricing|title.*highlight" --type json | head -40

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

frontend/messages/uk.json:    "pricing": {
frontend/messages/pl.json:    "pricing": {
frontend/messages/en.json:    "pricing": {
frontend/messages/en.json:      "disclaimer": "*No developers were harmed in the making of this pricing table. Only caffeine levels were impacted."

Script executed:

# Read the en.json file to see the pricing translations
cat frontend/messages/en.json | jq '.about.pricing' 2>/dev/null || rg -A 50 '"pricing"' frontend/messages/en.json | head -60

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

{
  "badge": "No Hidden Fees",
  "title": "Invest in your brain,",
  "titleHighlight": "not our subscriptions.",
  "subtitle": "We believe knowledge should be accessible. So we don't sell courses. But servers heat up and coffee runs out. The choice is yours.",
  "junior": {
    "title": "Junior Engineer",
    "description": "For those who want an offer, not expenses.",
    "price": "$0",
    "period": "/ forever",
    "features": {
      "unlimited": "Unlimited Questions",
      "fullAccess": "Full Quiz Access",
      "noCard": "No Credit Card Required",
      "noGuilt": "0% Guilt Trip"
    },
    "noYacht": "Personal Yacht",
    "cta": "Start Learning"
  },
  "hero": {
    "badge": "High Impact",
    "title": "Open Source Hero",
    "description": "For those who already landed an offer thanks to us.",
    "price": "$$$",
    "period": "/ karma points",
    "features": {
      "servers": "Keep Servers Alive",
      "coffee": "Buy Coffee for Developers",
      "badge": "Profile Badge (Big Flex)",
      "feeling": "Warm Fuzzy Feeling"
    },
    "drizzle": "We actually pay for Drizzle",
    "cta": "Support the Project"
  },
  "disclaimer": "*No developers were harmed in the making of this pricing table. Only caffeine levels were impacted."
}

Script executed:

# Check other sections for sr-only heading patterns to see how they handle localization
rg "sr-only" frontend/components/ -B 2 -A 0 | head -20

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 0

stdout:

frontend/components/leaderboard/LeaderboardTable.tsx-        <div className="w-full">
frontend/components/leaderboard/LeaderboardTable.tsx-          <table className="w-full text-left border-separate border-spacing-0 table-fixed">
frontend/components/leaderboard/LeaderboardTable.tsx:            <caption className="sr-only">{t('tableCaption')}</caption>
--
frontend/components/about/PricingSection.tsx-                        whileInView={{ opacity: 1, y: 0 }}
frontend/components/about/PricingSection.tsx-                    >
frontend/components/about/PricingSection.tsx:                        <h2 id="pricing-heading" className="sr-only">Pricing</h2>

Script executed:

# Check if there's a dedicated heading key in other locales
rg "heading|Pricing" frontend/messages/ --type json | grep -i pricing

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 1


Script executed:

# Check if "Pricing" is translated in other locale files
rg "Pricing" frontend/messages/

Repository: DevLoversTeam/devlovers.net


Repository: DevLoversTeam/devlovers.net
Exit code: 1


Localize the sr-only pricing heading with a dedicated translation key.

The screen-reader-only heading is hard-coded in English. Instead of composing the marketing tagline, add a dedicated translation key (e.g., "heading") to the about.pricing namespace that provides a concise, accessible section label across all locales.

🤖 Prompt for AI Agents
In `@frontend/components/about/PricingSection.tsx` around lines 45 - 50, The
sr-only heading in PricingSection.tsx is hard-coded as English; update it to use
the i18n translator by replacing the literal "Pricing" in the <h2
id="pricing-heading" className="sr-only"> with t("about.pricing.heading") (or
the chosen key in the about.pricing namespace), and add that dedicated "heading"
key to your translation files so the screen-reader label is localized across
locales; ensure the element still uses id="pricing-heading" and
className="sr-only" unchanged for accessibility.

Comment on lines +48 to 53
<Link
href="https://github.com/sponsors/DevLoversTeam"
target="_blank"
rel="noopener noreferrer"
aria-label="Become a sponsor to claim your spot"
className="relative flex items-center justify-center w-10 h-10 rounded-full border-2 border-dashed border-gray-300 dark:border-white/20 hover:border-[#1e5eff] dark:hover:border-[#ff2d55] bg-transparent hover:bg-[#1e5eff]/5 dark:hover:bg-[#ff2d55]/5 transition-all group/cta"
Copy link
Copy Markdown
Contributor

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

🏁 Script executed:

# First, check if the file exists and read the relevant section
head -60 frontend/components/about/SponsorsWall.tsx | tail -20

Repository: DevLoversTeam/devlovers.net

Length of output: 1409


🏁 Script executed:

# List locale files to understand structure
find frontend/messages -type f -name "*.json" | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 148


🏁 Script executed:

# Check the structure of one locale file to understand the schema
head -100 frontend/messages/en.json

Repository: DevLoversTeam/devlovers.net

Length of output: 3160


🏁 Script executed:

# Search for "about" section in locale files
grep -l "about" frontend/messages/*.json

Repository: DevLoversTeam/devlovers.net

Length of output: 148


🏁 Script executed:

# Check if the key path exists in en.json
python3 - <<'PY'
import json
with open("frontend/messages/en.json", "r", encoding="utf-8") as f:
    data = json.load(f)

# Check if about section exists
if "about" in data:
    print("'about' section found")
    about = data["about"]
    if "sponsors" in about:
        print("'sponsors' subsection found")
        if "ctaAriaLabel" in about["sponsors"]:
            print(f"'ctaAriaLabel' exists: {about['sponsors']['ctaAriaLabel']}")
        else:
            print("'ctaAriaLabel' is MISSING")
    else:
        print("'sponsors' subsection is MISSING")
else:
    print("'about' section is MISSING")
    print("Top-level keys:", list(data.keys()))
PY

Repository: DevLoversTeam/devlovers.net

Length of output: 146


🏁 Script executed:

# Run the verification script from the review comment
python3 - <<'PY'
import json, glob
key_path = ("about", "sponsors", "ctaAriaLabel")
for path in sorted(glob.glob("frontend/messages/*.json")):
    data = json.load(open(path, "r", encoding="utf-8"))
    cur = data
    for k in key_path:
        cur = cur.get(k, None) if isinstance(cur, dict) else None
    print(f"{path}: {'OK' if cur else 'MISSING'}")
PY

Repository: DevLoversTeam/devlovers.net

Length of output: 175


🏁 Script executed:

# Check the imports at the top of SponsorsWall.tsx to verify it uses useTranslation or similar
head -20 frontend/components/about/SponsorsWall.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 690


Add the missing localization key for sponsor CTA aria-label.

The aria-label "Become a sponsor to claim your spot" is hard-coded in English, breaking accessibility strings for localized users. The component already uses useTranslations("about.sponsors"), so add the key to the "about.sponsors" section in all locale files (en.json, pl.json, uk.json) and update the aria-label to use aria-label={t("ctaAriaLabel")}.

The key is currently missing from all three locale files:

  • frontend/messages/en.json
  • frontend/messages/pl.json
  • frontend/messages/uk.json
🤖 Prompt for AI Agents
In `@frontend/components/about/SponsorsWall.tsx` around lines 48 - 53, The
hard-coded aria-label in SponsorsWall.tsx should use localization; update the
Link's aria-label from the literal string to aria-label={t("ctaAriaLabel")}
using the existing useTranslations("about.sponsors") call, and add the
ctaAriaLabel key to the "about.sponsors" section in all locale files
(frontend/messages/en.json, pl.json, uk.json) with appropriate translations so
t("ctaAriaLabel") resolves for each locale.

 - Use dynamic import for groq-sdk (Netlify compatibility)                                                    - Bypass rate limiting for unknown IPs (serverless safety)
  - Safe JSON parsing with request.text() + empty body check
  - Fix ReferenceError: remove undefined errorMessage variable
  - Remove sensitive debug info from client responses
  - Add i18n keys: pricing.heading, sponsors.ctaAriaLabel
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/app/api/ai/explain/route.ts`:
- Around line 83-110: The current checkRateLimit function bypasses limits for ip
=== 'unknown', allowing abuse; change it to treat unknown IPs as a shared bucket
instead of skipping (e.g., map 'unknown' to a constant key like UNKNOWN_BUCKET
and proceed through the normal rateLimiter logic using rateLimiter,
MAX_REQUESTS_PER_WINDOW and RATE_LIMIT_WINDOW_MS), remove the early return that
sets skipped: true, ensure cleanupRateLimiter() still runs, and return skipped:
false for the shared-bucket behavior so unknown clients are rate-limited
consistently with other IPs.
🧹 Nitpick comments (1)
frontend/app/api/ai/explain/route.ts (1)

11-49: Gate verbose diagnostics behind a debug flag.
The new per‑request logs can get noisy/costly in production; consider enabling them only in dev or via an opt‑in flag.

🧰 Suggested guard for diagnostics
-// =============================================================================
-// SERVER-SIDE LOGGING (sanitized - no sensitive data exposed)
-// =============================================================================
+// =============================================================================
+// SERVER-SIDE LOGGING (sanitized - no sensitive data exposed)
+// =============================================================================
+const DEBUG_AI_EXPLAIN =
+  process.env.NODE_ENV !== 'production' ||
+  process.env.AI_EXPLAIN_DEBUG === 'true';
 
 function logEnvironmentDiagnostics() {
+  if (!DEBUG_AI_EXPLAIN) return;
   const apiKey = process.env.GROQ_API_KEY;
   console.log('[ENV] GROQ_API_KEY configured:', !!apiKey);
   console.log('[ENV] NODE_ENV:', process.env.NODE_ENV);
 }
 
 function logRequestDiagnostics(request: NextRequest) {
+  if (!DEBUG_AI_EXPLAIN) return;
   console.log('[REQ] Method:', request.method);
   console.log('[REQ] URL path:', new URL(request.url).pathname);
 }
 
 function logBodyParsingResult(success: boolean, error?: unknown) {
+  if (!DEBUG_AI_EXPLAIN) return;
   console.log('[BODY] Parse success:', success);
   if (error) {
     console.log('[BODY] Parse error:', error instanceof Error ? error.message : 'Unknown error');
   }
 }
 
 function logGroqInitialization(success: boolean, error?: unknown) {
+  if (!DEBUG_AI_EXPLAIN) return;
   console.log('[GROQ] Init success:', success);
   if (error) {
     const err = error as Error & { status?: number; code?: string };
     console.log('[GROQ] Init error:', err.name, err.message);
   }
 }
 
 function logGroqApiCall(phase: 'start' | 'success' | 'error', details?: unknown) {
+  if (!DEBUG_AI_EXPLAIN) return;
   if (phase === 'start') {
     console.log('[GROQ] Starting API call');
   } else if (phase === 'success') {
     console.log('[GROQ] API call successful');
   } else if (phase === 'error') {
     const err = details as Error & { status?: number; code?: string };
     console.log('[GROQ] API error:', err?.name, err?.message);
   }
 }

Comment on lines +83 to 110
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}

cleanupRateLimiter();

const now = Date.now();
const entry = rateLimiter.get(ip);

if (!entry || now > entry.resetAt) {
rateLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS };
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS, skipped: false };
}

if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
const resetIn = entry.resetAt - now;
return { allowed: false, remaining: 0, resetIn };
return { allowed: false, remaining: 0, resetIn, skipped: false };
}

entry.count++;
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
resetIn: entry.resetAt - now,
skipped: false,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t bypass rate limits when IP is unknown.
Clients can omit/obfuscate forwarding headers and get unlimited access, which can drive cost and abuse. Use a shared “unknown” bucket instead of skipping limits.

🔧 Suggested fix (rate‑limit unknown IPs with a shared bucket)
-function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
-  // Bypass rate limiting for unknown IPs (serverless safety)
-  if (ip === 'unknown') {
-    return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
-  }
+function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
+  const isUnknown = ip === 'unknown';
+  const key = isUnknown ? 'unknown' : ip;
 
   cleanupRateLimiter();
 
   const now = Date.now();
-  const entry = rateLimiter.get(ip);
+  const entry = rateLimiter.get(key);
 
   if (!entry || now > entry.resetAt) {
-    rateLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
-    return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS, skipped: false };
+    rateLimiter.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
+    return {
+      allowed: true,
+      remaining: MAX_REQUESTS_PER_WINDOW - 1,
+      resetIn: RATE_LIMIT_WINDOW_MS,
+      skipped: isUnknown,
+    };
   }
 
   if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
     const resetIn = entry.resetAt - now;
-    return { allowed: false, remaining: 0, resetIn, skipped: false };
+    return { allowed: false, remaining: 0, resetIn, skipped: isUnknown };
   }
 
   entry.count++;
   return {
     allowed: true,
     remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
     resetIn: entry.resetAt - now,
-    skipped: false,
+    skipped: isUnknown,
   };
 }
📝 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
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}
cleanupRateLimiter();
const now = Date.now();
const entry = rateLimiter.get(ip);
if (!entry || now > entry.resetAt) {
rateLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS };
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS, skipped: false };
}
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
const resetIn = entry.resetAt - now;
return { allowed: false, remaining: 0, resetIn };
return { allowed: false, remaining: 0, resetIn, skipped: false };
}
entry.count++;
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
resetIn: entry.resetAt - now,
skipped: false,
};
function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
const isUnknown = ip === 'unknown';
const key = isUnknown ? 'unknown' : ip;
cleanupRateLimiter();
const now = Date.now();
const entry = rateLimiter.get(key);
if (!entry || now > entry.resetAt) {
rateLimiter.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - 1,
resetIn: RATE_LIMIT_WINDOW_MS,
skipped: isUnknown,
};
}
if (entry.count >= MAX_REQUESTS_PER_WINDOW) {
const resetIn = entry.resetAt - now;
return { allowed: false, remaining: 0, resetIn, skipped: isUnknown };
}
entry.count++;
return {
allowed: true,
remaining: MAX_REQUESTS_PER_WINDOW - entry.count,
resetIn: entry.resetAt - now,
skipped: isUnknown,
};
}
🤖 Prompt for AI Agents
In `@frontend/app/api/ai/explain/route.ts` around lines 83 - 110, The current
checkRateLimit function bypasses limits for ip === 'unknown', allowing abuse;
change it to treat unknown IPs as a shared bucket instead of skipping (e.g., map
'unknown' to a constant key like UNKNOWN_BUCKET and proceed through the normal
rateLimiter logic using rateLimiter, MAX_REQUESTS_PER_WINDOW and
RATE_LIMIT_WINDOW_MS), remove the early return that sets skipped: true, ensure
cleanupRateLimiter() still runs, and return skipped: false for the shared-bucket
behavior so unknown clients are rate-limited consistently with other IPs.

@ViktorSvertoka ViktorSvertoka merged commit fb9e917 into develop Jan 31, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the feat/translate-about branch January 31, 2026 09:01
@coderabbitai coderabbitai Bot mentioned this pull request Jan 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants