diff --git a/resources/responses.ts b/resources/responses.ts index 366c691c8ec..7e70bd58d92 100644 --- a/resources/responses.ts +++ b/resources/responses.ts @@ -31,6 +31,7 @@ import { TriggerOutput, } from '../types/trigger'; +import type { Lang } from './languages'; import Outputs from './outputs'; type TargetedResponseOutput = ResponseOutput; @@ -164,6 +165,28 @@ const staticResponse = (field: SevText, text: LocaleText): StaticResponseFunc => }; }; +export const combineLocaleText = ( + first: LocaleText, + ...rest: [sep: string, text: LocaleText][] +): LocaleText => { + const parts: [sep: string, text: LocaleText][] = [['', first], ...rest]; + const langs = new Set(['en']); + + for (const [, text] of parts) { + for (const lang of Object.keys(text)) + langs.add(lang as Lang); + } + + const result: Record = {}; + for (const lang of langs) { + result[lang] = parts + .map(([sep, text]) => `${sep}${text[lang] ?? text.en}`) + .join(''); + } + + return result as LocaleText; +}; + type SingleSevToResponseFunc = (sev?: Severity) => TargetedResponseFunc | StaticResponseFunc; type DoubleSevToResponseFunc = (targetSev?: Severity, otherSev?: Severity) => TargetedResponseFunc; type ResponsesMap = { @@ -625,6 +648,29 @@ export const Responses = { getTowers: (sev?: Severity) => staticResponse(defaultInfoText(sev), Outputs.getTowers), } as const; +export const compose = ( + text1: LocaleText, + sep: string, + text2: LocaleText, + sev?: Severity, +): StaticResponseFunc => { + return staticResponse(defaultInfoText(sev), combineLocaleText(text1, [sep, text2])); +}; + +export const compose3 = ( + text1: LocaleText, + sep1: string, + text2: LocaleText, + sep2: string, + text3: LocaleText, + sev?: Severity, +): StaticResponseFunc => { + return staticResponse( + defaultInfoText(sev), + combineLocaleText(text1, [sep1, text2], [sep2, text3]), + ); +}; + // Don't give `Responses` a type in its declaration so that it can be treated as more strict // than `ResponsesMap`, but do assert that its type is correct. This allows callers to know // which properties are defined in Responses without having to conditionally check for undefined. diff --git a/test/unittests/responses_test.ts b/test/unittests/responses_test.ts index 39ef9184983..c9c416db0e4 100644 --- a/test/unittests/responses_test.ts +++ b/test/unittests/responses_test.ts @@ -1,7 +1,11 @@ import { assert } from 'chai'; +import Outputs from '../../resources/outputs'; import { builtInResponseStr, + combineLocaleText, + compose, + compose3, Responses, severityList, severityMap, @@ -9,7 +13,13 @@ import { } from '../../resources/responses'; import { RaidbossData } from '../../types/data'; import { Matches } from '../../types/net_matches'; -import { Output, ResponseFunc, ResponseOutput } from '../../types/trigger'; +import { + LocaleText, + Output, + OutputStrings, + ResponseFunc, + ResponseOutput, +} from '../../types/trigger'; // test_trigger.js will validate the field names, so no need to do that here. @@ -27,6 +37,19 @@ const runResponseFunc = ( return func(empty as RaidbossData, empty as Matches, empty as Output); }; +const runResponseFuncWithOutputStrings = ( + func: ResponseFunc, +): [ResponseFuncOutput, OutputStrings] => { + const empty = {}; + const output: { responseOutputStrings: OutputStrings } = { + responseOutputStrings: {}, + }; + return [ + func(empty as RaidbossData, empty as Matches, output as Output), + output.responseOutputStrings, + ]; +}; + describe('response tests', () => { it('responses with default severity are valid', () => { for (const responseFunc of Object.values(Responses)) { @@ -106,4 +129,88 @@ describe('response tests', () => { } } }); + it('combineLocaleText combines localized text with fallbacks', () => { + const fallbackText: LocaleText = { + en: 'Fallback', + ja: 'Japanese Fallback', + }; + + assert.deepEqual( + combineLocaleText( + Outputs.in, + [' => ', Outputs.out], + [' + ', fallbackText], + ), + { + en: 'In => Out + Fallback', + de: 'Rein => Raus + Fallback', + fr: 'Intérieur => Extérieur + Fallback', + ja: '中へ => 外へ + Japanese Fallback', + cn: '靠近 => 远离 + Fallback', + ko: '안으로 => 밖으로 + Fallback', + tc: '靠近 => 遠離 + Fallback', + }, + ); + }); + it('compose returns a built-in static response', () => { + const responseFunc = compose(Outputs.in, ' => ', Outputs.out); + assert.include(responseFunc.toString(), outputStringSetterStr); + assert.include(responseFunc.toString(), builtInResponseStr); + + const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc); + assert.isObject(result); + assert.property(result, 'infoText'); + assert.deepEqual(outputStrings.text, { + en: 'In => Out', + de: 'Rein => Raus', + fr: 'Intérieur => Extérieur', + ja: '中へ => 外へ', + cn: '靠近 => 远离', + ko: '안으로 => 밖으로', + tc: '靠近 => 遠離', + }); + }); + it('compose respects explicit severity and locale fallbacks', () => { + const fallbackText: LocaleText = { + en: 'Fallback', + ja: 'Japanese Fallback', + }; + const responseFunc = compose(Outputs.spread, ' 😗 ', fallbackText, 'alarm'); + + const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc); + assert.isObject(result); + assert.property(result, 'alarmText'); + assert.deepEqual(outputStrings.text, { + en: 'Spread 😗 Fallback', + de: 'Verteilen 😗 Fallback', + fr: 'Dispersez-vous 😗 Fallback', + ja: 'さんかい 😗 Japanese Fallback', + cn: '分散 😗 Fallback', + ko: '산개 😗 Fallback', + tc: '分散 😗 Fallback', + }); + }); + it('compose3 returns a built-in static response', () => { + const responseFunc = compose3( + Outputs.in, + ' => ', + Outputs.out, + ' + ', + Outputs.spread, + 'alert', + ); + + const [result, outputStrings] = runResponseFuncWithOutputStrings(responseFunc); + assert.isObject(result); + assert.property(result, 'alertText'); + assert.deepEqual(outputStrings.text, { + en: 'In => Out + Spread', + de: 'Rein => Raus + Verteilen', + fr: 'Intérieur => Extérieur + Dispersez-vous', + ja: '中へ => 外へ + さんかい', + cn: '靠近 => 远离 + 分散', + ko: '안으로 => 밖으로 + 산개', + tc: '靠近 => 遠離 + 分散', + }); + }); });