Skip to content
Merged
2 changes: 1 addition & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Urlaubsvertretung',
expensifyLogo: 'Expensify-Logo',
duplicateReport: 'Duplizierten Bericht',
explain: 'Erklären',
},
socials: {
podcast: 'Folgen Sie uns auf Podcast',
Expand Down Expand Up @@ -1534,7 +1535,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'Der Gesamtbetrag ist zu hoch. Verringere die Stunden oder reduziere den Satz.',
},
correctRateError: 'Beheben Sie den Kursfehler und versuchen Sie es erneut.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Erklären</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError:
'Sie können Per-Diem-Ausgaben nicht über mehrere Workspaces hinweg duplizieren, da sich die Sätze zwischen den Workspaces unterscheiden können.',
rulesModifiedFields: {
Expand Down
2 changes: 1 addition & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ const translations = {
quarter: 'Quarter',
vacationDelegate: 'Vacation delegate',
expensifyLogo: 'Expensify logo',
explain: 'Explain',
},
socials: {
podcast: 'Follow us on Podcast',
Expand Down Expand Up @@ -1552,7 +1553,6 @@ const translations = {
amountTooLargeError: 'The total amount is too large. Lower the hours or reduce the rate.',
},
correctRateError: 'Fix the rate error and try again.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Explain</strong></a> &#x2728;`,
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'),
billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ const translations: TranslationDeepObject<typeof en> = {
quarter: 'Trimestre',
vacationDelegate: 'Delegado de vacaciones',
expensifyLogo: 'Logo de Expensify',
explain: 'Explicar',
},
socials: {
podcast: 'Síguenos en Podcast',
Expand Down Expand Up @@ -1386,7 +1387,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'El importe total es demasiado alto. Reduce las horas o disminuye la tasa.',
},
correctRateError: 'Corrige el error de la tasa y vuelve a intentarlo.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Explicar</strong></a> &#x2728;`,
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcó el gasto como "reembolsable"' : 'marcó el gasto como "no reembolsable"'),
billable: (value: boolean) => (value ? 'marcó el gasto como "facturable"' : 'marcó el gasto como "no facturable"'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Délégué de vacances',
expensifyLogo: 'Logo Expensify',
duplicateReport: 'Note de frais en double',
explain: 'Expliquer',
},
socials: {
podcast: 'Suivez-nous sur Podcast',
Expand Down Expand Up @@ -1539,7 +1540,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'Le montant total est trop élevé. Réduisez le nombre d’heures ou diminuez le taux.',
},
correctRateError: 'Corrigez l’erreur de taux et réessayez.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Expliquer</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError:
'Vous ne pouvez pas dupliquer les indemnités journalières entre plusieurs espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.',
rulesModifiedFields: {
Expand Down
2 changes: 1 addition & 1 deletion src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Delega ferie',
expensifyLogo: 'Logo Expensify',
duplicateReport: 'Report duplicato',
explain: 'Spiega',
},
socials: {
podcast: 'Seguici su Podcast',
Expand Down Expand Up @@ -1531,7 +1532,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'L’importo totale è troppo alto. Riduci le ore o abbassa la tariffa.',
},
correctRateError: "Correggi l'errore di tariffa e riprova.",
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Spiega</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError: 'Non puoi duplicare le spese di diaria tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse tra gli spazi di lavoro.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'ha contrassegnato la spesa come "rimborsabile"' : 'ha contrassegnato la spesa come "non rimborsabile"'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: '休暇代理人',
expensifyLogo: 'Expensifyロゴ',
duplicateReport: 'レポートを複製',
explain: '説明',
},
socials: {
podcast: 'ポッドキャストでフォロー',
Expand Down Expand Up @@ -1521,7 +1522,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: '合計金額が大きすぎます。時間を減らすか、レートを下げてください。',
},
correctRateError: 'レートのエラーを修正して、もう一度お試しください。',
AskToExplain: `・<a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>説明</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError: 'ワークスペースごとに日当レートが異なる場合があるため、日当経費をワークスペース間で複製することはできません。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '経費を「精算対象」に指定しました' : '経費を「精算対象外」にマークしました'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Vertegenwoordiger tijdens vakantie',
expensifyLogo: 'Expensify-logo',
duplicateReport: 'Dubbel rapport',
explain: 'Uitleggen',
},
socials: {
podcast: 'Volg ons op Podcast',
Expand Down Expand Up @@ -1529,7 +1530,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'Het totale bedrag is te hoog. Verlaag het aantal uren of verlaag het tarief.',
},
correctRateError: 'Los de tarieffout op en probeer het opnieuw.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Uitleggen</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError: 'Je kunt dagvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.',
rulesModifiedFields: {

This comment was marked as off-topic.

reimbursable: (value: boolean) => (value ? 'markeerde de uitgave als „terugbetaalbaar”' : 'heeft de uitgave als ‘niet-vergoedbaar’ gemarkeerd'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Zastępca urlopowy',
expensifyLogo: 'Logo Expensify',
duplicateReport: 'Zduplikowany raport',
explain: 'Wyjaśnij',
},
socials: {
podcast: 'Śledź nas na Podcast',
Expand Down Expand Up @@ -1528,7 +1529,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'Łączna kwota jest zbyt wysoka. Zmniejsz liczbę godzin lub obniż stawkę.',
},
correctRateError: 'Napraw błąd stawki i spróbuj ponownie.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Wyjaśnij</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError:
'Nie możesz duplikować wydatków z tytułu diet między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.',
rulesModifiedFields: {
Expand Down
2 changes: 1 addition & 1 deletion src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: 'Delegado de férias',
expensifyLogo: 'Logo da Expensify',
duplicateReport: 'Duplicar relatório',
explain: 'Explicar',
},
socials: {
podcast: 'Siga-nos no Podcast',
Expand Down Expand Up @@ -1526,7 +1527,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: 'O valor total é muito alto. Diminua as horas ou reduza a tarifa.',
},
correctRateError: 'Corrija o erro de taxa e tente novamente.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>Explicar</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError: 'Você não pode duplicar despesas de diárias entre espaços de trabalho porque as tarifas podem variar entre eles.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcou a despesa como "reembolsável"' : 'marcou a despesa como “não reembolsável”'),
Expand Down
2 changes: 1 addition & 1 deletion src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ const translations: TranslationDeepObject<typeof en> = {
vacationDelegate: '休假代理',
expensifyLogo: 'Expensify徽标',
duplicateReport: '重复报销单',
explain: '说明',
},
socials: {
podcast: '在播客上关注我们',
Expand Down Expand Up @@ -1500,7 +1501,6 @@ const translations: TranslationDeepObject<typeof en> = {
amountTooLargeError: '总金额过大。请减少工时或降低费率。',
},
correctRateError: '修复费率错误后请重试。',
AskToExplain: `。<a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}"><strong>说明</strong></a> &#x2728;`,
duplicateNonDefaultWorkspacePerDiemError: '您无法在不同工作区之间复制每日津贴报销,因为各工作区的补贴标准可能不同。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '将该报销单标记为“可报销”' : '将该报销单标记为“不可报销”'),
Expand Down
60 changes: 42 additions & 18 deletions src/pages/inbox/report/ReportActionItemMessageWithExplain.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import type {GestureResponderEvent} from 'react-native';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Icon from '@components/Icon';
import RenderHTML from '@components/RenderHTML';
import TextBlock from '@components/TextBlock';
import TextLinkBlock from '@components/TextLinkBlock';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {openLink} from '@libs/actions/Link';
import {explain} from '@libs/actions/Report';
import {hasReasoning} from '@libs/ReportActionsUtils';
import CONST from '@src/CONST';
import variables from '@styles/variables';
import type {Report, ReportAction} from '@src/types/onyx';
import ReportActionItemBasicMessage from './ReportActionItemBasicMessage';

Expand All @@ -31,31 +37,49 @@ type ReportActionItemMessageWithExplainProps = {
* if the action has reasoning.
*/
function ReportActionItemMessageWithExplain({message, action, childReport, originalReport}: ReportActionItemMessageWithExplainProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const personalDetail = useCurrentUserPersonalDetails();
const icons = useMemoizedLazyExpensifyIcons(['Sparkles']);
const {environmentURL} = useEnvironment();

const actionHasReasoning = hasReasoning(action);
const computedMessage = actionHasReasoning ? `${message}${translate('iou.AskToExplain')}` : message;

const handleLinkPress = (event: GestureResponderEvent | KeyboardEvent, href: string) => {
// Handle the special "Explain" link
if (href.endsWith(CONST.CONCIERGE_EXPLAIN_LINK_PATH)) {
explain(childReport, originalReport, action, translate, personalDetail.accountID, personalDetail?.timezone);
return;
}

// For all other links, use the default link handler
openLink(href, environmentURL);
};
if (!actionHasReasoning) {
return (
<ReportActionItemBasicMessage>
<RenderHTML
html={`<comment><muted-text>${message}</muted-text></comment>`}
onLinkPress={(event, href) => {
openLink(href, environmentURL);
}}
/>
</ReportActionItemBasicMessage>
);
}

return (
<ReportActionItemBasicMessage>
<RenderHTML
html={`<comment><muted-text>${computedMessage}</muted-text></comment>`}
isSelectable={false}
onLinkPress={handleLinkPress}
/>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flexWrap]}>
<TextBlock
textStyles={[styles.chatItemMessage, styles.colorMuted]}
text={`${message}. `}
/>
Comment on lines +65 to +68

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Render reasoning message with HTML support

In the actionHasReasoning path, the base message is now rendered through TextBlock as plain text, but this message can contain HTML links (for example modified-expense copy built from iou.basedOnMCC/iou.policyRulesModifiedFields.format). When those actions also include reasoning, users will see raw <a ...> tags and lose the ability to open those links, whereas the previous RenderHTML flow preserved clickable links.

Useful? React with 👍 / 👎.

Copy link
Contributor

@FitseTLT FitseTLT Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about this comment @samranahm ?

This comment was marked as outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FitseTLT I noticed that delaySubmissions message that's related to harvesting and contain help site url have the reasoning. But we're not handling that existing link in TextLinkBlock properly. This will potentially cause a blocker. I will raise a follow-up ASAP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created the PR here #83802
@FitseTLT Please take a look.

<View style={[styles.flexRow, styles.alignItemsCenter]}>
<TextLinkBlock
onPress={() => explain(childReport, originalReport, action, translate, personalDetail.accountID, personalDetail?.timezone)}
style={[styles.chatItemMessage, styles.link, styles.mrHalf]}
text={translate('common.explain')}
/>
<Icon
src={icons.Sparkles}
width={variables.iconSizeExtraSmall}
height={variables.iconSizeExtraSmall}
fill={theme.link}
/>
</View>
</View>
</ReportActionItemBasicMessage>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/styles/utils/spacing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export default {
marginRight: 0,
},

mrHalf: {
marginRight: 2,
},

mr1: {
marginRight: 4,
},
Expand Down
Loading