-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: add Simplified Chinese (zh-CN) language support #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d54232c
269fcc0
f74c527
ec29398
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| registry=https://registry.npmjs.org/ |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTransition } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRouter, usePathname } from 'next/navigation'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Globe } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Select, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SelectContent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SelectItem, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SelectTrigger, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SelectValue, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@/components/ui/select'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useLocale } from '@/hooks/useLocale'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function LanguageSwitcher() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = useRouter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pathname = usePathname(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const locale = useLocale(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isPending, startTransition] = useTransition(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleLocaleChange = (newLocale: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| startTransition(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const segments = pathname.split('/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove existing locale segment if present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (segments[1] === 'en' || segments[1] === 'zh-CN') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| segments[1] = newLocale; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| segments.splice(1, 0, newLocale); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router.push(segments.join('/')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift Refactor to use This implementation has several maintainability concerns:
♻️ Recommended refactoring approachImport +import { useContext } from 'react';
+import { LocaleContext } from '@/components/LocaleProvider';
+import { supportedLocales } from '@/i18n';Then simplify the handler to delegate locale persistence and path logic: +const { setLocale } = useContext(LocaleContext);
+
const handleLocaleChange = (newLocale: string) => {
startTransition(() => {
+ setLocale(newLocale);
const segments = pathname.split('/');
- // Remove existing locale segment if present
- if (segments[1] === 'en' || segments[1] === 'zh-CN') {
+ if (supportedLocales.includes(segments[1] as any)) {
segments[1] = newLocale;
} else {
segments.splice(1, 0, newLocale);
}
router.push(segments.join('/'));
- document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
});
};This ensures a single source of truth for supported locales and consistent cookie handling. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Select value={locale} onValueChange={handleLocaleChange} disabled={isPending}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectTrigger className="w-[130px] h-9 bg-gray-700 border-gray-600 text-gray-300 hover:bg-gray-600 hover:text-white"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Globe className="h-4 w-4 mr-2" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectValue placeholder="Language" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SelectTrigger> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectContent className="bg-gray-800 border-gray-700 text-gray-200"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectItem value="en" className="hover:bg-gray-700 focus:bg-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| English | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SelectItem> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SelectItem value="zh-CN" className="hover:bg-gray-700 focus:bg-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 简体中文 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SelectItem> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SelectContent> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Select> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Render locale options dynamically from i18n configuration. The ♻️ Proposed fix to render options dynamicallyImport the i18n configuration: +import { supportedLocales, localeNames } from '@/i18n';Replace the hardcoded SelectItem list with a mapped array: <SelectContent className="bg-gray-800 border-gray-700 text-gray-200">
- <SelectItem value="en" className="hover:bg-gray-700 focus:bg-gray-700">
- English
- </SelectItem>
- <SelectItem value="zh-CN" className="hover:bg-gray-700 focus:bg-gray-700">
- 简体中文
- </SelectItem>
+ {supportedLocales.map((loc) => (
+ <SelectItem key={loc} value={loc} className="hover:bg-gray-700 focus:bg-gray-700">
+ {localeNames[loc]}
+ </SelectItem>
+ ))}
</SelectContent>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||
| 'use client'; | ||||||
|
|
||||||
| import React, { createContext, useEffect, useState } from 'react'; | ||||||
| import { type Locale, getDictionary } from '@/i18n'; | ||||||
| import type { Dictionary } from '@/i18n/en'; | ||||||
|
|
||||||
| interface LocaleContextType { | ||||||
| locale: Locale; | ||||||
| dictionary: Dictionary; | ||||||
| setLocale: (locale: Locale) => void; | ||||||
| } | ||||||
|
|
||||||
| export const LocaleContext = createContext<LocaleContextType>({ | ||||||
| locale: 'en', | ||||||
| dictionary: getDictionary('en'), | ||||||
| setLocale: () => {}, | ||||||
| }); | ||||||
|
|
||||||
| interface LocaleProviderProps { | ||||||
| children: React.ReactNode; | ||||||
| locale: Locale; | ||||||
| dictionary: Dictionary; | ||||||
| } | ||||||
|
|
||||||
| export function LocaleProvider({ children, locale: initialLocale, dictionary }: LocaleProviderProps) { | ||||||
| const [locale, setLocale] = useState<Locale>(initialLocale); | ||||||
| const [currentDictionary, setCurrentDictionary] = useState<Dictionary>(dictionary); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| setCurrentDictionary(getDictionary(locale)); | ||||||
| }, [locale]); | ||||||
|
Comment on lines
+25
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sync provider state when locale props change after mount.
Proposed fix export function LocaleProvider({ children, locale: initialLocale, dictionary }: LocaleProviderProps) {
const [locale, setLocale] = useState<Locale>(initialLocale);
const [currentDictionary, setCurrentDictionary] = useState<Dictionary>(dictionary);
+ useEffect(() => {
+ setLocale(initialLocale);
+ setCurrentDictionary(dictionary);
+ }, [initialLocale, dictionary]);
+
useEffect(() => {
setCurrentDictionary(getDictionary(locale));
}, [locale]);🤖 Prompt for AI Agents |
||||||
|
|
||||||
| const changeLocale = (newLocale: Locale) => { | ||||||
| setLocale(newLocale); | ||||||
| document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add The 🔒 Proposed fix to add SameSite attribute- document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
+ document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000;SameSite=Lax`;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <LocaleContext.Provider value={{ locale, dictionary: currentDictionary, setLocale: changeLocale }}> | ||||||
| {children} | ||||||
| </LocaleContext.Provider> | ||||||
| ); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ import mongoose from "mongoose"; | |
| const MONGODB_URI = process.env.MONGODB_URI; | ||
|
|
||
| // FIX: Set Google DNS and force IPv4 to avoid querySrv ECONNREFUSED | ||
| import dns from 'dns'; | ||
| import * as dns from 'dns'; | ||
| try { | ||
| // This is often more effective than setServers for Node 17+ | ||
| if (dns.setDefaultResultOrder) { | ||
|
|
@@ -30,7 +30,8 @@ if (!cached) { | |
|
|
||
| export const connectToDatabase = async () => { | ||
| if (!MONGODB_URI) { | ||
| throw new Error("MongoDB URI is missing"); | ||
| console.warn("MongoDB URI is missing — skipping database connection"); | ||
| return null; | ||
| } | ||
|
Comment on lines
31
to
35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find all consumers of connectToDatabase to verify they handle null
# Search for imports and usage of connectToDatabase
rg -n "connectToDatabase" --type ts --type tsx -C 3Repository: Open-Dev-Society/OpenStock Length of output: 98 🏁 Script executed: # First, let's examine the mongoose.ts file to see the actual function signature and implementation
cat -n database/mongoose.tsRepository: Open-Dev-Society/OpenStock Length of output: 1786 🏁 Script executed: # Search for connectToDatabase usage with corrected file type (remove tsx)
rg -n "connectToDatabase" --type ts -C 3Repository: Open-Dev-Society/OpenStock Length of output: 11379 Add explicit return type and fix consumers to handle null return value. The function signature lacks an explicit return type but can return Update the function signature: Return type fix-export const connectToDatabase = async () => {
+export const connectToDatabase = async (): Promise<typeof mongoose | null> => {Critical: Multiple consumers do not check for null before using the return value:
Only 🤖 Prompt for AI Agents |
||
|
|
||
| if (cached.conn) return cached.conn; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| 'use client'; | ||
|
|
||
| import { useContext } from 'react'; | ||
| import { LocaleContext } from '@/components/LocaleProvider'; | ||
| import type { Dictionary } from '@/i18n/en'; | ||
|
|
||
| export function useDictionary(): Dictionary { | ||
| const { dictionary } = useContext(LocaleContext); | ||
| return dictionary; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| 'use client'; | ||
|
|
||
| import { useContext } from 'react'; | ||
| import { LocaleContext } from '@/components/LocaleProvider'; | ||
| import type { Locale } from '@/i18n'; | ||
|
|
||
| export function useLocale(): Locale { | ||
| const { locale } = useContext(LocaleContext); | ||
| return locale; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Eliminate duplication: import locale constants from i18n module.
The
localesarray anddefaultLocaleare duplicated fromi18n/index.ts. This violates DRY and creates a maintenance risk where the two definitions could diverge.♻️ Proposed refactor to use centralized constants
Or better yet, if you export
localesfromi18n/index.ts:However, the cleanest approach is to export a
localesarray fromi18n/index.ts:In
i18n/index.ts:Then in
layout.tsx:🤖 Prompt for AI Agents