Skip to content

i18n: skeleton with Settings language picker and Home extracted (en + zh-CN) - #19

Merged
sivadaboi merged 2 commits into
Liquid-co:i18nfrom
Janlhin:feat/i18n-skeleton
Oct 2, 2026
Merged

sivadaboi merged 2 commits into
Liquid-co:i18nfrom
Janlhin:feat/i18n-skeleton

Conversation

@Janlhin

@Janlhin Janlhin commented Oct 1, 2026

Copy link
Copy Markdown

Implements the plan agreed in #18.

What's in this PR

  • lib/i18n.js — a small, zero-dependency module: t is a Svelte store whose value is the translate function, so $t('key') in templates re-renders on language switch with no wiring. Locale is persisted per device (opensave.locale), defaults to the system language, and any key missing from the active catalog falls back to English, so partial translations never show blanks.
  • src/locales/en.json (source of truth) + zh-CN.json — flat, dot-keyed, grouped by view (home.*, settings.*). A plural value is an object keyed by Intl.PluralRules categories ({ "one": …, "other": … }); single-form languages like Chinese are plain strings.
  • Settings → Appearance → Language dropdown, defaulting to the system language.
  • Home extracted as the first view (both locales), per the agreed scope.
  • format.js plural() now selects through the chosen locale's plural rules instead of n === 1.
  • Tests (i18n.test.js, 12 cases): catalog completeness both ways (no missing keys, no extra keys, no empty values), English fallback, {param} filling, plural category selection including n defaulting to 0, and locale detection (saved choice > system language > English, storage-refusal safe). Two existing suites that assert English wording now pin locale.set('en') so they don't depend on the machine's language.

npm test: 305/305 (was 249 + these new tests; dev gained tests since v2.4.0).

Notes

  • Scope kept to the window UI as discussed; background-service strings, CLI, and the tray/notifications are left for follow-up PRs.
  • One bug surfaced while testing: a plural template invoked with no params left {n} unfilled — translate now defaults it to 0 before substitution.
  • Follow-ups (after this lands): one PR per view for the remaining views and the home/ components, then the Go-side map for tray/notifications.

Closes #18 (the skeleton step; I'll keep referencing it from the per-view PRs).

- lib/i18n.js: zero-dependency t store + t(key, params), locale persisted
  per device, system-language default, English fallback for missing keys,
  plurals through Intl.PluralRules
- src/locales/en.json (source of truth) + zh-CN.json
- Settings > Appearance: language dropdown
- Home view extracted (en + zh-CN)
- format.js plural() now selects via the chosen locale's plural rules
- tests: catalog completeness (no missing/extra keys), fallback, params,
  plural categories, locale detection; language pinned where tests assert
  English wording
@vercel

vercel Bot commented Oct 1, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the sivadaboi's projects Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
open-save Ready Ready Preview Oct 1, 2026 6:42am UTC

@sivadaboi sivadaboi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New branch: I've made an i18n branch for this work and pointed this PR at it. Please open the next PRs against i18n instead of dev. That way the translation can land screen by screen, and I'll merge i18n into dev when it's in a good state.

Four small changes before I merge:

  1. The catalog tests break on the first plural. leafKeys() walks into plural objects, so { "one", "other" } in en.json becomes k.one + k.other while zh-CN's plain string is k, and all three catalog tests fail. Since the catalogs are flat, comparing top-level keys is enough, with a plural counted as one value that must have an other form.
  2. One sentence, one key. The welcome hint is split into hintA/hintB/hintC, which translators can't reorder (and it leaves stray spaces around 设备 and 云备份). Please make it one string with {devices} and {cloud} placeholders, plus a small helper to render those parts in bold. Let's use that rule for every view.
  3. Only save the language when someone picks one. Right now the detected language is saved on the first launch, so the app never follows the system language again. Please add a "System default" option at the top of the dropdown too.
  4. Set <html lang> to the chosen language. It stays en today, and for Chinese that can make Windows use Japanese glyph shapes for some characters.

Once those are in I'll merge, and you're welcome to start on the next views. Thanks again!

- catalog tests compare top-level keys and require an 'other' form on
  plural values (leaf-walking broke on the first plural)
- welcome hint is one key with {devices}/{cloud} placeholders; new
  segments() helper renders the filled parts in bold
- language is 'system' until explicitly picked; only an explicit pick is
  persisted, and the picker gains a System default option
- <html lang> follows the resolved locale in App.svelte
@Janlhin

Janlhin commented Oct 1, 2026

Copy link
Copy Markdown
Author

All four points are addressed in 5ab46ba:

  1. Catalog tests now compare top-level keys only, and a plural value must expose an other form (checked for both catalogs).
  2. One sentence, one key: the welcome hint is a single home.welcome.hint with {devices} / {cloud} placeholders, filled via a small segments() helper in lib/i18n.js that renders the named parts in bold — no more hintA/B/C, and no stray spaces. I'll follow this rule for every view.
  3. Language is 'system' until explicitly picked: only an explicit pick is persisted (choosing System default removes the saved value), the picker has a "System default" entry on top, and resolveLocale() settles 'system' against the OS language.
  4. <html lang> in App.svelte now follows the resolved locale.

npm test: 247/247.

Happy to rebase onto anything else — and I'll open the next PRs against i18n from here on. Thanks for the clear review!

@sivadaboi
sivadaboi merged commit 96485fd into Liquid-co:i18n Oct 2, 2026
10 of 11 checks passed
Janlhin pushed a commit to Janlhin/OpenSave that referenced this pull request Oct 5, 2026
Follows the pattern from Liquid-co#19: one sentence per key, bold parts through
segments() placeholders, counts through Intl.PluralRules.

- home/ScanDialog, ScanFolderList, SetupGuide, LibraryGrid, LibraryTile,
  LibraryViewOptions, HomeSummary, AddGameCard, OfferedGames,
  RecentActivity
- 146 new keys, grouped per component
- a few generic strings land in common.* (cancel, nowTracking) so the same
  wording is not translated twice; duplicated existing keys reuse the
  originals (home.devices, home.scanning, home.chooseFolderDialog,
  home.welcome.scanForSaves)
- Labels built in lib/ (cover styles, sort names, status text, time ago)
  are still English: they return words, not sentences, so they need their
  own pass to expose keys instead. Left out to keep this reviewable.
- Windows example path in AddGameCard's placeholder stays as typed

npm test: 271/271; vite build clean
Janlhin pushed a commit to Janlhin/OpenSave that referenced this pull request Oct 5, 2026
Follows the pattern from Liquid-co#19: one sentence per key, bold parts through
segments() placeholders, counts through Intl.PluralRules.

- home/ScanDialog, ScanFolderList, SetupGuide, LibraryGrid, LibraryTile,
  LibraryViewOptions, HomeSummary, AddGameCard, OfferedGames,
  RecentActivity
- 146 new keys, grouped per component
- a few generic strings land in common.* (cancel, nowTracking) so the same
  wording is not translated twice; duplicated existing keys reuse the
  originals (home.devices, home.scanning, home.chooseFolderDialog,
  home.welcome.scanForSaves)
- Labels built in lib/ (cover styles, sort names, status text, time ago)
  are still English: they return words, not sentences, so they need their
  own pass to expose keys instead. Left out to keep this reviewable.
- Windows example path in AddGameCard's placeholder stays as typed

npm test: 271/271; vite build clean
@Janlhin
Janlhin deleted the feat/i18n-skeleton branch October 11, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants