Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
9 changes: 8 additions & 1 deletion components/bill/BillTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Stage, useBillTracker } from "components/db/useBillStatus"
import styled from "styled-components"
import { BillProps, BillTracker } from "./types"
import { Row } from "react-bootstrap"
import { useTranslation } from "next-i18next"

export default function BillTrackerConnectedView({
bill,
Expand Down Expand Up @@ -31,7 +32,13 @@ export const BillTrackerView = ({
))}
</Row>
)
return <MapleCard className={className} header="Bill Tracker" body={body} />
return (
<MapleCard
className={className}
header={useTranslation("common").t("bill.bill_tracker")}
body={body}
/>
)
}

export const BillStageStrip = ({
Expand Down
11 changes: 6 additions & 5 deletions components/bill/Cosponsors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MemberReference, useMember } from "../db"
import { memberLink } from "../links"
import { FC } from "../types"
import { BillProps } from "./types"
import { useTranslation } from "next-i18next"

const CoSponsorRow = ({
court,
Expand All @@ -13,9 +14,6 @@ const CoSponsorRow = ({
court: number
coSponsor: MemberReference
}) => {
const url = coSponsor
? `https://malegislature.gov/Legislators/Profile/${coSponsor.Id}`
: ""
const { member, loading } = useMember(court, coSponsor.Id)
if (loading) {
return null
Expand Down Expand Up @@ -66,7 +64,6 @@ export const Cosponsors: FC<React.PropsWithChildren<BillProps>> = ({
bill,
children
}) => {
const billNumber = bill.id
const court = bill.court
const coSponsors = bill.content.Cosponsors
const numCoSponsors = coSponsors ? coSponsors.length : 0
Expand All @@ -93,7 +90,11 @@ export const Cosponsors: FC<React.PropsWithChildren<BillProps>> = ({
size="lg"
>
<Modal.Header closeButton onClick={handleCloseBillCosponsors}>
<Modal.Title>{billNumber + " CoSponsors"}</Modal.Title>
<Modal.Title>
{useTranslation("common").t("bill.bill_cosponsors", {
billId: bill.id
})}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<>
Expand Down
6 changes: 4 additions & 2 deletions components/bill/HistoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import styled from "styled-components"
import { Button, Modal } from "../bootstrap"
import { HistoryTable } from "./HistoryTable"
import { BillProps } from "./types"
import { useTranslation } from "next-i18next"

export const HistoryModal = ({ bill }: BillProps) => {
const [showBillHistory, setShowBillHistory] = useState(false)
const handleShowBillHistory = () => setShowBillHistory(true)
const handleCloseBillHistory = () => setShowBillHistory(false)
const { t } = useTranslation("common")

return (
<>
<Button variant="primary" className="m-1" onClick={handleShowBillHistory}>
History
{t("bill.history")}
</Button>
<Modal show={showBillHistory} onHide={handleCloseBillHistory} size="lg">
<Modal.Header closeButton onClick={handleCloseBillHistory}>
<StyledModalTitle>Status & History</StyledModalTitle>
<StyledModalTitle>{t("bill.status_and_history")}</StyledModalTitle>
</Modal.Header>
<StyledBillTitle>
{bill.id + " - " + bill.content.Title}
Expand Down
6 changes: 4 additions & 2 deletions components/bill/HistoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useContext } from "react"
import styled from "styled-components"
import { BillHistory } from "../db"
import { CourtContext } from "./Status"
import { useTranslation } from "next-i18next"

export type HistoryProps = { billHistory: BillHistory }

Expand Down Expand Up @@ -50,14 +51,15 @@ const BillHistoryActionRows = ({ billHistory }: HistoryProps) => {
}

export const HistoryTable = ({ billHistory }: HistoryProps) => {
const { t } = useTranslation("common")
return (
<div className="text-center">
<StyledTable>
<thead>
<tr>
<th></th>
<th>Status History</th>
<th>Branch</th>
<th>{t("bill.status_history")}</th>
<th>{t("bill.branch")}</th>
</tr>
</thead>
<tbody>
Expand Down
26 changes: 14 additions & 12 deletions components/bill/LobbyingTable.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useTranslation } from "components/i18n"
import { Table } from "react-bootstrap"
import { Card, Container } from "../bootstrap"
import { Card as MapleCard } from "../Card"
import { FC } from "../types"
import { BillProps } from "./types"
import { Table } from "react-bootstrap"
import { Card as MapleCard } from "../Card"

export const LobbyingTable: FC<React.PropsWithChildren<BillProps>> = ({
bill,
className
}) => {
const { t, tDate } = useTranslation("common")
const current = bill.currentCommittee
if (!current) return null
return (
Expand All @@ -16,29 +18,29 @@ export const LobbyingTable: FC<React.PropsWithChildren<BillProps>> = ({
className={`${className} bg-white`}
headerElement={
<Card.Header className="h4 bg-secondary text-light">
Lobbying Parties
{t("bill.lobbying_parties")}
</Card.Header>
}
body={
<Card.Body>
<Table>
<thead>
<tr>
<th>Client Name</th>
<th>Position</th>
<th>Disclosure Date</th>
<th>{t("bill.client_name")}</th>
<th>{t("bill.position")}</th>
<th>{t("bill.disclosure_date")}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Example Name</td>
<td>Pro</td>
<td>April 10, 2023</td>
<td>{t("bill.example_name")}</td>
<td>{t("bill.pro")}</td>
<td>{tDate("2023-03-29", "PP")}</td>
</tr>
<tr>
<td>Example Name</td>
<td>Neutral</td>
<td>March 29, 2023</td>
<td>{t("bill.example_name")}</td>
<td>{t("bill.neutral")}</td>
<td>{tDate("2023-04-15", "PP")}</td>
</tr>
</tbody>
</Table>
Expand Down
24 changes: 14 additions & 10 deletions components/bill/SponsorsAndCommittees.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { format, fromUnixTime } from "date-fns"
import { fromUnixTime } from "date-fns"
import { useTranslation } from "components/i18n"
import styled from "styled-components"
import { Card, Container, Row } from "../bootstrap"
import { Card, Container } from "../bootstrap"
import { External } from "../links"
import { LabeledIcon } from "../shared"
import { FC } from "../types"
Expand Down Expand Up @@ -29,14 +30,15 @@ export const Committees: FC<React.PropsWithChildren<BillProps>> = ({
className
}) => {
const current = bill.currentCommittee
const { t } = useTranslation("common")
if (!current) return null
return (
<Container className={`${className} p-0`}>
<MapleCard
className={className}
headerElement={
<Card.Header className="h4 bg-secondary text-light">
Committee
{t("bill.committee")}
</Card.Header>
}
body={
Expand Down Expand Up @@ -64,13 +66,15 @@ export const Hearing: FC<React.PropsWithChildren<BillProps>> = ({
bill,
className
}) => {
const { t, tDate } = useTranslation("common")
return (
<>
{bill.nextHearingAt && dateInFuture(bill.nextHearingAt) ? (
<LabeledContainer className={className}>
<HearingDate>
Hearing Scheduled for{" "}
{format(fromUnixTime(bill.nextHearingAt?.seconds), "MMM d, y p")}
{t("bill.hearing_scheduled_for", {
date: tDate(fromUnixTime(bill.nextHearingAt?.seconds), "PPp")
})}
</HearingDate>
</LabeledContainer>
) : null}
Expand All @@ -86,7 +90,7 @@ export const Sponsors: FC<React.PropsWithChildren<BillProps>> = ({
const cosponsors = bill.content.Cosponsors.filter(s => s.Id !== primary?.Id)
const more = cosponsors.length > 2
const isMobile = useMediaQuery("(max-width: 768px)")

const { t } = useTranslation("common")
const countShowSponsors = isMobile ? 1 : 2

return (
Expand All @@ -95,7 +99,7 @@ export const Sponsors: FC<React.PropsWithChildren<BillProps>> = ({
className={className}
headerElement={
<Card.Header className="h4 bg-secondary text-light">
Sponsors
{t("sponsors")}
</Card.Header>
}
body={
Expand All @@ -109,7 +113,7 @@ export const Sponsors: FC<React.PropsWithChildren<BillProps>> = ({
{primary && (
<LabeledIcon
idImage={`https://malegislature.gov/Legislators/Profile/170/${primary.Id}.jpg`}
mainText="Lead Sponsor"
mainText={t("leadSponsor")}
subText={
<External
href={`https://malegislature.gov/Legislators/Profile/${primary.Id}`}
Expand All @@ -126,7 +130,7 @@ export const Sponsors: FC<React.PropsWithChildren<BillProps>> = ({
<LabeledIcon
key={s.Id}
idImage={`https://malegislature.gov/Legislators/Profile/170/${s.Id}.jpg`}
mainText="Sponsor"
mainText={t("sponsor")}
subText={
<External
href={`https://malegislature.gov/Legislators/Profile/${s.Id}`}
Expand All @@ -140,7 +144,7 @@ export const Sponsors: FC<React.PropsWithChildren<BillProps>> = ({
<div className="d-flex justify-content-center">
{more && (
<Cosponsors bill={bill}>
See {bill.cosponsorCount} Sponsors
{t("bill.seeCosponsors", { count: bill.cosponsorCount })}
</Cosponsors>
)}
</div>
Expand Down
49 changes: 49 additions & 0 deletions components/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { format as dateFormat } from "date-fns"
import { enUS, es } from "date-fns/locale"
import { useTranslation as _useTranslation } from "next-i18next"

// Maps supported language codes to their corresponding date-fns locale objects.
// Used by tDate to ensure date formatting respects the current language's locale.
const localeMap: Record<string, Locale> = {
en: enUS,
es: es
}

/**
* A wrapper around `next-i18next`'s `useTranslation` hook.
*
* Extends the default translation hook by adding `tDate`, a date formatting helper
* that formats dates according to the active language's locale using date-fns.
*
* @returns The translation object from `next-i18next` plus `tDate` for localized date formatting.
*
* @example
* const { t, tDate } = useTranslation("common");
* console.log(t("welcome"));
* console.log(tDate(new Date(), "PP")); // e.g., "Sep 1, 2025"
*/
export const useTranslation = (...args: Parameters<typeof _useTranslation>) => {
const response = _useTranslation(...args)
return {
...response,
/**
* Formats a date or date string according to the active language's locale.
*
* Only supports localized format tokens from date-fns.
* See: https://date-fns.org/v2.0.0-alpha.25/docs/format
*
* @param date - ISO date string or Date object to format.
* @param format - A localized date-fns format string (e.g., "PPpp").
* @returns The localized, formatted date string.
*
* @example
* tDate(new Date(), "PP"); // "Sep 1, 2025" in English, "1 sept 2025" in Spanish
*/
tDate: (date: string | Date, format: string) => {
date = typeof date === "string" ? new Date(date) : date
return dateFormat(date, format, {
locale: localeMap[response.i18n.language] ?? enUS
})
}
}
}
30 changes: 20 additions & 10 deletions components/search/bills/BillHit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
} from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { maple } from "components/links"
import { format, fromUnixTime } from "date-fns"
import { fromUnixTime } from "date-fns"
import { Hit } from "instantsearch.js"
import Link from "next/link"
import styled from "styled-components"
import { Card, Col } from "../../bootstrap"
import { formatBillId } from "../../formatting"
import { Timestamp } from "firebase/firestore"
import { dateInFuture } from "components/db/events"
import { useTranslation } from "components/i18n"

type BillRecord = {
number: string
Expand Down Expand Up @@ -132,6 +133,7 @@ export const DisplayUpcomingHearing = ({
export const BillHit = ({ hit }: { hit: Hit<BillRecord> }) => {
const url = maple.bill({ id: hit.number, court: hit.court })
const hearingDate = hit.nextHearingAt && hit.nextHearingAt / 1000 // convert to seconds
const { t, tDate } = useTranslation("common")

return (
<Link href={url} legacyBehavior>
Expand All @@ -142,7 +144,9 @@ export const BillHit = ({ hit }: { hit: Hit<BillRecord> }) => {
<Col className="left">
<div className="d-flex justify-content-between">
{hit.court && (
<span className="blurb me-2">Court {hit.court}</span>
<span className="blurb me-2">
{t("bill.court", { court: hit.court })}
</span>
)}
<span className="blurb">{hit.city}</span>
<span style={{ flex: "1" }} />
Expand All @@ -154,24 +158,30 @@ export const BillHit = ({ hit }: { hit: Hit<BillRecord> }) => {
</Card.Title>
<div className="d-flex justify-content-between flex-column">
<span className="blurb">
Sponsor: {hit.primarySponsor}{" "}
{hit.cosponsorCount > 0
? `and ${hit.cosponsorCount} other${
hit.cosponsorCount > 1 ? "s" : ""
}`
: ""}
{(() => {
const count = hit.cosponsorCount
let title = `${t("sponsor")}: ${hit.primarySponsor}`
if (count > 1) {
title += ` ${t("bill.and_some_others", { count })}`
} else if (count === 1) {
title += ` ${t("bill.and_one_other")}`
}
return title
})()}
</span>
<span className="blurb">
{hit.currentCommittee &&
`Committee: ${hit.currentCommittee}`}
`${t("bill.committee")}: ${hit.currentCommittee}`}
</span>
</div>
</Col>
</div>
</Card.Body>
{hit.nextHearingAt && dateInFuture(hit.nextHearingAt) ? (
<Card.Footer className="card-footer">
Hearing Scheduled {format(fromUnixTime(hearingDate!), "M/d/y p")}
{t("bill.hearing_scheduled_for", {
date: tDate(fromUnixTime(hearingDate!), "PPp")
})}
</Card.Footer>
) : null}
</StyledCard>
Expand Down
Loading