diff --git a/.github/scripts/auto-label.cjs b/.github/scripts/auto-label.cjs index 490c503c405..67f3254520d 100644 --- a/.github/scripts/auto-label.cjs +++ b/.github/scripts/auto-label.cjs @@ -253,7 +253,10 @@ const getTimelineReplaceChanges = (changedFiles) => { const s = new Set(); changedFiles.forEach((file) => { - if (!file.filename.startsWith('ui/raidboss/data/')) + if ( + !file.filename.startsWith('ui/raidboss/data/') && + !file.filename.startsWith('ui/oopsyraidsy/data/') + ) return; if (path.extname(file.filename) === '.js') { diff --git a/docs/RaidbossGuide.md b/docs/RaidbossGuide.md index 3bb1cedf795..16f156b8ae9 100644 --- a/docs/RaidbossGuide.md +++ b/docs/RaidbossGuide.md @@ -246,7 +246,7 @@ There is a complete example that uses the **timeline** property in [test.ts](../ Key:value pairs to search and replace in timeline ability names. The display name for that ability is changed, but all `hideall`, `infotext`, `alerttext`, `alarmtext`, etc all refer to the original name. This enables translation/localization of the timeline files without having to edit those files directly. **replaceSync** -Key:value pairs to search and replace in timeline file sync expressions. Necessary if localized names differ in the sync regexes. +Key:value pairs to search and replace in timeline file sync expressions, and trigger sources. Necessary if localized names differ in the sync regexes. **resetWhenOutOfCombat** Boolean, defaults to true. diff --git a/resources/translations.ts b/resources/translations.ts index 3b46f66a0ea..3810b200cf4 100644 --- a/resources/translations.ts +++ b/resources/translations.ts @@ -1,11 +1,11 @@ import { NetParams } from '../types/net_props'; import { CactbotBaseRegExp, TriggerTypes } from '../types/net_trigger'; +import { TimelineReplacement } from '../types/trigger'; import { commonReplacement, partialCommonTimelineReplacementKeys, partialCommonTriggerReplacementKeys, } from '../ui/raidboss/common_replacement'; -import { TimelineReplacement } from '../ui/raidboss/timeline_parser'; import { Lang } from './languages'; import NetRegexes, { keysThatRequireTranslation } from './netregexes'; diff --git a/test/helper/test_timeline.ts b/test/helper/test_timeline.ts index 2e8a39fa42d..60318a32d2d 100644 --- a/test/helper/test_timeline.ts +++ b/test/helper/test_timeline.ts @@ -7,14 +7,9 @@ import { keysThatRequireTranslation } from '../../resources/netregexes'; import { UnreachableCode } from '../../resources/not_reached'; import Regexes from '../../resources/regexes'; import { translateWithReplacements } from '../../resources/translations'; -import { LooseTimelineTrigger, LooseTriggerSet } from '../../types/trigger'; +import { LooseTimelineTrigger, LooseTriggerSet, TimelineReplacement } from '../../types/trigger'; import { CommonReplacement, commonReplacement } from '../../ui/raidboss/common_replacement'; -import { - Error, - regexes, - TimelineParser, - TimelineReplacement, -} from '../../ui/raidboss/timeline_parser'; +import { Error, regexes, TimelineParser } from '../../ui/raidboss/timeline_parser'; const parseTimelineFileFromTriggerFile = (filepath: string) => { const fileContents = fs.readFileSync(filepath, 'utf8'); diff --git a/types/oopsy.d.ts b/types/oopsy.d.ts index 3683796cfa7..4e006bbae01 100644 --- a/types/oopsy.d.ts +++ b/types/oopsy.d.ts @@ -4,7 +4,7 @@ import { TrackedEvent } from '../ui/oopsyraidsy/player_state_tracker'; import { OopsyData } from './data'; import { NetAnyMatches, NetMatches } from './net_matches'; import { CactbotBaseRegExp, TriggerTypes } from './net_trigger'; -import { LocaleText, ZoneIdType } from './trigger'; +import { LocaleText, TimelineReplacement, ZoneIdType } from './trigger'; export type OopsyMistakeType = | 'pull' @@ -130,6 +130,7 @@ type SimpleOopsyTriggerSet = { zoneId: ZoneIdType | ZoneIdType[]; zoneLabel?: LocaleText; triggers?: OopsyTrigger[]; + timelineReplace?: TimelineReplacement[]; } & OopsyMistakeMapFields; // If Data contains required properties that are not on OopsyData, require initData diff --git a/types/trigger.d.ts b/types/trigger.d.ts index 0b320d566ab..abc43250ecd 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -1,6 +1,6 @@ import { Lang, NonEnLang } from '../resources/languages'; import { NamedConfigEntry } from '../resources/user_config'; -import { TimelineReplacement, TimelineStyle } from '../ui/raidboss/timeline_parser'; +import { TimelineStyle } from '../ui/raidboss/timeline_parser'; import { RaidbossData } from './data'; import { NetAnyMatches, NetMatches } from './net_matches'; @@ -183,6 +183,13 @@ export type TimelineTrigger = BaseTrigger TimelineField; export type TimelineField = string | TimelineFunc | undefined | TimelineField[]; diff --git a/ui/oopsyraidsy/damage_tracker.ts b/ui/oopsyraidsy/damage_tracker.ts index 29581f21787..4fea1e42762 100644 --- a/ui/oopsyraidsy/damage_tracker.ts +++ b/ui/oopsyraidsy/damage_tracker.ts @@ -3,7 +3,7 @@ import NetRegexes, { commonNetRegex } from '../../resources/netregexes'; import PartyTracker from '../../resources/party'; import { PlayerChangedDetail } from '../../resources/player_override'; import Regexes from '../../resources/regexes'; -import { LocaleNetRegex } from '../../resources/translations'; +import { LocaleNetRegex, translateRegex } from '../../resources/translations'; import Util from '../../resources/util'; import ZoneId from '../../resources/zone_id'; import ZoneInfo from '../../resources/zone_info'; @@ -726,25 +726,38 @@ export class DamageTracker { this.AddSoloTriggers('fail', set.soloFail); for (const trigger of set.triggers ?? []) - this.ProcessTrigger(trigger); + this.ProcessTrigger(trigger, set); this.playerStateTracker.PushTriggerSet(set); } } - ProcessTrigger(trigger: OopsyTrigger): void { + ProcessTrigger(trigger: OopsyTrigger, set?: ProcessedOopsyTriggerSet): void { // This is a bit of a hack, but LooseOopsyTrigger extends OopsyTrigger // but not vice versa. Because the NetMatches['Ability'] requires a number // of fields, Matches cannot be assigned to Matches & NetMatches['Ability']. const looseTrigger = trigger as LooseOopsyTrigger; - const regex = looseTrigger.netRegex; + const netRegex = looseTrigger.netRegex; // Some oopsy triggers (e.g. early pull) have only an id. - if (!regex) + if (!netRegex) return; + + const parserLang = this.options.ParserLanguage; + const timelineReplace = set?.timelineReplace; + + let localRegex: RegExp; + if (Array.isArray(netRegex)) { + localRegex = Regexes.parse(Regexes.anyOf(netRegex)); + } else { + // RegExp (e.g. from NetRegexes.xxx()), translate the regex string + const translated = translateRegex(netRegex, parserLang, timelineReplace); + localRegex = Regexes.parse(translated); + } + this.triggers.push({ ...looseTrigger, - localRegex: Regexes.parse(Array.isArray(regex) ? Regexes.anyOf(regex) : regex), + localRegex, }); } diff --git a/ui/oopsyraidsy/data/04-sb/raid/o3n.ts b/ui/oopsyraidsy/data/04-sb/raid/o3n.ts index c2f68d9cdc8..8369a2a883c 100644 --- a/ui/oopsyraidsy/data/04-sb/raid/o3n.ts +++ b/ui/oopsyraidsy/data/04-sb/raid/o3n.ts @@ -32,11 +32,6 @@ const triggerSet: OopsyTriggerSet = { id: 'O3N Phase Tracker', type: 'StartsUsing', netRegex: NetRegexes.startsUsing({ id: '2304', source: 'Halicarnassus', capture: false }), - netRegexDe: NetRegexes.startsUsing({ id: '2304', source: 'Halikarnassos', capture: false }), - netRegexFr: NetRegexes.startsUsing({ id: '2304', source: 'Halicarnasse', capture: false }), - netRegexJa: NetRegexes.startsUsing({ id: '2304', source: 'ハリカルナッソス', capture: false }), - netRegexCn: NetRegexes.startsUsing({ id: '2304', source: '哈利卡纳苏斯', capture: false }), - netRegexKo: NetRegexes.startsUsing({ id: '2304', source: '할리카르나소스', capture: false }), run: (data) => data.phaseNumber = (data.phaseNumber ?? 0) + 1, }, { @@ -45,11 +40,6 @@ const triggerSet: OopsyTriggerSet = { id: 'O3N Initializing', type: 'Ability', netRegex: NetRegexes.ability({ id: '367', source: 'Halicarnassus', capture: false }), - netRegexDe: NetRegexes.ability({ id: '367', source: 'Halikarnassos', capture: false }), - netRegexFr: NetRegexes.ability({ id: '367', source: 'Halicarnasse', capture: false }), - netRegexJa: NetRegexes.ability({ id: '367', source: 'ハリカルナッソス', capture: false }), - netRegexCn: NetRegexes.ability({ id: '367', source: '哈利卡纳苏斯', capture: false }), - netRegexKo: NetRegexes.ability({ id: '367', source: '할리카르나소스', capture: false }), condition: (data) => !data.initialized, run: (data) => { data.gameCount = 0; @@ -100,6 +90,44 @@ const triggerSet: OopsyTriggerSet = { run: (data) => data.gameCount = (data.gameCount ?? 0) + 1, }, ], + timelineReplace: [ + { + 'locale': 'de', + 'replaceSync': { + 'Halicarnassus': 'Halikarnassos', + }, + }, + { + 'locale': 'fr', + 'replaceSync': { + 'Halicarnassus': 'Halicarnasse', + }, + }, + { + 'locale': 'ja', + 'replaceSync': { + 'Halicarnassus': 'ハリカルナッソス', + }, + }, + { + 'locale': 'cn', + 'replaceSync': { + 'Halicarnassus': '哈利卡纳苏斯', + }, + }, + { + 'locale': 'tc', + 'replaceSync': { + 'Halicarnassus': '哈利卡納蘇斯', + }, + }, + { + 'locale': 'ko', + 'replaceSync': { + 'Halicarnassus': '할리카르나소스', + }, + }, + ], }; export default triggerSet; diff --git a/ui/oopsyraidsy/data/05-shb/raid/e10s.ts b/ui/oopsyraidsy/data/05-shb/raid/e10s.ts index efc0d60a8ec..93d8feaf08c 100644 --- a/ui/oopsyraidsy/data/05-shb/raid/e10s.ts +++ b/ui/oopsyraidsy/data/05-shb/raid/e10s.ts @@ -48,10 +48,6 @@ const triggerSet: OopsyTriggerSet = { id: 'E10S Damage Down Orbs', type: 'GainsEffect', netRegex: NetRegexes.gainsEffect({ source: 'Flameshadow', effectId: '82C' }), - netRegexDe: NetRegexes.gainsEffect({ source: 'Schattenflamme', effectId: '82C' }), - netRegexFr: NetRegexes.gainsEffect({ source: 'Flamme ombrale', effectId: '82C' }), - netRegexJa: NetRegexes.gainsEffect({ source: 'シャドウフレイム', effectId: '82C' }), - netRegexCn: NetRegexes.gainsEffect({ source: '影烈火', effectId: '82C' }), mistake: (_data, matches) => { return { type: 'damage', @@ -69,10 +65,6 @@ const triggerSet: OopsyTriggerSet = { // TODO: some of these will be duplicated with others, like `E10S Throne Of Shadow`. // Maybe it'd be nice to figure out how to put the damage marker on that? netRegex: NetRegexes.gainsEffect({ source: 'Shadowkeeper', effectId: '82C' }), - netRegexDe: NetRegexes.gainsEffect({ source: 'Schattenkönig', effectId: '82C' }), - netRegexFr: NetRegexes.gainsEffect({ source: 'Roi De L\'Ombre', effectId: '82C' }), - netRegexJa: NetRegexes.gainsEffect({ source: '影の王', effectId: '82C' }), - netRegexCn: NetRegexes.gainsEffect({ source: '影之王', effectId: '82C' }), mistake: (_data, matches) => { return { type: 'damage', @@ -99,6 +91,36 @@ const triggerSet: OopsyTriggerSet = { }, }, ], + timelineReplace: [ + { + 'locale': 'de', + 'replaceSync': { + 'Flameshadow': 'Schattenflamme', + 'Shadowkeeper': 'Schattenkönig', + }, + }, + { + 'locale': 'fr', + 'replaceSync': { + 'Flameshadow': 'Flamme ombrale', + 'Shadowkeeper': 'Roi De L\'Ombre', + }, + }, + { + 'locale': 'ja', + 'replaceSync': { + 'Flameshadow': 'シャドウフレイム', + 'Shadowkeeper': '影の王', + }, + }, + { + 'locale': 'cn', + 'replaceSync': { + 'Flameshadow': '影烈火', + 'Shadowkeeper': '影之王', + }, + }, + ], }; export default triggerSet; diff --git a/ui/oopsyraidsy/data/05-shb/raid/e12s.ts b/ui/oopsyraidsy/data/05-shb/raid/e12s.ts index d4b1b03e0b5..edb224c6a23 100644 --- a/ui/oopsyraidsy/data/05-shb/raid/e12s.ts +++ b/ui/oopsyraidsy/data/05-shb/raid/e12s.ts @@ -298,10 +298,6 @@ const triggerSet: OopsyTriggerSet = { id: 'E12S Promise Small Lion Tether', type: 'Tether', netRegex: NetRegexes.tether({ source: 'Beastly Sculpture', id: '0011' }), - netRegexDe: NetRegexes.tether({ source: 'Abbild Eines Löwen', id: '0011' }), - netRegexFr: NetRegexes.tether({ source: 'Création Léonine', id: '0011' }), - netRegexJa: NetRegexes.tether({ source: '創られた獅子', id: '0011' }), - netRegexCn: NetRegexes.tether({ source: '被创造的狮子', id: '0011' }), run: (data, matches) => { data.smallLionIdToOwner ??= {}; data.smallLionIdToOwner[matches.sourceId.toUpperCase()] = matches.target; @@ -313,10 +309,6 @@ const triggerSet: OopsyTriggerSet = { id: 'E12S Promise Small Lion Lionsblaze', type: 'Ability', netRegex: NetRegexes.ability({ source: 'Beastly Sculpture', id: '58B9' }), - netRegexDe: NetRegexes.ability({ source: 'Abbild Eines Löwen', id: '58B9' }), - netRegexFr: NetRegexes.ability({ source: 'Création Léonine', id: '58B9' }), - netRegexJa: NetRegexes.ability({ source: '創られた獅子', id: '58B9' }), - netRegexCn: NetRegexes.ability({ source: '被创造的狮子', id: '58B9' }), mistake: (data, matches) => { // Folks baiting the big lion second can take the first small lion hit, // so it's not sufficient to check only the owner. @@ -385,10 +377,6 @@ const triggerSet: OopsyTriggerSet = { id: 'E12S Promise Big Lion Kingsblaze', type: 'Ability', netRegex: NetRegexes.ability({ source: 'Regal Sculpture', id: '4F9E' }), - netRegexDe: NetRegexes.ability({ source: 'Abbild eines großen Löwen', id: '4F9E' }), - netRegexFr: NetRegexes.ability({ source: 'création léonine royale', id: '4F9E' }), - netRegexJa: NetRegexes.ability({ source: '創られた獅子王', id: '4F9E' }), - netRegexCn: NetRegexes.ability({ source: '被创造的狮子王', id: '4F9E' }), mistake: (data, matches) => { const singleTarget = matches.type === '21'; const hasFireDebuff = data.fire && data.fire[matches.target]; @@ -495,6 +483,36 @@ const triggerSet: OopsyTriggerSet = { }, }, ], + timelineReplace: [ + { + 'locale': 'de', + 'replaceSync': { + 'Beastly Sculpture': 'Abbild Eines Löwen', + 'Regal Sculpture': 'Abbild eines großen Löwen', + }, + }, + { + 'locale': 'fr', + 'replaceSync': { + 'Beastly Sculpture': 'Création Léonine', + 'Regal Sculpture': 'création léonine royale', + }, + }, + { + 'locale': 'ja', + 'replaceSync': { + 'Beastly Sculpture': '創られた獅子', + 'Regal Sculpture': '創られた獅子王', + }, + }, + { + 'locale': 'cn', + 'replaceSync': { + 'Beastly Sculpture': '被创造的狮子', + 'Regal Sculpture': '被创造的狮子王', + }, + }, + ], }; export default triggerSet; diff --git a/ui/oopsyraidsy/data/05-shb/raid/e4s.ts b/ui/oopsyraidsy/data/05-shb/raid/e4s.ts index 77dc5983e0a..c37e98fa1d9 100644 --- a/ui/oopsyraidsy/data/05-shb/raid/e4s.ts +++ b/ui/oopsyraidsy/data/05-shb/raid/e4s.ts @@ -39,11 +39,6 @@ const triggerSet: OopsyTriggerSet = { id: 'E4S Fault Line Collect', type: 'StartsUsing', netRegex: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }), - netRegexDe: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }), - netRegexFr: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }), - netRegexJa: NetRegexes.startsUsing({ id: '411E', source: 'タイタン' }), - netRegexCn: NetRegexes.startsUsing({ id: '411E', source: '泰坦' }), - netRegexKo: NetRegexes.startsUsing({ id: '411E', source: '타이탄' }), run: (data, matches) => { data.faultLineTarget = matches.target; }, @@ -71,6 +66,38 @@ const triggerSet: OopsyTriggerSet = { }, }, ], + timelineReplace: [ + { + 'locale': 'de', + 'replaceSync': { + 'Titan': 'Titan', + }, + }, + { + 'locale': 'fr', + 'replaceSync': { + 'Titan': 'Titan', + }, + }, + { + 'locale': 'ja', + 'replaceSync': { + 'Titan': 'タイタン', + }, + }, + { + 'locale': 'cn', + 'replaceSync': { + 'Titan': '泰坦', + }, + }, + { + 'locale': 'ko', + 'replaceSync': { + 'Titan': '타이탄', + }, + }, + ], }; export default triggerSet; diff --git a/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts b/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts index 64300125948..70a3d45f848 100644 --- a/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts +++ b/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts @@ -1,7 +1,7 @@ -import { LooseTimelineTrigger } from '../../../../types/trigger'; +import { LooseTimelineTrigger, TimelineReplacement } from '../../../../types/trigger'; import { RaidbossOptions } from '../../raidboss_options'; import { Timeline } from '../../timeline'; -import { TimelineReplacement, TimelineStyle } from '../../timeline_parser'; +import { TimelineStyle } from '../../timeline_parser'; import RaidEmulator from '../data/RaidEmulator'; export default class RaidEmulatorTimeline extends Timeline { diff --git a/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts b/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts index 68052b182b2..450448262b8 100644 --- a/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts +++ b/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts @@ -1,8 +1,8 @@ import { UnreachableCode } from '../../../../resources/not_reached'; import { EventResponses, LogEvent } from '../../../../types/event'; -import { LooseTimelineTrigger } from '../../../../types/trigger'; +import { LooseTimelineTrigger, TimelineReplacement } from '../../../../types/trigger'; import { TimelineController } from '../../timeline'; -import { TimelineReplacement, TimelineStyle } from '../../timeline_parser'; +import { TimelineStyle } from '../../timeline_parser'; import LineEvent from '../data/network_log_converter/LineEvent'; import RaidEmulator from '../data/RaidEmulator'; diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 017c4f3744d..798674c6727 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -30,6 +30,7 @@ import { ResponseOutput, TimelineField, TimelineFunc, + TimelineReplacement, TriggerAutoConfig, TriggerField, TriggerOutput, @@ -40,7 +41,6 @@ import AutoplayHelper from './autoplay_helper'; import BrowserTTSEngine from './browser_tts_engine'; import { PerTriggerAutoConfig, PerTriggerOption, RaidbossOptions } from './raidboss_options'; import { TimelineLoader } from './timeline'; -import { TimelineReplacement } from './timeline_parser'; const isRaidbossLooseTimelineTrigger = ( trigger: ProcessedTrigger, diff --git a/ui/raidboss/timeline.ts b/ui/raidboss/timeline.ts index 1dbd9a91b3d..e3f4635be8f 100644 --- a/ui/raidboss/timeline.ts +++ b/ui/raidboss/timeline.ts @@ -3,18 +3,11 @@ import { UnreachableCode } from '../../resources/not_reached'; import { LocaleRegex } from '../../resources/translations'; import { EventResponses, LogEvent } from '../../types/event'; import { CactbotBaseRegExp } from '../../types/net_trigger'; -import { LooseTimelineTrigger, RaidbossFileData } from '../../types/trigger'; +import { LooseTimelineTrigger, RaidbossFileData, TimelineReplacement } from '../../types/trigger'; import { PopupTextGenerator } from './popup-text'; import { RaidbossOptions } from './raidboss_options'; -import { - Event, - Sync, - Text, - TimelineParser, - TimelineReplacement, - TimelineStyle, -} from './timeline_parser'; +import { Event, Sync, Text, TimelineParser, TimelineStyle } from './timeline_parser'; // Hi, sorry about this whole class. This is all pretty old code and honestly could // probably all be entirely rewritten at this point if anybody has the time or brain. diff --git a/ui/raidboss/timeline_parser.ts b/ui/raidboss/timeline_parser.ts index 2d878bd1594..e5a8e3fd9a1 100644 --- a/ui/raidboss/timeline_parser.ts +++ b/ui/raidboss/timeline_parser.ts @@ -1,6 +1,5 @@ import JSON5 from 'json5'; -import { Lang } from '../../resources/languages'; import logDefinitions, { LogDefinitionName } from '../../resources/netlog_defs'; import { buildNetRegexForTrigger } from '../../resources/netregexes'; import { UnreachableCode } from '../../resources/not_reached'; @@ -11,7 +10,7 @@ import { translateText, } from '../../resources/translations'; import { NetParams } from '../../types/net_props'; -import { LooseTimelineTrigger, TriggerAutoConfig } from '../../types/trigger'; +import { LooseTimelineTrigger, TimelineReplacement, TriggerAutoConfig } from '../../types/trigger'; import defaultOptions, { RaidbossOptions, TimelineConfig } from './raidboss_options'; @@ -98,13 +97,6 @@ const isObject = (x: unknown): x is { [key: string]: unknown } => { return x instanceof Object && !Array.isArray(x); }; -export type TimelineReplacement = { - locale: Lang; - missingTranslations?: boolean; - replaceSync?: { [regexString: string]: string }; - replaceText?: { [timelineText: string]: string }; -}; - export type TimelineStyle = { style: { [key: string]: string }; regex: RegExp; diff --git a/util/find_missing_timeline_translations.ts b/util/find_missing_timeline_translations.ts index 9efb8738d00..e4b111cb6ce 100644 --- a/util/find_missing_timeline_translations.ts +++ b/util/find_missing_timeline_translations.ts @@ -6,12 +6,12 @@ import NetRegexes from '../resources/netregexes'; import { UnreachableCode } from '../resources/not_reached'; import Regexes from '../resources/regexes'; import { AnonNetRegexParams, translateRegexBuildParamAnon } from '../resources/translations'; -import { LooseTriggerSet } from '../types/trigger'; +import { LooseTriggerSet, TimelineReplacement } from '../types/trigger'; import { commonReplacement, partialCommonTimelineReplacementKeys, } from '../ui/raidboss/common_replacement'; -import { TimelineParser, TimelineReplacement } from '../ui/raidboss/timeline_parser'; +import { TimelineParser } from '../ui/raidboss/timeline_parser'; import { ErrorFuncType } from './find_missing_translations';