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">
|
<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>
|
||||||
|
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
],
|
],
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue