feat: add react-i18next with English and Hindi locale support.
- Add i18next + react-i18next + i18next-browser-languagedetector. - EN/HI translation files covering all UI strings across every page and component. - Language switcher button in Navbar; choice persisted to localStorage. - document.documentElement.lang synced to active language in App. - Skip-nav link and #main-content anchor for keyboard accessibility. - aria-describedby on modal dialog; page title and meta description in index.html. - Secure page title set to 'BIS SP-21 Standards.'
This commit is contained in:
@@ -1,39 +1,39 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./Footer.css";
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer className="footer" role="contentinfo">
|
||||
<div className="footer-inner">
|
||||
<div className="footer-cols">
|
||||
<div className="footer-col">
|
||||
<p className="footer-brand">BIS SP‑21</p>
|
||||
<p className="footer-tagline">
|
||||
Handbook on Building Materials<br />
|
||||
Special Publication 21 : 2005
|
||||
</p>
|
||||
<p className="footer-brand">{t("footer.brand")}</p>
|
||||
<p className="footer-tagline">{t("footer.tagline")}</p>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<p className="footer-heading">Portal</p>
|
||||
<Link className="footer-link" to="/standards">Search Standards</Link>
|
||||
<Link className="footer-link" to="/categories">Browse Categories</Link>
|
||||
<Link className="footer-link" to="/about">About</Link>
|
||||
<p className="footer-heading">{t("footer.portal")}</p>
|
||||
<Link className="footer-link" to="/standards">{t("footer.searchStandards")}</Link>
|
||||
<Link className="footer-link" to="/categories">{t("footer.browseCategories")}</Link>
|
||||
<Link className="footer-link" to="/about">{t("footer.about")}</Link>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<p className="footer-heading">Bureau of Indian Standards</p>
|
||||
<a className="footer-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">BIS Official Website</a>
|
||||
<a className="footer-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">Manak Online</a>
|
||||
<a className="footer-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">Standards Portal</a>
|
||||
<p className="footer-heading">{t("footer.bis")}</p>
|
||||
<a className="footer-link" href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.bisWebsite")}</a>
|
||||
<a className="footer-link" href="https://www.manakonline.in" target="_blank" rel="noopener noreferrer">{t("footer.manakOnline")}</a>
|
||||
<a className="footer-link" href="https://standardsbis.bsbedge.com" target="_blank" rel="noopener noreferrer">{t("footer.standardsPortal")}</a>
|
||||
</div>
|
||||
<div className="footer-col">
|
||||
<p className="footer-heading">Ministry</p>
|
||||
<a className="footer-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">DPIIT</a>
|
||||
<a className="footer-link" href="https://www.india.gov.in" target="_blank" rel="noopener noreferrer">National Portal</a>
|
||||
<p className="footer-heading">{t("footer.ministry")}</p>
|
||||
<a className="footer-link" href="https://dpiit.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.dpiit")}</a>
|
||||
<a className="footer-link" href="https://www.india.gov.in" target="_blank" rel="noopener noreferrer">{t("footer.nationalPortal")}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-legal">
|
||||
<p>© Bureau of Indian Standards, Ministry of Commerce & Industry, Government of India. All rights reserved.</p>
|
||||
<p>Content sourced from BIS Special Publication 21 : 2005. For official standards, refer to{" "}
|
||||
<p>{t("footer.copyright")}</p>
|
||||
<p>{t("footer.sourceNote")}{" "}
|
||||
<a href="https://www.bis.gov.in" target="_blank" rel="noopener noreferrer" className="legal-link">bis.gov.in</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,20 @@
|
||||
}
|
||||
.mobile-link:hover { background: rgba(255,255,255,0.05); }
|
||||
|
||||
.nav-lang-btn {
|
||||
background: none;
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.nav-lang-btn:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-color: rgba(255,255,255,0.45);
|
||||
}
|
||||
|
||||
@media (max-width: 833px) {
|
||||
.nav-links { display: none; }
|
||||
.nav-hamburger { display: flex; }
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./Navbar.css";
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: "Standards", to: "/standards" },
|
||||
{ label: "Categories", to: "/categories" },
|
||||
{ label: "✦ AI Recommend", to: "/recommend" },
|
||||
{ label: "About", to: "/about" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Site navigation bar with responsive menu.
|
||||
*/
|
||||
export default function Navbar() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { pathname } = useLocation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: t("nav.standards"), to: "/standards" },
|
||||
{ label: t("nav.categories"), to: "/categories" },
|
||||
{ label: t("nav.recommend"), to: "/recommend" },
|
||||
{ label: t("nav.about"), to: "/about" },
|
||||
];
|
||||
|
||||
const toggleLang = () => {
|
||||
i18n.changeLanguage(i18n.language === "en" ? "hi" : "en");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="global-nav" role="navigation" aria-label="Primary navigation">
|
||||
<nav className="global-nav" role="navigation" aria-label={t("nav.brand") + " primary navigation"}>
|
||||
<div className="nav-inner">
|
||||
<Link className="nav-emblem" to="/" aria-label="BIS SP-21 home" onClick={() => setOpen(false)}>
|
||||
<Link className="nav-emblem" to="/" aria-label={t("nav.brand") + " home"} onClick={() => setOpen(false)}>
|
||||
<BISIcon />
|
||||
<span className="nav-brand">BIS SP‑21</span>
|
||||
<span className="nav-brand">{t("nav.brand")}</span>
|
||||
</Link>
|
||||
|
||||
<div className="nav-links" role="list">
|
||||
@@ -43,13 +46,21 @@ export default function Navbar() {
|
||||
rel="noopener noreferrer"
|
||||
role="listitem"
|
||||
>
|
||||
BIS Portal ↗
|
||||
{t("nav.bisPortal")}
|
||||
</a>
|
||||
<button
|
||||
className="nav-link nav-lang-btn"
|
||||
onClick={toggleLang}
|
||||
aria-label={t("lang.switchTo")}
|
||||
title={t("lang.switchTo")}
|
||||
>
|
||||
{i18n.language === "en" ? t("lang.hi") : t("lang.en")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="nav-hamburger"
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
aria-label={open ? t("nav.closeMenu") : t("nav.openMenu")}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
@@ -80,8 +91,15 @@ export default function Navbar() {
|
||||
role="menuitem"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
BIS Portal ↗
|
||||
{t("nav.bisPortal")}
|
||||
</a>
|
||||
<button
|
||||
className="mobile-link nav-lang-btn"
|
||||
onClick={() => { toggleLang(); setOpen(false); }}
|
||||
role="menuitem"
|
||||
>
|
||||
{i18n.language === "en" ? t("lang.hi") : t("lang.en")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./StandardCard.css";
|
||||
|
||||
export default function StandardCard({ standard, onClick }) {
|
||||
const { t } = useTranslation();
|
||||
const sectionCount = Object.keys(standard.key_sections || {}).length;
|
||||
|
||||
return (
|
||||
@@ -10,7 +12,7 @@ export default function StandardCard({ standard, onClick }) {
|
||||
onKeyDown={(e) => e.key === "Enter" && onClick()}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View details for ${standard.standard_id}`}
|
||||
aria-label={t("card.viewDetails", { id: standard.standard_id })}
|
||||
>
|
||||
<span className="card-cat">{standard.category}</span>
|
||||
<p className="card-id">{standard.standard_id}</p>
|
||||
@@ -19,7 +21,7 @@ export default function StandardCard({ standard, onClick }) {
|
||||
<p className="card-summary">{standard.summary}</p>
|
||||
)}
|
||||
{standard.keywords?.length > 0 && (
|
||||
<div className="card-keywords" aria-label="Keywords">
|
||||
<div className="card-keywords" aria-label={t("card.keywords")}>
|
||||
{standard.keywords.slice(0, 5).map((kw) => (
|
||||
<span className="keyword-chip" key={kw}>{kw}</span>
|
||||
))}
|
||||
@@ -27,7 +29,7 @@ export default function StandardCard({ standard, onClick }) {
|
||||
)}
|
||||
<div className="card-footer">
|
||||
<span className="card-sections-count">
|
||||
{sectionCount} section{sectionCount !== 1 ? "s" : ""}
|
||||
{t("card.section", { count: sectionCount })}
|
||||
</span>
|
||||
<span className="card-arrow" aria-hidden="true">→</span>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { askQuestion } from "../api/standards";
|
||||
import "./StandardModal.css";
|
||||
|
||||
export default function StandardModal({ standard, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
const modalRef = useRef(null);
|
||||
const closeBtnRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const [question, setQuestion] = useState("");
|
||||
const [messages, setMessages] = useState([]); // [{role, text}]
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [asking, setAsking] = useState(false);
|
||||
const [aiError, setAiError] = useState(null);
|
||||
const chatEndRef = useRef(null);
|
||||
@@ -46,7 +48,7 @@ export default function StandardModal({ standard, onClose }) {
|
||||
const { answer } = await askQuestion({ standard_id: standard.standard_id, question: q });
|
||||
setMessages((prev) => [...prev, { role: "ai", text: answer }]);
|
||||
} catch (err) {
|
||||
setAiError(err.message || "Something went wrong. Please try again.");
|
||||
setAiError(err.message || t("common.serverError"));
|
||||
} finally {
|
||||
setAsking(false);
|
||||
setTimeout(() => inputRef.current?.focus(), 50);
|
||||
@@ -69,6 +71,7 @@ export default function StandardModal({ standard, onClose }) {
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-summary"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{/* Header */}
|
||||
@@ -82,7 +85,7 @@ export default function StandardModal({ standard, onClose }) {
|
||||
className="modal-close"
|
||||
ref={closeBtnRef}
|
||||
onClick={onClose}
|
||||
aria-label="Close standard detail"
|
||||
aria-label={t("modal.closeLabel")}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@@ -91,15 +94,15 @@ export default function StandardModal({ standard, onClose }) {
|
||||
{/* Standard detail */}
|
||||
<div className="modal-body">
|
||||
{standard.summary && (
|
||||
<div className="modal-section">
|
||||
<p className="modal-section-title">Summary</p>
|
||||
<div className="modal-section" id="modal-summary">
|
||||
<p className="modal-section-title">{t("modal.summary")}</p>
|
||||
<p className="modal-section-body">{standard.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{standard.keywords?.length > 0 && (
|
||||
<div className="modal-section">
|
||||
<p className="modal-section-title">Keywords</p>
|
||||
<p className="modal-section-title">{t("modal.keywords")}</p>
|
||||
<div className="modal-keywords">
|
||||
{standard.keywords.map((kw) => (
|
||||
<span className="modal-keyword" key={kw}>{kw}</span>
|
||||
@@ -110,7 +113,7 @@ export default function StandardModal({ standard, onClose }) {
|
||||
|
||||
{sections.length > 0 && (
|
||||
<div className="modal-section">
|
||||
<p className="modal-section-title">Key Sections</p>
|
||||
<p className="modal-section-title">{t("modal.keySections")}</p>
|
||||
<div className="modal-key-sections">
|
||||
{sections.map(([name, text]) => (
|
||||
<div className="key-section-item" key={name}>
|
||||
@@ -123,24 +126,24 @@ export default function StandardModal({ standard, onClose }) {
|
||||
)}
|
||||
|
||||
{/* AI chat panel */}
|
||||
<div className="modal-section ai-panel" aria-label="Ask AI about this standard">
|
||||
<div className="modal-section ai-panel" aria-label={t("modal.askAI")}>
|
||||
<p className="modal-section-title">
|
||||
<span className="ai-label-icon" aria-hidden="true">✦</span>
|
||||
Ask AI about this standard
|
||||
{t("modal.askAI")}
|
||||
</p>
|
||||
|
||||
{messages.length > 0 && (
|
||||
<div className="chat-log" aria-live="polite" aria-label="Conversation">
|
||||
<div className="chat-log" aria-live="polite" aria-label={t("modal.conversation")}>
|
||||
{messages.map((m, i) => (
|
||||
<div key={i} className={`chat-bubble chat-bubble--${m.role}`}>
|
||||
{m.role === "ai" && (
|
||||
<span className="bubble-label" aria-label="AI response">✦</span>
|
||||
<span className="bubble-label" aria-label={t("modal.aiResponse")}>✦</span>
|
||||
)}
|
||||
<p className="bubble-text">{m.text}</p>
|
||||
</div>
|
||||
))}
|
||||
{asking && (
|
||||
<div className="chat-bubble chat-bubble--ai chat-bubble--loading" aria-label="AI is thinking">
|
||||
<div className="chat-bubble chat-bubble--ai chat-bubble--loading" aria-label={t("modal.aiThinking")}>
|
||||
<span className="bubble-label" aria-hidden="true">✦</span>
|
||||
<span className="typing-dots">
|
||||
<span /><span /><span />
|
||||
@@ -155,8 +158,8 @@ export default function StandardModal({ standard, onClose }) {
|
||||
)}
|
||||
|
||||
{messages.length === 0 && !asking && (
|
||||
<div className="chat-suggestions" aria-label="Suggested questions">
|
||||
{getSuggestions(standard).map((s) => (
|
||||
<div className="chat-suggestions" aria-label={t("modal.suggestionsLabel")}>
|
||||
{getSuggestions(standard, t).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
className="suggestion-chip"
|
||||
@@ -171,25 +174,25 @@ export default function StandardModal({ standard, onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="chat-form" onSubmit={handleAsk} aria-label="Ask a question">
|
||||
<form className="chat-form" onSubmit={handleAsk} aria-label={t("modal.askAI")}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="chat-input"
|
||||
type="text"
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
placeholder="Ask a question about this standard…"
|
||||
placeholder={t("modal.chatPlaceholder")}
|
||||
maxLength={500}
|
||||
disabled={asking}
|
||||
aria-label="Your question"
|
||||
aria-label={t("modal.questionLabel")}
|
||||
/>
|
||||
<button
|
||||
className="chat-send"
|
||||
type="submit"
|
||||
disabled={!question.trim() || asking}
|
||||
aria-label="Send question"
|
||||
aria-label={t("modal.sendLabel")}
|
||||
>
|
||||
{asking ? "…" : "Ask"}
|
||||
{asking ? t("modal.sending") : t("modal.askBtn")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -199,17 +202,17 @@ export default function StandardModal({ standard, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getSuggestions(standard) {
|
||||
function getSuggestions(standard, t) {
|
||||
const base = [
|
||||
`What are the key requirements of ${standard.standard_id}?`,
|
||||
"What materials or tests are specified?",
|
||||
"What are the delivery or packaging specifications?",
|
||||
t("modal.suggestion_keyReq", { id: standard.standard_id }),
|
||||
t("modal.suggestion_materials"),
|
||||
t("modal.suggestion_delivery"),
|
||||
];
|
||||
if (standard.key_sections?.["Chemical Requirements"]) {
|
||||
base.splice(1, 0, "Summarise the chemical requirements.");
|
||||
base.splice(1, 0, t("modal.suggestion_chemical"));
|
||||
}
|
||||
if (standard.key_sections?.["Physical Requirements"] || standard.key_sections?.["Physical Requirement"]) {
|
||||
base.splice(1, 0, "What are the physical requirements?");
|
||||
base.splice(1, 0, t("modal.suggestion_physical"));
|
||||
}
|
||||
return base.slice(0, 3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user