use singleton + free functions for i18n in ts

This allows for tree shaking, and reduces the congrats page from 150k
with the old enum solution to about 80k.
This commit is contained in:
Damien Elmes 2021-03-26 20:23:43 +10:00
parent 0de7ab87a5
commit c039845c16
34 changed files with 231 additions and 267 deletions

View file

@ -1,23 +1,22 @@
<script lang="ts"> <script lang="ts">
import "../sass/core.css"; import "../sass/core.css";
import { I18n } from "anki/i18n"; import type pb from "anki/backend_proto";
import pb from "anki/backend_proto";
import { buildNextLearnMsg } from "./lib"; import { buildNextLearnMsg } from "./lib";
import { bridgeLink } from "anki/bridgecommand"; import { bridgeLink } from "anki/bridgecommand";
export let info: pb.BackendProto.CongratsInfoOut; export let info: pb.BackendProto.CongratsInfoOut;
export let i18n: I18n; import * as tr from "anki/i18n";
const congrats = i18n.schedulingCongratulationsFinished(); const congrats = tr.schedulingCongratulationsFinished();
const nextLearnMsg = buildNextLearnMsg(info, i18n); const nextLearnMsg = buildNextLearnMsg(info);
const today_reviews = i18n.schedulingTodayReviewLimitReached(); const today_reviews = tr.schedulingTodayReviewLimitReached();
const today_new = i18n.schedulingTodayNewLimitReached(); const today_new = tr.schedulingTodayNewLimitReached();
const unburyThem = bridgeLink("unbury", i18n.schedulingUnburyThem()); const unburyThem = bridgeLink("unbury", tr.schedulingUnburyThem());
const buriedMsg = i18n.schedulingBuriedCardsFound({ unburyThem }); const buriedMsg = tr.schedulingBuriedCardsFound({ unburyThem });
const customStudy = bridgeLink("customStudy", i18n.schedulingCustomStudy()); const customStudy = bridgeLink("customStudy", tr.schedulingCustomStudy());
const customStudyMsg = i18n.schedulingHowToCustomStudy({ const customStudyMsg = tr.schedulingHowToCustomStudy({
customStudy, customStudy,
}); });
</script> </script>

View file

@ -9,10 +9,10 @@ import CongratsPage from "./CongratsPage.svelte";
export async function congrats(target: HTMLDivElement): Promise<void> { export async function congrats(target: HTMLDivElement): Promise<void> {
checkNightMode(); checkNightMode();
const i18n = await setupI18n(); await setupI18n();
const info = await getCongratsInfo(); const info = await getCongratsInfo();
new CongratsPage({ new CongratsPage({
target, target,
props: { info, i18n }, props: { info },
}); });
} }

View file

@ -4,7 +4,8 @@
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import { postRequest } from "anki/postrequest"; import { postRequest } from "anki/postrequest";
import { naturalUnit, unitAmount, unitName } from "anki/time"; import { naturalUnit, unitAmount, unitName } from "anki/time";
import type { I18n } from "anki/i18n";
import * as tr from "anki/i18n";
export async function getCongratsInfo(): Promise<pb.BackendProto.CongratsInfoOut> { export async function getCongratsInfo(): Promise<pb.BackendProto.CongratsInfoOut> {
return pb.BackendProto.CongratsInfoOut.decode( return pb.BackendProto.CongratsInfoOut.decode(
@ -12,10 +13,7 @@ export async function getCongratsInfo(): Promise<pb.BackendProto.CongratsInfoOut
); );
} }
export function buildNextLearnMsg( export function buildNextLearnMsg(info: pb.BackendProto.CongratsInfoOut): string {
info: pb.BackendProto.CongratsInfoOut,
i18n: I18n
): string {
const secsUntil = info.secsUntilNextLearn; const secsUntil = info.secsUntilNextLearn;
// next learning card not due (/ until tomorrow)? // next learning card not due (/ until tomorrow)?
if (secsUntil == 0 || secsUntil > 86_400) { if (secsUntil == 0 || secsUntil > 86_400) {
@ -25,11 +23,11 @@ export function buildNextLearnMsg(
const unit = naturalUnit(secsUntil); const unit = naturalUnit(secsUntil);
const amount = Math.round(unitAmount(unit, secsUntil)); const amount = Math.round(unitAmount(unit, secsUntil));
const unitStr = unitName(unit); const unitStr = unitName(unit);
const nextLearnDue = i18n.schedulingNextLearnDue({ const nextLearnDue = tr.schedulingNextLearnDue({
amount, amount,
unit: unitStr, unit: unitStr,
}); });
const remaining = i18n.schedulingLearnRemaining({ const remaining = tr.schedulingLearnRemaining({
remaining: info.learnRemaining, remaining: info.learnRemaining,
}); });
return `${nextLearnDue} ${remaining}`; return `${nextLearnDue} ${remaining}`;

View file

@ -1,5 +1,4 @@
<script lang="typescript"> <script lang="typescript">
import type { I18n } from "anki/i18n";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
@ -17,7 +16,7 @@
import type { PreferenceStore } from "./preferences"; import type { PreferenceStore } from "./preferences";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
@ -36,22 +35,21 @@
[histogramData, tableData] = buildHistogram( [histogramData, tableData] = buildHistogram(
addedData, addedData,
graphRange, graphRange,
i18n,
dispatch, dispatch,
$browserLinksSupported $browserLinksSupported
); );
} }
const title = i18n.statisticsAddedTitle(); const title = tr.statisticsAddedTitle();
const subtitle = i18n.statisticsAddedSubtitle(); const subtitle = tr.statisticsAddedSubtitle();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
<InputBox> <InputBox>
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} /> <GraphRangeRadios bind:graphRange revlogRange={RevlogRange.All} />
</InputBox> </InputBox>
<HistogramGraph data={histogramData} {i18n} /> <HistogramGraph data={histogramData} />
<TableData {i18n} {tableData} /> <TableData {tableData} />
</Graph> </Graph>

View file

@ -1,6 +1,5 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
import InputBox from "./InputBox.svelte"; import InputBox from "./InputBox.svelte";
@ -12,7 +11,7 @@
import { defaultGraphBounds, GraphRange, RevlogRange } from "./graph-helpers"; import { defaultGraphBounds, GraphRange, RevlogRange } from "./graph-helpers";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
let graphRange: GraphRange = GraphRange.Year; let graphRange: GraphRange = GraphRange.Year;
@ -22,22 +21,22 @@
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
$: if (sourceData) { $: if (sourceData) {
renderButtons(svg as SVGElement, bounds, sourceData, i18n, graphRange); renderButtons(svg as SVGElement, bounds, sourceData, graphRange);
} }
const title = i18n.statisticsAnswerButtonsTitle(); const title = tr.statisticsAnswerButtonsTitle();
const subtitle = i18n.statisticsAnswerButtonsSubtitle(); const subtitle = tr.statisticsAnswerButtonsSubtitle();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
<InputBox> <InputBox>
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} /> <GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
</InputBox> </InputBox>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
<g class="bars" /> <g class="bars" />
<HoverColumns /> <HoverColumns />
<AxisTicks {bounds} /> <AxisTicks {bounds} />
<NoDataOverlay {bounds} {i18n} /> <NoDataOverlay {bounds} />
</svg> </svg>
</Graph> </Graph>

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
@ -17,7 +17,7 @@
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let preferences: PreferenceStore | null = null; export let preferences: PreferenceStore | null = null;
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
export let i18n: I18n; import * as tr from "anki/i18n";
export let nightMode: boolean; export let nightMode: boolean;
let { calendarFirstDayOfWeek } = preferences; let { calendarFirstDayOfWeek } = preferences;
@ -59,14 +59,13 @@
graphData, graphData,
dispatch, dispatch,
targetYear, targetYear,
i18n,
nightMode, nightMode,
revlogRange, revlogRange,
calendarFirstDayOfWeek.set calendarFirstDayOfWeek.set
); );
} }
const title = i18n.statisticsCalendarTitle(); const title = tr.statisticsCalendarTitle();
</script> </script>
<Graph {title}> <Graph {title}>
@ -88,6 +87,6 @@
<g class="weekdays" /> <g class="weekdays" />
<g class="days" /> <g class="days" />
<AxisTicks {bounds} /> <AxisTicks {bounds} />
<NoDataOverlay {bounds} {i18n} /> <NoDataOverlay {bounds} />
</svg> </svg>
</Graph> </Graph>

View file

@ -1,7 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
import InputBox from "./InputBox.svelte"; import InputBox from "./InputBox.svelte";
@ -13,7 +12,7 @@
import type { PreferenceStore } from "./preferences"; import type { PreferenceStore } from "./preferences";
export let sourceData: pb.BackendProto.GraphsOut; export let sourceData: pb.BackendProto.GraphsOut;
export let i18n: I18n; import * as tr2 from "anki/i18n";
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
let { cardCountsSeparateInactive, browserLinksSupported } = preferences; let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
@ -29,12 +28,12 @@
let tableData = (null as unknown) as TableDatum[]; let tableData = (null as unknown) as TableDatum[];
$: { $: {
graphData = gatherData(sourceData, $cardCountsSeparateInactive, i18n); graphData = gatherData(sourceData, $cardCountsSeparateInactive);
tableData = renderCards(svg as any, bounds, graphData); tableData = renderCards(svg as any, bounds, graphData);
} }
const label = i18n.statisticsCountsSeparateSuspendedBuriedCards(); const label = tr2.statisticsCountsSeparateSuspendedBuriedCards();
const total = i18n.statisticsCountsTotalCards(); const total = tr2.statisticsCountsTotalCards();
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
@ -13,7 +13,7 @@
import type { PreferenceStore } from "./preferences"; import type { PreferenceStore } from "./preferences";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
const dispatch = createEventDispatcher<SearchEventMap>(); const dispatch = createEventDispatcher<SearchEventMap>();
@ -25,18 +25,17 @@
$: if (sourceData) { $: if (sourceData) {
[histogramData, tableData] = prepareData( [histogramData, tableData] = prepareData(
gatherData(sourceData), gatherData(sourceData),
i18n,
dispatch, dispatch,
$browserLinksSupported $browserLinksSupported
); );
} }
const title = i18n.statisticsCardEaseTitle(); const title = tr.statisticsCardEaseTitle();
const subtitle = i18n.statisticsCardEaseSubtitle(); const subtitle = tr.statisticsCardEaseSubtitle();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
<HistogramGraph data={histogramData} {i18n} /> <HistogramGraph data={histogramData} />
<TableData {i18n} {tableData} /> <TableData {tableData} />
</Graph> </Graph>

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import type { I18n } from "anki/i18n";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
@ -17,7 +17,7 @@
import type { PreferenceStore } from "./preferences"; import type { PreferenceStore } from "./preferences";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
const dispatch = createEventDispatcher<SearchEventMap>(); const dispatch = createEventDispatcher<SearchEventMap>();
@ -37,15 +37,14 @@
graphData, graphData,
graphRange, graphRange,
$futureDueShowBacklog, $futureDueShowBacklog,
i18n,
dispatch, dispatch,
$browserLinksSupported $browserLinksSupported
)); ));
} }
const title = i18n.statisticsFutureDueTitle(); const title = tr.statisticsFutureDueTitle();
const subtitle = i18n.statisticsFutureDueSubtitle(); const subtitle = tr.statisticsFutureDueSubtitle();
const backlogLabel = i18n.statisticsBacklogCheckbox(); const backlogLabel = tr.statisticsBacklogCheckbox();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
@ -57,10 +56,10 @@
</label> </label>
{/if} {/if}
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} /> <GraphRangeRadios bind:graphRange revlogRange={RevlogRange.All} />
</InputBox> </InputBox>
<HistogramGraph data={histogramData} {i18n} /> <HistogramGraph data={histogramData} />
<TableData {i18n} {tableData} /> <TableData {tableData} />
</Graph> </Graph>

View file

@ -1,9 +1,8 @@
<script lang="typescript"> <script lang="typescript">
import type { I18n } from "anki/i18n";
import { RevlogRange, GraphRange } from "./graph-helpers"; import { RevlogRange, GraphRange } from "./graph-helpers";
import { timeSpan, MONTH, YEAR } from "anki/time"; import { timeSpan, MONTH, YEAR } from "anki/time";
export let i18n: I18n; import * as tr from "anki/i18n";
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
export let graphRange: GraphRange; export let graphRange: GraphRange;
export let followRevlog: boolean = false; export let followRevlog: boolean = false;
@ -22,10 +21,10 @@
onFollowRevlog(revlogRange); onFollowRevlog(revlogRange);
} }
const month = timeSpan(i18n, 1 * MONTH); const month = timeSpan(1 * MONTH);
const month3 = timeSpan(i18n, 3 * MONTH); const month3 = timeSpan(3 * MONTH);
const year = timeSpan(i18n, 1 * YEAR); const year = timeSpan(1 * YEAR);
const all = i18n.statisticsRangeAllTime(); const all = tr.statisticsRangeAllTime();
</script> </script>
<label> <label>

View file

@ -3,12 +3,12 @@
import type { SvelteComponent } from "svelte/internal"; import type { SvelteComponent } from "svelte/internal";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { I18n } from "anki/i18n";
import { bridgeCommand } from "anki/bridgecommand"; import { bridgeCommand } from "anki/bridgecommand";
import WithGraphData from "./WithGraphData.svelte"; import WithGraphData from "./WithGraphData.svelte";
export let i18n: I18n; import * as tr from "anki/i18n";
export let nightMode: boolean; export let nightMode: boolean;
export let graphs: SvelteComponent[]; export let graphs: SvelteComponent[];
@ -41,7 +41,7 @@
let:preferences let:preferences
let:revlogRange> let:revlogRange>
{#if controller} {#if controller}
<svelte:component this={controller} {i18n} {search} {days} {loading} /> <svelte:component this={controller} {search} {days} {loading} />
{/if} {/if}
{#if sourceData && preferences && revlogRange} {#if sourceData && preferences && revlogRange}
@ -51,7 +51,6 @@
{sourceData} {sourceData}
{preferences} {preferences}
{revlogRange} {revlogRange}
{i18n}
{nightMode} {nightMode}
on:search={browserSearch} /> on:search={browserSearch} />
{/each} {/each}

View file

@ -1,6 +1,4 @@
<script lang="typescript"> <script lang="typescript">
import type { I18n } from "anki/i18n";
import AxisTicks from "./AxisTicks.svelte"; import AxisTicks from "./AxisTicks.svelte";
import NoDataOverlay from "./NoDataOverlay.svelte"; import NoDataOverlay from "./NoDataOverlay.svelte";
import CumulativeOverlay from "./CumulativeOverlay.svelte"; import CumulativeOverlay from "./CumulativeOverlay.svelte";
@ -11,7 +9,7 @@
import { defaultGraphBounds } from "./graph-helpers"; import { defaultGraphBounds } from "./graph-helpers";
export let data: HistogramData | null = null; export let data: HistogramData | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
let bounds = defaultGraphBounds(); let bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
@ -24,5 +22,5 @@
<HoverColumns /> <HoverColumns />
<CumulativeOverlay /> <CumulativeOverlay />
<AxisTicks {bounds} /> <AxisTicks {bounds} />
<NoDataOverlay {bounds} {i18n} /> <NoDataOverlay {bounds} />
</svg> </svg>

View file

@ -1,6 +1,5 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
import InputBox from "./InputBox.svelte"; import InputBox from "./InputBox.svelte";
@ -13,7 +12,7 @@
import { renderHours } from "./hours"; import { renderHours } from "./hours";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
let graphRange: GraphRange = GraphRange.Year; let graphRange: GraphRange = GraphRange.Year;
@ -22,16 +21,16 @@
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
$: if (sourceData) { $: if (sourceData) {
renderHours(svg as SVGElement, bounds, sourceData, i18n, graphRange); renderHours(svg as SVGElement, bounds, sourceData, graphRange);
} }
const title = i18n.statisticsHoursTitle(); const title = tr.statisticsHoursTitle();
const subtitle = i18n.statisticsHoursSubtitle(); const subtitle = tr.statisticsHoursSubtitle();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
<InputBox> <InputBox>
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} /> <GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
</InputBox> </InputBox>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
@ -39,6 +38,6 @@
<CumulativeOverlay /> <CumulativeOverlay />
<HoverColumns /> <HoverColumns />
<AxisTicks {bounds} /> <AxisTicks {bounds} />
<NoDataOverlay {bounds} {i18n} /> <NoDataOverlay {bounds} />
</svg> </svg>
</Graph> </Graph>

View file

@ -1,6 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import { timeSpan, MONTH } from "anki/time"; import { timeSpan, MONTH } from "anki/time";
import type { I18n } from "anki/i18n";
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
@ -20,7 +20,7 @@
import type { PreferenceStore } from "./preferences"; import type { PreferenceStore } from "./preferences";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; import * as tr from "anki/i18n";
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
const dispatch = createEventDispatcher<SearchEventMap>(); const dispatch = createEventDispatcher<SearchEventMap>();
@ -39,16 +39,15 @@
[histogramData, tableData] = prepareIntervalData( [histogramData, tableData] = prepareIntervalData(
intervalData, intervalData,
range, range,
i18n,
dispatch, dispatch,
$browserLinksSupported $browserLinksSupported
); );
} }
const title = i18n.statisticsIntervalsTitle(); const title = tr.statisticsIntervalsTitle();
const subtitle = i18n.statisticsIntervalsSubtitle(); const subtitle = tr.statisticsIntervalsSubtitle();
const month = timeSpan(i18n, 1 * MONTH); const month = timeSpan(1 * MONTH);
const all = i18n.statisticsRangeAllTime(); const all = tr.statisticsRangeAllTime();
</script> </script>
<Graph {title} {subtitle}> <Graph {title} {subtitle}>
@ -71,7 +70,7 @@
</label> </label>
</InputBox> </InputBox>
<HistogramGraph data={histogramData} {i18n} /> <HistogramGraph data={histogramData} />
<TableData {i18n} {tableData} /> <TableData {tableData} />
</Graph> </Graph>

View file

@ -1,9 +1,8 @@
<script lang="typescript"> <script lang="typescript">
import type { I18n } from "anki/i18n";
import type { GraphBounds } from "./graph-helpers"; import type { GraphBounds } from "./graph-helpers";
export let bounds: GraphBounds; export let bounds: GraphBounds;
export let i18n: I18n; import * as tr from "anki/i18n";
const noData = i18n.statisticsNoData(); const noData = tr.statisticsNoData();
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -3,7 +3,7 @@
import InputBox from "./InputBox.svelte"; import InputBox from "./InputBox.svelte";
import type { I18n } from "anki/i18n"; import * as tr from "anki/i18n";
import { RevlogRange, daysToRevlogRange } from "./graph-helpers"; import { RevlogRange, daysToRevlogRange } from "./graph-helpers";
enum SearchRange { enum SearchRange {
@ -12,7 +12,6 @@
Custom = 3, Custom = 3,
} }
export let i18n: I18n;
export let loading: boolean; export let loading: boolean;
export let days: Writable<number>; export let days: Writable<number>;
@ -57,11 +56,11 @@
} }
} }
const year = i18n.statisticsRange_1YearHistory(); const year = tr.statisticsRange_1YearHistory();
const deck = i18n.statisticsRangeDeck(); const deck = tr.statisticsRangeDeck();
const collection = i18n.statisticsRangeCollection(); const collection = tr.statisticsRangeCollection();
const searchLabel = i18n.statisticsRangeSearch(); const searchLabel = tr.statisticsRangeSearch();
const all = i18n.statisticsRangeAllHistory(); const all = tr.statisticsRangeAllHistory();
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -1,6 +1,5 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
import InputBox from "./InputBox.svelte"; import InputBox from "./InputBox.svelte";
@ -18,7 +17,7 @@
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
export let i18n: I18n; import * as tr from "anki/i18n";
let graphData: GraphData | null = null; let graphData: GraphData | null = null;
@ -38,19 +37,18 @@
bounds, bounds,
graphData, graphData,
graphRange, graphRange,
showTime, showTime
i18n
); );
} }
const title = i18n.statisticsReviewsTitle(); const title = tr.statisticsReviewsTitle();
const time = i18n.statisticsReviewsTimeCheckbox(); const time = tr.statisticsReviewsTimeCheckbox();
let subtitle = ""; let subtitle = "";
$: if (showTime) { $: if (showTime) {
subtitle = i18n.statisticsReviewsTimeSubtitle(); subtitle = tr.statisticsReviewsTimeSubtitle();
} else { } else {
subtitle = i18n.statisticsReviewsCountSubtitle(); subtitle = tr.statisticsReviewsCountSubtitle();
} }
</script> </script>
@ -58,7 +56,7 @@
<InputBox> <InputBox>
<label> <input type="checkbox" bind:checked={showTime} /> {time} </label> <label> <input type="checkbox" bind:checked={showTime} /> {time} </label>
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} /> <GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
</InputBox> </InputBox>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
@ -68,8 +66,8 @@
<CumulativeOverlay /> <CumulativeOverlay />
<HoverColumns /> <HoverColumns />
<AxisTicks {bounds} /> <AxisTicks {bounds} />
<NoDataOverlay {bounds} {i18n} /> <NoDataOverlay {bounds} />
</svg> </svg>
<TableData {i18n} {tableData} /> <TableData {tableData} />
</Graph> </Graph>

View file

@ -1,8 +1,6 @@
<script lang="typescript"> <script lang="typescript">
import type { I18n } from "anki/i18n";
import type { TableDatum } from "./graph-helpers"; import type { TableDatum } from "./graph-helpers";
import { i18n } from "anki/i18n";
export let i18n: I18n;
export let tableData: TableDatum[]; export let tableData: TableDatum[];
</script> </script>

View file

@ -1,6 +1,5 @@
<script lang="typescript"> <script lang="typescript">
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import Graph from "./Graph.svelte"; import Graph from "./Graph.svelte";
@ -8,11 +7,10 @@
import { gatherData } from "./today"; import { gatherData } from "./today";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n;
let todayData: TodayData | null = null; let todayData: TodayData | null = null;
$: if (sourceData) { $: if (sourceData) {
todayData = gatherData(sourceData, i18n); todayData = gatherData(sourceData);
} }
</script> </script>

View file

@ -18,10 +18,11 @@ import {
} from "d3"; } from "d3";
import type { Bin } from "d3"; import type { Bin } from "d3";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
import type { I18n } from "anki/i18n";
import { dayLabel } from "anki/time"; import { dayLabel } from "anki/time";
import { GraphRange } from "./graph-helpers"; import { GraphRange } from "./graph-helpers";
import type { TableDatum, SearchDispatch } from "./graph-helpers"; import type { TableDatum, SearchDispatch } from "./graph-helpers";
import * as tr from "anki/i18n";
export interface GraphData { export interface GraphData {
daysAdded: number[]; daysAdded: number[];
@ -49,7 +50,6 @@ function makeQuery(start: number, end: number): string {
export function buildHistogram( export function buildHistogram(
data: GraphData, data: GraphData,
range: GraphRange, range: GraphRange,
i18n: I18n,
dispatch: SearchDispatch, dispatch: SearchDispatch,
browserLinksSupported: boolean browserLinksSupported: boolean
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
@ -99,12 +99,12 @@ export function buildHistogram(
const cardsPerDay = Math.round(totalInPeriod / periodDays); const cardsPerDay = Math.round(totalInPeriod / periodDays);
const tableData = [ const tableData = [
{ {
label: i18n.statisticsTotal(), label: tr.statisticsTotal(),
value: i18n.statisticsCards({ cards: totalInPeriod }), value: tr.statisticsCards({ cards: totalInPeriod }),
}, },
{ {
label: i18n.statisticsAverage(), label: tr.statisticsAverage(),
value: i18n.statisticsCardsPerDay({ count: cardsPerDay }), value: tr.statisticsCardsPerDay({ count: cardsPerDay }),
}, },
]; ];
@ -113,10 +113,10 @@ export function buildHistogram(
cumulative: number, cumulative: number,
_percent: number _percent: number
): string { ): string {
const day = dayLabel(i18n, bin.x0!, bin.x1!); const day = dayLabel(bin.x0!, bin.x1!);
const cards = i18n.statisticsCards({ cards: bin.length }); const cards = tr.statisticsCards({ cards: bin.length });
const total = i18n.statisticsRunningTotal(); const total = tr.statisticsRunningTotal();
const totalCards = i18n.statisticsCards({ cards: cumulative }); const totalCards = tr.statisticsCards({ cards: cumulative });
return `${day}:<br>${cards}<br>${total}: ${totalCards}`; return `${day}:<br>${cards}<br>${total}: ${totalCards}`;
} }

View file

@ -7,7 +7,7 @@
*/ */
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import { import {
interpolateRdYlGn, interpolateRdYlGn,
select, select,
@ -26,6 +26,7 @@ import {
GraphRange, GraphRange,
millisecondCutoffForRange, millisecondCutoffForRange,
} from "./graph-helpers"; } from "./graph-helpers";
import * as tr from "anki/i18n";
type ButtonCounts = [number, number, number, number]; type ButtonCounts = [number, number, number, number];
@ -99,7 +100,6 @@ export function renderButtons(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
origData: pb.BackendProto.GraphsOut, origData: pb.BackendProto.GraphsOut,
i18n: I18n,
range: GraphRange range: GraphRange
): void { ): void {
const sourceData = gatherData(origData, range); const sourceData = gatherData(origData, range);
@ -160,14 +160,14 @@ export function renderButtons(
let kind: string; let kind: string;
switch (d) { switch (d) {
case "learning": case "learning":
kind = i18n.statisticsCountsLearningCards(); kind = tr.statisticsCountsLearningCards();
break; break;
case "young": case "young":
kind = i18n.statisticsCountsYoungCards(); kind = tr.statisticsCountsYoungCards();
break; break;
case "mature": case "mature":
default: default:
kind = i18n.statisticsCountsMatureCards(); kind = tr.statisticsCountsMatureCards();
break; break;
} }
return `${kind} \u200e(${totalCorrect(d).percent}%)`; return `${kind} \u200e(${totalCorrect(d).percent}%)`;
@ -239,9 +239,9 @@ export function renderButtons(
// hover/tooltip // hover/tooltip
function tooltipText(d: Datum): string { function tooltipText(d: Datum): string {
const button = i18n.statisticsAnswerButtonsButtonNumber(); const button = tr.statisticsAnswerButtonsButtonNumber();
const timesPressed = i18n.statisticsAnswerButtonsButtonPressed(); const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
const correctStr = i18n.statisticsHoursCorrect(totalCorrect(d.group)); const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
return `${button}: ${d.buttonNum}<br>${timesPressed}: ${d.count}<br>${correctStr}`; return `${button}: ${d.buttonNum}<br>${timesPressed}: ${d.count}<br>${correctStr}`;
} }

View file

@ -5,7 +5,6 @@
@typescript-eslint/no-non-null-assertion: "off", @typescript-eslint/no-non-null-assertion: "off",
*/ */
import type { I18n } from "anki/i18n";
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import { import {
interpolateBlues, interpolateBlues,
@ -30,6 +29,8 @@ import {
SearchDispatch, SearchDispatch,
} from "./graph-helpers"; } from "./graph-helpers";
import { clickableClass } from "./graph-styles"; import { clickableClass } from "./graph-styles";
import { i18n } from "anki/i18n";
import * as tr from "anki/i18n";
export interface GraphData { export interface GraphData {
// indexed by day, where day is relative to today // indexed by day, where day is relative to today
@ -91,7 +92,6 @@ export function renderCalendar(
sourceData: GraphData, sourceData: GraphData,
dispatch: SearchDispatch, dispatch: SearchDispatch,
targetYear: number, targetYear: number,
i18n: I18n,
nightMode: boolean, nightMode: boolean,
revlogRange: RevlogRange, revlogRange: RevlogRange,
setFirstDayOfWeek: (d: number) => void setFirstDayOfWeek: (d: number) => void
@ -169,7 +169,7 @@ export function renderCalendar(
month: "long", month: "long",
day: "numeric", day: "numeric",
}); });
const cards = i18n.statisticsReviews({ reviews: d.count }); const cards = tr.statisticsReviews({ reviews: d.count });
return `${date}<br>${cards}`; return `${date}<br>${cards}`;
} }

View file

@ -21,7 +21,8 @@ import {
cumsum, cumsum,
} from "d3"; } from "d3";
import type { GraphBounds } from "./graph-helpers"; import type { GraphBounds } from "./graph-helpers";
import type { I18n } from "anki/i18n";
import * as tr from "anki/i18n";
type Count = [string, number, boolean, string]; type Count = [string, number, boolean, string];
export interface GraphData { export interface GraphData {
@ -42,8 +43,7 @@ const barColours = [
function countCards( function countCards(
cards: pb.BackendProto.ICard[], cards: pb.BackendProto.ICard[],
separateInactive: boolean, separateInactive: boolean
i18n: I18n
): Count[] { ): Count[] {
let newCards = 0; let newCards = 0;
let learn = 0; let learn = 0;
@ -89,38 +89,38 @@ function countCards(
const extraQuery = separateInactive ? 'AND -("is:buried" OR "is:suspended")' : ""; const extraQuery = separateInactive ? 'AND -("is:buried" OR "is:suspended")' : "";
const counts: Count[] = [ const counts: Count[] = [
[i18n.statisticsCountsNewCards(), newCards, true, `"is:new"${extraQuery}`], [tr.statisticsCountsNewCards(), newCards, true, `"is:new"${extraQuery}`],
[ [
i18n.statisticsCountsLearningCards(), tr.statisticsCountsLearningCards(),
learn, learn,
true, true,
`(-"is:review" AND "is:learn")${extraQuery}`, `(-"is:review" AND "is:learn")${extraQuery}`,
], ],
[ [
i18n.statisticsCountsRelearningCards(), tr.statisticsCountsRelearningCards(),
relearn, relearn,
true, true,
`("is:review" AND "is:learn")${extraQuery}`, `("is:review" AND "is:learn")${extraQuery}`,
], ],
[ [
i18n.statisticsCountsYoungCards(), tr.statisticsCountsYoungCards(),
young, young,
true, true,
`("is:review" AND -"is:learn") AND "prop:ivl<21"${extraQuery}`, `("is:review" AND -"is:learn") AND "prop:ivl<21"${extraQuery}`,
], ],
[ [
i18n.statisticsCountsMatureCards(), tr.statisticsCountsMatureCards(),
mature, mature,
true, true,
`("is:review" -"is:learn") AND "prop:ivl>=21"${extraQuery}`, `("is:review" -"is:learn") AND "prop:ivl>=21"${extraQuery}`,
], ],
[ [
i18n.statisticsCountsSuspendedCards(), tr.statisticsCountsSuspendedCards(),
suspended, suspended,
separateInactive, separateInactive,
'"is:suspended"', '"is:suspended"',
], ],
[i18n.statisticsCountsBuriedCards(), buried, separateInactive, '"is:buried"'], [tr.statisticsCountsBuriedCards(), buried, separateInactive, '"is:buried"'],
]; ];
return counts; return counts;
@ -128,14 +128,13 @@ function countCards(
export function gatherData( export function gatherData(
data: pb.BackendProto.GraphsOut, data: pb.BackendProto.GraphsOut,
separateInactive: boolean, separateInactive: boolean
i18n: I18n
): GraphData { ): GraphData {
const totalCards = data.cards.length; const totalCards = data.cards.length;
const counts = countCards(data.cards, separateInactive, i18n); const counts = countCards(data.cards, separateInactive);
return { return {
title: i18n.statisticsCountsTitle(), title: tr.statisticsCountsTitle(),
counts, counts,
totalCards, totalCards,
}; };

View file

@ -18,8 +18,9 @@ import {
import type { Bin, ScaleLinear } from "d3"; import type { Bin, ScaleLinear } from "d3";
import { CardType } from "anki/cards"; import { CardType } from "anki/cards";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
import type { I18n } from "anki/i18n";
import type { TableDatum, SearchDispatch } from "./graph-helpers"; import type { TableDatum, SearchDispatch } from "./graph-helpers";
import * as tr from "anki/i18n";
export interface GraphData { export interface GraphData {
eases: number[]; eases: number[];
@ -69,7 +70,6 @@ function getAdjustedScaleAndTicks(
export function prepareData( export function prepareData(
data: GraphData, data: GraphData,
i18n: I18n,
dispatch: SearchDispatch, dispatch: SearchDispatch,
browserLinksSupported: boolean browserLinksSupported: boolean
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
@ -96,7 +96,7 @@ export function prepareData(
const minPct = Math.floor(bin.x0!); const minPct = Math.floor(bin.x0!);
const maxPct = Math.floor(bin.x1!); const maxPct = Math.floor(bin.x1!);
const percent = maxPct - minPct <= 10 ? `${bin.x0}%` : `${bin.x0}%-${bin.x1}%`; const percent = maxPct - minPct <= 10 ? `${bin.x0}%` : `${bin.x0}%-${bin.x1}%`;
return i18n.statisticsCardEaseTooltip({ return tr.statisticsCardEaseTooltip({
cards: bin.length, cards: bin.length,
percent, percent,
}); });
@ -112,7 +112,7 @@ export function prepareData(
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`; const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
const tableData = [ const tableData = [
{ {
label: i18n.statisticsAverageEase(), label: tr.statisticsAverageEase(),
value: xTickFormat(sum(allEases) / total), value: xTickFormat(sum(allEases) / total),
}, },
]; ];

View file

@ -20,9 +20,10 @@ import type { Bin } from "d3";
import { CardQueue } from "anki/cards"; import { CardQueue } from "anki/cards";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
import { dayLabel } from "anki/time"; import { dayLabel } from "anki/time";
import type { I18n } from "anki/i18n";
import { GraphRange } from "./graph-helpers"; import { GraphRange } from "./graph-helpers";
import type { TableDatum, SearchDispatch } from "./graph-helpers"; import type { TableDatum, SearchDispatch } from "./graph-helpers";
import * as tr from "anki/i18n";
export interface GraphData { export interface GraphData {
dueCounts: Map<number, number>; dueCounts: Map<number, number>;
@ -93,7 +94,6 @@ export function buildHistogram(
sourceData: GraphData, sourceData: GraphData,
range: GraphRange, range: GraphRange,
backlog: boolean, backlog: boolean,
i18n: I18n,
dispatch: SearchDispatch, dispatch: SearchDispatch,
browserLinksSupported: boolean browserLinksSupported: boolean
): FutureDueOut { ): FutureDueOut {
@ -153,11 +153,11 @@ export function buildHistogram(
cumulative: number, cumulative: number,
_percent: number _percent: number
): string { ): string {
const days = dayLabel(i18n, bin.x0!, bin.x1!); const days = dayLabel(bin.x0!, bin.x1!);
const cards = i18n.statisticsCardsDue({ const cards = tr.statisticsCardsDue({
cards: binValue(bin as any), cards: binValue(bin as any),
}); });
const totalLabel = i18n.statisticsRunningTotal(); const totalLabel = tr.statisticsRunningTotal();
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`; return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
} }
@ -172,18 +172,18 @@ export function buildHistogram(
const periodDays = xMax! - xMin!; const periodDays = xMax! - xMin!;
const tableData = [ const tableData = [
{ {
label: i18n.statisticsTotal(), label: tr.statisticsTotal(),
value: i18n.statisticsReviews({ reviews: total }), value: tr.statisticsReviews({ reviews: total }),
}, },
{ {
label: i18n.statisticsAverage(), label: tr.statisticsAverage(),
value: i18n.statisticsReviewsPerDay({ value: tr.statisticsReviewsPerDay({
count: Math.round(total / periodDays), count: Math.round(total / periodDays),
}), }),
}, },
{ {
label: i18n.statisticsDueTomorrow(), label: tr.statisticsDueTomorrow(),
value: i18n.statisticsReviews({ value: tr.statisticsReviews({
reviews: sourceData.dueCounts.get(1) ?? 0, reviews: sourceData.dueCounts.get(1) ?? 0,
}), }),
}, },

View file

@ -6,7 +6,6 @@
@typescript-eslint/no-explicit-any: "off", @typescript-eslint/no-explicit-any: "off",
*/ */
import type { I18n } from "anki/i18n";
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import { import {
interpolateBlues, interpolateBlues,
@ -30,6 +29,7 @@ import {
millisecondCutoffForRange, millisecondCutoffForRange,
} from "./graph-helpers"; } from "./graph-helpers";
import { oddTickClass } from "./graph-styles"; import { oddTickClass } from "./graph-styles";
import * as tr from "anki/i18n";
interface Hour { interface Hour {
hour: number; hour: number;
@ -75,7 +75,6 @@ export function renderHours(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
origData: pb.BackendProto.GraphsOut, origData: pb.BackendProto.GraphsOut,
i18n: I18n,
range: GraphRange range: GraphRange
): void { ): void {
const data = gatherData(origData, range); const data = gatherData(origData, range);
@ -185,11 +184,11 @@ export function renderHours(
); );
function tooltipText(d: Hour): string { function tooltipText(d: Hour): string {
const hour = i18n.statisticsHoursRange({ const hour = tr.statisticsHoursRange({
hourStart: d.hour, hourStart: d.hour,
hourEnd: d.hour + 1, hourEnd: d.hour + 1,
}); });
const correct = i18n.statisticsHoursCorrect({ const correct = tr.statisticsHoursCorrect({
correct: d.correctCount, correct: d.correctCount,
total: d.totalCount, total: d.totalCount,
percent: d.totalCount ? (d.correctCount / d.totalCount) * 100 : 0, percent: d.totalCount ? (d.correctCount / d.totalCount) * 100 : 0,

View file

@ -33,11 +33,10 @@ export function graphs(
): void { ): void {
const nightMode = checkNightMode(); const nightMode = checkNightMode();
setupI18n().then((i18n) => { setupI18n().then(() => {
new GraphsPage({ new GraphsPage({
target, target,
props: { props: {
i18n,
graphs, graphs,
nightMode, nightMode,
initialSearch: search, initialSearch: search,

View file

@ -20,9 +20,10 @@ import {
import type { Bin } from "d3"; import type { Bin } from "d3";
import { CardType } from "anki/cards"; import { CardType } from "anki/cards";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
import type { I18n } from "anki/i18n";
import type { TableDatum, SearchDispatch } from "./graph-helpers"; import type { TableDatum, SearchDispatch } from "./graph-helpers";
import { timeSpan } from "anki/time"; import { timeSpan } from "anki/time";
import * as tr from "anki/i18n";
export interface IntervalGraphData { export interface IntervalGraphData {
intervals: number[]; intervals: number[];
@ -43,20 +44,19 @@ export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGra
} }
export function intervalLabel( export function intervalLabel(
i18n: I18n,
daysStart: number, daysStart: number,
daysEnd: number, daysEnd: number,
cards: number cards: number
): string { ): string {
if (daysEnd - daysStart <= 1) { if (daysEnd - daysStart <= 1) {
// singular // singular
return i18n.statisticsIntervalsDaySingle({ return tr.statisticsIntervalsDaySingle({
day: daysStart, day: daysStart,
cards, cards,
}); });
} else { } else {
// range // range
return i18n.statisticsIntervalsDayRange({ return tr.statisticsIntervalsDayRange({
daysStart, daysStart,
daysEnd: daysEnd - 1, daysEnd: daysEnd - 1,
cards, cards,
@ -78,7 +78,6 @@ function makeQuery(start: number, end: number): string {
export function prepareIntervalData( export function prepareIntervalData(
data: IntervalGraphData, data: IntervalGraphData,
range: IntervalRange, range: IntervalRange,
i18n: I18n,
dispatch: SearchDispatch, dispatch: SearchDispatch,
browserLinksSupported: boolean browserLinksSupported: boolean
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
@ -146,9 +145,9 @@ export function prepareIntervalData(
_cumulative: number, _cumulative: number,
percent: number percent: number
): string { ): string {
// const day = dayLabel(i18n, bin.x0!, bin.x1!); // const day = dayLabel(bin.x0!, bin.x1!);
const interval = intervalLabel(i18n, bin.x0!, bin.x1!, bin.length); const interval = intervalLabel(bin.x0!, bin.x1!, bin.length);
const total = i18n.statisticsRunningTotal(); const total = tr.statisticsRunningTotal();
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`; return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
} }
@ -160,10 +159,10 @@ export function prepareIntervalData(
} }
const meanInterval = Math.round(mean(allIntervals) ?? 0); const meanInterval = Math.round(mean(allIntervals) ?? 0);
const meanIntervalString = timeSpan(i18n, meanInterval * 86400, false); const meanIntervalString = timeSpan(meanInterval * 86400, false);
const tableData = [ const tableData = [
{ {
label: i18n.statisticsAverageInterval(), label: tr.statisticsAverageInterval(),
value: meanIntervalString, value: meanIntervalString,
}, },
]; ];

View file

@ -7,7 +7,7 @@
*/ */
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import type { I18n } from "anki/i18n";
import { timeSpan, dayLabel } from "anki/time"; import { timeSpan, dayLabel } from "anki/time";
import { import {
interpolateGreens, interpolateGreens,
@ -34,6 +34,7 @@ import type { Bin } from "d3";
import type { TableDatum } from "./graph-helpers"; import type { TableDatum } from "./graph-helpers";
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers"; import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
import { showTooltip, hideTooltip } from "./tooltip"; import { showTooltip, hideTooltip } from "./tooltip";
import * as tr from "anki/i18n";
interface Reviews { interface Reviews {
learn: number; learn: number;
@ -121,8 +122,7 @@ export function renderReviews(
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData, sourceData: GraphData,
range: GraphRange, range: GraphRange,
showTime: boolean, showTime: boolean
i18n: I18n
): TableDatum[] { ): TableDatum[] {
const svg = select(svgElem); const svg = select(svgElem);
const trans = svg.transition().duration(600) as any; const trans = svg.transition().duration(600) as any;
@ -175,7 +175,7 @@ export function renderReviews(
const yTickFormat = (n: number): string => { const yTickFormat = (n: number): string => {
if (showTime) { if (showTime) {
return timeSpan(i18n, n / 1000, true); return timeSpan(n / 1000, true);
} else { } else {
if (Math.round(n) != n) { if (Math.round(n) != n) {
return ""; return "";
@ -226,32 +226,24 @@ export function renderReviews(
function valueLabel(n: number): string { function valueLabel(n: number): string {
if (showTime) { if (showTime) {
return timeSpan(i18n, n / 1000); return timeSpan(n / 1000);
} else { } else {
return i18n.statisticsReviews({ reviews: n }); return tr.statisticsReviews({ reviews: n });
} }
} }
function tooltipText(d: BinType, cumulative: number): string { function tooltipText(d: BinType, cumulative: number): string {
const day = dayLabel(i18n, d.x0!, d.x1!); const day = dayLabel(d.x0!, d.x1!);
const totals = totalsForBin(d); const totals = totalsForBin(d);
const dayTotal = valueLabel(sum(totals)); const dayTotal = valueLabel(sum(totals));
let buf = `<table><tr><td>${day}</td><td align=right>${dayTotal}</td></tr>`; let buf = `<table><tr><td>${day}</td><td align=right>${dayTotal}</td></tr>`;
const lines = [ const lines = [
[oranges(1), i18n.statisticsCountsLearningCards(), valueLabel(totals[0])], [oranges(1), tr.statisticsCountsLearningCards(), valueLabel(totals[0])],
[reds(1), i18n.statisticsCountsRelearningCards(), valueLabel(totals[1])], [reds(1), tr.statisticsCountsRelearningCards(), valueLabel(totals[1])],
[ [lighterGreens(1), tr.statisticsCountsYoungCards(), valueLabel(totals[2])],
lighterGreens(1), [darkerGreens(1), tr.statisticsCountsMatureCards(), valueLabel(totals[3])],
i18n.statisticsCountsYoungCards(), [purples(1), tr.statisticsCountsEarlyCards(), valueLabel(totals[4])],
valueLabel(totals[2]), ["transparent", tr.statisticsRunningTotal(), valueLabel(cumulative)],
],
[
darkerGreens(1),
i18n.statisticsCountsMatureCards(),
valueLabel(totals[3]),
],
[purples(1), i18n.statisticsCountsEarlyCards(), valueLabel(totals[4])],
["transparent", i18n.statisticsRunningTotal(), valueLabel(cumulative)],
]; ];
for (const [colour, label, detail] of lines) { for (const [colour, label, detail] of lines) {
buf += `<tr> buf += `<tr>
@ -377,14 +369,14 @@ export function renderReviews(
averageAnswerTime: string, averageAnswerTime: string,
averageAnswerTimeLabel: string; averageAnswerTimeLabel: string;
if (showTime) { if (showTime) {
totalString = timeSpan(i18n, total / 1000, false); totalString = timeSpan(total / 1000, false);
averageForDaysStudied = i18n.statisticsMinutesPerDay({ averageForDaysStudied = tr.statisticsMinutesPerDay({
count: Math.round(studiedAvg / 1000 / 60), count: Math.round(studiedAvg / 1000 / 60),
}); });
averageForPeriod = i18n.statisticsMinutesPerDay({ averageForPeriod = tr.statisticsMinutesPerDay({
count: Math.round(periodAvg / 1000 / 60), count: Math.round(periodAvg / 1000 / 60),
}); });
averageAnswerTimeLabel = i18n.statisticsAverageAnswerTimeLabel(); averageAnswerTimeLabel = tr.statisticsAverageAnswerTimeLabel();
// need to get total review count to calculate average time // need to get total review count to calculate average time
const countBins = histogram() const countBins = histogram()
@ -396,16 +388,16 @@ export function renderReviews(
const totalSecs = total / 1000; const totalSecs = total / 1000;
const avgSecs = totalSecs / totalReviews; const avgSecs = totalSecs / totalReviews;
const cardsPerMin = (totalReviews * 60) / totalSecs; const cardsPerMin = (totalReviews * 60) / totalSecs;
averageAnswerTime = i18n.statisticsAverageAnswerTime({ averageAnswerTime = tr.statisticsAverageAnswerTime({
averageSeconds: avgSecs, averageSeconds: avgSecs,
cardsPerMinute: cardsPerMin, cardsPerMinute: cardsPerMin,
}); });
} else { } else {
totalString = i18n.statisticsReviews({ reviews: total }); totalString = tr.statisticsReviews({ reviews: total });
averageForDaysStudied = i18n.statisticsReviewsPerDay({ averageForDaysStudied = tr.statisticsReviewsPerDay({
count: Math.round(studiedAvg), count: Math.round(studiedAvg),
}); });
averageForPeriod = i18n.statisticsReviewsPerDay({ averageForPeriod = tr.statisticsReviewsPerDay({
count: Math.round(periodAvg), count: Math.round(periodAvg),
}); });
averageAnswerTime = averageAnswerTimeLabel = ""; averageAnswerTime = averageAnswerTimeLabel = "";
@ -413,23 +405,23 @@ export function renderReviews(
const tableData: TableDatum[] = [ const tableData: TableDatum[] = [
{ {
label: i18n.statisticsDaysStudied(), label: tr.statisticsDaysStudied(),
value: i18n.statisticsAmountOfTotalWithPercentage({ value: tr.statisticsAmountOfTotalWithPercentage({
amount: studiedDays, amount: studiedDays,
total: periodDays, total: periodDays,
percent: Math.round((studiedDays / periodDays) * 100), percent: Math.round((studiedDays / periodDays) * 100),
}), }),
}, },
{ label: i18n.statisticsTotal(), value: totalString }, { label: tr.statisticsTotal(), value: totalString },
{ {
label: i18n.statisticsAverageForDaysStudied(), label: tr.statisticsAverageForDaysStudied(),
value: averageForDaysStudied, value: averageForDaysStudied,
}, },
{ {
label: i18n.statisticsAverageOverPeriod(), label: tr.statisticsAverageOverPeriod(),
value: averageForPeriod, value: averageForPeriod,
}, },
]; ];

View file

@ -3,7 +3,8 @@
import pb from "anki/backend_proto"; import pb from "anki/backend_proto";
import { studiedToday } from "anki/time"; import { studiedToday } from "anki/time";
import type { I18n } from "anki/i18n";
import * as tr from "anki/i18n";
export interface TodayData { export interface TodayData {
title: string; title: string;
@ -12,7 +13,7 @@ export interface TodayData {
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayData { export function gatherData(data: pb.BackendProto.GraphsOut): TodayData {
let answerCount = 0; let answerCount = 0;
let answerMillis = 0; let answerMillis = 0;
let correctCount = 0; let correctCount = 0;
@ -70,13 +71,13 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
let lines: string[]; let lines: string[];
if (answerCount) { if (answerCount) {
const studiedTodayText = studiedToday(i18n, answerCount, answerMillis / 1000); const studiedTodayText = studiedToday(answerCount, answerMillis / 1000);
const againCount = answerCount - correctCount; const againCount = answerCount - correctCount;
let againCountText = i18n.statisticsTodayAgainCount(); let againCountText = tr.statisticsTodayAgainCount();
againCountText += ` ${againCount} (${((againCount / answerCount) * 100).toFixed( againCountText += ` ${againCount} (${((againCount / answerCount) * 100).toFixed(
2 2
)}%)`; )}%)`;
const typeCounts = i18n.statisticsTodayTypeCounts({ const typeCounts = tr.statisticsTodayTypeCounts({
learnCount, learnCount,
reviewCount, reviewCount,
relearnCount, relearnCount,
@ -84,22 +85,22 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
}); });
let matureText: string; let matureText: string;
if (matureCount) { if (matureCount) {
matureText = i18n.statisticsTodayCorrectMature({ matureText = tr.statisticsTodayCorrectMature({
correct: matureCorrect, correct: matureCorrect,
total: matureCount, total: matureCount,
percent: (matureCorrect / matureCount) * 100, percent: (matureCorrect / matureCount) * 100,
}); });
} else { } else {
matureText = i18n.statisticsTodayNoMatureCards(); matureText = tr.statisticsTodayNoMatureCards();
} }
lines = [studiedTodayText, againCountText, typeCounts, matureText]; lines = [studiedTodayText, againCountText, typeCounts, matureText];
} else { } else {
lines = [i18n.statisticsTodayNoCards()]; lines = [tr.statisticsTodayNoCards()];
} }
return { return {
title: i18n.statisticsTodayTitle(), title: tr.statisticsTodayTitle(),
lines, lines,
}; };
} }

View file

@ -31,7 +31,7 @@ py_binary(
genrule( genrule(
name = "fluent_gen", name = "fluent_gen",
outs = ["i18n_generated.ts"], outs = ["i18n.ts"],
cmd = "$(location genfluent) $(location //rslib/i18n:strings.json) $@", cmd = "$(location genfluent) $(location //rslib/i18n:strings.json) $@",
tools = [ tools = [
"genfluent", "genfluent",
@ -44,7 +44,7 @@ genrule(
ts_library( ts_library(
name = "lib", name = "lib",
srcs = glob(["**/*.ts"]) + [":i18n_generated.ts"], srcs = glob(["**/*.ts"]) + [":i18n.ts"],
data = [ data = [
"backend_proto", "backend_proto",
], ],

View file

@ -19,8 +19,8 @@ class Variable(TypedDict):
def methods() -> str: def methods() -> str:
out = [ out = [
"export class GeneratedTranslations {", 'import { i18n } from "./i18n_helpers";',
" translate(key: string, args?: Record<string, any>): string { return 'nyi' } ", 'export { i18n, setupI18n } from "./i18n_helpers";',
] ]
for module in modules: for module in modules:
for translation in module["translations"]: for translation in module["translations"]:
@ -30,15 +30,13 @@ def methods() -> str:
doc = translation["text"] doc = translation["text"]
out.append( out.append(
f""" f"""
/** {doc} */ /** {doc} */
{key}({arg_types}): string {{ export function {key}({arg_types}): string {{
return this.translate("{translation["key"]}"{args}) return i18n.translate("{translation["key"]}"{args})
}} }}
""" """
) )
out.append("}")
return "\n".join(out) + "\n" return "\n".join(out) + "\n"

View file

@ -1,9 +1,11 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// An i18n singleton and setupI18n is re-exported via the generated i18n.ts file,
// so you should not need to access this file directly.
import "intl-pluralrules"; import "intl-pluralrules";
import { FluentBundle, FluentResource, FluentNumber } from "@fluent/bundle/compat"; import { FluentBundle, FluentResource, FluentNumber } from "@fluent/bundle/compat";
import { GeneratedTranslations } from "anki/i18n_generated";
type RecordVal = number | string | FluentNumber; type RecordVal = number | string | FluentNumber;
@ -20,11 +22,11 @@ function formatNumbers(args?: Record<string, RecordVal>): void {
} }
} }
export class I18n extends GeneratedTranslations { export class I18n {
bundles: FluentBundle[] = []; bundles: FluentBundle[] = [];
langs: string[] = []; langs: string[] = [];
translate(key: string, args: Record<string, RecordVal>): string { translate(key: string, args?: Record<string, RecordVal>): string {
formatNumbers(args); formatNumbers(args);
for (const bundle of this.bundles) { for (const bundle of this.bundles) {
const msg = bundle.getMessage(key); const msg = bundle.getMessage(key);
@ -65,15 +67,17 @@ export class I18n extends GeneratedTranslations {
} }
} }
export async function setupI18n(): Promise<I18n> { // global singleton
const i18n = new I18n(); export const i18n = new I18n();
export async function setupI18n(): Promise<void> {
const resp = await fetch("/_anki/i18nResources", { method: "POST" }); const resp = await fetch("/_anki/i18nResources", { method: "POST" });
if (!resp.ok) { if (!resp.ok) {
throw Error(`unexpected reply: ${resp.statusText}`); throw Error(`unexpected reply: ${resp.statusText}`);
} }
const json = await resp.json(); const json = await resp.json();
i18n.bundles = [];
for (const i in json.resources) { for (const i in json.resources) {
const text = json.resources[i]; const text = json.resources[i];
const lang = json.langs[i]; const lang = json.langs[i];
@ -83,6 +87,4 @@ export async function setupI18n(): Promise<I18n> {
i18n.bundles.push(bundle); i18n.bundles.push(bundle);
} }
i18n.langs = json.langs; i18n.langs = json.langs;
return i18n;
} }

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { I18n } from "./i18n"; import * as tr from "./i18n";
export const SECOND = 1.0; export const SECOND = 1.0;
export const MINUTE = 60.0 * SECOND; export const MINUTE = 60.0 * SECOND;
@ -70,7 +70,7 @@ export function unitAmount(unit: TimespanUnit, secs: number): number {
} }
} }
export function studiedToday(i18n: I18n, cards: number, secs: number): string { export function studiedToday(cards: number, secs: number): string {
const unit = naturalUnit(secs); const unit = naturalUnit(secs);
const amount = unitAmount(unit, secs); const amount = unitAmount(unit, secs);
const name = unitName(unit); const name = unitName(unit);
@ -79,7 +79,7 @@ export function studiedToday(i18n: I18n, cards: number, secs: number): string {
if (cards > 0) { if (cards > 0) {
secsPer = secs / cards; secsPer = secs / cards;
} }
return i18n.statisticsStudiedToday({ return tr.statisticsStudiedToday({
unit: name, unit: name,
secsPerCard: secsPer, secsPerCard: secsPer,
// these two are required, but don't appear in the generated code // these two are required, but don't appear in the generated code
@ -91,39 +91,38 @@ export function studiedToday(i18n: I18n, cards: number, secs: number): string {
} }
function i18nFuncForUnit( function i18nFuncForUnit(
i18n: I18n,
unit: TimespanUnit, unit: TimespanUnit,
short: boolean short: boolean
): ({ amount: number }) => string { ): ({ amount: number }) => string {
if (short) { if (short) {
switch (unit) { switch (unit) {
case TimespanUnit.Seconds: case TimespanUnit.Seconds:
return i18n.statisticsElapsedTimeSeconds; return tr.statisticsElapsedTimeSeconds;
case TimespanUnit.Minutes: case TimespanUnit.Minutes:
return i18n.statisticsElapsedTimeMinutes; return tr.statisticsElapsedTimeMinutes;
case TimespanUnit.Hours: case TimespanUnit.Hours:
return i18n.statisticsElapsedTimeHours; return tr.statisticsElapsedTimeHours;
case TimespanUnit.Days: case TimespanUnit.Days:
return i18n.statisticsElapsedTimeDays; return tr.statisticsElapsedTimeDays;
case TimespanUnit.Months: case TimespanUnit.Months:
return i18n.statisticsElapsedTimeMonths; return tr.statisticsElapsedTimeMonths;
case TimespanUnit.Years: case TimespanUnit.Years:
return i18n.statisticsElapsedTimeYears; return tr.statisticsElapsedTimeYears;
} }
} else { } else {
switch (unit) { switch (unit) {
case TimespanUnit.Seconds: case TimespanUnit.Seconds:
return i18n.schedulingTimeSpanSeconds; return tr.schedulingTimeSpanSeconds;
case TimespanUnit.Minutes: case TimespanUnit.Minutes:
return i18n.schedulingTimeSpanMinutes; return tr.schedulingTimeSpanMinutes;
case TimespanUnit.Hours: case TimespanUnit.Hours:
return i18n.schedulingTimeSpanHours; return tr.schedulingTimeSpanHours;
case TimespanUnit.Days: case TimespanUnit.Days:
return i18n.schedulingTimeSpanDays; return tr.schedulingTimeSpanDays;
case TimespanUnit.Months: case TimespanUnit.Months:
return i18n.schedulingTimeSpanMonths; return tr.schedulingTimeSpanMonths;
case TimespanUnit.Years: case TimespanUnit.Years:
return i18n.schedulingTimeSpanYears; return tr.schedulingTimeSpanYears;
} }
} }
} }
@ -132,31 +131,31 @@ function i18nFuncForUnit(
/// If precise is true, show to two decimal places, eg /// If precise is true, show to two decimal places, eg
/// eg 70 seconds -> "1.17 minutes" /// eg 70 seconds -> "1.17 minutes"
/// If false, seconds and days are shown without decimals. /// If false, seconds and days are shown without decimals.
export function timeSpan(i18n: I18n, seconds: number, short = false): string { export function timeSpan(seconds: number, short = false): string {
const unit = naturalUnit(seconds); const unit = naturalUnit(seconds);
const amount = unitAmount(unit, seconds); const amount = unitAmount(unit, seconds);
return i18nFuncForUnit(i18n, unit, short).call(i18n, { amount }); return i18nFuncForUnit(unit, short)({ amount });
} }
export function dayLabel(i18n: I18n, daysStart: number, daysEnd: number): string { export function dayLabel(daysStart: number, daysEnd: number): string {
const larger = Math.max(Math.abs(daysStart), Math.abs(daysEnd)); const larger = Math.max(Math.abs(daysStart), Math.abs(daysEnd));
const smaller = Math.min(Math.abs(daysStart), Math.abs(daysEnd)); const smaller = Math.min(Math.abs(daysStart), Math.abs(daysEnd));
if (larger - smaller <= 1) { if (larger - smaller <= 1) {
// singular // singular
if (daysStart >= 0) { if (daysStart >= 0) {
return i18n.statisticsInDaysSingle({ days: daysStart }); return tr.statisticsInDaysSingle({ days: daysStart });
} else { } else {
return i18n.statisticsDaysAgoSingle({ days: -daysStart }); return tr.statisticsDaysAgoSingle({ days: -daysStart });
} }
} else { } else {
// range // range
if (daysStart >= 0) { if (daysStart >= 0) {
return i18n.statisticsInDaysRange({ return tr.statisticsInDaysRange({
daysStart, daysStart,
daysEnd: daysEnd - 1, daysEnd: daysEnd - 1,
}); });
} else { } else {
return i18n.statisticsDaysAgoRange({ return tr.statisticsDaysAgoRange({
daysStart: Math.abs(daysEnd - 1), daysStart: Math.abs(daysEnd - 1),
daysEnd: -daysStart, daysEnd: -daysStart,
}); });