Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,13 @@ running terminals: see [development](docs/development.md#checks).
- **Theme colors:** edit them only in the theme blocks of `src/styles.css`. Never edit
`src/lib/themes.generated.ts` or the generated copies in DESIGN.md, `index.html` and
`public/manifest.webmanifest`; run `bun run generate:themes`.
- **UI text:** every new `t("…")` string needs an entry in `src/lib/i18n.ko.ts`, `i18n.ja.ts`
and `i18n.zh.ts`, with the same placeholders. `bun run test:unit` fails on a missing, unused or
untranslated entry.
- **UI text:** every new `t("…")` string needs a Korean, Japanese and Simplified Chinese entry with
the same placeholders. A string that only one feature's files use goes in that feature's files in
`src/lib/i18n/` when it has them (`settings.ko.ts`, `settings.ja.ts` and `settings.zh.ts` for the
Settings dialog; each file's header lists its feature's files). Any other string goes in
`src/lib/i18n.ko.ts`, `i18n.ja.ts` and `i18n.zh.ts`, and so does a feature's string once another
feature uses it too. `bun run test:unit` fails on a missing, unused or untranslated entry, and on one
the three languages keep in different files.
- **UI languages:** the app keeps English, Korean, Japanese and Simplified Chinese and does not take
new ones. Every language has to translate each later string, so a PR that adds a language is
closed.
Expand Down
297 changes: 14 additions & 283 deletions src/lib/i18n.ja.ts

Large diffs are not rendered by default.

297 changes: 14 additions & 283 deletions src/lib/i18n.ko.ts

Large diffs are not rendered by default.

37 changes: 33 additions & 4 deletions src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, it } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { JA } from "./i18n.ja.ts";
import { KO } from "./i18n.ko.ts";
import { ZH } from "./i18n.zh.ts";
import { JA, JA_PARTS } from "./i18n.ja.ts";
import { KO, KO_PARTS } from "./i18n.ko.ts";
import { ZH, ZH_PARTS } from "./i18n.zh.ts";
import { resolveLanguage, translate } from "./i18n.ts";

const root = join(import.meta.dir, "..");
Expand All @@ -16,7 +16,8 @@ const root = join(import.meta.dir, "..");
function keysInCode(): Map<string, string[]> {
const found = new Map<string, string[]>();
for (const file of new Bun.Glob("**/*.{ts,tsx}").scanSync({ cwd: root })) {
if (file.endsWith(".test.ts") || /i18n\.\w+\.ts$/.test(file)) continue;
// the dictionaries: i18n.ko.ts and the like, and the parts in lib/i18n/
if (file.endsWith(".test.ts") || /i18n\.\w+\.ts$/.test(file) || /^lib[\\/]i18n[\\/]/.test(file)) continue;
const text = readFileSync(join(root, file), "utf8");
for (const match of text.matchAll(/(?<![\w.])(?:t|tt)\(/g)) {
const firstArg = firstArgument(text, match.index + match[0].length);
Expand Down Expand Up @@ -112,6 +113,34 @@ describe.each(Object.entries(DICTIONARIES))("%s dictionary", (_name, dictionary)
});
});

/** Parts in src/lib/i18n/ hold the keys only one feature uses; the base dictionary holds the rest. */
const PARTS = { Korean: [KO, KO_PARTS], Japanese: [JA, JA_PARTS], Chinese: [ZH, ZH_PARTS] } as const;

describe("dictionary parts", () => {
it.each(Object.entries(PARTS))("%s keeps each key in one part", (_name, [dictionary, parts]) => {
// the dictionary is the parts spread together, and a spread keeps only the last copy of a key
const seen = new Map<string, string>();
const twice = Object.entries(parts).flatMap(([part, entries]) => Object.keys(entries).flatMap((key) => {
const first = seen.get(key);
seen.set(key, part);
return first === undefined ? [] : [`${key} (${first}, ${part})`];
}));
expect(twice).toEqual([]);
expect(Object.keys(dictionary).length).toBe(seen.size);
});

it("puts each key in the same part in every language", () => {
const placement = (parts: Record<string, Record<string, string>>) => Object.fromEntries(Object.entries(parts).map(([part, entries]) => [part, Object.keys(entries).sort()]));
expect(placement(JA_PARTS)).toEqual(placement(KO_PARTS));
expect(placement(ZH_PARTS)).toEqual(placement(KO_PARTS));
});

it("keeps each part file at 400 lines or fewer", () => {
const long = [...new Bun.Glob("lib/i18n/*.ts").scanSync({ cwd: root })].filter((file) => readFileSync(join(root, file), "utf8").split("\n").length > 400);
expect(long).toEqual([]);
});
});

describe("translate", () => {
it("fills placeholders, falls back to English, and leaves unknown placeholders alone", () => {
expect(translate("en", "Worked for {duration}", { duration: "7s" })).toBe("Worked for 7s");
Expand Down
297 changes: 14 additions & 283 deletions src/lib/i18n.zh.ts

Large diffs are not rendered by default.

289 changes: 289 additions & 0 deletions src/lib/i18n/settings.ja.ts

Large diffs are not rendered by default.

289 changes: 289 additions & 0 deletions src/lib/i18n/settings.ko.ts

Large diffs are not rendered by default.

289 changes: 289 additions & 0 deletions src/lib/i18n/settings.zh.ts

Large diffs are not rendered by default.