Skip to content
Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CodSpeed

on:
push:
branches:
- main
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

credential persistence through GitHub Actions artifacts [zizmor:zizmor/artipacked]


- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Run benchmarks
uses: CodSpeedHQ/action@v4

Check warning on line 32 in .github/workflows/codspeed.yml

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.github/workflows/codspeed.yml#L32

An action sourced from a third-party repository on GitHub is not pinned to a full length commit SHA. Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

unpinned action reference [zizmor:zizmor/unpinned-uses]

with:
mode: simulation
run: npx vitest bench --run
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
![GitHub Created At](https://img.shields.io/github/created-at/LCSOGthb/Games)
![GitHub last commit](https://img.shields.io/github/last-commit/LCSOGthb/Games)

CodSpeed
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/LCSOGthb/Games?utm_source=badge)

CodeRabbit
![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/LCSOGthb/Games?utm_source=oss&utm_medium=github&utm_campaign=LCSOGthb%2FGames&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)

Expand Down
18 changes: 2 additions & 16 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
"use client";

import { useState } from "react";

const translations = {
en: {
title: "Coming Soon",
subtitle: "Something amazing is on the way",
language: "English",
},
zh: {
title: "即将上线",
subtitle: "精彩内容即将呈现",
language: "中文",
},
};

type Language = keyof typeof translations;
import { getTranslation, type Language } from "@/lib/translations";

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Can't resolve '@/lib/translations' in '/src/app'

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

ES2015 modules are forbidden.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Expected "@/lib/translations" to come before "react".

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Expected 'multiple' syntax before 'single' syntax.

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Identifier 'Language' does not match the pattern '^[a-z]+([A-Z][a-z]+)*$'.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Imports should be sorted alphabetically.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Prefer using a top-level type-only import instead of inline type specifiers.

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Reaching to "@/lib/translations" is not allowed.

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Relative imports from parent directories are not allowed. Please either pass what you're importing through at runtime (dependency injection), move `page.tsx` to same directory as `@/lib/translations` or consider making `@/lib/translations` a package.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Replace `"@/lib/translations"` with `'@/lib/translations'`

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

There should be no space after '{'.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

There should be no space after '{'.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

There should be no space after '{'.

Check notice on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

There should be no space after '{'.

Check warning on line 4 in app/page.tsx

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app/page.tsx#L4

Type imports require valid Flow declaration.

export default function Home() {
const [lang, setLang] = useState<Language>("en");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Type Safety Concern:
The setLang function is called with e.target.value as Language, which relies on a type assertion. If the select input is ever extended or manipulated, this could allow invalid values to be set, potentially causing issues in translation lookup.

Recommendation:
Validate the selected value before updating the state, or restrict the select options to only valid Language values. For example:

const validLanguages: Language[] = ['en', 'zh'];
const selectedLang = e.target.value;
if (validLanguages.includes(selectedLang as Language)) {
  setLang(selectedLang as Language);
}

This ensures only valid languages are set.

const t = translations[lang];
const t = getTranslation(lang);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Potential Error Handling Issue:
The getTranslation(lang) function is called directly without any error handling or validation. If getTranslation can throw an error or return an undefined/null value (e.g., for an unsupported language), this could lead to runtime errors or broken UI.

Recommendation:
Add error handling or validation to ensure t is always a valid translation object. For example:

const t = getTranslation(lang) || { title: 'Coming Soon', subtitle: '' };

Or wrap in a try/catch if exceptions are possible.


return (
<>
Expand Down
91 changes: 91 additions & 0 deletions bench/games.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { bench, describe } from "vitest";

Check warning on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Can't resolve 'vitest' in '/src/bench'

Check warning on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Dependency vitest cannot be verified because no package.json file was found.

Check warning on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

ES2015 modules are forbidden.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Filename 'games.bench.ts' does not match the naming convention.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Filename games.bench.ts does not match the file name pattern ^([a-z][a-z0-9]*)(-[a-z0-9]+)*(.spec|.test)?.ts$

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Imports should be sorted.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Missing @file

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Replace `"vitest"` with `'vitest'`

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Run autofix to sort these imports!

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

There should be no space after '{'.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

There should be no space after '{'.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

There should be no space after '{'.

Check notice on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

There should be no space after '{'.

Check warning on line 1 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L1

Unable to resolve path to module 'vitest'.
import {

Check warning on line 2 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L2

ES2015 modules are forbidden.

Check notice on line 2 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L2

Import path mush be a path alias

Check notice on line 2 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L2

Missed spacing between "vitest" and "../lib/games" imports.

Check warning on line 2 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L2

Type imports require valid Flow declaration.
filterGamesByCategory,
sortGamesByTitle,

Check notice on line 4 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L4

Import members should be sorted alphabetically.
searchGames,

Check notice on line 5 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L5

Expected "searchGames" to come before "sortGamesByTitle".

Check notice on line 5 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L5

Member 'searchGames' of the import declaration should be sorted alphabetically.

Check notice on line 5 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L5

Member 'searchGames' of the import declaration should be sorted alphabetically.
getUniqueCategories,

Check notice on line 6 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L6

Expected "getUniqueCategories" to come before "searchGames".
paginateGames,
type Game,

Check notice on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Expected "Game" to come before "paginateGames".

Check warning on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Identifier 'Game' does not match the pattern '^[a-z]+([A-Z][a-z]+)*$'.

Check notice on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Prefer using a top-level type-only import instead of inline type specifiers.

Check notice on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Unexpected trailing comma.

Check notice on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Unexpected trailing comma.

Check notice on line 8 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L8

Unexpected trailing comma.
} from "../lib/games";

Check warning on line 9 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L9

Reaching to "../lib/games" is not allowed.

Check warning on line 9 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L9

Relative imports from parent directories are not allowed. Please either pass what you're importing through at runtime (dependency injection), move `games.bench.ts` to same directory as `../lib/games` or consider making `../lib/games` a package.

Check notice on line 9 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L9

Replace `"../lib/games"` with `'../lib/games'`

Check notice on line 9 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L9

require file extension '.js'.
import { getTranslation, getAvailableLanguages } from "../lib/translations";

Check warning on line 10 in bench/games.bench.ts

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

bench/games.bench.ts#L10

ES2015 modules are forbidden.

// --- Test data ---

const categories = ["Action", "Puzzle", "Strategy", "RPG", "Sports", "Racing"];

function generateGames(count: number): Game[] {
const games: Game[] = [];
for (let i = 0; i < count; i++) {
games.push({
id: i,
title: `Game ${String(i).padStart(4, "0")}`,
description: `An exciting ${categories[i % categories.length].toLowerCase()} game with unique mechanics and challenging levels`,
image: `/images/game-${i}.jpg`,
category: categories[i % categories.length],
});
}
return games;
}

const smallCatalog = generateGames(50);
const largeCatalog = generateGames(1000);

// --- Benchmarks ---

describe("translations", () => {
bench("getTranslation - existing language", () => {
getTranslation("en");
});

bench("getTranslation - fallback to default", () => {
getTranslation("fr");
});

bench("getAvailableLanguages", () => {
getAvailableLanguages();
});
});

describe("game catalog - small (50 games)", () => {
bench("filterGamesByCategory", () => {
filterGamesByCategory(smallCatalog, "Action");
});

bench("sortGamesByTitle", () => {
sortGamesByTitle(smallCatalog);
});

bench("searchGames", () => {
searchGames(smallCatalog, "unique mechanics");
});

bench("getUniqueCategories", () => {
getUniqueCategories(smallCatalog);
});

bench("paginateGames", () => {
paginateGames(smallCatalog, 2, 10);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 21 lines of similar code in 2 locations (mass = 85) [qlty:similar-code]

});

describe("game catalog - large (1000 games)", () => {
bench("filterGamesByCategory", () => {
filterGamesByCategory(largeCatalog, "Strategy");
});

bench("sortGamesByTitle", () => {
sortGamesByTitle(largeCatalog);
});

bench("searchGames", () => {
searchGames(largeCatalog, "unique mechanics");
});

bench("getUniqueCategories", () => {
getUniqueCategories(largeCatalog);
});

bench("paginateGames", () => {
paginateGames(largeCatalog, 5, 20);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found 21 lines of similar code in 2 locations (mass = 85) [qlty:similar-code]

});
Comment on lines +35 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The benchmarks currently do not validate the correctness of the results returned by the functions under test. This means that if a function's implementation changes and produces incorrect results, the benchmarks will not detect it. Consider adding assertions to verify that the output is as expected, even in performance tests. For example:

bench("filterGamesByCategory", () => {
  const result = filterGamesByCategory(smallCatalog, "Action");
  if (!Array.isArray(result) || result.some(g => g.category !== "Action")) {
    throw new Error("filterGamesByCategory returned incorrect results");
  }
});

This ensures that performance measurements are only taken for correct implementations.

37 changes: 37 additions & 0 deletions lib/games.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export interface Game {
id: number;
title: string;
description: string;
image: string;
category: string;
}
Comment on lines +1 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

The Game interface in lib/games.ts is structurally identical to the inline type defined in GameCardProps within components/GameCard.tsx (see related_context). This duplication introduces a maintenance burden: any future addition or modification to the game shape (e.g., adding a releaseDate field) must be replicated in both places, increasing the risk of inconsistencies. The presence of the explicit type definition in lib/games.ts makes it the natural single source of truth. Refactoring GameCard.tsx to import Game from lib/games.ts would eliminate duplication and improve long-term maintainability.

Code Suggestion:

// components/GameCard.tsx
import type { Game } from "@/lib/games";

interface GameCardProps {
  game: Game;
}

Evidence: path:components/GameCard.tsx


export function filterGamesByCategory(games: Game[], category: string): Game[] {
return games.filter((game) => game.category === category);
}

export function sortGamesByTitle(games: Game[]): Game[] {
return [...games].sort((a, b) => a.title.localeCompare(b.title));
}

export function searchGames(games: Game[], query: string): Game[] {
const lowerQuery = query.toLowerCase();
return games.filter(
(game) =>
game.title.toLowerCase().includes(lowerQuery) ||
game.description.toLowerCase().includes(lowerQuery),
);
}
Comment on lines +17 to +24

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.

🚩 searchGames returns all games when query is empty string

searchGames at lib/games.ts:17-24 calls query.toLowerCase() and checks includes(lowerQuery). When query is "", "".toLowerCase() is "", and String.prototype.includes("") always returns true, so every game matches. This may or may not be the intended behavior — it's fine if callers treat an empty query as "show all", but could be surprising if callers expect an empty query to return no results. Worth documenting the contract.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


export function getUniqueCategories(games: Game[]): string[] {
return [...new Set(games.map((game) => game.category))].sort();
}

export function paginateGames(
games: Game[],
page: number,
pageSize: number,
): Game[] {
const start = (page - 1) * pageSize;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Quality - The "paginateGames" function fails to validate "page" and "pageSize", causing incorrect array slicing, such as negative start indexes when "page=0". This misaligns the control flow from its intended pagination logic. View in Corgea ↗

More Details
🎟️Issue Explanation: The "paginateGames" function fails to validate "page" and "pageSize", causing incorrect array slicing, such as negative start indexes when "page=0". This misaligns the control flow from its intended pagination logic.

- Negative or zero "page" causes "start" to be negative, making "slice" return unexpected results by counting from the array's end.
- Passing non-integer or negative "pageSize" leads to inconsistent slicing behavior, breaking assumptions in pagination.
- This flaw increases debugging complexity and reduces reliability, as pagination outputs don't align with expected page boundaries.

🪄Fix Explanation: Added input validation and normalization for pagination parameters to prevent unexpected behavior and ensure the function only processes valid positive integers.
Input validation with "Number.isFinite" ensures that non-numeric or infinite values immediately return an empty array, preventing runtime errors.
Flooring page and pageSize via "Math.floor" normalizes fractional inputs, improving consistency and expected pagination behavior.
Checks for positive values ("safePage < 1 || safePageSize < 1") guard against invalid pagination requests like zero or negative numbers.
Calculating "start" with validated and normalized inputs guarantees the slice range is always valid, improving robustness.

💡Important Instructions: Audit other pagination or indexing functions to apply similar input validation patterns for consistent robustness across the codebase.
Suggested change
const start = (page - 1) * pageSize;
// Validate inputs: require finite positive integers; return empty array if invalid
if (!Number.isFinite(page) || !Number.isFinite(pageSize)) {
return [];
}
const safePage = Math.floor(page);
const safePageSize = Math.floor(pageSize);
if (safePage < 1 || safePageSize < 1) {
return [];
}
const start = (safePage - 1) * safePageSize;
return games.slice(start, start + safePageSize);

return games.slice(start, start + pageSize);
}
Comment on lines +30 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lack of input validation in paginateGames

The paginateGames function does not validate the page and pageSize parameters. If page is less than 1 or pageSize is less than 1, the function may return an empty array or unexpected results, which could lead to bugs or inconsistent behavior in the application.

Recommended solution:
Add input validation to ensure page and pageSize are positive integers:

if (page < 1 || pageSize < 1) {
  throw new Error('Page and pageSize must be positive integers');
}

Comment on lines +30 to +37

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.

📝 Info: paginateGames uses 1-based page indexing without validation

paginateGames at lib/games.ts:30-37 computes start = (page - 1) * pageSize, which assumes 1-based page numbers. If page is 0 or negative, start becomes negative, but Array.prototype.slice with a negative start counts from the end of the array, returning unexpected results. This isn't necessarily a bug (callers are expected to pass valid page numbers), but the contract is implicit and could bite future callers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

25 changes: 25 additions & 0 deletions lib/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const translations: Record<
string,
Comment on lines +1 to +2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: The explicit Record<string, ...> annotation widens Language to string and loses the concrete language union.

Because translations is explicitly typed as Record<string, { ... }>, keyof typeof translations is just string, so Language is widened to string instead of the intended union of concrete language codes. This removes useful type checking and allows any string to be passed where a known language is expected.

You can keep the literal key union by relying on inference and as const satisfies, for example:

export const translations = {
  en: { ... },
  zh: { ... },
} as const satisfies Record<string, { title: string; subtitle: string; language: string }>;

export type Language = keyof typeof translations; // 'en' | 'zh'

Then getAvailableLanguages can return Language[] instead of string[] for stronger type safety.

{ title: string; subtitle: string; language: string }
> = {
en: {
title: "Coming Soon",
subtitle: "Something amazing is on the way",
language: "English",
},
zh: {
title: "即将上线",
subtitle: "精彩内容即将呈现",
language: "中文",
},
};
Comment on lines +1 to +15

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.

📝 Info: translations object is mutable and could be modified at runtime

The translations object at lib/translations.ts:1-15 is exported as a mutable Record. Any consumer can do translations["en"].title = "Hacked" and affect all subsequent calls to getTranslation. Using as const satisfies Record<...> (as suggested in the bug fix) or making the export readonly would prevent accidental mutation. Low risk in a small codebase but worth noting for defensive design.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


export type Language = keyof typeof translations;
Comment on lines +1 to +17

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.

🟡 Exported Language type resolves to string due to explicit Record<string, ...> annotation

The translations object is annotated as Record<string, { ... }> at lib/translations.ts:1-3, which causes export type Language = keyof typeof translations (line 17) to resolve to string instead of "en" | "zh". In the original code in app/page.tsx, the translations object had no explicit type annotation, so Language was correctly inferred as "en" | "zh". Now, useState<Language>("en") at app/page.tsx:7 is effectively useState<string>, and the cast as Language at app/page.tsx:28 is a no-op. This silently removes compile-time safety against passing invalid language keys.

Suggested change
export const translations: Record<
string,
{ title: string; subtitle: string; language: string }
> = {
en: {
title: "Coming Soon",
subtitle: "Something amazing is on the way",
language: "English",
},
zh: {
title: "即将上线",
subtitle: "精彩内容即将呈现",
language: "中文",
},
};
export type Language = keyof typeof translations;
const translations = {
en: {
title: "Coming Soon",
subtitle: "Something amazing is on the way",
language: "English",
},
zh: {
title: "即将上线",
subtitle: "精彩内容即将呈现",
language: "中文",
},
} as const satisfies Record<
string,
{ title: string; subtitle: string; language: string }
>;
export type Language = keyof typeof translations;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


export function getTranslation(lang: string) {
return translations[lang] ?? translations["en"];
}
Comment on lines +19 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Logic & Type Safety:
The getTranslation function accepts any string as input, but only known language keys are valid. This can lead to unexpected behavior if an invalid language code is passed. Consider restricting the input type to Language or validating the input before accessing the translations object:

export function getTranslation(lang: Language) {
  return translations[lang];
}

Or, if you must accept any string, validate it:

export function getTranslation(lang: string) {
  if (lang in translations) {
    return translations[lang];
  }
  return translations["en"];
}

Comment on lines +17 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: The getTranslation signature could better reflect supported languages and its return shape.

Since you already expose a Language type and the translations shape is fixed, you can make the function more type-safe and ergonomic by tightening the signature, e.g.:

type Translation = (typeof translations)[Language];

export function getTranslation(lang: Language | string): Translation {
  return translations[lang as Language] ?? translations.en;
}

This keeps Language-typed callers fully checked while still accepting arbitrary strings (e.g. from URL params) and always returning a valid translation object.

Suggested change
export type Language = keyof typeof translations;
export function getTranslation(lang: string) {
return translations[lang] ?? translations["en"];
}
export type Language = keyof typeof translations;
export type Translation = (typeof translations)[Language];
export function getTranslation(lang: Language | string): Translation {
return translations[lang as Language] ?? translations.en;
}

Comment on lines +19 to +21

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.

📝 Info: getTranslation fallback is safe at runtime despite type widening

Despite the Language type being widened to string (reported as a bug), the getTranslation function at lib/translations.ts:19-21 uses ?? to fall back to translations["en"] for any unrecognized key. Since the select element in app/page.tsx:31-32 only offers "en" and "zh" as values, and the fallback handles any hypothetical invalid value, there is no runtime crash risk from this type regression — only a loss of compile-time safety.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +19 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 | Confidence: High

The getTranslation function accepts lang: string, which discards the compile-time safety provided by the Language type (keyof typeof translations). While the only current caller (app/page.tsx) narrows its argument using the Language type, custom type assertion e.target.value as Language cannot guarantee correctness if a new language code is added to translations but the <option> elements are not updated. Accepting Language directly in the function signature would catch such mismatches at compile time and make the API more self-documenting. This is a minor degradation in type safety compared to the previous inline code, where translations[lang] was only indexed with a Language-typed variable.

Suggested change
export function getTranslation(lang: string) {
return translations[lang] ?? translations["en"];
}
export function getTranslation(lang: Language) {
return translations[lang] ?? translations["en"];
}


export function getAvailableLanguages(): string[] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Returning string[] for available languages is looser than necessary and can be narrowed to the Language union.

If Language is the literal union of translation keys, this can return Language[]:

export function getAvailableLanguages(): Language[] {
  return Object.keys(translations) as Language[];
}

This tightens the type so consumers can’t pass arbitrary strings where only supported languages are allowed.

return Object.keys(translations);
}
Comment on lines +23 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Type Consistency:
The getAvailableLanguages function returns string[], but the actual set of keys is defined by the Language type. For improved type safety and consistency, update the return type to Language[]:

export function getAvailableLanguages(): Language[] {
  return Object.keys(translations) as Language[];
}

Loading
Loading