From a45bfc0f95c44da098c25661c82cb559729bd759 Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Thu, 2 May 2024 15:39:41 -0400 Subject: [PATCH 1/3] Handle multi-item incentives, and more items ## Description This adds human-friendly labels for all the new items, plus labels for a few specific groups of items. (E.g. an incentive for both `ducted_heat_pump` and `ductless_heat_pump` will be shown as being for "an air-source heat pump".) I factored out the name logic into a new file because it's getting pretty extensive. The Spanish translations are taken from existing translations of incentive descriptions (which, helpfully, serves to validate that all the new items exist in incentives we already have). So I can't fully vouch for them myself, but they are sourced from our human-vetted translations. ## Test Plan Cypress tests pass. New unit test for names of multi-item incentives. Manual testing of a few queries. The results should still match prod exactly because none of the new items are used in the backend yet. (The exception is the non-HP dryers and water heaters -- those are already being used in the backend so will newly show up in the frontend with this PR. Looked at those in English and Spanish to make sure the headline looks good.) --- src/api/calculator-types-v1.ts | 61 ++++++---- src/i18n/strings/es.ts | 22 ++++ src/i18n/use-translated.ts | 6 + src/ira-rebates.ts | 4 +- src/item-name.ts | 206 ++++++++++++++++++++++++++++++++ src/projects.tsx | 38 ++++-- src/state-incentive-details.tsx | 75 +++--------- test/item-names.test.ts | 14 +++ translations/es.xlf | 110 +++++++++++++++++ 9 files changed, 449 insertions(+), 87 deletions(-) create mode 100644 src/item-name.ts create mode 100644 test/item-names.test.ts 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..00276b5 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,7 @@ 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.`, + s2361ab00b9155f96: `una bomba de calor de aire a agua`, s24ea3f43230aae41: `usted@example.com`, s255857544a9d5ec0: `Reiniciar`, s26946b3c896d36d6: `Vehículo eléctrico`, @@ -58,6 +61,7 @@ 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}`, @@ -75,6 +79,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 +94,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.`, @@ -97,8 +104,11 @@ export const templates = { s82397872ac9bddcf: `Tamaño del hogar`, s863623721dcb0072: str`Esperado en ${0}`, 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 +121,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 +145,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 +179,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 +188,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..8d6aa6a --- /dev/null +++ b/src/item-name.ts @@ -0,0 +1,206 @@ +import { ItemType } from './api/calculator-types-v1'; +import { MsgFn } from './i18n/use-translated'; + +type ItemGroup = + | 'air_source_heat_pump' + | 'generic_heat_pump' + | 'electric_vehicle' + | 'insulation'; + +/** + * 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: '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: 'insulation', + members: new Set([ + 'attic_or_roof_insulation', + 'basement_insulation', + 'crawlspace_insulation', + 'floor_insulation', + 'other_insulation', + 'wall_insulation', + ]), + }, +]; + +/** + * TODO this is an internationalization sin. Figure out something better! + */ +export const itemName = (items: ItemType[], msg: MsgFn) => { + if (items.length === 1) { + const item = items[0]; + if (item === 'air_sealing') { + return msg('air sealing', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'air_to_water_heat_pump') { + return msg('an air-to-water heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'attic_or_roof_insulation') { + return msg('attic/roof insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'basement_insulation') { + return msg('basement insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'battery_storage_installation') { + return msg('battery storage', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'central_air_conditioner') { + return msg('a central air conditioner', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'crawlspace_insulation') { + return msg('crawlspace insulation', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'door_replacement') { + return msg('door replacement', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'duct_replacement') { + return msg('duct replacement', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'duct_sealing') { + return msg('duct sealing', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'ducted_heat_pump') { + return msg('a ducted heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'ductless_heat_pump') { + return msg('a ductless heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'electric_panel') { + return msg('an electric panel', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'electric_stove') { + return msg('an electric/induction stove', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'electric_vehicle_charger') { + return msg('an EV charger', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'electric_wiring') { + return msg('electric wiring', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'energy_audit') { + return msg('an energy audit', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'floor_insulation') { + return msg('floor insulation', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'geothermal_heating_installation') { + return msg('geothermal heating installation', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'heat_pump_air_conditioner_heater') { + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'heat_pump_clothes_dryer') { + return msg('a heat pump clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'heat_pump_water_heater') { + return msg('a heat pump water heater', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'new_electric_vehicle') { + return msg('a new electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'new_plugin_hybrid_vehicle') { + return msg('a new plug-in hybrid', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'non_heat_pump_clothes_dryer') { + return msg('an electric clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'non_heat_pump_water_heater') { + return msg('an electric water heater', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'other_heat_pump') { + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'other_insulation') { + return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'other_weatherization') { + return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'rooftop_solar_installation') { + return msg('rooftop solar', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'used_electric_vehicle') { + return msg('a used electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'used_plugin_hybrid_vehicle') { + return msg('a used plug-in hybrid', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'wall_insulation') { + return msg('wall insulation', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'weatherization') { + return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); + } else if (item === 'window_replacement') { + return msg('window replacement', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (item === 'efficiency_rebates') { + return msg('an energy efficiency retrofit', { + desc: 'e.g. "$100 off [this string]"', + }); + } + + // 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; + } + + // 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 (items.every(i => members.has(i))) { + if (group === 'air_source_heat_pump') { + return msg('an air source heat pump', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (group === 'electric_vehicle') { + return msg('an electric vehicle', { + desc: 'e.g. "$100 off [this string]"', + }); + } else if (group === 'generic_heat_pump') { + return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); + } else if (group === 'insulation') { + return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); + } else { + // This will be a type error if the above if-else is not exhaustive + const unknownGroup: never = group; + console.error(`no name for ${unknownGroup}`); + } + } + } + + 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..7a6c91a --- /dev/null +++ b/test/item-names.test.ts @@ -0,0 +1,14 @@ +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'); + }); +}); diff --git a/translations/es.xlf b/translations/es.xlf index f5beaec..c43ae47 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -660,6 +660,116 @@ 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]" + From 97a8b7c4c1275365f384d06c4cc908bcbfd18ef9 Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Thu, 2 May 2024 19:09:19 -0400 Subject: [PATCH 2/3] add clothes dryer and water heater groups --- src/i18n/strings/es.ts | 2 ++ src/item-name.ts | 21 ++++++++++++++++++++- translations/es.xlf | 10 ++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.ts b/src/i18n/strings/es.ts index 00276b5..d3e1558 100644 --- a/src/i18n/strings/es.ts +++ b/src/i18n/strings/es.ts @@ -65,6 +65,7 @@ export const templates = { s48b23a6c3431c19e: `una secadora con bomba de calor`, s4b0d7bd386d61b46: `Colorado`, s4b3298c4c6db9310: str`\$${0} de descuento en ${1}`, + s4c498b8628becdec: `un calentador de agua`, s50994ba3f0a45429: `Presentado en colaboración con`, s50e95c2064db3522: `Calculadora por`, s56f9b2194465973e: `tonelada`, @@ -103,6 +104,7 @@ 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`, diff --git a/src/item-name.ts b/src/item-name.ts index 8d6aa6a..473f72e 100644 --- a/src/item-name.ts +++ b/src/item-name.ts @@ -3,9 +3,11 @@ import { MsgFn } from './i18n/use-translated'; type ItemGroup = | 'air_source_heat_pump' + | 'clothes_dryer' | 'generic_heat_pump' | 'electric_vehicle' - | 'insulation'; + | 'insulation' + | 'water_heater'; /** * Some incentives are for multiple items. These groups define headlines for @@ -25,6 +27,13 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ '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([ @@ -50,6 +59,10 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ 'wall_insulation', ]), }, + { + group: 'water_heater', + members: new Set(['heat_pump_water_heater', 'non_heat_pump_water_heater']), + }, ]; /** @@ -186,6 +199,10 @@ export const itemName = (items: ItemType[], msg: MsgFn) => { return msg('an air source heat pump', { desc: 'e.g. "$100 off [this string]"', }); + } else if (group === 'clothes_dryer') { + return msg('a clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }); } else if (group === 'electric_vehicle') { return msg('an electric vehicle', { desc: 'e.g. "$100 off [this string]"', @@ -194,6 +211,8 @@ export const itemName = (items: ItemType[], msg: MsgFn) => { return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); } else if (group === 'insulation') { return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); + } else if (group === 'water_heater') { + return msg('a water heater', { desc: 'e.g. "$100 off [this string]"' }); } else { // This will be a type error if the above if-else is not exhaustive const unknownGroup: never = group; diff --git a/translations/es.xlf b/translations/es.xlf index c43ae47..700db86 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -770,6 +770,16 @@ 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]" + From c188d8577b4a0f7a979d20ae8e376c494c4d682c Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Tue, 7 May 2024 18:40:05 -0400 Subject: [PATCH 3/3] review feedback; add a few more groups and tests --- src/i18n/strings/es.ts | 3 + src/item-name.ts | 222 ++++++++++++++++++++++++++-------------- test/item-names.test.ts | 46 +++++++++ translations/es.xlf | 15 +++ 4 files changed, 211 insertions(+), 75 deletions(-) diff --git a/src/i18n/strings/es.ts b/src/i18n/strings/es.ts index d3e1558..dbe06ab 100644 --- a/src/i18n/strings/es.ts +++ b/src/i18n/strings/es.ts @@ -28,6 +28,7 @@ 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`, @@ -66,7 +67,9 @@ export const templates = { 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`, diff --git a/src/item-name.ts b/src/item-name.ts index 473f72e..64a93a2 100644 --- a/src/item-name.ts +++ b/src/item-name.ts @@ -6,7 +6,11 @@ type ItemGroup = | 'clothes_dryer' | 'generic_heat_pump' | 'electric_vehicle' + | 'plugin_hybrid' + | 'new_vehicle' + | 'used_vehicle' | 'insulation' + | 'weatherization' | 'water_heater'; /** @@ -48,6 +52,21 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ 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([ @@ -59,167 +78,220 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ '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) { - const item = items[0]; - if (item === 'air_sealing') { + 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]"' }); - } else if (item === 'air_to_water_heat_pump') { + case 'air_to_water_heat_pump': return msg('an air-to-water heat pump', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'attic_or_roof_insulation') { + case 'attic_or_roof_insulation': return msg('attic/roof insulation', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'basement_insulation') { + case 'basement_insulation': return msg('basement insulation', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'battery_storage_installation') { + case 'battery_storage_installation': return msg('battery storage', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'central_air_conditioner') { + case 'central_air_conditioner': return msg('a central air conditioner', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'crawlspace_insulation') { + case 'crawlspace_insulation': return msg('crawlspace insulation', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'door_replacement') { + case 'door_replacement': return msg('door replacement', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'duct_replacement') { + case 'duct_replacement': return msg('duct replacement', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'duct_sealing') { + case 'duct_sealing': return msg('duct sealing', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'ducted_heat_pump') { + case 'ducted_heat_pump': return msg('a ducted heat pump', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'ductless_heat_pump') { + case 'ductless_heat_pump': return msg('a ductless heat pump', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'electric_panel') { + case 'electric_panel': return msg('an electric panel', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'electric_stove') { + case 'electric_stove': return msg('an electric/induction stove', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'electric_vehicle_charger') { + case 'electric_vehicle_charger': return msg('an EV charger', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'electric_wiring') { + case 'electric_wiring': return msg('electric wiring', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'energy_audit') { + case 'energy_audit': return msg('an energy audit', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'floor_insulation') { + case 'floor_insulation': return msg('floor insulation', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'geothermal_heating_installation') { + case 'geothermal_heating_installation': return msg('geothermal heating installation', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'heat_pump_air_conditioner_heater') { + case 'heat_pump_air_conditioner_heater': return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'heat_pump_clothes_dryer') { + case 'heat_pump_clothes_dryer': return msg('a heat pump clothes dryer', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'heat_pump_water_heater') { + case 'heat_pump_water_heater': return msg('a heat pump water heater', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'new_electric_vehicle') { + case 'new_electric_vehicle': return msg('a new electric vehicle', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'new_plugin_hybrid_vehicle') { + case 'new_plugin_hybrid_vehicle': return msg('a new plug-in hybrid', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'non_heat_pump_clothes_dryer') { + case 'non_heat_pump_clothes_dryer': return msg('an electric clothes dryer', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'non_heat_pump_water_heater') { + case 'non_heat_pump_water_heater': return msg('an electric water heater', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'other_heat_pump') { + case 'other_heat_pump': return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'other_insulation') { + case 'other_insulation': return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'other_weatherization') { + case 'other_weatherization': return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'rooftop_solar_installation') { + case 'rooftop_solar_installation': return msg('rooftop solar', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'used_electric_vehicle') { + case 'used_electric_vehicle': return msg('a used electric vehicle', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'used_plugin_hybrid_vehicle') { + case 'used_plugin_hybrid_vehicle': return msg('a used plug-in hybrid', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'wall_insulation') { + case 'wall_insulation': return msg('wall insulation', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'weatherization') { + case 'weatherization': return msg('weatherization', { desc: 'e.g. "$100 off [this string]"' }); - } else if (item === 'window_replacement') { + case 'window_replacement': return msg('window replacement', { desc: 'e.g. "$100 off [this string]"', }); - } else if (item === 'efficiency_rebates') { + 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; } - - // 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; } - - // 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 (items.every(i => members.has(i))) { - if (group === 'air_source_heat_pump') { - return msg('an air source heat pump', { - desc: 'e.g. "$100 off [this string]"', - }); - } else if (group === 'clothes_dryer') { - return msg('a clothes dryer', { - desc: 'e.g. "$100 off [this string]"', - }); - } else if (group === 'electric_vehicle') { - return msg('an electric vehicle', { - desc: 'e.g. "$100 off [this string]"', - }); - } else if (group === 'generic_heat_pump') { - return msg('a heat pump', { desc: 'e.g. "$100 off [this string]"' }); - } else if (group === 'insulation') { - return msg('insulation', { desc: 'e.g. "$100 off [this string]"' }); - } else if (group === 'water_heater') { - return msg('a water heater', { desc: 'e.g. "$100 off [this string]"' }); - } else { - // This will be a type error if the above if-else is not exhaustive - const unknownGroup: never = group; - console.error(`no name for ${unknownGroup}`); - } - } - } - - return null; }; diff --git a/test/item-names.test.ts b/test/item-names.test.ts index 7a6c91a..80e7377 100644 --- a/test/item-names.test.ts +++ b/test/item-names.test.ts @@ -11,4 +11,50 @@ describe('group names', () => { 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 700db86..0d9b0e6 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -780,6 +780,21 @@ 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]" +