mirror of
https://github.com/zadam/trilium.git
synced 2026-09-12 19:50:20 +05:00
fix(scripts): read every page of the Weblate language stats
`getLanguageStats()` returned the first response page, so anything past it was invisible to both callers. The coverage gate would pass by never seeing the languages it was meant to catch, and the README language bar would quietly drop them. Weblate serves 50 translations per page and each component is at 40, so the current lists are complete and nothing was missed. That is ten languages of headroom, and a language appears whenever someone starts translating one. Follow `next` until it runs out, and report the count actually read. A non-OK response now names the URL rather than surfacing as a JSON parse error on the error body. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5897d741bc
commit
768bc3946e
@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { CACHE_MAX_AGE_MS, isCacheFresh } from "./utils";
|
||||
import { CACHE_MAX_AGE_MS, fetchAllPages, isCacheFresh } from "./utils";
|
||||
|
||||
describe("isCacheFresh", () => {
|
||||
const now = new Date("2026-08-20T12:00:00Z").getTime();
|
||||
@ -21,3 +21,43 @@ describe("isCacheFresh", () => {
|
||||
expect(isCacheFresh(now + CACHE_MAX_AGE_MS, now)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchAllPages", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
/** Answers each URL with a canned page, and records the order they were requested in. */
|
||||
function stubPages(pages: Record<string, unknown>) {
|
||||
const requested: string[] = [];
|
||||
vi.stubGlobal("fetch", async (url: string) => {
|
||||
requested.push(url);
|
||||
const page = pages[url];
|
||||
if (!page) return { ok: false, status: 404, statusText: "Not Found" };
|
||||
return { ok: true, json: async () => page };
|
||||
});
|
||||
return requested;
|
||||
}
|
||||
|
||||
it("follows every page and merges the results in order", async () => {
|
||||
const requested = stubPages({
|
||||
"/first": { count: 3, next: "/second", results: [ "a", "b" ] },
|
||||
"/second": { count: 3, next: null, results: [ "c" ] }
|
||||
});
|
||||
|
||||
const merged = await fetchAllPages("/first");
|
||||
expect(merged).toStrictEqual({ count: 3, results: [ "a", "b", "c" ] });
|
||||
expect(requested).toStrictEqual([ "/first", "/second" ]);
|
||||
});
|
||||
|
||||
it("counts what it read rather than trusting the reported total", async () => {
|
||||
stubPages({ "/only": { count: 99, next: null, results: [ "a" ] } });
|
||||
|
||||
expect(await fetchAllPages("/only")).toStrictEqual({ count: 1, results: [ "a" ] });
|
||||
});
|
||||
|
||||
it("names the failing URL instead of failing to parse the body", async () => {
|
||||
stubPages({ "/first": { count: 2, next: "/gone", results: [ "a" ] } });
|
||||
|
||||
await expect(fetchAllPages("/first"))
|
||||
.rejects.toThrow("Weblate answered 404 Not Found for /gone.");
|
||||
});
|
||||
});
|
||||
|
||||
@ -27,8 +27,8 @@ export async function getLanguageStats(project: WeblateProject) {
|
||||
|
||||
// Make the request
|
||||
console.log("Reading language stats from Weblate API.");
|
||||
const request = await fetch(`https://hosted.weblate.org/api/components/trilium/${project}/translations/`);
|
||||
const stats = JSON.parse(await request.text());
|
||||
const stats = await fetchAllPages(
|
||||
`https://hosted.weblate.org/api/components/trilium/${project}/translations/`);
|
||||
|
||||
// Update the cache
|
||||
await writeFile(cacheFile, JSON.stringify(stats, null, 4));
|
||||
@ -36,6 +36,32 @@ export async function getLanguageStats(project: WeblateProject) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads every page of a paginated Weblate endpoint into a single `results` array.
|
||||
*
|
||||
* The API serves 50 entries per page. Each component is already at 40 languages, so a
|
||||
* caller reading only the first response would start losing languages once translators
|
||||
* pick up ten more, and the coverage gate would pass by never seeing them.
|
||||
*/
|
||||
export async function fetchAllPages(firstPageUrl: string) {
|
||||
const results: unknown[] = [];
|
||||
let url: string | null = firstPageUrl;
|
||||
|
||||
while (url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Weblate answered ${response.status} ${response.statusText} for ${url}.`);
|
||||
}
|
||||
|
||||
const page = await response.json();
|
||||
results.push(...page.results);
|
||||
url = page.next;
|
||||
}
|
||||
|
||||
return { count: results.length, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a cache file written at `mtimeMs` can still be used at `nowMs`.
|
||||
*
|
||||
|
||||
Loading…
Reference in New Issue
Block a user