mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00
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:
parent
0de7ab87a5
commit
c039845c16
34 changed files with 231 additions and 267 deletions
|
@ -1,23 +1,22 @@
|
|||
<script lang="ts">
|
||||
import "../sass/core.css";
|
||||
|
||||
import { I18n } from "anki/i18n";
|
||||
import pb from "anki/backend_proto";
|
||||
import type pb from "anki/backend_proto";
|
||||
import { buildNextLearnMsg } from "./lib";
|
||||
import { bridgeLink } from "anki/bridgecommand";
|
||||
|
||||
export let info: pb.BackendProto.CongratsInfoOut;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
const congrats = i18n.schedulingCongratulationsFinished();
|
||||
const nextLearnMsg = buildNextLearnMsg(info, i18n);
|
||||
const today_reviews = i18n.schedulingTodayReviewLimitReached();
|
||||
const today_new = i18n.schedulingTodayNewLimitReached();
|
||||
const congrats = tr.schedulingCongratulationsFinished();
|
||||
const nextLearnMsg = buildNextLearnMsg(info);
|
||||
const today_reviews = tr.schedulingTodayReviewLimitReached();
|
||||
const today_new = tr.schedulingTodayNewLimitReached();
|
||||
|
||||
const unburyThem = bridgeLink("unbury", i18n.schedulingUnburyThem());
|
||||
const buriedMsg = i18n.schedulingBuriedCardsFound({ unburyThem });
|
||||
const customStudy = bridgeLink("customStudy", i18n.schedulingCustomStudy());
|
||||
const customStudyMsg = i18n.schedulingHowToCustomStudy({
|
||||
const unburyThem = bridgeLink("unbury", tr.schedulingUnburyThem());
|
||||
const buriedMsg = tr.schedulingBuriedCardsFound({ unburyThem });
|
||||
const customStudy = bridgeLink("customStudy", tr.schedulingCustomStudy());
|
||||
const customStudyMsg = tr.schedulingHowToCustomStudy({
|
||||
customStudy,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,10 +9,10 @@ import CongratsPage from "./CongratsPage.svelte";
|
|||
|
||||
export async function congrats(target: HTMLDivElement): Promise<void> {
|
||||
checkNightMode();
|
||||
const i18n = await setupI18n();
|
||||
await setupI18n();
|
||||
const info = await getCongratsInfo();
|
||||
new CongratsPage({
|
||||
target,
|
||||
props: { info, i18n },
|
||||
props: { info },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
import pb from "anki/backend_proto";
|
||||
import { postRequest } from "anki/postrequest";
|
||||
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> {
|
||||
return pb.BackendProto.CongratsInfoOut.decode(
|
||||
|
@ -12,10 +13,7 @@ export async function getCongratsInfo(): Promise<pb.BackendProto.CongratsInfoOut
|
|||
);
|
||||
}
|
||||
|
||||
export function buildNextLearnMsg(
|
||||
info: pb.BackendProto.CongratsInfoOut,
|
||||
i18n: I18n
|
||||
): string {
|
||||
export function buildNextLearnMsg(info: pb.BackendProto.CongratsInfoOut): string {
|
||||
const secsUntil = info.secsUntilNextLearn;
|
||||
// next learning card not due (/ until tomorrow)?
|
||||
if (secsUntil == 0 || secsUntil > 86_400) {
|
||||
|
@ -25,11 +23,11 @@ export function buildNextLearnMsg(
|
|||
const unit = naturalUnit(secsUntil);
|
||||
const amount = Math.round(unitAmount(unit, secsUntil));
|
||||
const unitStr = unitName(unit);
|
||||
const nextLearnDue = i18n.schedulingNextLearnDue({
|
||||
const nextLearnDue = tr.schedulingNextLearnDue({
|
||||
amount,
|
||||
unit: unitStr,
|
||||
});
|
||||
const remaining = i18n.schedulingLearnRemaining({
|
||||
const remaining = tr.schedulingLearnRemaining({
|
||||
remaining: info.learnRemaining,
|
||||
});
|
||||
return `${nextLearnDue} ${remaining}`;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="typescript">
|
||||
import type { I18n } from "anki/i18n";
|
||||
import type pb from "anki/backend_proto";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
|
@ -17,7 +16,7 @@
|
|||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
|
||||
let histogramData = null as HistogramData | null;
|
||||
|
@ -36,22 +35,21 @@
|
|||
[histogramData, tableData] = buildHistogram(
|
||||
addedData,
|
||||
graphRange,
|
||||
i18n,
|
||||
dispatch,
|
||||
$browserLinksSupported
|
||||
);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsAddedTitle();
|
||||
const subtitle = i18n.statisticsAddedSubtitle();
|
||||
const title = tr.statisticsAddedTitle();
|
||||
const subtitle = tr.statisticsAddedSubtitle();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||
<GraphRangeRadios bind:graphRange revlogRange={RevlogRange.All} />
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
<HistogramGraph data={histogramData} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
<TableData {tableData} />
|
||||
</Graph>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
@ -12,7 +11,7 @@
|
|||
import { defaultGraphBounds, GraphRange, RevlogRange } from "./graph-helpers";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let revlogRange: RevlogRange;
|
||||
|
||||
let graphRange: GraphRange = GraphRange.Year;
|
||||
|
@ -22,22 +21,22 @@
|
|||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
$: if (sourceData) {
|
||||
renderButtons(svg as SVGElement, bounds, sourceData, i18n, graphRange);
|
||||
renderButtons(svg as SVGElement, bounds, sourceData, graphRange);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsAnswerButtonsTitle();
|
||||
const subtitle = i18n.statisticsAnswerButtonsSubtitle();
|
||||
const title = tr.statisticsAnswerButtonsTitle();
|
||||
const subtitle = tr.statisticsAnswerButtonsSubtitle();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
<GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="bars" />
|
||||
<HoverColumns />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
<NoDataOverlay {bounds} />
|
||||
</svg>
|
||||
</Graph>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
|
@ -17,7 +17,7 @@
|
|||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let preferences: PreferenceStore | null = null;
|
||||
export let revlogRange: RevlogRange;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let nightMode: boolean;
|
||||
|
||||
let { calendarFirstDayOfWeek } = preferences;
|
||||
|
@ -59,14 +59,13 @@
|
|||
graphData,
|
||||
dispatch,
|
||||
targetYear,
|
||||
i18n,
|
||||
nightMode,
|
||||
revlogRange,
|
||||
calendarFirstDayOfWeek.set
|
||||
);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsCalendarTitle();
|
||||
const title = tr.statisticsCalendarTitle();
|
||||
</script>
|
||||
|
||||
<Graph {title}>
|
||||
|
@ -88,6 +87,6 @@
|
|||
<g class="weekdays" />
|
||||
<g class="days" />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
<NoDataOverlay {bounds} />
|
||||
</svg>
|
||||
</Graph>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
@ -13,7 +12,7 @@
|
|||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut;
|
||||
export let i18n: I18n;
|
||||
import * as tr2 from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
|
||||
let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
|
||||
|
@ -29,12 +28,12 @@
|
|||
let tableData = (null as unknown) as TableDatum[];
|
||||
|
||||
$: {
|
||||
graphData = gatherData(sourceData, $cardCountsSeparateInactive, i18n);
|
||||
graphData = gatherData(sourceData, $cardCountsSeparateInactive);
|
||||
tableData = renderCards(svg as any, bounds, graphData);
|
||||
}
|
||||
|
||||
const label = i18n.statisticsCountsSeparateSuspendedBuriedCards();
|
||||
const total = i18n.statisticsCountsTotalCards();
|
||||
const label = tr2.statisticsCountsSeparateSuspendedBuriedCards();
|
||||
const total = tr2.statisticsCountsTotalCards();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
@ -13,7 +13,7 @@
|
|||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
|
||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||
|
@ -25,18 +25,17 @@
|
|||
$: if (sourceData) {
|
||||
[histogramData, tableData] = prepareData(
|
||||
gatherData(sourceData),
|
||||
i18n,
|
||||
dispatch,
|
||||
$browserLinksSupported
|
||||
);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsCardEaseTitle();
|
||||
const subtitle = i18n.statisticsCardEaseSubtitle();
|
||||
const title = tr.statisticsCardEaseTitle();
|
||||
const subtitle = tr.statisticsCardEaseSubtitle();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
<HistogramGraph data={histogramData} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
<TableData {tableData} />
|
||||
</Graph>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import type pb from "anki/backend_proto";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
|
@ -17,7 +17,7 @@
|
|||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
|
||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||
|
@ -37,15 +37,14 @@
|
|||
graphData,
|
||||
graphRange,
|
||||
$futureDueShowBacklog,
|
||||
i18n,
|
||||
dispatch,
|
||||
$browserLinksSupported
|
||||
));
|
||||
}
|
||||
|
||||
const title = i18n.statisticsFutureDueTitle();
|
||||
const subtitle = i18n.statisticsFutureDueSubtitle();
|
||||
const backlogLabel = i18n.statisticsBacklogCheckbox();
|
||||
const title = tr.statisticsFutureDueTitle();
|
||||
const subtitle = tr.statisticsFutureDueSubtitle();
|
||||
const backlogLabel = tr.statisticsBacklogCheckbox();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
|
@ -57,10 +56,10 @@
|
|||
</label>
|
||||
{/if}
|
||||
|
||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||
<GraphRangeRadios bind:graphRange revlogRange={RevlogRange.All} />
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
<HistogramGraph data={histogramData} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
<TableData {tableData} />
|
||||
</Graph>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="typescript">
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { RevlogRange, GraphRange } from "./graph-helpers";
|
||||
import { timeSpan, MONTH, YEAR } from "anki/time";
|
||||
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let revlogRange: RevlogRange;
|
||||
export let graphRange: GraphRange;
|
||||
export let followRevlog: boolean = false;
|
||||
|
@ -22,10 +21,10 @@
|
|||
onFollowRevlog(revlogRange);
|
||||
}
|
||||
|
||||
const month = timeSpan(i18n, 1 * MONTH);
|
||||
const month3 = timeSpan(i18n, 3 * MONTH);
|
||||
const year = timeSpan(i18n, 1 * YEAR);
|
||||
const all = i18n.statisticsRangeAllTime();
|
||||
const month = timeSpan(1 * MONTH);
|
||||
const month3 = timeSpan(3 * MONTH);
|
||||
const year = timeSpan(1 * YEAR);
|
||||
const all = tr.statisticsRangeAllTime();
|
||||
</script>
|
||||
|
||||
<label>
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
import type { SvelteComponent } from "svelte/internal";
|
||||
import { writable } from "svelte/store";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { bridgeCommand } from "anki/bridgecommand";
|
||||
|
||||
import WithGraphData from "./WithGraphData.svelte";
|
||||
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let nightMode: boolean;
|
||||
export let graphs: SvelteComponent[];
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
|||
let:preferences
|
||||
let:revlogRange>
|
||||
{#if controller}
|
||||
<svelte:component this={controller} {i18n} {search} {days} {loading} />
|
||||
<svelte:component this={controller} {search} {days} {loading} />
|
||||
{/if}
|
||||
|
||||
{#if sourceData && preferences && revlogRange}
|
||||
|
@ -51,7 +51,6 @@
|
|||
{sourceData}
|
||||
{preferences}
|
||||
{revlogRange}
|
||||
{i18n}
|
||||
{nightMode}
|
||||
on:search={browserSearch} />
|
||||
{/each}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="typescript">
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
||||
|
@ -11,7 +9,7 @@
|
|||
import { defaultGraphBounds } from "./graph-helpers";
|
||||
|
||||
export let data: HistogramData | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
let bounds = defaultGraphBounds();
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
@ -24,5 +22,5 @@
|
|||
<HoverColumns />
|
||||
<CumulativeOverlay />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
<NoDataOverlay {bounds} />
|
||||
</svg>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
@ -13,7 +12,7 @@
|
|||
import { renderHours } from "./hours";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let revlogRange: RevlogRange;
|
||||
let graphRange: GraphRange = GraphRange.Year;
|
||||
|
||||
|
@ -22,16 +21,16 @@
|
|||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
$: if (sourceData) {
|
||||
renderHours(svg as SVGElement, bounds, sourceData, i18n, graphRange);
|
||||
renderHours(svg as SVGElement, bounds, sourceData, graphRange);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsHoursTitle();
|
||||
const subtitle = i18n.statisticsHoursSubtitle();
|
||||
const title = tr.statisticsHoursTitle();
|
||||
const subtitle = tr.statisticsHoursSubtitle();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
<GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
|
@ -39,6 +38,6 @@
|
|||
<CumulativeOverlay />
|
||||
<HoverColumns />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
<NoDataOverlay {bounds} />
|
||||
</svg>
|
||||
</Graph>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import { timeSpan, MONTH } from "anki/time";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import type pb from "anki/backend_proto";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
|
||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||
|
@ -39,16 +39,15 @@
|
|||
[histogramData, tableData] = prepareIntervalData(
|
||||
intervalData,
|
||||
range,
|
||||
i18n,
|
||||
dispatch,
|
||||
$browserLinksSupported
|
||||
);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsIntervalsTitle();
|
||||
const subtitle = i18n.statisticsIntervalsSubtitle();
|
||||
const month = timeSpan(i18n, 1 * MONTH);
|
||||
const all = i18n.statisticsRangeAllTime();
|
||||
const title = tr.statisticsIntervalsTitle();
|
||||
const subtitle = tr.statisticsIntervalsSubtitle();
|
||||
const month = timeSpan(1 * MONTH);
|
||||
const all = tr.statisticsRangeAllTime();
|
||||
</script>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
|
@ -71,7 +70,7 @@
|
|||
</label>
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
<HistogramGraph data={histogramData} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
<TableData {tableData} />
|
||||
</Graph>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="typescript">
|
||||
import type { I18n } from "anki/i18n";
|
||||
import type { GraphBounds } from "./graph-helpers";
|
||||
export let bounds: GraphBounds;
|
||||
export let i18n: I18n;
|
||||
const noData = i18n.statisticsNoData();
|
||||
import * as tr from "anki/i18n";
|
||||
const noData = tr.statisticsNoData();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
||||
import type { I18n } from "anki/i18n";
|
||||
import * as tr from "anki/i18n";
|
||||
import { RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
||||
|
||||
enum SearchRange {
|
||||
|
@ -12,7 +12,6 @@
|
|||
Custom = 3,
|
||||
}
|
||||
|
||||
export let i18n: I18n;
|
||||
export let loading: boolean;
|
||||
|
||||
export let days: Writable<number>;
|
||||
|
@ -57,11 +56,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const year = i18n.statisticsRange_1YearHistory();
|
||||
const deck = i18n.statisticsRangeDeck();
|
||||
const collection = i18n.statisticsRangeCollection();
|
||||
const searchLabel = i18n.statisticsRangeSearch();
|
||||
const all = i18n.statisticsRangeAllHistory();
|
||||
const year = tr.statisticsRange_1YearHistory();
|
||||
const deck = tr.statisticsRangeDeck();
|
||||
const collection = tr.statisticsRangeCollection();
|
||||
const searchLabel = tr.statisticsRangeSearch();
|
||||
const all = tr.statisticsRangeAllHistory();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
@ -18,7 +17,7 @@
|
|||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let revlogRange: RevlogRange;
|
||||
export let i18n: I18n;
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
let graphData: GraphData | null = null;
|
||||
|
||||
|
@ -38,19 +37,18 @@
|
|||
bounds,
|
||||
graphData,
|
||||
graphRange,
|
||||
showTime,
|
||||
i18n
|
||||
showTime
|
||||
);
|
||||
}
|
||||
|
||||
const title = i18n.statisticsReviewsTitle();
|
||||
const time = i18n.statisticsReviewsTimeCheckbox();
|
||||
const title = tr.statisticsReviewsTitle();
|
||||
const time = tr.statisticsReviewsTimeCheckbox();
|
||||
|
||||
let subtitle = "";
|
||||
$: if (showTime) {
|
||||
subtitle = i18n.statisticsReviewsTimeSubtitle();
|
||||
subtitle = tr.statisticsReviewsTimeSubtitle();
|
||||
} else {
|
||||
subtitle = i18n.statisticsReviewsCountSubtitle();
|
||||
subtitle = tr.statisticsReviewsCountSubtitle();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -58,7 +56,7 @@
|
|||
<InputBox>
|
||||
<label> <input type="checkbox" bind:checked={showTime} /> {time} </label>
|
||||
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
<GraphRangeRadios bind:graphRange {revlogRange} followRevlog={true} />
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
|
@ -68,8 +66,8 @@
|
|||
<CumulativeOverlay />
|
||||
<HoverColumns />
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
<NoDataOverlay {bounds} />
|
||||
</svg>
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
<TableData {tableData} />
|
||||
</Graph>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="typescript">
|
||||
import type { I18n } from "anki/i18n";
|
||||
import type { TableDatum } from "./graph-helpers";
|
||||
|
||||
export let i18n: I18n;
|
||||
import { i18n } from "anki/i18n";
|
||||
export let tableData: TableDatum[];
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
|
||||
|
@ -8,11 +7,10 @@
|
|||
import { gatherData } from "./today";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let i18n: I18n;
|
||||
|
||||
let todayData: TodayData | null = null;
|
||||
$: if (sourceData) {
|
||||
todayData = gatherData(sourceData, i18n);
|
||||
todayData = gatherData(sourceData);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -18,10 +18,11 @@ import {
|
|||
} from "d3";
|
||||
import type { Bin } from "d3";
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { dayLabel } from "anki/time";
|
||||
import { GraphRange } from "./graph-helpers";
|
||||
import type { TableDatum, SearchDispatch } from "./graph-helpers";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface GraphData {
|
||||
daysAdded: number[];
|
||||
|
@ -49,7 +50,6 @@ function makeQuery(start: number, end: number): string {
|
|||
export function buildHistogram(
|
||||
data: GraphData,
|
||||
range: GraphRange,
|
||||
i18n: I18n,
|
||||
dispatch: SearchDispatch,
|
||||
browserLinksSupported: boolean
|
||||
): [HistogramData | null, TableDatum[]] {
|
||||
|
@ -99,12 +99,12 @@ export function buildHistogram(
|
|||
const cardsPerDay = Math.round(totalInPeriod / periodDays);
|
||||
const tableData = [
|
||||
{
|
||||
label: i18n.statisticsTotal(),
|
||||
value: i18n.statisticsCards({ cards: totalInPeriod }),
|
||||
label: tr.statisticsTotal(),
|
||||
value: tr.statisticsCards({ cards: totalInPeriod }),
|
||||
},
|
||||
{
|
||||
label: i18n.statisticsAverage(),
|
||||
value: i18n.statisticsCardsPerDay({ count: cardsPerDay }),
|
||||
label: tr.statisticsAverage(),
|
||||
value: tr.statisticsCardsPerDay({ count: cardsPerDay }),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -113,10 +113,10 @@ export function buildHistogram(
|
|||
cumulative: number,
|
||||
_percent: number
|
||||
): string {
|
||||
const day = dayLabel(i18n, bin.x0!, bin.x1!);
|
||||
const cards = i18n.statisticsCards({ cards: bin.length });
|
||||
const total = i18n.statisticsRunningTotal();
|
||||
const totalCards = i18n.statisticsCards({ cards: cumulative });
|
||||
const day = dayLabel(bin.x0!, bin.x1!);
|
||||
const cards = tr.statisticsCards({ cards: bin.length });
|
||||
const total = tr.statisticsRunningTotal();
|
||||
const totalCards = tr.statisticsCards({ cards: cumulative });
|
||||
return `${day}:<br>${cards}<br>${total}: ${totalCards}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import {
|
||||
interpolateRdYlGn,
|
||||
select,
|
||||
|
@ -26,6 +26,7 @@ import {
|
|||
GraphRange,
|
||||
millisecondCutoffForRange,
|
||||
} from "./graph-helpers";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
||||
|
@ -99,7 +100,6 @@ export function renderButtons(
|
|||
svgElem: SVGElement,
|
||||
bounds: GraphBounds,
|
||||
origData: pb.BackendProto.GraphsOut,
|
||||
i18n: I18n,
|
||||
range: GraphRange
|
||||
): void {
|
||||
const sourceData = gatherData(origData, range);
|
||||
|
@ -160,14 +160,14 @@ export function renderButtons(
|
|||
let kind: string;
|
||||
switch (d) {
|
||||
case "learning":
|
||||
kind = i18n.statisticsCountsLearningCards();
|
||||
kind = tr.statisticsCountsLearningCards();
|
||||
break;
|
||||
case "young":
|
||||
kind = i18n.statisticsCountsYoungCards();
|
||||
kind = tr.statisticsCountsYoungCards();
|
||||
break;
|
||||
case "mature":
|
||||
default:
|
||||
kind = i18n.statisticsCountsMatureCards();
|
||||
kind = tr.statisticsCountsMatureCards();
|
||||
break;
|
||||
}
|
||||
return `${kind} \u200e(${totalCorrect(d).percent}%)`;
|
||||
|
@ -239,9 +239,9 @@ export function renderButtons(
|
|||
// hover/tooltip
|
||||
|
||||
function tooltipText(d: Datum): string {
|
||||
const button = i18n.statisticsAnswerButtonsButtonNumber();
|
||||
const timesPressed = i18n.statisticsAnswerButtonsButtonPressed();
|
||||
const correctStr = i18n.statisticsHoursCorrect(totalCorrect(d.group));
|
||||
const button = tr.statisticsAnswerButtonsButtonNumber();
|
||||
const timesPressed = tr.statisticsAnswerButtonsButtonPressed();
|
||||
const correctStr = tr.statisticsHoursCorrect(totalCorrect(d.group));
|
||||
return `${button}: ${d.buttonNum}<br>${timesPressed}: ${d.count}<br>${correctStr}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import type { I18n } from "anki/i18n";
|
||||
import pb from "anki/backend_proto";
|
||||
import {
|
||||
interpolateBlues,
|
||||
|
@ -30,6 +29,8 @@ import {
|
|||
SearchDispatch,
|
||||
} from "./graph-helpers";
|
||||
import { clickableClass } from "./graph-styles";
|
||||
import { i18n } from "anki/i18n";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface GraphData {
|
||||
// indexed by day, where day is relative to today
|
||||
|
@ -91,7 +92,6 @@ export function renderCalendar(
|
|||
sourceData: GraphData,
|
||||
dispatch: SearchDispatch,
|
||||
targetYear: number,
|
||||
i18n: I18n,
|
||||
nightMode: boolean,
|
||||
revlogRange: RevlogRange,
|
||||
setFirstDayOfWeek: (d: number) => void
|
||||
|
@ -169,7 +169,7 @@ export function renderCalendar(
|
|||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
const cards = i18n.statisticsReviews({ reviews: d.count });
|
||||
const cards = tr.statisticsReviews({ reviews: d.count });
|
||||
return `${date}<br>${cards}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ import {
|
|||
cumsum,
|
||||
} from "d3";
|
||||
import type { GraphBounds } from "./graph-helpers";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
type Count = [string, number, boolean, string];
|
||||
export interface GraphData {
|
||||
|
@ -42,8 +43,7 @@ const barColours = [
|
|||
|
||||
function countCards(
|
||||
cards: pb.BackendProto.ICard[],
|
||||
separateInactive: boolean,
|
||||
i18n: I18n
|
||||
separateInactive: boolean
|
||||
): Count[] {
|
||||
let newCards = 0;
|
||||
let learn = 0;
|
||||
|
@ -89,38 +89,38 @@ function countCards(
|
|||
const extraQuery = separateInactive ? 'AND -("is:buried" OR "is:suspended")' : "";
|
||||
|
||||
const counts: Count[] = [
|
||||
[i18n.statisticsCountsNewCards(), newCards, true, `"is:new"${extraQuery}`],
|
||||
[tr.statisticsCountsNewCards(), newCards, true, `"is:new"${extraQuery}`],
|
||||
[
|
||||
i18n.statisticsCountsLearningCards(),
|
||||
tr.statisticsCountsLearningCards(),
|
||||
learn,
|
||||
true,
|
||||
`(-"is:review" AND "is:learn")${extraQuery}`,
|
||||
],
|
||||
[
|
||||
i18n.statisticsCountsRelearningCards(),
|
||||
tr.statisticsCountsRelearningCards(),
|
||||
relearn,
|
||||
true,
|
||||
`("is:review" AND "is:learn")${extraQuery}`,
|
||||
],
|
||||
[
|
||||
i18n.statisticsCountsYoungCards(),
|
||||
tr.statisticsCountsYoungCards(),
|
||||
young,
|
||||
true,
|
||||
`("is:review" AND -"is:learn") AND "prop:ivl<21"${extraQuery}`,
|
||||
],
|
||||
[
|
||||
i18n.statisticsCountsMatureCards(),
|
||||
tr.statisticsCountsMatureCards(),
|
||||
mature,
|
||||
true,
|
||||
`("is:review" -"is:learn") AND "prop:ivl>=21"${extraQuery}`,
|
||||
],
|
||||
[
|
||||
i18n.statisticsCountsSuspendedCards(),
|
||||
tr.statisticsCountsSuspendedCards(),
|
||||
suspended,
|
||||
separateInactive,
|
||||
'"is:suspended"',
|
||||
],
|
||||
[i18n.statisticsCountsBuriedCards(), buried, separateInactive, '"is:buried"'],
|
||||
[tr.statisticsCountsBuriedCards(), buried, separateInactive, '"is:buried"'],
|
||||
];
|
||||
|
||||
return counts;
|
||||
|
@ -128,14 +128,13 @@ function countCards(
|
|||
|
||||
export function gatherData(
|
||||
data: pb.BackendProto.GraphsOut,
|
||||
separateInactive: boolean,
|
||||
i18n: I18n
|
||||
separateInactive: boolean
|
||||
): GraphData {
|
||||
const totalCards = data.cards.length;
|
||||
const counts = countCards(data.cards, separateInactive, i18n);
|
||||
const counts = countCards(data.cards, separateInactive);
|
||||
|
||||
return {
|
||||
title: i18n.statisticsCountsTitle(),
|
||||
title: tr.statisticsCountsTitle(),
|
||||
counts,
|
||||
totalCards,
|
||||
};
|
||||
|
|
|
@ -18,8 +18,9 @@ import {
|
|||
import type { Bin, ScaleLinear } from "d3";
|
||||
import { CardType } from "anki/cards";
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import type { TableDatum, SearchDispatch } from "./graph-helpers";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface GraphData {
|
||||
eases: number[];
|
||||
|
@ -69,7 +70,6 @@ function getAdjustedScaleAndTicks(
|
|||
|
||||
export function prepareData(
|
||||
data: GraphData,
|
||||
i18n: I18n,
|
||||
dispatch: SearchDispatch,
|
||||
browserLinksSupported: boolean
|
||||
): [HistogramData | null, TableDatum[]] {
|
||||
|
@ -96,7 +96,7 @@ export function prepareData(
|
|||
const minPct = Math.floor(bin.x0!);
|
||||
const maxPct = Math.floor(bin.x1!);
|
||||
const percent = maxPct - minPct <= 10 ? `${bin.x0}%` : `${bin.x0}%-${bin.x1}%`;
|
||||
return i18n.statisticsCardEaseTooltip({
|
||||
return tr.statisticsCardEaseTooltip({
|
||||
cards: bin.length,
|
||||
percent,
|
||||
});
|
||||
|
@ -112,7 +112,7 @@ export function prepareData(
|
|||
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
|
||||
const tableData = [
|
||||
{
|
||||
label: i18n.statisticsAverageEase(),
|
||||
label: tr.statisticsAverageEase(),
|
||||
value: xTickFormat(sum(allEases) / total),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -20,9 +20,10 @@ import type { Bin } from "d3";
|
|||
import { CardQueue } from "anki/cards";
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import { dayLabel } from "anki/time";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { GraphRange } from "./graph-helpers";
|
||||
import type { TableDatum, SearchDispatch } from "./graph-helpers";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface GraphData {
|
||||
dueCounts: Map<number, number>;
|
||||
|
@ -93,7 +94,6 @@ export function buildHistogram(
|
|||
sourceData: GraphData,
|
||||
range: GraphRange,
|
||||
backlog: boolean,
|
||||
i18n: I18n,
|
||||
dispatch: SearchDispatch,
|
||||
browserLinksSupported: boolean
|
||||
): FutureDueOut {
|
||||
|
@ -153,11 +153,11 @@ export function buildHistogram(
|
|||
cumulative: number,
|
||||
_percent: number
|
||||
): string {
|
||||
const days = dayLabel(i18n, bin.x0!, bin.x1!);
|
||||
const cards = i18n.statisticsCardsDue({
|
||||
const days = dayLabel(bin.x0!, bin.x1!);
|
||||
const cards = tr.statisticsCardsDue({
|
||||
cards: binValue(bin as any),
|
||||
});
|
||||
const totalLabel = i18n.statisticsRunningTotal();
|
||||
const totalLabel = tr.statisticsRunningTotal();
|
||||
|
||||
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
|
||||
}
|
||||
|
@ -172,18 +172,18 @@ export function buildHistogram(
|
|||
const periodDays = xMax! - xMin!;
|
||||
const tableData = [
|
||||
{
|
||||
label: i18n.statisticsTotal(),
|
||||
value: i18n.statisticsReviews({ reviews: total }),
|
||||
label: tr.statisticsTotal(),
|
||||
value: tr.statisticsReviews({ reviews: total }),
|
||||
},
|
||||
{
|
||||
label: i18n.statisticsAverage(),
|
||||
value: i18n.statisticsReviewsPerDay({
|
||||
label: tr.statisticsAverage(),
|
||||
value: tr.statisticsReviewsPerDay({
|
||||
count: Math.round(total / periodDays),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: i18n.statisticsDueTomorrow(),
|
||||
value: i18n.statisticsReviews({
|
||||
label: tr.statisticsDueTomorrow(),
|
||||
value: tr.statisticsReviews({
|
||||
reviews: sourceData.dueCounts.get(1) ?? 0,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import type { I18n } from "anki/i18n";
|
||||
import pb from "anki/backend_proto";
|
||||
import {
|
||||
interpolateBlues,
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
millisecondCutoffForRange,
|
||||
} from "./graph-helpers";
|
||||
import { oddTickClass } from "./graph-styles";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
interface Hour {
|
||||
hour: number;
|
||||
|
@ -75,7 +75,6 @@ export function renderHours(
|
|||
svgElem: SVGElement,
|
||||
bounds: GraphBounds,
|
||||
origData: pb.BackendProto.GraphsOut,
|
||||
i18n: I18n,
|
||||
range: GraphRange
|
||||
): void {
|
||||
const data = gatherData(origData, range);
|
||||
|
@ -185,11 +184,11 @@ export function renderHours(
|
|||
);
|
||||
|
||||
function tooltipText(d: Hour): string {
|
||||
const hour = i18n.statisticsHoursRange({
|
||||
const hour = tr.statisticsHoursRange({
|
||||
hourStart: d.hour,
|
||||
hourEnd: d.hour + 1,
|
||||
});
|
||||
const correct = i18n.statisticsHoursCorrect({
|
||||
const correct = tr.statisticsHoursCorrect({
|
||||
correct: d.correctCount,
|
||||
total: d.totalCount,
|
||||
percent: d.totalCount ? (d.correctCount / d.totalCount) * 100 : 0,
|
||||
|
|
|
@ -33,11 +33,10 @@ export function graphs(
|
|||
): void {
|
||||
const nightMode = checkNightMode();
|
||||
|
||||
setupI18n().then((i18n) => {
|
||||
setupI18n().then(() => {
|
||||
new GraphsPage({
|
||||
target,
|
||||
props: {
|
||||
i18n,
|
||||
graphs,
|
||||
nightMode,
|
||||
initialSearch: search,
|
||||
|
|
|
@ -20,9 +20,10 @@ import {
|
|||
import type { Bin } from "d3";
|
||||
import { CardType } from "anki/cards";
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import type { TableDatum, SearchDispatch } from "./graph-helpers";
|
||||
import { timeSpan } from "anki/time";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface IntervalGraphData {
|
||||
intervals: number[];
|
||||
|
@ -43,20 +44,19 @@ export function gatherIntervalData(data: pb.BackendProto.GraphsOut): IntervalGra
|
|||
}
|
||||
|
||||
export function intervalLabel(
|
||||
i18n: I18n,
|
||||
daysStart: number,
|
||||
daysEnd: number,
|
||||
cards: number
|
||||
): string {
|
||||
if (daysEnd - daysStart <= 1) {
|
||||
// singular
|
||||
return i18n.statisticsIntervalsDaySingle({
|
||||
return tr.statisticsIntervalsDaySingle({
|
||||
day: daysStart,
|
||||
cards,
|
||||
});
|
||||
} else {
|
||||
// range
|
||||
return i18n.statisticsIntervalsDayRange({
|
||||
return tr.statisticsIntervalsDayRange({
|
||||
daysStart,
|
||||
daysEnd: daysEnd - 1,
|
||||
cards,
|
||||
|
@ -78,7 +78,6 @@ function makeQuery(start: number, end: number): string {
|
|||
export function prepareIntervalData(
|
||||
data: IntervalGraphData,
|
||||
range: IntervalRange,
|
||||
i18n: I18n,
|
||||
dispatch: SearchDispatch,
|
||||
browserLinksSupported: boolean
|
||||
): [HistogramData | null, TableDatum[]] {
|
||||
|
@ -146,9 +145,9 @@ export function prepareIntervalData(
|
|||
_cumulative: number,
|
||||
percent: number
|
||||
): string {
|
||||
// const day = dayLabel(i18n, bin.x0!, bin.x1!);
|
||||
const interval = intervalLabel(i18n, bin.x0!, bin.x1!, bin.length);
|
||||
const total = i18n.statisticsRunningTotal();
|
||||
// const day = dayLabel(bin.x0!, bin.x1!);
|
||||
const interval = intervalLabel(bin.x0!, bin.x1!, bin.length);
|
||||
const total = tr.statisticsRunningTotal();
|
||||
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
|
@ -160,10 +159,10 @@ export function prepareIntervalData(
|
|||
}
|
||||
|
||||
const meanInterval = Math.round(mean(allIntervals) ?? 0);
|
||||
const meanIntervalString = timeSpan(i18n, meanInterval * 86400, false);
|
||||
const meanIntervalString = timeSpan(meanInterval * 86400, false);
|
||||
const tableData = [
|
||||
{
|
||||
label: i18n.statisticsAverageInterval(),
|
||||
label: tr.statisticsAverageInterval(),
|
||||
value: meanIntervalString,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import { timeSpan, dayLabel } from "anki/time";
|
||||
import {
|
||||
interpolateGreens,
|
||||
|
@ -34,6 +34,7 @@ import type { Bin } from "d3";
|
|||
import type { TableDatum } from "./graph-helpers";
|
||||
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
interface Reviews {
|
||||
learn: number;
|
||||
|
@ -121,8 +122,7 @@ export function renderReviews(
|
|||
bounds: GraphBounds,
|
||||
sourceData: GraphData,
|
||||
range: GraphRange,
|
||||
showTime: boolean,
|
||||
i18n: I18n
|
||||
showTime: boolean
|
||||
): TableDatum[] {
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
@ -175,7 +175,7 @@ export function renderReviews(
|
|||
|
||||
const yTickFormat = (n: number): string => {
|
||||
if (showTime) {
|
||||
return timeSpan(i18n, n / 1000, true);
|
||||
return timeSpan(n / 1000, true);
|
||||
} else {
|
||||
if (Math.round(n) != n) {
|
||||
return "";
|
||||
|
@ -226,32 +226,24 @@ export function renderReviews(
|
|||
|
||||
function valueLabel(n: number): string {
|
||||
if (showTime) {
|
||||
return timeSpan(i18n, n / 1000);
|
||||
return timeSpan(n / 1000);
|
||||
} else {
|
||||
return i18n.statisticsReviews({ reviews: n });
|
||||
return tr.statisticsReviews({ reviews: n });
|
||||
}
|
||||
}
|
||||
|
||||
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 dayTotal = valueLabel(sum(totals));
|
||||
let buf = `<table><tr><td>${day}</td><td align=right>${dayTotal}</td></tr>`;
|
||||
const lines = [
|
||||
[oranges(1), i18n.statisticsCountsLearningCards(), valueLabel(totals[0])],
|
||||
[reds(1), i18n.statisticsCountsRelearningCards(), valueLabel(totals[1])],
|
||||
[
|
||||
lighterGreens(1),
|
||||
i18n.statisticsCountsYoungCards(),
|
||||
valueLabel(totals[2]),
|
||||
],
|
||||
[
|
||||
darkerGreens(1),
|
||||
i18n.statisticsCountsMatureCards(),
|
||||
valueLabel(totals[3]),
|
||||
],
|
||||
[purples(1), i18n.statisticsCountsEarlyCards(), valueLabel(totals[4])],
|
||||
["transparent", i18n.statisticsRunningTotal(), valueLabel(cumulative)],
|
||||
[oranges(1), tr.statisticsCountsLearningCards(), valueLabel(totals[0])],
|
||||
[reds(1), tr.statisticsCountsRelearningCards(), valueLabel(totals[1])],
|
||||
[lighterGreens(1), tr.statisticsCountsYoungCards(), valueLabel(totals[2])],
|
||||
[darkerGreens(1), tr.statisticsCountsMatureCards(), valueLabel(totals[3])],
|
||||
[purples(1), tr.statisticsCountsEarlyCards(), valueLabel(totals[4])],
|
||||
["transparent", tr.statisticsRunningTotal(), valueLabel(cumulative)],
|
||||
];
|
||||
for (const [colour, label, detail] of lines) {
|
||||
buf += `<tr>
|
||||
|
@ -377,14 +369,14 @@ export function renderReviews(
|
|||
averageAnswerTime: string,
|
||||
averageAnswerTimeLabel: string;
|
||||
if (showTime) {
|
||||
totalString = timeSpan(i18n, total / 1000, false);
|
||||
averageForDaysStudied = i18n.statisticsMinutesPerDay({
|
||||
totalString = timeSpan(total / 1000, false);
|
||||
averageForDaysStudied = tr.statisticsMinutesPerDay({
|
||||
count: Math.round(studiedAvg / 1000 / 60),
|
||||
});
|
||||
averageForPeriod = i18n.statisticsMinutesPerDay({
|
||||
averageForPeriod = tr.statisticsMinutesPerDay({
|
||||
count: Math.round(periodAvg / 1000 / 60),
|
||||
});
|
||||
averageAnswerTimeLabel = i18n.statisticsAverageAnswerTimeLabel();
|
||||
averageAnswerTimeLabel = tr.statisticsAverageAnswerTimeLabel();
|
||||
|
||||
// need to get total review count to calculate average time
|
||||
const countBins = histogram()
|
||||
|
@ -396,16 +388,16 @@ export function renderReviews(
|
|||
const totalSecs = total / 1000;
|
||||
const avgSecs = totalSecs / totalReviews;
|
||||
const cardsPerMin = (totalReviews * 60) / totalSecs;
|
||||
averageAnswerTime = i18n.statisticsAverageAnswerTime({
|
||||
averageAnswerTime = tr.statisticsAverageAnswerTime({
|
||||
averageSeconds: avgSecs,
|
||||
cardsPerMinute: cardsPerMin,
|
||||
});
|
||||
} else {
|
||||
totalString = i18n.statisticsReviews({ reviews: total });
|
||||
averageForDaysStudied = i18n.statisticsReviewsPerDay({
|
||||
totalString = tr.statisticsReviews({ reviews: total });
|
||||
averageForDaysStudied = tr.statisticsReviewsPerDay({
|
||||
count: Math.round(studiedAvg),
|
||||
});
|
||||
averageForPeriod = i18n.statisticsReviewsPerDay({
|
||||
averageForPeriod = tr.statisticsReviewsPerDay({
|
||||
count: Math.round(periodAvg),
|
||||
});
|
||||
averageAnswerTime = averageAnswerTimeLabel = "";
|
||||
|
@ -413,23 +405,23 @@ export function renderReviews(
|
|||
|
||||
const tableData: TableDatum[] = [
|
||||
{
|
||||
label: i18n.statisticsDaysStudied(),
|
||||
value: i18n.statisticsAmountOfTotalWithPercentage({
|
||||
label: tr.statisticsDaysStudied(),
|
||||
value: tr.statisticsAmountOfTotalWithPercentage({
|
||||
amount: studiedDays,
|
||||
total: periodDays,
|
||||
percent: Math.round((studiedDays / periodDays) * 100),
|
||||
}),
|
||||
},
|
||||
|
||||
{ label: i18n.statisticsTotal(), value: totalString },
|
||||
{ label: tr.statisticsTotal(), value: totalString },
|
||||
|
||||
{
|
||||
label: i18n.statisticsAverageForDaysStudied(),
|
||||
label: tr.statisticsAverageForDaysStudied(),
|
||||
value: averageForDaysStudied,
|
||||
},
|
||||
|
||||
{
|
||||
label: i18n.statisticsAverageOverPeriod(),
|
||||
label: tr.statisticsAverageOverPeriod(),
|
||||
value: averageForPeriod,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import pb from "anki/backend_proto";
|
||||
import { studiedToday } from "anki/time";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import * as tr from "anki/i18n";
|
||||
|
||||
export interface TodayData {
|
||||
title: string;
|
||||
|
@ -12,7 +13,7 @@ export interface TodayData {
|
|||
|
||||
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 answerMillis = 0;
|
||||
let correctCount = 0;
|
||||
|
@ -70,13 +71,13 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
|
|||
|
||||
let lines: string[];
|
||||
if (answerCount) {
|
||||
const studiedTodayText = studiedToday(i18n, answerCount, answerMillis / 1000);
|
||||
const studiedTodayText = studiedToday(answerCount, answerMillis / 1000);
|
||||
const againCount = answerCount - correctCount;
|
||||
let againCountText = i18n.statisticsTodayAgainCount();
|
||||
let againCountText = tr.statisticsTodayAgainCount();
|
||||
againCountText += ` ${againCount} (${((againCount / answerCount) * 100).toFixed(
|
||||
2
|
||||
)}%)`;
|
||||
const typeCounts = i18n.statisticsTodayTypeCounts({
|
||||
const typeCounts = tr.statisticsTodayTypeCounts({
|
||||
learnCount,
|
||||
reviewCount,
|
||||
relearnCount,
|
||||
|
@ -84,22 +85,22 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
|
|||
});
|
||||
let matureText: string;
|
||||
if (matureCount) {
|
||||
matureText = i18n.statisticsTodayCorrectMature({
|
||||
matureText = tr.statisticsTodayCorrectMature({
|
||||
correct: matureCorrect,
|
||||
total: matureCount,
|
||||
percent: (matureCorrect / matureCount) * 100,
|
||||
});
|
||||
} else {
|
||||
matureText = i18n.statisticsTodayNoMatureCards();
|
||||
matureText = tr.statisticsTodayNoMatureCards();
|
||||
}
|
||||
|
||||
lines = [studiedTodayText, againCountText, typeCounts, matureText];
|
||||
} else {
|
||||
lines = [i18n.statisticsTodayNoCards()];
|
||||
lines = [tr.statisticsTodayNoCards()];
|
||||
}
|
||||
|
||||
return {
|
||||
title: i18n.statisticsTodayTitle(),
|
||||
title: tr.statisticsTodayTitle(),
|
||||
lines,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ py_binary(
|
|||
|
||||
genrule(
|
||||
name = "fluent_gen",
|
||||
outs = ["i18n_generated.ts"],
|
||||
outs = ["i18n.ts"],
|
||||
cmd = "$(location genfluent) $(location //rslib/i18n:strings.json) $@",
|
||||
tools = [
|
||||
"genfluent",
|
||||
|
@ -44,7 +44,7 @@ genrule(
|
|||
|
||||
ts_library(
|
||||
name = "lib",
|
||||
srcs = glob(["**/*.ts"]) + [":i18n_generated.ts"],
|
||||
srcs = glob(["**/*.ts"]) + [":i18n.ts"],
|
||||
data = [
|
||||
"backend_proto",
|
||||
],
|
||||
|
|
|
@ -19,8 +19,8 @@ class Variable(TypedDict):
|
|||
|
||||
def methods() -> str:
|
||||
out = [
|
||||
"export class GeneratedTranslations {",
|
||||
" translate(key: string, args?: Record<string, any>): string { return 'nyi' } ",
|
||||
'import { i18n } from "./i18n_helpers";',
|
||||
'export { i18n, setupI18n } from "./i18n_helpers";',
|
||||
]
|
||||
for module in modules:
|
||||
for translation in module["translations"]:
|
||||
|
@ -31,14 +31,12 @@ def methods() -> str:
|
|||
out.append(
|
||||
f"""
|
||||
/** {doc} */
|
||||
{key}({arg_types}): string {{
|
||||
return this.translate("{translation["key"]}"{args})
|
||||
export function {key}({arg_types}): string {{
|
||||
return i18n.translate("{translation["key"]}"{args})
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
out.append("}")
|
||||
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { FluentBundle, FluentResource, FluentNumber } from "@fluent/bundle/compat";
|
||||
import { GeneratedTranslations } from "anki/i18n_generated";
|
||||
|
||||
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[] = [];
|
||||
langs: string[] = [];
|
||||
|
||||
translate(key: string, args: Record<string, RecordVal>): string {
|
||||
translate(key: string, args?: Record<string, RecordVal>): string {
|
||||
formatNumbers(args);
|
||||
for (const bundle of this.bundles) {
|
||||
const msg = bundle.getMessage(key);
|
||||
|
@ -65,15 +67,17 @@ export class I18n extends GeneratedTranslations {
|
|||
}
|
||||
}
|
||||
|
||||
export async function setupI18n(): Promise<I18n> {
|
||||
const i18n = new I18n();
|
||||
// global singleton
|
||||
export const i18n = new I18n();
|
||||
|
||||
export async function setupI18n(): Promise<void> {
|
||||
const resp = await fetch("/_anki/i18nResources", { method: "POST" });
|
||||
if (!resp.ok) {
|
||||
throw Error(`unexpected reply: ${resp.statusText}`);
|
||||
}
|
||||
const json = await resp.json();
|
||||
|
||||
i18n.bundles = [];
|
||||
for (const i in json.resources) {
|
||||
const text = json.resources[i];
|
||||
const lang = json.langs[i];
|
||||
|
@ -83,6 +87,4 @@ export async function setupI18n(): Promise<I18n> {
|
|||
i18n.bundles.push(bundle);
|
||||
}
|
||||
i18n.langs = json.langs;
|
||||
|
||||
return i18n;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 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 amount = unitAmount(unit, secs);
|
||||
const name = unitName(unit);
|
||||
|
@ -79,7 +79,7 @@ export function studiedToday(i18n: I18n, cards: number, secs: number): string {
|
|||
if (cards > 0) {
|
||||
secsPer = secs / cards;
|
||||
}
|
||||
return i18n.statisticsStudiedToday({
|
||||
return tr.statisticsStudiedToday({
|
||||
unit: name,
|
||||
secsPerCard: secsPer,
|
||||
// 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(
|
||||
i18n: I18n,
|
||||
unit: TimespanUnit,
|
||||
short: boolean
|
||||
): ({ amount: number }) => string {
|
||||
if (short) {
|
||||
switch (unit) {
|
||||
case TimespanUnit.Seconds:
|
||||
return i18n.statisticsElapsedTimeSeconds;
|
||||
return tr.statisticsElapsedTimeSeconds;
|
||||
case TimespanUnit.Minutes:
|
||||
return i18n.statisticsElapsedTimeMinutes;
|
||||
return tr.statisticsElapsedTimeMinutes;
|
||||
case TimespanUnit.Hours:
|
||||
return i18n.statisticsElapsedTimeHours;
|
||||
return tr.statisticsElapsedTimeHours;
|
||||
case TimespanUnit.Days:
|
||||
return i18n.statisticsElapsedTimeDays;
|
||||
return tr.statisticsElapsedTimeDays;
|
||||
case TimespanUnit.Months:
|
||||
return i18n.statisticsElapsedTimeMonths;
|
||||
return tr.statisticsElapsedTimeMonths;
|
||||
case TimespanUnit.Years:
|
||||
return i18n.statisticsElapsedTimeYears;
|
||||
return tr.statisticsElapsedTimeYears;
|
||||
}
|
||||
} else {
|
||||
switch (unit) {
|
||||
case TimespanUnit.Seconds:
|
||||
return i18n.schedulingTimeSpanSeconds;
|
||||
return tr.schedulingTimeSpanSeconds;
|
||||
case TimespanUnit.Minutes:
|
||||
return i18n.schedulingTimeSpanMinutes;
|
||||
return tr.schedulingTimeSpanMinutes;
|
||||
case TimespanUnit.Hours:
|
||||
return i18n.schedulingTimeSpanHours;
|
||||
return tr.schedulingTimeSpanHours;
|
||||
case TimespanUnit.Days:
|
||||
return i18n.schedulingTimeSpanDays;
|
||||
return tr.schedulingTimeSpanDays;
|
||||
case TimespanUnit.Months:
|
||||
return i18n.schedulingTimeSpanMonths;
|
||||
return tr.schedulingTimeSpanMonths;
|
||||
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
|
||||
/// eg 70 seconds -> "1.17 minutes"
|
||||
/// 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 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 smaller = Math.min(Math.abs(daysStart), Math.abs(daysEnd));
|
||||
if (larger - smaller <= 1) {
|
||||
// singular
|
||||
if (daysStart >= 0) {
|
||||
return i18n.statisticsInDaysSingle({ days: daysStart });
|
||||
return tr.statisticsInDaysSingle({ days: daysStart });
|
||||
} else {
|
||||
return i18n.statisticsDaysAgoSingle({ days: -daysStart });
|
||||
return tr.statisticsDaysAgoSingle({ days: -daysStart });
|
||||
}
|
||||
} else {
|
||||
// range
|
||||
if (daysStart >= 0) {
|
||||
return i18n.statisticsInDaysRange({
|
||||
return tr.statisticsInDaysRange({
|
||||
daysStart,
|
||||
daysEnd: daysEnd - 1,
|
||||
});
|
||||
} else {
|
||||
return i18n.statisticsDaysAgoRange({
|
||||
return tr.statisticsDaysAgoRange({
|
||||
daysStart: Math.abs(daysEnd - 1),
|
||||
daysEnd: -daysStart,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue