From 4287ff1411129fc337e87d3e1617373b4dc8f052 Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 3 Jun 2026 15:22:43 +0900 Subject: [PATCH 1/5] resources: add response compose helpers --- resources/responses.ts | 48 ++++++++++++++++++ test/unittests/responses_test.ts | 85 +++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/resources/responses.ts b/resources/responses.ts index 366c691c8ec..7b69735e323 100644 --- a/resources/responses.ts +++ b/resources/responses.ts @@ -164,6 +164,31 @@ const staticResponse = (field: SevText, text: LocaleText): StaticResponseFunc => }; }; +type LocaleTextPart = [sep: string, text: LocaleText]; + +const combineLocaleText = (first: LocaleText, rest: LocaleTextPart[]): LocaleText => { + const parts: LocaleTextPart[] = [['', first], ...rest]; + const langs = new Set(['en']); + + for (const [, text] of parts) { + for (const lang of Object.keys(text)) + langs.add(lang as keyof LocaleText); + } + + const result: Record = {}; + for (const lang of langs) { + let combined = ''; + for (const [sep, text] of parts) { + combined += sep; + combined += text[lang] ?? text.en; + } + + result[lang] = combined; + } + + return result as LocaleText; +}; + type SingleSevToResponseFunc = (sev?: Severity) => TargetedResponseFunc | StaticResponseFunc; type DoubleSevToResponseFunc = (targetSev?: Severity, otherSev?: Severity) => TargetedResponseFunc; type ResponsesMap = { @@ -625,6 +650,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..d83ef4c90ee 100644 --- a/test/unittests/responses_test.ts +++ b/test/unittests/responses_test.ts @@ -1,7 +1,10 @@ import { assert } from 'chai'; +import Outputs from '../../resources/outputs'; import { builtInResponseStr, + compose, + compose3, Responses, severityList, severityMap, @@ -9,7 +12,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 +36,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: {}, + }; + 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 +128,65 @@ describe('response tests', () => { } } }); + 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: '靠近 => 遠離 + 分散', + }); + }); }); From 1e1b5e94e4a0db55640c9099c126aa15217802ff Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 3 Jun 2026 15:40:44 +0900 Subject: [PATCH 2/5] test: output object type in responses_test --- test/unittests/responses_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unittests/responses_test.ts b/test/unittests/responses_test.ts index d83ef4c90ee..21ef8fe57c4 100644 --- a/test/unittests/responses_test.ts +++ b/test/unittests/responses_test.ts @@ -40,7 +40,7 @@ const runResponseFuncWithOutputStrings = ( func: ResponseFunc, ): [ResponseFuncOutput, OutputStrings] => { const empty = {}; - const output = { + const output: { responseOutputStrings: OutputStrings } = { responseOutputStrings: {}, }; return [ From 595704daaa331a900a513091c6f621fe54834fb3 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 4 Jun 2026 12:47:53 +0900 Subject: [PATCH 3/5] fix: use Lang type in combineLocaleText, fix type name --- resources/responses.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/resources/responses.ts b/resources/responses.ts index 7b69735e323..315da18ff9c 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,15 +165,15 @@ const staticResponse = (field: SevText, text: LocaleText): StaticResponseFunc => }; }; -type LocaleTextPart = [sep: string, text: LocaleText]; +type LocaleTextWithSeparator = [sep: string, text: LocaleText]; -const combineLocaleText = (first: LocaleText, rest: LocaleTextPart[]): LocaleText => { - const parts: LocaleTextPart[] = [['', first], ...rest]; - const langs = new Set(['en']); +const combineLocaleText = (first: LocaleText, rest: LocaleTextWithSeparator[]): LocaleText => { + const parts: LocaleTextWithSeparator[] = [['', first], ...rest]; + const langs = new Set(['en']); for (const [, text] of parts) { for (const lang of Object.keys(text)) - langs.add(lang as keyof LocaleText); + langs.add(lang as Lang); } const result: Record = {}; From 0af50a4eae2aecb45428d4e5d81f8116003e1f29 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 4 Jun 2026 12:51:45 +0900 Subject: [PATCH 4/5] fix: simplify combining LocaleTexts Use array mapping and joining --- resources/responses.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/resources/responses.ts b/resources/responses.ts index 315da18ff9c..bf3332e9738 100644 --- a/resources/responses.ts +++ b/resources/responses.ts @@ -178,13 +178,9 @@ const combineLocaleText = (first: LocaleText, rest: LocaleTextWithSeparator[]): const result: Record = {}; for (const lang of langs) { - let combined = ''; - for (const [sep, text] of parts) { - combined += sep; - combined += text[lang] ?? text.en; - } - - result[lang] = combined; + result[lang] = parts + .map(([sep, text]) => `${sep}${text[lang] ?? text.en}`) + .join(''); } return result as LocaleText; From 4df861f7680362d8a988aa00bc67d29814d858c2 Mon Sep 17 00:00:00 2001 From: Dowon Date: Fri, 5 Jun 2026 10:26:16 +0900 Subject: [PATCH 5/5] fix: make combineLocaleText public --- resources/responses.ts | 13 +++++++------ test/unittests/responses_test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/resources/responses.ts b/resources/responses.ts index bf3332e9738..7e70bd58d92 100644 --- a/resources/responses.ts +++ b/resources/responses.ts @@ -165,10 +165,11 @@ const staticResponse = (field: SevText, text: LocaleText): StaticResponseFunc => }; }; -type LocaleTextWithSeparator = [sep: string, text: LocaleText]; - -const combineLocaleText = (first: LocaleText, rest: LocaleTextWithSeparator[]): LocaleText => { - const parts: LocaleTextWithSeparator[] = [['', first], ...rest]; +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) { @@ -653,7 +654,7 @@ export const compose = ( text2: LocaleText, sev?: Severity, ): StaticResponseFunc => { - return staticResponse(defaultInfoText(sev), combineLocaleText(text1, [[sep, text2]])); + return staticResponse(defaultInfoText(sev), combineLocaleText(text1, [sep, text2])); }; export const compose3 = ( @@ -666,7 +667,7 @@ export const compose3 = ( ): StaticResponseFunc => { return staticResponse( defaultInfoText(sev), - combineLocaleText(text1, [[sep1, text2], [sep2, text3]]), + combineLocaleText(text1, [sep1, text2], [sep2, text3]), ); }; diff --git a/test/unittests/responses_test.ts b/test/unittests/responses_test.ts index 21ef8fe57c4..c9c416db0e4 100644 --- a/test/unittests/responses_test.ts +++ b/test/unittests/responses_test.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import Outputs from '../../resources/outputs'; import { builtInResponseStr, + combineLocaleText, compose, compose3, Responses, @@ -128,6 +129,29 @@ 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);