+ {/* Language selector */}
+
+
+
- );
- }
- if (isError) {
- return (
-
- Failed to load FAQ. Please try again later.
-
- );
- }
+ {/* States */}
+ {isLoading && (
+
+ Loading help articles…
+
+ )}
- if (categories.length === 0) {
- return (
-
- No FAQ articles available.
-
- );
- }
+ {isError && (
+
+ Failed to load FAQ. Please try again later.
+
+ )}
- return (
-
- {categories.map((category) => (
-
- ))}
+ {!isLoading && !isError && categories.length === 0 && (
+
+ No FAQ articles available.
+
+ )}
+
+ {!isLoading && !isError && categories.length > 0 && (
+
+ {categories.map((category) => (
+
+ ))}
+
+ )}
);
}
\ No newline at end of file
diff --git a/contexts/LanguageContext.tsx b/contexts/LanguageContext.tsx
new file mode 100644
index 0000000..656d9a6
--- /dev/null
+++ b/contexts/LanguageContext.tsx
@@ -0,0 +1,40 @@
+'use client';
+import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
+
+export type Locale = 'en' | 'pcm' | 'ha' | 'yo' | 'ig';
+
+export interface LanguageContextValue {
+ locale: Locale;
+ setLocale: (locale: Locale) => void;
+}
+
+export const LANGUAGE_LABELS: Record
= {
+ en: 'English',
+ pcm: 'Pidgin',
+ ha: 'Hausa',
+ yo: 'Yoruba',
+ ig: 'Igbo',
+};
+
+export const LanguageContext = createContext({
+ locale: 'en',
+ setLocale: () => {},
+});
+
+export function LanguageProvider({ children }: { children: ReactNode }) {
+ const [locale, setLocaleState] = useState('en');
+
+ const setLocale = useCallback((next: Locale) => {
+ setLocaleState(next);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useLanguageContext() {
+ return useContext(LanguageContext);
+}
\ No newline at end of file
diff --git a/hooks/__tests__/useLanguage.test.ts b/hooks/__tests__/useLanguage.test.ts
new file mode 100644
index 0000000..aa060ec
--- /dev/null
+++ b/hooks/__tests__/useLanguage.test.ts
@@ -0,0 +1,67 @@
+import { renderHook, act } from '@testing-library/react';
+import { useLanguage } from '@/hooks/useLanguage';
+import { languageService } from '@/services/languageService';
+import { LanguageProvider } from '@/contexts/LanguageContext';
+import React from 'react';
+
+jest.mock('@/services/languageService', () => ({
+ languageService: {
+ getLanguage: jest.fn(),
+ saveLanguage: jest.fn(),
+ },
+}));
+
+const wrapper = ({ children }: { children: React.ReactNode }) =>
+ React.createElement(LanguageProvider, null, children);
+
+describe('useLanguage', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (languageService.getLanguage as jest.Mock).mockReturnValue('en');
+ });
+
+ it('returns default locale as en', () => {
+ const { result } = renderHook(() => useLanguage(), { wrapper });
+ expect(result.current.locale).toBe('en');
+ });
+
+ it('updates locale when setLocale is called', () => {
+ const { result } = renderHook(() => useLanguage(), { wrapper });
+
+ act(() => {
+ result.current.setLocale('yo');
+ });
+
+ expect(result.current.locale).toBe('yo');
+ });
+
+ it('persists locale via languageService when setLocale is called', () => {
+ const { result } = renderHook(() => useLanguage(), { wrapper });
+
+ act(() => {
+ result.current.setLocale('ha');
+ });
+
+ expect(languageService.saveLanguage).toHaveBeenCalledWith('ha');
+ });
+
+ it('restores saved locale on mount', () => {
+ (languageService.getLanguage as jest.Mock).mockReturnValue('pcm');
+
+ const { result } = renderHook(() => useLanguage(), { wrapper });
+
+ expect(result.current.locale).toBe('pcm');
+ });
+
+ it('supports all five locales', () => {
+ const { result } = renderHook(() => useLanguage(), { wrapper });
+ const locales = ['en', 'pcm', 'ha', 'yo', 'ig'] as const;
+
+ locales.forEach((l) => {
+ act(() => {
+ result.current.setLocale(l);
+ });
+ expect(result.current.locale).toBe(l);
+ });
+ });
+});
\ No newline at end of file
diff --git a/hooks/useFaq.ts b/hooks/useFaq.ts
index 7747ac8..1cd176e 100644
--- a/hooks/useFaq.ts
+++ b/hooks/useFaq.ts
@@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { faqService, type FaqResponse } from '@/services/faqService';
+import type { Locale } from '@/contexts/LanguageContext';
-export const FAQ_QUERY_KEY = ['faq'] as const;
+export const FAQ_QUERY_KEY = (locale: Locale) => ['faq', locale] as const;
export interface UseFaqReturn {
categories: FaqResponse;
@@ -12,12 +13,13 @@ export interface UseFaqReturn {
/**
* useFaq — fetches FAQ categories and items from the backend API.
- * Wraps faqService with TanStack Query for caching and loading states.
+ * Accepts a locale so the query key changes when language switches,
+ * triggering a fresh fetch for localised content.
*/
-export function useFaq(): UseFaqReturn {
+export function useFaq(locale: Locale = 'en'): UseFaqReturn {
const { data, isLoading, isError, error } = useQuery({
- queryKey: FAQ_QUERY_KEY,
- queryFn: faqService.getFaqs,
+ queryKey: FAQ_QUERY_KEY(locale),
+ queryFn: () => faqService.getFaqs(locale),
});
return {
diff --git a/hooks/useLanguage.ts b/hooks/useLanguage.ts
new file mode 100644
index 0000000..60bf3ec
--- /dev/null
+++ b/hooks/useLanguage.ts
@@ -0,0 +1,31 @@
+import { useCallback, useEffect } from 'react';
+import { useLanguageContext, type Locale } from '@/contexts/LanguageContext';
+import { languageService } from '@/services/languageService';
+import type { Locale as LocaleType } from '@/contexts/LanguageContext';
+
+export interface UseLanguageReturn {
+ locale: LocaleType;
+ setLocale: (locale: LocaleType) => void;
+}
+
+export function useLanguage(): UseLanguageReturn {
+ const { locale, setLocale } = useLanguageContext();
+
+ useEffect(() => {
+ const saved = languageService.getLanguage();
+ if (saved !== locale) {
+ setLocale(saved);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const handleSetLocale = useCallback((next: Locale) => {
+ languageService.saveLanguage(next);
+ setLocale(next);
+ }, [setLocale]);
+
+ return {
+ locale,
+ setLocale: handleSetLocale,
+ };
+}
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
new file mode 100644
index 0000000..714e044
--- /dev/null
+++ b/locales/en.json
@@ -0,0 +1,8 @@
+{
+ "languageLabel": "Language",
+ "loading": "Loading help articles…",
+ "error": "Failed to load FAQ. Please try again later.",
+ "empty": "No FAQ articles available.",
+ "pageTitle": "FAQ & Help Center",
+ "pageSubtitle": "Find answers to common questions about SwiftChain."
+}
\ No newline at end of file
diff --git a/locales/ha.json b/locales/ha.json
new file mode 100644
index 0000000..4d36fe8
--- /dev/null
+++ b/locales/ha.json
@@ -0,0 +1,8 @@
+{
+ "languageLabel": "Harshe",
+ "loading": "Ana loda kasidu na taimako…",
+ "error": "An kasa loda FAQ. Da fatan za a sake gwadawa daga baya.",
+ "empty": "Babu kasidu na FAQ.",
+ "pageTitle": "Cibiyar Taimako ta FAQ",
+ "pageSubtitle": "Sami amsa ga tambayoyi game da SwiftChain."
+}
\ No newline at end of file
diff --git a/locales/ig.json b/locales/ig.json
new file mode 100644
index 0000000..8cfa7b7
--- /dev/null
+++ b/locales/ig.json
@@ -0,0 +1,8 @@
+{
+ "languageLabel": "Asụsụ",
+ "loading": "A na-ebunye isiokwu enyemaka…",
+ "error": "Ọ dịghị ike ibunye FAQ. Biko nwalee ọzọ.",
+ "empty": "Enweghị isiokwu FAQ dị ebe a.",
+ "pageTitle": "Ebe Enyemaka FAQ",
+ "pageSubtitle": "Chọta azịza maka ajụjụ gbasara SwiftChain."
+}
\ No newline at end of file
diff --git a/locales/pcm.json b/locales/pcm.json
new file mode 100644
index 0000000..690f163
--- /dev/null
+++ b/locales/pcm.json
@@ -0,0 +1,8 @@
+{
+ "languageLabel": "Language",
+ "loading": "We dey load the help articles…",
+ "error": "E no work. Abeg try again later.",
+ "empty": "No FAQ articles dey here.",
+ "pageTitle": "FAQ & Help Center",
+ "pageSubtitle": "Find answer to questions wey you get about SwiftChain."
+}
\ No newline at end of file
diff --git a/locales/yo.json b/locales/yo.json
new file mode 100644
index 0000000..fd8823d
--- /dev/null
+++ b/locales/yo.json
@@ -0,0 +1,8 @@
+{
+ "languageLabel": "Ede",
+ "loading": "A n gbe awọn nkan iranlọwọ wọle…",
+ "error": "A kuna lati gbe FAQ wọle. Jọwọ gbiyanju lẹẹkansi.",
+ "empty": "Ko si awọn nkan FAQ ti o wa.",
+ "pageTitle": "Ile-iṣẹ Iranlọwọ FAQ",
+ "pageSubtitle": "Wa awọn idahun si awọn ibeere nipa SwiftChain."
+}
\ No newline at end of file
diff --git a/services/faqService.ts b/services/faqService.ts
index 22d37c3..54723fb 100644
--- a/services/faqService.ts
+++ b/services/faqService.ts
@@ -1,4 +1,5 @@
import axios from 'axios';
+import type { Locale } from '@/contexts/LanguageContext';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
@@ -17,9 +18,9 @@ export interface FaqCategory {
export type FaqResponse = FaqCategory[];
export const faqService = {
- async getFaqs(): Promise {
+ async getFaqs(locale: Locale = 'en'): Promise {
const { data } = await axios.get(
- `${API_BASE_URL}/api/faq`,
+ `${API_BASE_URL}/api/faq?lang=${locale}`,
);
return data;
},
diff --git a/services/languageService.ts b/services/languageService.ts
new file mode 100644
index 0000000..ff8bfe5
--- /dev/null
+++ b/services/languageService.ts
@@ -0,0 +1,15 @@
+import type { Locale } from '@/contexts/LanguageContext';
+
+const STORAGE_KEY = 'swiftchain-language';
+
+export const languageService = {
+ getLanguage(): Locale {
+ if (typeof window === 'undefined') return 'en';
+ return (localStorage.getItem(STORAGE_KEY) as Locale) ?? 'en';
+ },
+
+ saveLanguage(locale: Locale): void {
+ if (typeof window === 'undefined') return;
+ localStorage.setItem(STORAGE_KEY, locale);
+ },
+};
\ No newline at end of file