diff --git a/src/api/calculator-types-v1.ts b/src/api/calculator-types-v1.ts index 5daecad..8cf0ec9 100644 --- a/src/api/calculator-types-v1.ts +++ b/src/api/calculator-types-v1.ts @@ -28,25 +28,46 @@ export interface Amount { unit?: AmountUnit; } -export type ItemType = - | 'battery_storage_installation' - | 'efficiency_rebates' - | 'electric_panel' - | 'electric_stove' - | 'electric_vehicle_charger' - | 'electric_wiring' - | 'geothermal_heating_installation' - | 'heat_pump_air_conditioner_heater' - | 'heat_pump_clothes_dryer' - | 'heat_pump_water_heater' - | 'new_electric_vehicle' - | 'rooftop_solar_installation' - | 'used_electric_vehicle' - | 'weatherization'; +export const ITEMS = [ + 'air_sealing', + 'air_to_water_heat_pump', + 'attic_or_roof_insulation', + 'basement_insulation', + 'battery_storage_installation', + 'central_air_conditioner', + 'crawlspace_insulation', + 'door_replacement', + 'duct_replacement', + 'duct_sealing', + 'ducted_heat_pump', + 'ductless_heat_pump', + 'efficiency_rebates', + 'electric_panel', + 'electric_stove', + 'electric_vehicle_charger', + 'electric_wiring', + 'energy_audit', + 'floor_insulation', + 'geothermal_heating_installation', + 'heat_pump_air_conditioner_heater', + 'heat_pump_clothes_dryer', + 'heat_pump_water_heater', + 'new_electric_vehicle', + 'new_plugin_hybrid_vehicle', + 'non_heat_pump_clothes_dryer', + 'non_heat_pump_water_heater', + 'other_heat_pump', + 'other_insulation', + 'other_weatherization', + 'rooftop_solar_installation', + 'used_electric_vehicle', + 'used_plugin_hybrid_vehicle', + 'wall_insulation', + 'weatherization', + 'window_replacement', +] as const; -export interface Item { - type: ItemType; -} +export type ItemType = (typeof ITEMS)[number]; export interface Incentive { payment_methods: IncentiveType[]; @@ -55,9 +76,7 @@ export interface Incentive { program: string; program_url: string; more_info_url?: string; - // TODO when "items" backend change is deployed, remove "item" - item: Item; - items?: ItemType[]; + items: ItemType[]; amount: Amount; start_date?: string; end_date?: string; diff --git a/src/i18n/strings/es.ts b/src/i18n/strings/es.ts index e84a519..dbe06ab 100644 --- a/src/i18n/strings/es.ts +++ b/src/i18n/strings/es.ts @@ -8,6 +8,7 @@ import { str } from '../str'; export const templates = { s019265199296d7b0: `Hawái`, + s025c15001425a689: `reemplazo de ventanas`, s04009ba1826b5fca: `Idaho`, s04c4b18eb3941d37: `California`, s07ecbff17886eb50: `un vehículo eléctrico usado`, @@ -15,6 +16,7 @@ export const templates = { s0ec469a91a15f4a5: `kilovatio hora`, s0f06604c95e47b84: `Wisconsin`, s12603afef80b713c: `Estufa/cocina`, + s12bb428bacdba1ab: `un calentador de agua eléctrico`, s12c2889034735d5e: `Empresa de servicios eléctricos`, s13c6836e829c6f0d: `instalación de calefacción geotérmica`, s16c70c46d6c9d383: `techo solar`, @@ -26,6 +28,8 @@ export const templates = { s1cc63fbd986ae5d9: `Descuento en impermeabilización`, s1e73057deee4510e: `Sin embargo, los reembolsos se aplicarán de forma diferente en cada estado, de manera que no podemos garantizar los montos finales, la elegibilidad ni los plazos.`, s1f0dc611dbdf4115: `Ingresa sus ingresos brutos (ingresos antes de impuestos). Incluye salarios y sueldos, además de otras formas de ingresos, como pensiones, intereses, dividendos e ingresos por alquiler. Si está casado y presenta una declaración conjunta, incluye los ingresos de su cónyuge.`, + s22fdf6e102292d52: `un vehículo híbrido enchufable`, + s2361ab00b9155f96: `una bomba de calor de aire a agua`, s24ea3f43230aae41: `usted@example.com`, s255857544a9d5ec0: `Reiniciar`, s26946b3c896d36d6: `Vehículo eléctrico`, @@ -58,10 +62,14 @@ export const templates = { s459781c939bc83e5: `Ingresos del hogar`, s45d344440b59c309: `Manténgase al día sobre incentivos, reembolsos, y más de Rewiring America.`, s475d01cc643aa9fc: `Subscríbase para recibir actualizaciones de Rewiring America.`, + s48ad5711856cfb44: `un vehículo híbrido enchufable nuevo`, s48b23a6c3431c19e: `una secadora con bomba de calor`, s4b0d7bd386d61b46: `Colorado`, s4b3298c4c6db9310: str`\$${0} de descuento en ${1}`, + s4c498b8628becdec: `un calentador de agua`, + s4fc629e3c9345af1: `un vehículo usado`, s50994ba3f0a45429: `Presentado en colaboración con`, + s50c22fc6197c004a: `un vehículo nuevo`, s50e95c2064db3522: `Calculadora por`, s56f9b2194465973e: `tonelada`, s57355ce02837df3c: `Illinois`, @@ -75,6 +83,7 @@ export const templates = { s635e3d7c474426ce: `Descuento en una secadora con bomba de calor`, s64fbe9b97e397b12: `Estamos dedicados a proteger su privacidad.`, s675cdfc08b387d5a: `Ohio`, + s67d20e39f7ca49f9: `aislamiento del piso`, s67ef2eb5002eaaa5: `Podría ser que no hay incentivos en su área o que usted no califica financieramente para recibir ningún incentivo.`, s682a379fdd67cad0: `Alabama`, s68467de4d67c9732: `pie cuadrado`, @@ -89,6 +98,8 @@ export const templates = { s777612e8117ff021: `Tennessee`, s79fd9ba9e498d3da: str`${0} del costo de ${1}, hasta \$${2}`, s7a73c90a7c3b43b4: `un calentador de agua con bomba de calor`, + s7ac656d7ee9d3ee9: `una auditoría energética`, + s7b55523ddae83552: `sellado de ductos`, s7bd0de02e230dc75: `Calentador de agua`, s7d340cf80adae3a4: `Misuri`, s7f5b705d1bd02849: `La elegibilidad depende de la ubicación de residencia.`, @@ -96,9 +107,13 @@ export const templates = { s81aa671e64f2010e: `Dakota del Norte`, s82397872ac9bddcf: `Tamaño del hogar`, s863623721dcb0072: str`Esperado en ${0}`, + s8735995522894316: `una secadora`, s8b29a87eb1bdd138: `Otros incentivos disponibles para usted`, + s8d144dc64bfb64a5: `un vehículo híbrido enchufable usado`, + s8e22cc4a5315cc18: `sellado del aire`, s8e35f6b4e6e0adb9: `Pensilvania`, s8e7e52ad112342ab: `un cargador de vehículos eléctricos`, + s8eb01eb62cbb17ed: `reemplazo de puertas`, s8ee1a34f2ada948d: str`Los lineamientos federals permiten un reembolso de hasta \$${0}, basado en el ahorro de energía simulado o en el ahorro de energía medido logrado por el reacondicionamiento.`, s8f668dd0fa1b8c4e: `Reembolso por eficiencia`, s8fd029fdcc452602: `Ingrese su código postal para seleccionar una empresa de servicios eléctricos.`, @@ -111,9 +126,11 @@ export const templates = { s9eba0933010df96e: `Más información`, s9fbb21f25571fff1: `Le informaremos sobre incentivos, reembolsos, y más de Rewiring America.`, s9fc7054353f62fda: `Casado que presenta una declaración por separado`, + sa23c90be33fdda77: `una secadora eléctrica`, sa413e4572603726f: `Almacenamiento de baterías`, sa59611704b7a37f7: `Maryland`, sa8693630efda6dd3: `Crédito fiscal`, + sa9e16de0ec0154b3: `aire acondicionado central`, saa36bebd89cbd670: `Alaska`, sab07cf2bfae8483f: `Política de privacidad`, sabd5662f3f0a76be: `Descuento por adelantado`, @@ -133,18 +150,23 @@ export const templates = { sb661e8297dd681e2: `Incentivo`, sb694f3d582a0dbb9: `Selecciona "Cabeza de familia" si tiene un hijo o pariente que vive con usted y paga más de la mitad de los gastos de su hogar. Selecciona "Conjunta" si declara sus impuestos como pareja casada.`, sb6975fd8aa3fa26f: `Climatización`, + sb842284a98e8aed1: `una bomba de calor sin ductos`, sb932cf0734934406: `Su código postal ayuda a determinar la cantidad de descuentos y créditos fiscales para los que califica.`, sb9e8a9bfc8bcf398: `Rhode Island`, + sba0e115b257c6647: `una bomba de calor de fuente de aire`, sbafed52303428041: `https://homes.rewiringamerica.org/es/federal-incentives/home-efficiency-rebates`, + sbc39e1d2cd027ca9: `una bomba de calor con ductos`, sbc39ee880f922370: `Ninguno seleccionado`, sbc6f031a2851e3fd: str`\$${0}/${1} de descuento en ${2}, hasta \$${3}`, sbd6f35e4bb46fc68: `una estufa eléctrica/inducción`, sbd73a9b9860a2ae3: `Massachusetts`, sbe4508f09222c889: `Soltero`, sc026bbb64721085f: `Casado que presenta una declaración conjunta`, + sc2e0d466583b17f8: `aislamiento del entrepiso`, sc373af4c1a974b57: `Cocina`, sc5b20cb72269bc4f: `Los propietarios y inquilinos califican para diferentes incentivos.`, sc9266b1b6ae1aad4: `Esperado en 2024-2025`, + sc991c5ecbb3023ef: `aislamiento térmico`, sc997cfdf24ba9b58: `Aún no tenemos datos sobre las empresas de servicios eléctricos en su área.`, sc9e494c8346b7cb5: `Otra`, sca61f9664c0f6099: `https://homes.rewiringamerica.org/es/federal-incentives/home-electrification-appliance-rebates`, @@ -162,6 +184,7 @@ export const templates = { sdb1f28de105f079e: `Dirección de correo electrónico (opcional)`, sdc1f8a2139793221: `Volver a la calculadora`, sdca5ca4d7a86693d: `Deseleccionar todo`, + se024b39a39b61641: `aislamiento del muro`, se0f90ce39968b1d5: `Cabeza de familia`, se4572a1a2528a7d4: `Código postal`, se510125be9bcd015: `Seleccione su empresa de servicios eléctricos`, @@ -170,11 +193,15 @@ export const templates = { se6845b7d2d664ad8: `Dakota del Sur`, se86b0c32ca2d7413: `Ahora está suscrito a nuestro boletín informativo.`, se94a76041107bbf3: `Eléctrico`, + sec13c0ca288e67eb: `aislamiento del desván/techo`, + secc3df112f8ab69d: `un vehículo eléctrico`, sf05c1c17e90b0ddd: `Inquilino`, sf12bd6cb13bc154f: `Arizona`, sf1e2063bc64f3539: `Arkansas`, sf3affd5c9f2915e0: `Connecticut`, sf459faf797f733a9: `Impermeabilización`, + sf59668536419404a: `aislamiento del sótano`, + sf685070e5a85e7d1: `reemplazo de ductos`, sf844ac31d5fba2c9: `impermeabilización`, sf8b5deb9ea9f5054: `Descuento en una estufa eléctrica`, sfa7338035e1ef173: `Alquilar o poseer`, diff --git a/src/i18n/use-translated.ts b/src/i18n/use-translated.ts index fd48e6e..0978290 100644 --- a/src/i18n/use-translated.ts +++ b/src/i18n/use-translated.ts @@ -107,3 +107,9 @@ export const useTranslated = (): { msg: MsgFn; locale: Locale } => { return { msg, locale }; }; + +/** FOR TESTING ONLY: a version of msg that just passes the original through. */ +export const passthroughMsg: MsgFn = str => + typeof str === 'string' + ? str + : assembleStrResult(str.template, str.values, null); diff --git a/src/ira-rebates.ts b/src/ira-rebates.ts index 3a1805a..3b1d100 100644 --- a/src/ira-rebates.ts +++ b/src/ira-rebates.ts @@ -34,7 +34,7 @@ const hearRebates: { maxAmount: 2500, }, { - project: 'heat_pump_water_heater', + project: 'water_heater', getHeadline: msg => msg('Discount off a heat pump water heater'), maxAmount: 1750, }, @@ -44,7 +44,7 @@ const hearRebates: { maxAmount: 8000, }, { - project: 'heat_pump_clothes_dryer', + project: 'clothes_dryer', getHeadline: msg => msg('Discount off a heat pump clothes dryer'), maxAmount: 840, }, diff --git a/src/item-name.ts b/src/item-name.ts new file mode 100644 index 0000000..64a93a2 --- /dev/null +++ b/src/item-name.ts @@ -0,0 +1,297 @@ +import { ItemType } from './api/calculator-types-v1'; +import { MsgFn } from './i18n/use-translated'; + +type ItemGroup = + | 'air_source_heat_pump' + | 'clothes_dryer' + | 'generic_heat_pump' + | 'electric_vehicle' + | 'plugin_hybrid' + | 'new_vehicle' + | 'used_vehicle' + | 'insulation' + | 'weatherization' + | 'water_heater'; + +/** + * Some incentives are for multiple items. These groups define headlines for + * such incentives: if the incentive's items are a subset of one of these + * groups, it will be shown with a unified name. + * + * Note that the groups are checked in order, so if one group is a subset of + * another, the smaller group should be listed first. (E.g. air source heat + * pumps and generic heat pumps.) + */ +const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ + { + group: 'air_source_heat_pump', + members: new Set([ + 'ducted_heat_pump', + 'ductless_heat_pump', + 'air_to_water_heat_pump', + ]), + }, + { + group: 'clothes_dryer', + members: new Set([ + 'heat_pump_clothes_dryer', + 'non_heat_pump_clothes_dryer', + ]), + }, + { + group: 'generic_heat_pump', + members: new Set([ + 'air_to_water_heat_pump', + 'ducted_heat_pump', + 'ductless_heat_pump', + 'geothermal_heating_installation', + 'other_heat_pump', + ]), + }, + { + group: 'electric_vehicle', + members: new Set(['new_electric_vehicle', 'used_electric_vehicle']), + }, + { + group: 'plugin_hybrid', + members: new Set([ + 'new_plugin_hybrid_vehicle', + 'used_plugin_hybrid_vehicle', + ]), + }, + { + group: 'new_vehicle', + members: new Set(['new_electric_vehicle', 'new_plugin_hybrid_vehicle']), + }, + { + group: 'used_vehicle', + members: new Set(['used_electric_vehicle', 'used_plugin_hybrid_vehicle']), + }, + { + group: 'insulation', + members: new Set([ + 'attic_or_roof_insulation', + 'basement_insulation', + 'crawlspace_insulation', + 'floor_insulation', + 'other_insulation', + 'wall_insulation', + ]), + }, + { + group: 'weatherization', + members: new Set([ + 'air_sealing', + 'attic_or_roof_insulation', + 'basement_insulation', + 'crawlspace_insulation', + 'door_replacement', + 'duct_replacement', + 'duct_sealing', + 'floor_insulation', + 'wall_insulation', + 'weatherization', + 'window_replacement', + 'efficiency_rebates', + 'other_insulation', + 'other_weatherization', + ]), + }, + { + group: 'water_heater', + members: new Set(['heat_pump_water_heater', 'non_heat_pump_water_heater']), + }, +]; + +const itemsBelongToGroup = (items: ItemType[], members: Set) => { + return items.every(i => members.has(i)); +}; + +const multipleItemsName = (items: ItemType[], msg: MsgFn) => { + // For a multiple-items case, check whether all the items are in one of the + // defined groups. + for (const { group, members } of ITEM_GROUPS) { + if (itemsBelongToGroup(items, members)) { + switch (group) { + case 'air_source_heat_pump': + return msg('an air source heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'clothes_dryer': + return msg('a clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'electric_vehicle': + return msg('an electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'plugin_hybrid': + return msg('a plug-in hybrid', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'new_vehicle': + return msg('a new vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'used_vehicle': + return msg('a used vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'generic_heat_pump': + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + case 'insulation': + return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); + case 'weatherization': + return msg('weatherization', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'water_heater': + return msg('a water heater', { + desc: 'e.g. "$100 off [this string]"', + }); + default: { + // This will be a type error if the above switch is not exhaustive + const unknownGroup: never = group; + console.error(`no name for ${unknownGroup}`); + } + } + } + } + + return null; +}; + +/** + * TODO this is an internationalization sin. Figure out something better! + */ +export const itemName = (items: ItemType[], msg: MsgFn) => { + if (items.length > 1) { + return multipleItemsName(items, msg); + } + + if (items.length !== 1) { + return null; + } + + const item = items[0]; + switch (item) { + case 'air_sealing': + return msg('air sealing', { desc: 'e.g. "$100 off [this string]"' }); + case 'air_to_water_heat_pump': + return msg('an air-to-water heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'attic_or_roof_insulation': + return msg('attic/roof insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'basement_insulation': + return msg('basement insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'battery_storage_installation': + return msg('battery storage', { desc: 'e.g. "$100 off [this string]"' }); + case 'central_air_conditioner': + return msg('a central air conditioner', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'crawlspace_insulation': + return msg('crawlspace insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'door_replacement': + return msg('door replacement', { desc: 'e.g. "$100 off [this string]"' }); + case 'duct_replacement': + return msg('duct replacement', { desc: 'e.g. "$100 off [this string]"' }); + case 'duct_sealing': + return msg('duct sealing', { desc: 'e.g. "$100 off [this string]"' }); + case 'ducted_heat_pump': + return msg('a ducted heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'ductless_heat_pump': + return msg('a ductless heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'electric_panel': + return msg('an electric panel', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'electric_stove': + return msg('an electric/induction stove', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'electric_vehicle_charger': + return msg('an EV charger', { desc: 'e.g. "$100 off [this string]"' }); + case 'electric_wiring': + return msg('electric wiring', { desc: 'e.g. "$100 off [this string]"' }); + case 'energy_audit': + return msg('an energy audit', { desc: 'e.g. "$100 off [this string]"' }); + case 'floor_insulation': + return msg('floor insulation', { desc: 'e.g. "$100 off [this string]"' }); + case 'geothermal_heating_installation': + return msg('geothermal heating installation', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'heat_pump_air_conditioner_heater': + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + case 'heat_pump_clothes_dryer': + return msg('a heat pump clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'heat_pump_water_heater': + return msg('a heat pump water heater', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'new_electric_vehicle': + return msg('a new electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'new_plugin_hybrid_vehicle': + return msg('a new plug-in hybrid', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'non_heat_pump_clothes_dryer': + return msg('an electric clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'non_heat_pump_water_heater': + return msg('an electric water heater', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'other_heat_pump': + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + case 'other_insulation': + return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); + case 'other_weatherization': + return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); + case 'rooftop_solar_installation': + return msg('rooftop solar', { desc: 'e.g. "$100 off [this string]"' }); + case 'used_electric_vehicle': + return msg('a used electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'used_plugin_hybrid_vehicle': + return msg('a used plug-in hybrid', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'wall_insulation': + return msg('wall insulation', { desc: 'e.g. "$100 off [this string]"' }); + case 'weatherization': + return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); + case 'window_replacement': + return msg('window replacement', { + desc: 'e.g. "$100 off [this string]"', + }); + case 'efficiency_rebates': + return msg('an energy efficiency retrofit', { + desc: 'e.g. "$100 off [this string]"', + }); + default: { + // This will be a type error if the above if-else is not exhaustive + const unknownItem: never = item; + console.error(`no name for item ${unknownItem}`); + return null; + } + } +}; diff --git a/src/projects.tsx b/src/projects.tsx index 729cbe3..256538b 100644 --- a/src/projects.tsx +++ b/src/projects.tsx @@ -18,12 +18,12 @@ type ProjectInfo = { }; export type Project = - | 'heat_pump_clothes_dryer' + | 'clothes_dryer' | 'hvac' | 'ev' | 'solar' | 'battery' - | 'heat_pump_water_heater' + | 'water_heater' | 'cooking' | 'wiring' | 'weatherization_and_efficiency'; @@ -38,15 +38,20 @@ export const shortLabel = (p: Project, msg: MsgFn) => * show incentives. */ export const PROJECTS: Record = { - heat_pump_clothes_dryer: { - items: ['heat_pump_clothes_dryer'], + clothes_dryer: { + items: ['heat_pump_clothes_dryer', 'non_heat_pump_clothes_dryer'], label: msg => msg('Clothes dryer'), getIcon: () => , }, hvac: { items: [ - 'heat_pump_air_conditioner_heater', + 'air_to_water_heat_pump', + 'central_air_conditioner', + 'ducted_heat_pump', + 'ductless_heat_pump', 'geothermal_heating_installation', + 'other_heat_pump', + 'heat_pump_air_conditioner_heater', ], label: msg => msg('Heating, ventilation & cooling'), shortLabel: msg => @@ -59,6 +64,8 @@ export const PROJECTS: Record = { items: [ 'new_electric_vehicle', 'used_electric_vehicle', + 'new_plugin_hybrid_vehicle', + 'used_plugin_hybrid_vehicle', 'electric_vehicle_charger', ], label: msg => msg('Electric vehicle'), @@ -76,8 +83,8 @@ export const PROJECTS: Record = { label: msg => msg('Battery storage'), getIcon: () => , }, - heat_pump_water_heater: { - items: ['heat_pump_water_heater'], + water_heater: { + items: ['heat_pump_water_heater', 'non_heat_pump_water_heater'], label: msg => msg('Water heater'), getIcon: () => , }, @@ -96,7 +103,22 @@ export const PROJECTS: Record = { getIcon: () => , }, weatherization_and_efficiency: { - items: ['weatherization', 'efficiency_rebates'], + items: [ + 'air_sealing', + 'attic_or_roof_insulation', + 'basement_insulation', + 'crawlspace_insulation', + 'door_replacement', + 'duct_replacement', + 'duct_sealing', + 'floor_insulation', + 'wall_insulation', + 'weatherization', + 'window_replacement', + 'efficiency_rebates', + 'other_insulation', + 'other_weatherization', + ], label: msg => msg('Weatherization & efficiency'), shortLabel: msg => msg('Weatherization'), getIcon: () => , diff --git a/src/state-incentive-details.tsx b/src/state-incentive-details.tsx index cf98990..291780e 100644 --- a/src/state-incentive-details.tsx +++ b/src/state-incentive-details.tsx @@ -6,7 +6,6 @@ import { AmountUnit, Incentive, IncentiveType, - ItemType, } from './api/calculator-types-v1'; import { getYear, isInFuture } from './api/dates'; import { PrimaryButton, TextButton } from './buttons'; @@ -18,6 +17,7 @@ import { MsgFn, useTranslated } from './i18n/use-translated'; import { IconTabBar } from './icon-tab-bar'; import { ExclamationPoint, UpRightArrow } from './icons'; import { IRARebate, getRebatesFor } from './ira-rebates'; +import { itemName } from './item-name'; import { PartnerLogos } from './partner-logos'; import { PROJECTS, Project, shortLabel } from './projects'; import { Separator } from './separator'; @@ -38,8 +38,11 @@ const formatUnit = (unit: AmountUnit, msg: MsgFn) => : unit; const formatTitle = (incentive: Incentive, msg: MsgFn) => { - const itemValue = incentive.items ? incentive.items[0] : incentive.item.type; - const item = itemName(itemValue, msg); + const item = itemName(incentive.items, msg); + if (!item) { + return null; + } + const amount = incentive.amount; if (amount.type === 'dollar_amount') { return amount.maximum @@ -86,56 +89,6 @@ const formatTitle = (incentive: Incentive, msg: MsgFn) => { } }; -/** - * TODO this is an internationalization sin. Figure out something better! - */ -const itemName = (itemType: ItemType, msg: MsgFn) => - itemType === 'battery_storage_installation' - ? msg('battery storage', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'electric_panel' - ? msg('an electric panel', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'electric_stove' - ? msg('an electric/induction stove', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'electric_vehicle_charger' - ? msg('an EV charger', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'electric_wiring' - ? msg('electric wiring', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'geothermal_heating_installation' - ? msg('geothermal heating installation', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'heat_pump_air_conditioner_heater' - ? msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'heat_pump_clothes_dryer' - ? msg('a heat pump clothes dryer', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'heat_pump_water_heater' - ? msg('a heat pump water heater', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'new_electric_vehicle' - ? msg('a new electric vehicle', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'rooftop_solar_installation' - ? msg('rooftop solar', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'used_electric_vehicle' - ? msg('a used electric vehicle', { - desc: 'e.g. "$100 off [this string]"', - }) - : itemType === 'weatherization' - ? msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }) - : itemType === 'efficiency_rebates' - ? msg('an energy efficiency retrofit', { - desc: 'e.g. "$100 off [this string]"', - }) - : null; - const formatIncentiveType = (payment_methods: IncentiveType[], msg: MsgFn) => payment_methods[0] === 'tax_credit' ? msg('Tax credit') @@ -349,6 +302,13 @@ const renderCardCollection = ( (getStartYearIfInFuture(a) ?? 0) - (getStartYearIfInFuture(b) ?? 0), ) .map((incentive, index) => { + const headline = formatTitle(incentive, msg); + if (!headline) { + // We couldn't generate a headline either because the items are + // unknown, or the amount type is unknown. Don't show a card. + return null; + } + const futureStartYear = getStartYearIfInFuture(incentive); // The API cannot precisely tell, from zip code alone, whether the @@ -378,7 +338,7 @@ const renderCardCollection = ( = ({ .filter(i => i.eligible) .filter(i => !isIRARebate(i)); + // Map each project to all incentives that involve it. An incentive may + // be in multiple projects, if it has multiple items and those items pertain + // to different projects. const incentivesByProject = Object.fromEntries( Object.entries(PROJECTS).map(([project, projectInfo]) => [ project, - allEligible.filter(i => - projectInfo.items.includes(i.items ? i.items[0] : i.item.type), + allEligible.filter(incentive => + incentive.items.every(item => projectInfo.items.includes(item)), ), ]), ) as Record; diff --git a/test/item-names.test.ts b/test/item-names.test.ts new file mode 100644 index 0000000..80e7377 --- /dev/null +++ b/test/item-names.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from '@jest/globals'; +import { passthroughMsg as msg } from '../src/i18n/use-translated'; +import { itemName } from '../src/item-name'; + +describe('group names', () => { + test('heat pumps', () => { + expect(itemName(['ducted_heat_pump', 'ductless_heat_pump'], msg)).toBe( + 'an air source heat pump', + ); + expect( + itemName(['ducted_heat_pump', 'geothermal_heating_installation'], msg), + ).toBe('a heat pump'); + }); + + test('weatherization and insulation', () => { + expect( + itemName(['attic_or_roof_insulation', 'basement_insulation'], msg), + ).toBe('insulation'); + expect(itemName(['attic_or_roof_insulation', 'air_sealing'], msg)).toBe( + 'weatherization', + ); + expect(itemName(['wall_insulation', 'other_insulation'], msg)).toBe( + 'insulation', + ); + expect(itemName(['wall_insulation', 'other_weatherization'], msg)).toBe( + 'weatherization', + ); + expect(itemName(['other_weatherization', 'energy_audit'], msg)).toBeNull(); + }); + + test('vehicles', () => { + expect( + itemName(['new_electric_vehicle', 'used_electric_vehicle'], msg), + ).toBe('an electric vehicle'); + expect( + itemName( + ['new_plugin_hybrid_vehicle', 'used_plugin_hybrid_vehicle'], + msg, + ), + ).toBe('a plug-in hybrid'); + expect( + itemName(['new_electric_vehicle', 'new_plugin_hybrid_vehicle'], msg), + ).toBe('a new vehicle'); + expect( + itemName(['used_electric_vehicle', 'used_plugin_hybrid_vehicle'], msg), + ).toBe('a used vehicle'); + + expect( + itemName( + [ + 'new_electric_vehicle', + 'used_electric_vehicle', + 'new_plugin_hybrid_vehicle', + 'used_plugin_hybrid_vehicle', + ], + msg, + ), + ).toBeNull(); + }); +}); diff --git a/translations/es.xlf b/translations/es.xlf index f5beaec..0d9b0e6 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -660,6 +660,141 @@ The federal guidelines allow for a rebate of up to $, based on the modeled energy savings or measured energy savings achieved by the retrofit. Los lineamientos federals permiten un reembolso de hasta $, basado en el ahorro de energía simulado o en el ahorro de energía medido logrado por el reacondicionamiento. + + air sealing + sellado del aire + e.g. "$100 off [this string]" + + + an air-to-water heat pump + una bomba de calor de aire a agua + e.g. "$100 off [this string]" + + + attic/roof insulation + aislamiento del desván/techo + e.g. "$100 off [this string]" + + + basement insulation + aislamiento del sótano + e.g. "$100 off [this string]" + + + a central air conditioner + aire acondicionado central + e.g. "$100 off [this string]" + + + crawlspace insulation + aislamiento del entrepiso + e.g. "$100 off [this string]" + + + door replacement + reemplazo de puertas + e.g. "$100 off [this string]" + + + duct replacement + reemplazo de ductos + e.g. "$100 off [this string]" + + + duct sealing + sellado de ductos + e.g. "$100 off [this string]" + + + a ducted heat pump + una bomba de calor con ductos + e.g. "$100 off [this string]" + + + a ductless heat pump + una bomba de calor sin ductos + e.g. "$100 off [this string]" + + + an energy audit + una auditoría energética + e.g. "$100 off [this string]" + + + floor insulation + aislamiento del piso + e.g. "$100 off [this string]" + + + a new plug-in hybrid + un vehículo híbrido enchufable nuevo + e.g. "$100 off [this string]" + + + an electric clothes dryer + una secadora eléctrica + e.g. "$100 off [this string]" + + + an electric water heater + un calentador de agua eléctrico + e.g. "$100 off [this string]" + + + insulation + aislamiento térmico + e.g. "$100 off [this string]" + + + a used plug-in hybrid + un vehículo híbrido enchufable usado + e.g. "$100 off [this string]" + + + wall insulation + aislamiento del muro + e.g. "$100 off [this string]" + + + window replacement + reemplazo de ventanas + e.g. "$100 off [this string]" + + + an air source heat pump + una bomba de calor de fuente de aire + e.g. "$100 off [this string]" + + + an electric vehicle + un vehículo eléctrico + e.g. "$100 off [this string]" + + + a clothes dryer + una secadora + e.g. "$100 off [this string]" + + + a water heater + un calentador de agua + e.g. "$100 off [this string]" + + + a plug-in hybrid + un vehículo híbrido enchufable + e.g. "$100 off [this string]" + + + a new vehicle + un vehículo nuevo + e.g. "$100 off [this string]" + + + a used vehicle + un vehículo usado + e.g. "$100 off [this string]" +