diff --git a/packages/client/src/data_commons_web_client.ts b/packages/client/src/data_commons_web_client.ts index fb74822b54..cfcb4ad08a 100644 --- a/packages/client/src/data_commons_web_client.ts +++ b/packages/client/src/data_commons_web_client.ts @@ -32,6 +32,8 @@ export interface DatacommonsWebClientParams { apiRoot?: string; } +const LOCALE_PARAM = "hl"; + class DataCommonsWebClient { /** Website API root */ apiRoot?: string; @@ -213,12 +215,17 @@ class DataCommonsWebClient { * @param params.placeDcid place dcid to fetch data for */ async getPlaceCharts(params: { - category?: string; placeDcid: string; + category?: string; + locale?: string; }): Promise { + const queryString = toURLSearchParams({ + category: params.category, + [LOCALE_PARAM]: params.locale, + }); const url = `${this.apiRoot || ""}/api/dev-place/charts/${ params.placeDcid - }${params.category ? "?category=" + params.category : ""}`; + }?${queryString}`; const response = await fetch(url); return (await response.json()) as PlaceChartsApiResponse; } @@ -230,10 +237,14 @@ class DataCommonsWebClient { */ async getRelatedPLaces(params: { placeDcid: string; + locale?: string; }): Promise { + const queryString = toURLSearchParams({ + [LOCALE_PARAM]: params.locale, + }); const url = `${this.apiRoot || ""}/api/dev-place/related-places/${ params.placeDcid - }`; + }?${queryString}`; const response = await fetch(url); return (await response.json()) as RelatedPlacesApiResponse; } diff --git a/packages/client/src/data_commons_web_client_types.ts b/packages/client/src/data_commons_web_client_types.ts index 7bc937d6be..4a5d8578b0 100644 --- a/packages/client/src/data_commons_web_client_types.ts +++ b/packages/client/src/data_commons_web_client_types.ts @@ -148,7 +148,7 @@ export type ObservationDatesApiResponse = { type ChartType = "BAR" | "LINE" | "MAP" | "RANKING" | "HIGHLIGHT"; export interface Chart { type: ChartType; - maxPlaces?: number; + maxPlaces?: number; } export interface Place { @@ -158,7 +158,7 @@ export interface Place { } export interface BlockConfig { - charts: Chart[] + charts: Chart[]; childPlaceType: string; childPlaces: Place[]; nearbyPlaces: Place[]; @@ -175,13 +175,18 @@ export interface BlockConfig { scaling?: number; // Optional } +export interface Category { + name: string; + translatedName: string; +} + /** * Website API response for /api/dev-place/charts/ */ export interface PlaceChartsApiResponse { blocks: BlockConfig[]; place: Place; - translatedCategoryStrings: Record; + categories: Category[]; } /** diff --git a/server/routes/dev_place/api.py b/server/routes/dev_place/api.py index 6cd6934950..fc3a5007af 100644 --- a/server/routes/dev_place/api.py +++ b/server/routes/dev_place/api.py @@ -29,12 +29,6 @@ from server.routes.dev_place.types import PlaceChartsApiResponse from server.routes.dev_place.types import RelatedPlacesApiResponse -OVERVIEW_CATEGORY = "Overview" -CATEGORIES = { - OVERVIEW_CATEGORY, "Economics", "Health", "Equity", "Crime", "Education", - "Demographics", "Housing", "Environment", "Energy" -} - # Define blueprint bp = Blueprint("dev_place_api", __name__, url_prefix='/api/dev-place') @@ -58,15 +52,18 @@ def place_charts(place_dcid: str): - Charts specific to the place - Translated category strings for the charts """ + # Ensure category is valid - place_category = request.args.get("category", OVERVIEW_CATEGORY) - parent_place_dcid = place_utils.get_place_override( - place_utils.get_parent_places(place_dcid)) - if place_category not in CATEGORIES: + place_category = request.args.get("category", place_utils.OVERVIEW_CATEGORY) + if place_category not in place_utils.CATEGORIES: return error_response( - f"Argument 'category' {place_category} must be one of: {', '.join(CATEGORIES)}" + f"Argument 'category' {place_category} must be one of: {', '.join(place_utils.CATEGORIES)}" ) + # Get parent place DCID + parent_place_dcid = place_utils.get_place_override( + place_utils.get_parent_places(place_dcid)) + # Fetch place info place = place_utils.fetch_place(place_dcid, locale=g.locale) @@ -98,14 +95,13 @@ def place_charts(place_dcid: str): blocks = place_utils.chart_config_to_overview_charts(translated_chart_config, child_place_type) - # Translate category strings for all charts that have any data. - translated_category_strings = place_utils.get_translated_category_strings( + # Translate category strings + categories_with_translations = place_utils.get_categories_with_translations( chart_config_existing_data) - response = PlaceChartsApiResponse( - blocks=blocks, - place=place, - translatedCategoryStrings=translated_category_strings) + response = PlaceChartsApiResponse(blocks=blocks, + place=place, + categories=categories_with_translations) return jsonify(response) diff --git a/server/routes/dev_place/types.py b/server/routes/dev_place/types.py index 2f5861c525..a6fcae7d39 100644 --- a/server/routes/dev_place/types.py +++ b/server/routes/dev_place/types.py @@ -56,6 +56,12 @@ class Place: dissolved: bool = False +@dataclass +class Category: + name: str + translatedName: str + + @dataclass class PlaceChartsApiResponse: """ @@ -63,7 +69,7 @@ class PlaceChartsApiResponse: """ blocks: List[BlockConfig] place: Place - translatedCategoryStrings: Dict[str, str] + categories: List[Category] @dataclass diff --git a/server/routes/dev_place/utils.py b/server/routes/dev_place/utils.py index 275aa57a50..239e78ccd7 100644 --- a/server/routes/dev_place/utils.py +++ b/server/routes/dev_place/utils.py @@ -26,6 +26,7 @@ from server.lib.i18n import DEFAULT_LOCALE from server.routes import TIMEOUT from server.routes.dev_place.types import BlockConfig +from server.routes.dev_place.types import Category from server.routes.dev_place.types import Chart from server.routes.dev_place.types import Place from server.routes.dev_place.types import ServerBlockMetadata @@ -46,6 +47,14 @@ 'Continent', ] +# Place page categories +OVERVIEW_CATEGORY = "Overview" +ORDERED_CATEGORIES = [ + OVERVIEW_CATEGORY, "Economics", "Health", "Equity", "Crime", "Education", + "Demographics", "Housing", "Environment", "Energy" +] +CATEGORIES = set(ORDERED_CATEGORIES) + def get_place_html_link(place_dcid: str, place_name: str) -> str: """Get tag linking to the place page for a place @@ -580,18 +589,43 @@ def translate_chart_config(chart_config: List[ServerChartConfiguration]): return translated_chart_config -def get_translated_category_strings( +def get_categories_with_translations( chart_config: List[ServerChartConfiguration]) -> Dict[str, str]: - translated_category_strings: Dict[str, str] = {} + """ + Returns a list of categories with their translated names from the chart config. + Args: + chart_config (List[ServerChartConfiguration]): The chart configuration to use for determining categories. + + Returns: + List[Category]: A list of categories with their translated names. + """ + categories: List[Category] = [] + + overview_category = Category( + name=OVERVIEW_CATEGORY, + translatedName=get_translated_category_string(OVERVIEW_CATEGORY)) + categories.append(overview_category) + + categories_set: Set[str] = set() for page_config_item in chart_config: category = page_config_item.category - if category in translated_category_strings: + if category in categories_set: + continue + categories_set.add(category) + + for category in ORDERED_CATEGORIES: + if not category in categories_set: continue - translated_category_strings[category] = gettext( - f'CHART_TITLE-CHART_CATEGORY-{category}') + category = Category(name=category, + translatedName=get_translated_category_string(category)) + categories.append(category) + + return categories + - return translated_category_strings +def get_translated_category_string(category: str) -> str: + return gettext(f'CHART_TITLE-CHART_CATEGORY-{category}') def get_place_cohort(place: Place) -> str: diff --git a/server/tests/routes/api/dev_place_test.py b/server/tests/routes/api/dev_place_test.py index f9da1c6a01..f48480b2cc 100644 --- a/server/tests/routes/api/dev_place_test.py +++ b/server/tests/routes/api/dev_place_test.py @@ -74,7 +74,7 @@ def test_dev_place_charts(self, mock_obs_point_within, mock_obs_point, response_json = response.get_json() self.assertIn('blocks', response_json) self.assertIn('place', response_json) - self.assertIn('translatedCategoryStrings', response_json) + self.assertIn('categories', response_json) self.assertIn('charts', response_json['blocks'][0]) # Check that the 'charts' field contains the expected number of charts @@ -94,9 +94,12 @@ def test_dev_place_charts(self, mock_obs_point_within, mock_obs_point, self.assertEqual(response_json['place']['name'], 'United States') self.assertEqual(response_json['place']['types'], ['Country']) - # Check that 'translatedCategoryStrings' contains expected categories - self.assertIn('Crime', response_json['translatedCategoryStrings']) - self.assertIn('Education', response_json['translatedCategoryStrings']) + # Check that 'categories' contains expected categories + categories = [ + category['translatedName'] for category in response_json['categories'] + ] + self.assertIn('Crime', categories) + self.assertIn('Education', categories) # Ensure the denominator is present in chart results self.assertEqual(1, len(response_json["blocks"][0]["denominator"])) diff --git a/server/webdriver/tests/place_explorer_test.py b/server/webdriver/tests/place_explorer_test.py index 0f5288a26a..9c04d82db7 100644 --- a/server/webdriver/tests/place_explorer_test.py +++ b/server/webdriver/tests/place_explorer_test.py @@ -16,6 +16,7 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait +from server.routes.dev_place.utils import ORDERED_CATEGORIES from server.webdriver import base_utils from server.webdriver import shared from server.webdriver.base_dc_webdriver import BaseDcWebdriverTest @@ -30,10 +31,7 @@ def test_dev_place_overview_california(self): """Ensure experimental dev place page content loads""" self.driver.get(self.url_ + '/place/geoId/06?force_dev_places=true') - expected_topics = [ - "Overview", "Crime", "Demographics", "Economics", "Education", "Energy", - "Environment", "Equity", "Health", "Housing" - ] + expected_topics = ORDERED_CATEGORIES shared.assert_topics(self, self.driver, path_to_topics=['explore-topics-box'], diff --git a/static/js/place/child_places_menu.tsx b/static/js/place/child_places_menu.tsx index bf26ead9fa..14542a2368 100644 --- a/static/js/place/child_places_menu.tsx +++ b/static/js/place/child_places_menu.tsx @@ -17,7 +17,7 @@ import React from "react"; import { intl, LocalizedLink } from "../i18n/i18n"; -import { displayNameForPlaceType } from "./util"; +import { displayNameForPlaceType, pageMessages } from "./util"; interface ChildPlacePropType { childPlaces: { string: string[] }; @@ -32,15 +32,9 @@ class ChildPlace extends React.Component { return ( - {intl.formatMessage( - { - id: "child_places_menu-places_in_place", - defaultMessage: "Places in {placeName}", - description: - 'Used for the child places navigation sidebar. Shows a list of place contained in the current place. For example, the sidebar for the Austria place page shows links to child places under the header "Places in {Austria}".', - }, - { placeName: this.props.placeName } - )} + {intl.formatMessage(pageMessages.placesInPlace, { + placeName: this.props.placeName, + })} {Object.keys(this.props.childPlaces) .sort() diff --git a/static/js/place/dev_place.ts b/static/js/place/dev_place.ts index 1115f231e2..8380dfbf88 100644 --- a/static/js/place/dev_place.ts +++ b/static/js/place/dev_place.ts @@ -26,7 +26,11 @@ window.addEventListener("load", async (): Promise => { const locale = metadataContainer.dataset.locale; // Load locale data - await loadLocaleData(locale, []); + await loadLocaleData(locale, [ + import(`../i18n/compiled-lang/${locale}/place.json`), + import(`../i18n/compiled-lang/${locale}/stats_var_labels.json`), + import(`../i18n/compiled-lang/${locale}/units.json`), + ]); // Render page renderPage(); diff --git a/static/js/place/dev_place_main.tsx b/static/js/place/dev_place_main.tsx index 320ce34085..c006ed220b 100644 --- a/static/js/place/dev_place_main.tsx +++ b/static/js/place/dev_place_main.tsx @@ -16,6 +16,7 @@ import { DataRow } from "@datacommonsorg/client"; import { + Category, PlaceChartsApiResponse, RelatedPlacesApiResponse, } from "@datacommonsorg/client/dist/data_commons_web_client_types"; @@ -24,7 +25,7 @@ import { RawIntlProvider } from "react-intl"; import { GoogleMap } from "../components/google_map"; import { SubjectPageMainPane } from "../components/subject_page/main_pane"; -import { intl } from "../i18n/i18n"; +import { intl, LocalizedLink } from "../i18n/i18n"; import { NamedTypedPlace, StatVarSpec } from "../shared/types"; import { SubjectPageConfig } from "../types/subject_page_proto_types"; import { @@ -34,6 +35,7 @@ import { import { TileSources } from "../utils/tile_utils"; import { isPlaceContainedInUsa, + pageMessages, placeChartsApiResponsesToPageConfig, } from "./util"; @@ -83,8 +85,8 @@ const PlaceHeader = (props: { * @param props.place The place object containing the DCID for generating URLs * @returns Button component for the current topic */ -const TopicItem = (props: { - category: string; +const CategoryItem = (props: { + category: Category; selectedCategory: string; forceDevPlaces: boolean; place: NamedTypedPlace; @@ -111,14 +113,13 @@ const TopicItem = (props: { return (
- - {category} - + text={category.translatedName} + />
); }; @@ -132,18 +133,18 @@ const TopicItem = (props: { * @param props.place The place object containing the DCID for generating URLs * @returns Navigation component with topic tabs */ -const PlaceTopicTabs = ({ - topics, +const PlaceCategoryTabs = ({ + categories, forceDevPlaces, - category, + selectedCategory, place, }: { - topics: string[]; + categories: Category[]; forceDevPlaces: boolean; - category: string; + selectedCategory: string; place: NamedTypedPlace; }): React.JSX.Element => { - if (!topics || topics.length == 0) { + if (!categories || categories.length == 0) { return <>; } @@ -152,11 +153,11 @@ const PlaceTopicTabs = ({ Relevant topics
- {topics.map((topic) => ( - ( + @@ -336,14 +337,20 @@ const RelatedPlaces = (props: { return (
-
Places in {place.name}
+
+ {intl.formatMessage(pageMessages.placesInPlace, { + placeName: place.name, + })} +
{(isCollapsed ? truncatedPlaces : childPlaces).map((place) => ( ))}
@@ -413,7 +420,11 @@ export const DevPlaceMain = (): React.JSX.Element => { const [pageConfig, setPageConfig] = useState(); const [hasError, setHasError] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [categories, setCategories] = useState(); + const [categories, setCategories] = useState(); + + // Get locale + const metadataContainer = document.getElementById("metadata-base"); + const locale = metadataContainer.dataset.locale; const urlParams = new URLSearchParams(window.location.search); const category = urlParams.get("category") || overviewString; @@ -471,9 +482,11 @@ export const DevPlaceMain = (): React.JSX.Element => { await Promise.all([ defaultDataCommonsWebClient.getPlaceCharts({ category, + locale, placeDcid: place.dcid, }), defaultDataCommonsWebClient.getRelatedPLaces({ + locale, placeDcid: place.dcid, }), ]); @@ -496,14 +509,7 @@ export const DevPlaceMain = (): React.JSX.Element => { useEffect(() => { if (placeChartsApiResponse && placeChartsApiResponse.blocks) { - // TODO(gmechali): Refactor this to use the translations correctly. - // Move overview to be added in the response with translations. Use the - // translation in the tabs, but the english version in the URL. - setCategories( - ["Overview"].concat( - Object.values(placeChartsApiResponse.translatedCategoryStrings) - ) - ); + setCategories(placeChartsApiResponse.categories); } }, [placeChartsApiResponse, setPlaceChartsApiResponse]); @@ -520,9 +526,9 @@ export const DevPlaceMain = (): React.JSX.Element => { place={place} placeSubheader={placeSubheader} /> - diff --git a/static/js/place/util.ts b/static/js/place/util.ts index 9751d4daf4..54bea75a9b 100644 --- a/static/js/place/util.ts +++ b/static/js/place/util.ts @@ -179,6 +179,13 @@ export function placeChartsApiResponsesToPageConfig( (item) => item.category ); + const categoryNameToTranslatedName = _.fromPairs( + placeChartsApiResponse.categories.map((category) => [ + category.name, + category.translatedName, + ]) + ); + const categoryConfig: CategoryConfig[] = Object.keys(blocksByCategory).map( (categoryName) => { const blocks = blocksByCategory[categoryName]; @@ -188,9 +195,11 @@ export function placeChartsApiResponsesToPageConfig( blocks.forEach((block: BlockConfig) => { const tiles = []; block.charts.forEach((chart: Chart) => { + const title = getTitle(block.title, block.placeScope); const tileConfig: TileConfig = { - description: block.description, - title: getTitle(block.title, block.placeScope), + /** Highlight charts use title as description */ + description: title, + title, type: chart.type, statVarKey: block.statisticalVariableDcids.map( @@ -284,7 +293,7 @@ export function placeChartsApiResponsesToPageConfig( const category: CategoryConfig = { blocks: newblocks, statVarSpec, - title: categoryName, + title: categoryNameToTranslatedName[categoryName] || categoryName, }; return category; } @@ -423,6 +432,15 @@ const pluralPlaceTypeMessages = defineMessages({ }, }); +export const pageMessages = defineMessages({ + placesInPlace: { + id: "child_places_menu-places_in_place", + defaultMessage: "Places in {placeName}", + description: + 'Used for the child places navigation sidebar. Shows a list of place contained in the current place. For example, the sidebar for the Austria place page shows links to child places under the header "Places in {Austria}".', + }, +}); + /** * Returns place type, possibly pluralized if requested. *