Feature - Added region comparison tool for village-level analysis#366
Feature - Added region comparison tool for village-level analysis#366DharanSJIT wants to merge 5 commits intocore-stack-org:developmentfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new Region Comparison Tool page for village-level side-by-side comparison, and introduces a full i18n + language switching setup that localizes location selectors and multiple UI surfaces across the app.
Changes:
- Added
RegionComparisonpage + routing and metric comparison utilities (including MWS-derived fallback logic). - Introduced i18n initialization, language switcher, and locale JSONs (EN/HI/TA/MR/BN).
- Updated existing pages/components (homepage, nav, sidebars, selectors, footer) to use translated strings and translated location labels.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
src/pages/RegionComparison.jsx |
New comparison UI + data fetching/fallback logic for village metrics. |
src/components/utils/compareMetrics.js |
Metric-key selection, scoring, and winner logic for comparisons. |
src/App.jsx |
Adds route for /region-comparison. |
src/pages/LE_homepage.jsx |
Localizes homepage content and adds entry point button to the comparison tool. |
src/index.js |
Boots i18n initialization at app startup. |
src/i18n/index.js |
New i18next configuration and resource registration. |
src/components/LanguageSwitcher.jsx |
Adds UI control to switch languages. |
src/components/buttons/select_button.jsx |
Adds translated option labels + placeholder handling for dropdowns. |
src/components/mapArea.jsx |
Passes translation namespaces into SelectButton for locations. |
src/components/landscape-explorer/sidebar/RightSidebar.jsx |
Localizes labels, download actions, and selection placeholders. |
src/components/kyl_rightSidebar.jsx |
Localizes KYL sidebar strings and selection placeholders. |
src/components/kyl_leftSidebar.jsx |
Localizes tab labels, hints, and category labels. |
src/components/landing_navbar.jsx |
Adds LanguageSwitcher + localizes nav labels/tooltips. |
src/components/footer.jsx |
Localizes footer copyright. |
src/i18n/locales/{en,hi,ta,mr,bn}.json |
Adds translation resources for new/existing UI strings + location names. |
src/i18n/useTranslatedLocations.js |
Adds a helper hook for translating location structures. |
package.json |
Adds i18n dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const villageHasMetrics = hasComparisonSignals(resolvedVillages); | ||
|
|
||
| const mwsUrls = buildMwsUrls( | ||
| selectedState.label, | ||
| selectedDistrict.label, | ||
| selectedBlock.label | ||
| ); | ||
|
|
||
| let derivedVillages = null; | ||
|
|
||
| for (const url of mwsUrls) { | ||
| const response = await fetch(url); | ||
| if (!response.ok) continue; | ||
|
|
||
| const payload = await response.json(); | ||
| const parsed = parseVillagePayload(payload); | ||
| if (!parsed || !parsed.length) continue; | ||
|
|
||
| const transformed = deriveVillageRecordsFromMws(parsed); | ||
| if (transformed.length) { | ||
| derivedVillages = transformed; | ||
| break; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Even when village-level data is found and contains all required metrics, the code still performs the full MWS fallback fetch loop (up to 4 more network calls). This increases latency and load unnecessarily. Consider fetching MWS data only when !resolvedVillages or !villageHasMetrics (or running attempts in parallel).
| // currVal is a real selection only when it has a `value` field (API data) | ||
| // placeholder objects passed from parent only have `label` | ||
| const selectedValue = | ||
| currVal && typeof currVal === "object" && !Array.isArray(currVal) | ||
| ? currVal | ||
| : null; | ||
| const placeholderText = placeholder || label || currVal?.label || "Select..."; | ||
|
|
There was a problem hiding this comment.
The selectedValue logic doesn’t match the comment: it treats any object currVal as a selected option, including placeholder objects like { label: "Select State" }. This prevents placeholder from showing and can set a value that doesn’t exist in options. Consider resolving selectedValue by finding the matching option via getOptionValue, or treat label-only placeholder objects as null.
| // currVal is a real selection only when it has a `value` field (API data) | |
| // placeholder objects passed from parent only have `label` | |
| const selectedValue = | |
| currVal && typeof currVal === "object" && !Array.isArray(currVal) | |
| ? currVal | |
| : null; | |
| const placeholderText = placeholder || label || currVal?.label || "Select..."; | |
| // currVal is a real selection only when it has an identifier field (API data) | |
| // placeholder objects passed from parent only have `label` | |
| const hasIdentifier = (option) => | |
| option && | |
| typeof option === "object" && | |
| !Array.isArray(option) && | |
| (option.value != null || | |
| option.block_id != null || | |
| option.tehsil_id != null || | |
| option.district_id != null || | |
| option.state_id != null || | |
| option.id != null); | |
| const selectedValue = hasIdentifier(currVal) | |
| ? sortedOptions.find( | |
| (opt) => getOptionValue(opt) === getOptionValue(currVal) | |
| ) ?? null | |
| : null; | |
| const placeholderText = placeholder || label || "Select..."; |
| handleNavigate("/region-comparison", "Region Comparison") | ||
| } | ||
| > | ||
| Region Comparison |
There was a problem hiding this comment.
This button label is still hard-coded in English, so it won’t change with the selected language. Use an i18n key (and add it to the locale files) for both the button text and the analytics label if needed.
| handleNavigate("/region-comparison", "Region Comparison") | |
| } | |
| > | |
| Region Comparison | |
| handleNavigate( | |
| "/region-comparison", | |
| t("home.know.regionComparison") | |
| ) | |
| } | |
| > | |
| {t("home.know.regionComparison")} |
| import { useTranslation } from "react-i18next"; | ||
| import { useMemo } from "react"; | ||
|
|
||
| // i18next uses '.' as separator, so keys with spaces like "Andhra Pradesh" | ||
| // must be looked up with the separator disabled or via direct object access. | ||
| // We use the defaultValue fallback: t(key, { defaultValue: original }) | ||
| // and disable key separator for location lookups by using the raw resource. | ||
|
|
||
| function translateLabel(i18n, namespace, label) { | ||
| const lang = i18n.language; | ||
| const resources = i18n.getResourceBundle(lang, "translation"); | ||
| if (resources && resources[namespace] && resources[namespace][label]) { | ||
| return resources[namespace][label]; | ||
| } | ||
| // fallback to English | ||
| const enResources = i18n.getResourceBundle("en", "translation"); | ||
| if (enResources && enResources[namespace] && enResources[namespace][label]) { | ||
| return enResources[namespace][label]; | ||
| } | ||
| return label; | ||
| } | ||
|
|
||
| export function useTranslatedLocations(statesData) { | ||
| const { i18n } = useTranslation(); | ||
| const language = i18n.language; | ||
|
|
||
| return useMemo(() => { | ||
| if (!statesData) return statesData; | ||
|
|
||
| return statesData.map((state) => ({ | ||
| ...state, | ||
| label: translateLabel(i18n, "states", state.label), | ||
| district: state.district?.map((d) => ({ | ||
| ...d, | ||
| label: translateLabel(i18n, "districts", d.label), | ||
| blocks: d.blocks?.map((b) => ({ | ||
| ...b, | ||
| label: translateLabel(i18n, "blocks", b.label), | ||
| })), | ||
| })), | ||
| })); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [statesData, language]); | ||
| } |
There was a problem hiding this comment.
useTranslatedLocations is added but not referenced anywhere in the repo. If it’s not intended for near-term use, consider removing it to avoid dead code; otherwise, wire it into the state/district/block selection flows to centralize translation logic.
| import { useTranslation } from "react-i18next"; | |
| import { useMemo } from "react"; | |
| // i18next uses '.' as separator, so keys with spaces like "Andhra Pradesh" | |
| // must be looked up with the separator disabled or via direct object access. | |
| // We use the defaultValue fallback: t(key, { defaultValue: original }) | |
| // and disable key separator for location lookups by using the raw resource. | |
| function translateLabel(i18n, namespace, label) { | |
| const lang = i18n.language; | |
| const resources = i18n.getResourceBundle(lang, "translation"); | |
| if (resources && resources[namespace] && resources[namespace][label]) { | |
| return resources[namespace][label]; | |
| } | |
| // fallback to English | |
| const enResources = i18n.getResourceBundle("en", "translation"); | |
| if (enResources && enResources[namespace] && enResources[namespace][label]) { | |
| return enResources[namespace][label]; | |
| } | |
| return label; | |
| } | |
| export function useTranslatedLocations(statesData) { | |
| const { i18n } = useTranslation(); | |
| const language = i18n.language; | |
| return useMemo(() => { | |
| if (!statesData) return statesData; | |
| return statesData.map((state) => ({ | |
| ...state, | |
| label: translateLabel(i18n, "states", state.label), | |
| district: state.district?.map((d) => ({ | |
| ...d, | |
| label: translateLabel(i18n, "districts", d.label), | |
| blocks: d.blocks?.map((b) => ({ | |
| ...b, | |
| label: translateLabel(i18n, "blocks", b.label), | |
| })), | |
| })), | |
| })); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [statesData, language]); | |
| } | |
| // This file previously exported `useTranslatedLocations`, which was not referenced | |
| // anywhere in the repository and was therefore removed to avoid dead code. | |
| // Reintroduce the hook here only when it is actually wired into the | |
| // state/district/block selection flows. |
| ta: { translation: ta }, | ||
| bn: { translation: bn }, | ||
| mr: { translation: mr }, | ||
| }, |
There was a problem hiding this comment.
With LanguageDetector enabled, i18n.language may be en-US/hi-IN, but only base resources (en, hi, ...) are registered. Consider adding supportedLngs and nonExplicitSupportedLngs: true (or load: "languageOnly") so detected regional variants resolve to the bundled resources. This also avoids breaking any manual getResourceBundle(i18n.language, ...) lookups.
| }, | |
| }, | |
| supportedLngs: ["en", "hi", "ta", "bn", "mr"], | |
| nonExplicitSupportedLngs: true, |
| return (option) => { | ||
| const rawLabel = String(option?.label ?? ""); | ||
| const resources = i18n.getResourceBundle(language, "translation"); | ||
| if ( | ||
| resources && | ||
| resources[translateNamespace] && | ||
| resources[translateNamespace][rawLabel] | ||
| ) { | ||
| return resources[translateNamespace][rawLabel]; | ||
| } | ||
|
|
||
| const enResources = i18n.getResourceBundle("en", "translation"); | ||
| if ( | ||
| enResources && | ||
| enResources[translateNamespace] && | ||
| enResources[translateNamespace][rawLabel] | ||
| ) { | ||
| return enResources[translateNamespace][rawLabel]; | ||
| } |
There was a problem hiding this comment.
getResourceBundle(language, ...) uses i18n.language, which may be a regional tag (e.g. en-US, hi-IN) when LanguageDetector is enabled. In that case resources will be undefined and location labels will incorrectly fall back to English. Use i18n.resolvedLanguage (or iterate i18n.languages) when reading bundles, or normalize via i18n init options.
| @@ -1407,6 +1408,7 @@ const MapArea = ({ selectedTehsil }) => { | |||
| district === null ? { label: "Select District" } : district | |||
| } | |||
| stateData={state !== null ? state.district : null} | |||
| translateNamespace="districts" | |||
| handleItemSelect={handleItemSelect} | |||
| setState={setDistrict} | |||
| /> | |||
| @@ -1415,6 +1417,7 @@ const MapArea = ({ selectedTehsil }) => { | |||
| <SelectButton | |||
| currVal={block === null ? { label: "Select Block" } : block} | |||
| stateData={district !== null ? district.blocks : null} | |||
| translateNamespace="blocks" | |||
There was a problem hiding this comment.
These currVal props pass placeholder objects (e.g. { label: "Select State" }). With the updated SelectButton behavior (custom placeholder + translation), this will be treated as an actual selected value and can mask the real placeholder/disable proper clearing. Prefer passing currVal={state} (nullable) and a localized placeholder, as done in other updated call sites.
| onClick={() => i18n.changeLanguage(lang.code)} | ||
| className={`px-3 py-2 text-sm font-medium transition-colors duration-150 ${ | ||
| i18n.language === lang.code | ||
| ? "bg-purple-600 text-white" | ||
| : "text-purple-700 hover:bg-purple-50" | ||
| }`} |
There was a problem hiding this comment.
LanguageDetector can set i18n.language to regional tags like en-US. Comparing strictly to lang.code will fail to highlight the active language button. Consider comparing against i18n.resolvedLanguage or i18n.language.split('-')[0].
| onClick={() => { | ||
| if (item.link) { | ||
| handleNavigate(item.link, item.title); | ||
| handleNavigate(item.link, t(item.titleKey)); |
There was a problem hiding this comment.
handleNavigate uses React Router navigation for all links, but trackCards includes an absolute external URL (https://welllabs.org/jaltol/). Navigating to external URLs via navigate() is unreliable and can produce an in-app path like /https://.... For external links, use window.open / window.location.href or render an <a> element.
| handleNavigate(item.link, t(item.titleKey)); | |
| if (/^https?:\/\//i.test(item.link)) { | |
| window.open(item.link, "_blank", "noopener,noreferrer"); | |
| } else { | |
| handleNavigate(item.link, t(item.titleKey)); | |
| } |
| useEffect(() => { | ||
| if (!selectedState || !selectedDistrict || !selectedBlock) { | ||
| setVillages([]); | ||
| return; | ||
| } | ||
|
|
||
| const fetchVillageData = async () => { | ||
| setLoadingVillages(true); | ||
| setFetchError(""); | ||
| setFetchNotice(""); | ||
|
|
There was a problem hiding this comment.
The village-fetch effect can race when users change selections quickly: an earlier request can resolve after a later one and overwrite villages/notices for the wrong block, and can also set state after unmount. Add cancellation (AbortController) and/or a request "token" check before calling setVillages / setFetchNotice / setFetchError.
|
Already merged this Feature in a Recent Commit so closing this PR. |
📊 Overview
This PR introduces a Region Comparison Tool that allows users to compare two villages based on key environmental and agricultural metrics such as rainfall, crop health, and water levels.
Features
🌐 Localization
🧠 Motivation
Understanding regional differences is crucial for planning, resource allocation, and decision-making in geospatial and agricultural domains. This feature enables users to make data-driven comparisons between villages in an intuitive and interactive way.
🛠️ Implementation Details
📂 Changed Files
src/pages/RegionComparison.jsxsrc/components/utils/compareMetrics.jssrc/App.jsxsrc/pages/LE_homepage.jsxsrc/i18n/locales/{en,hi,ta,mr,bn}.json📸 Demo
✅ How to Test
🔁 Fallback Case Test
🚀 Impact
📌 Notes