Repository navigation
Conversation
Adds nb.json and sv.json covering every en.json key except one literal placeholder (the audiobook category example, which should stay the value a download client expects). Both are listed in the language picker by their native names. Region tags already resolve through i18next's own hierarchy (nb-NO, sv-SE and sv-FI need nothing). The no and nn tags do not, so fallbackLng now sends them to nb before en; Nynorsk readers read Bokmal routinely and there is no Nynorsk bundle. Tests check that every nb and sv key exists in en.json, that placeholders, naming tokens and code spans survive, that plurals are paired and that no value is untranslated English; that the detector maps each Norwegian and Swedish browser tag to the right bundle; and that three pages render translated with no raw keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
|
@francisrath here's the Norwegian PR you offered to proofread on #2985. No rush, and thanks a lot! The Bokmål is in
Suggestions right in the review are perfect. I won't merge until you've had a look. |
There was a problem hiding this comment.
Clean. A few observations:
fallbackLng object form (web/src/i18n/index.ts:23-27): backward-compatible — all existing locales not in the map (fr, de, ko, etc.) hit default: ['en'], which is the same as the old 'en' string. The only net change is no/nn routing to nb first. i18next's region-stripping means no-NO → no and nn-NO → nn before the map lookup, so neither needs its own key; the detection tests at nordicLocales.test.ts:107 confirm the full set of 11 browser tags.
Test coverage is thorough: structural checks (unknown keys, placeholder/token/code-span parity, plural pairing, English-filler guard) + render tests for Calendar/Wanted/Series in both locales + the da-DK → en regression that guards against Danish users accidentally getting Bokmål.
One missing key (settings.clients.audiobookCategoryPlaceholder) is intentional and noted in the PR body. Fine.
No blocking issues found.
— 🤖 Bindery triage bot (automated). Reply to correct me; a human will see it.
| // {{interpolation}}, {NamingToken} and `code spans` must survive translation | ||
| // untouched; a renamed placeholder renders as literal braces. | ||
| const placeholders = (text: string) => [...text.matchAll(/{{\s*(\w+)\s*}}/g)].map(m => m[1]).sort() | ||
| const namingTokens = (text: string) => [...text.replace(/{{[^}]*}}/g, '').matchAll(/{[A-Za-z][\w ]*(?::[^}]*)?}/g)].map(m => m[0]).sort() |
| const words = text.replace(/{{[^}]*}}|{[^}]*}|`[^`]*`/g, ' ').match(/\p{L}[\p{L}\d'.-]*/gu) ?? [] | ||
| return words.every(w => { | ||
| const bare = w.replace(/[.'-]+$/, '').toLowerCase() | ||
| return SHARED_WORDS.has(bare) || SHARED_BY_LOCALE[locale].has(bare) |
| ])('resolves a browser set to %s to %s', async (tag, expected) => { | ||
| const instance = await detect([tag]) | ||
| expect(instance.resolvedLanguage).toBe(expected) | ||
| const bundle = { nb, sv, en, de: instance.getResourceBundle('de', 'translation') }[expected] as Tree |
I'll have a think about it. Just skimmed the terms so far, and everything looks good down to adopt/rebind/path remap. I'll see if I can come up with better terms -- but they are already understandable. Will probably have a deeper look tomorrow! |
francisrath
left a comment
There was a problem hiding this comment.
Overall pretty good, just a few comments below!
| "saving": "Lagrer...", | ||
| "deleting": "Sletter...", | ||
| "test": "Test", | ||
| "testing": "Tester…", |
There was a problem hiding this comment.
Use three dots instead of triple-dot to match elsewhere?
i.e. replace all "…" with "..." in the translations? (or the other way around -- as long as it's consistent)
There was a problem hiding this comment.
Done, nb and sv now use … everywhere. en.json mixes both too, so I noted that as a follow up.
| "title": "Manuell import", | ||
| "description": "Skann en mappe med filer hvor som helst på disken, finn riktig bok i katalogen din for hver av dem, og importer dem til biblioteket. Treffene hentes bare fra katalogen, så legg til forfatteren først hvis vedkommende ikke finnes i Bindery ennå. En fil uten treff kan legges til fra et metadatasøk.", | ||
| "pathLabel": "Mappe som skal skannes", | ||
| "pathPlaceholder": "/downloads/books", |
| "more": "Mer", | ||
| "moreFor": "Flere handlinger for {{name}}", | ||
| "match": { | ||
| "strong": "Sterkt treff", |
There was a problem hiding this comment.
Done, changed here and in the matching hint sentence.
…wedish-locales Signed-off-by: vavallee <vavallee@protonmail.com>
Uses a single ellipsis character in every nb and sv string instead of the mix of three dots and the ellipsis that en.json has, swaps the manual import path placeholder for a Norwegian one, and calls a strong adoption match "Godt treff". Also translates the eight keys main added to en.json since this branch was cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
… interval hint (#3027) * fix(web): translate series, calendar and push all strings, fix search interval hint The wanted search interval is the gap between scheduled sweeps (scheduler.go runs search-wanted "@every <interval>"), so the hint telling rate limited users to reduce it made Bindery search more often. It now says to choose a longer interval. The de, es, fr, nl, tl and id files carried the same wrong advice as untranslated English, so the key is removed there and falls back to the corrected English. The Series page, its name modal, the Calendar month and weekday names and the Calibre Push all button and progress modal were hardcoded English. They now go through i18n; the Calendar formats names with Intl.DateTimeFormat in the active language. en.json prose now uses the ellipsis character throughout, with matching inline defaults. The other locales (excluding nb and sv, which #2994 handles) get the same change for translated values only; English filler values are left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com> * fix(web): drop leftover English filler and keep calendar month on one line Removes common.deleting and discover.refreshing from locales where they were still verbatim English, so they fall back to en.json like the search interval hint. The calendar month label no longer wraps on phones for long names such as Spanish "septiembre de 2026". The changelog fragment carries the PR number and notes the capitalised series badges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com> * test(web): match CalibreTab coverage tests on i18n keys, not English The Push all block and its modal now go through t(), and this file mocks t to return keys, so the helper and the assertions for the description, the bridge warning, the missing plugin URL tooltip, the done summary and the Close button match on keys instead of English text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com> --------- Signed-off-by: vavallee <vavallee@protonmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
The series work has added English strings that nb and sv don't have yet. Here they are in both, following your glossary (and «» / ”” quotes):
I've proofread the Norwegian. The Swedish looks right to me, but I'm not a native speaker, so a Swedish reader is still welcome. These keys aren't in this branch's nb.json additions{
"authors": {
"sortAZ": "Etternavn A–Å",
"sortZA": "Etternavn Å–A"
},
"books": {
"sortAuthorAZ": "Forfatter A–Å",
"sortAuthorZA": "Forfatter Å–A"
},
"bookDetail": {
"edit": {
"fieldSeries": "Serie",
"seriesNone": "Ingen serie",
"seriesNew": "Ny serie…",
"fieldNewSeries": "Navn på ny serie",
"fieldPosition": "Nummer",
"unlockAllHint": "La metadataoppdateringer styre alle felt og bokens serier igjen"
},
"series": {
"keptOutHeading": "Holdt utenfor",
"keptOutExplainer": "Du tok denne boken ut av disse seriene, så oppdateringer legger den ikke inn igjen. Gjenopprett setter den tilbake der den var.",
"restore": "Gjenopprett",
"restoreFailed": "Kunne ikke legge boken tilbake i serien"
}
},
"series": {
"merge": {
"button": "Slå sammen…",
"buttonHint": "Slå andre serier sammen med denne",
"title": "Slå sammen med «{{title}}»",
"description": "Seriene du velger, slås inn i denne og slettes. Bøkene deres beholder nummereringen i denne serien og fyller hullene i den, og ID-ene deres hos leverandøren beholdes som alias, så en oppdatering ikke oppretter dem på nytt.",
"filterPlaceholder": "Finn en serie…",
"sourcesLabel": "Serier som skal slås inn",
"bookCount_one": "{{count}} bok",
"bookCount_other": "{{count}} bøker",
"noCandidates": "Ingen andre serier passer",
"renameLabel": "Nytt navn (valgfritt)",
"preview": "Forhåndsvis",
"apply": "Slå sammen",
"sourceSummary": "{{title}}: {{moved}} flyttes inn, {{kept}} er her fra før",
"conflict": "{{book}} beholder #{{kept}} (den sammenslåtte serien hadde #{{dropped}})",
"renamed": "Får nytt navn: «{{title}}»",
"takesHardcoverLink": "Overtar Hardcover-koblingen fra «{{title}}»",
"takesGenres": "Overtar sjangeroverstyringen fra «{{title}}»",
"becomesMonitored": "Blir overvåket, fordi en av de sammenslåtte seriene var det",
"aliases_one": "{{count}} gammel leverandør-ID vil peke hit",
"aliases_other": "{{count}} gamle leverandør-ID-er vil peke hit",
"confirmBody_one": "Slå {{count}} serie sammen med «{{title}}»? Den sammenslåtte serien slettes, og dette kan ikke angres.",
"confirmBody_other": "Slå {{count}} serier sammen med «{{title}}»? De sammenslåtte seriene slettes, og dette kan ikke angres.",
"failed": "Sammenslåingen mislyktes"
}
}
}sv.json additions{
"authors": {
"sortAZ": "Efternamn A–Ö",
"sortZA": "Efternamn Ö–A"
},
"books": {
"sortAuthorAZ": "Författare A–Ö",
"sortAuthorZA": "Författare Ö–A"
},
"bookDetail": {
"edit": {
"fieldSeries": "Serie",
"seriesNone": "Ingen serie",
"seriesNew": "Ny serie…",
"fieldNewSeries": "Namn på ny serie",
"fieldPosition": "Nummer",
"unlockAllHint": "Låt metadatauppdateringar hantera alla fält och bokens serier igen"
},
"series": {
"keptOutHeading": "Utesluten från",
"keptOutExplainer": "Du tog bort boken ur de här serierna, så uppdateringar lägger inte tillbaka den. Återställ sätter tillbaka den där den var.",
"restore": "Återställ",
"restoreFailed": "Det gick inte att lägga tillbaka boken i serien"
}
},
"series": {
"merge": {
"button": "Slå ihop…",
"buttonHint": "Slå ihop andra serier med den här",
"title": "Slå ihop med ”{{title}}”",
"description": "Serierna du väljer slås in i den här och tas bort. Deras böcker behåller numreringen i den här serien och fyller dess luckor, och deras ID:n hos leverantören behålls som alias så att en uppdatering inte skapar dem igen.",
"filterPlaceholder": "Hitta en serie…",
"sourcesLabel": "Serier att slå in",
"bookCount_one": "{{count}} bok",
"bookCount_other": "{{count}} böcker",
"noCandidates": "Inga andra serier matchar",
"renameLabel": "Nytt namn (valfritt)",
"preview": "Förhandsgranska",
"apply": "Slå ihop",
"sourceSummary": "{{title}}: {{moved}} flyttas in, {{kept}} finns redan här",
"conflict": "{{book}} behåller #{{kept}} (den ihopslagna serien hade #{{dropped}})",
"renamed": "Byter namn till ”{{title}}”",
"takesHardcoverLink": "Tar över Hardcover-länken från ”{{title}}”",
"takesGenres": "Tar över genreåsidosättningen från ”{{title}}”",
"becomesMonitored": "Blir bevakad, eftersom en av de ihopslagna serierna var det",
"aliases_one": "{{count}} gammalt leverantörs-ID kommer att peka hit",
"aliases_other": "{{count}} gamla leverantörs-ID:n kommer att peka hit",
"confirmBody_one": "Slå ihop {{count}} serie med ”{{title}}”? Den ihopslagna serien tas bort, och det går inte att ångra.",
"confirmBody_other": "Slå ihop {{count}} serier med ”{{title}}”? De ihopslagna serierna tas bort, och det går inte att ångra.",
"failed": "Sammanslagningen misslyckades"
}
}
} |
…wedish-locales main now loads every locale but English lazily through a glob backend, so nb and sv no longer need static imports. FALLBACK_LNG is kept and passed as fallbackLng so no and nn still resolve to nb. The detection tests waited on isInitialized, which stays true on the i18next singleton across resetModules; with async locale loading that returned before the new init settled. They now await i18nReady. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Adds the series.merge strings from #3057 to both locales. The keys from #3090 and #3116 wait until those land in en.json. Translations by francisrath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9 Signed-off-by: vavallee <vavallee@protonmail.com>
Summary
Adds Norwegian Bokmål (
nb) and Swedish (sv) UI translations, listed in the language picker as "Norsk bokmål" and "Svenska". Both cover 1764 of 1765 en.json keys. Closes #2985.These translations are machine assisted and need native review. francisrath offered to proofread the Norwegian; a Swedish reader would be welcome too. The glossary below is the fastest place to start: if a term is wrong, it is wrong everywhere, and fixing it is a find and replace.
Glossary
Same term everywhere it appears. Sentence case throughout, "du" form, «» quotes in nb and ”” in sv.
Implementation notes
de-DEthende, sonb-NO,sv-SEandsv-FIalready resolve. The exceptions areno(the macrolanguage tag some browsers send) andnn(Nynorsk, no bundle of its own).fallbackLngis now an object sendingno*andnn*tonbbeforeen. Nynorsk readers read Bokmål routinely, so that beats English; the detected tag is kept, so dates still format in the reader's own variant.settings.clients.audiobookCategoryPlaceholder("audiobooks") is an example category value for the download client, not prose, so it falls back to English.SeriesPage.tsx, Calendar month names are hardcoded English, and the "Push all to Calibre" button text inCalibreTab.tsxis not in en.json. Worth a follow-up so every locale benefits.Strings worth a native eye
queue.status.importHeld("Held for Pair"): long for a badge.discover.rows.serendipity: loose rewording.settings.general.dropFolder: "Leveringsmappe" / "Leveransmapp" coined.settings.oidc.fieldScopes: translated as Omfang / Omfång, though IdP screens often say "Scopes".settings.import.goodreadsShelf*: should match what Goodreads shows in nb/sv.settings.general.searchIntervalHint: English says "Reduce it" when hitting rate limits, but it means search less often; translated by meaning. The English may want the same fix.Checklist
Test plan
cd web && npm ci && npm run lint(0 errors; 7 warnings, all in files this PR does not touch)npm run typechecknpx vitest run src/i18n(41 tests): nb/sv keys exist in en, placeholders, naming tokens and code spans match, plurals paired, no English filler; browser tagsnb,nb-NO,no,no-NO,nn,nn-NO,sv,sv-SE,sv-FIresolve to the right bundle (plusde-DEandda-DKunchanged); Calendar, Wanted and Series render translated with no raw keys in both localesfallbackLngreverted to'en', and pages left in English each turn them rednpm run buildnpm test(107 files, 1247 tests)🤖 Generated with Claude Code
https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9