GSoC 2026: Set Up BookBrainz for Internationalization
Personal Information:
Name: Garv Thakre
Nickname: garv
Matrix handle: @garvthakre:matrix.org
Email: garvthakre0@gmail.com
GitHub: garvthakre
LinkedIn: linkedin.com/in/garv-thakre
Timezone: IST (UTC+5:30)
Introduction:
Iβm Garv Thakre, a B.Tech student who loves building products and exploring technology and i started coding and then i started to work on different projects and anything that my senior told me too , and then i started to participated in the hackathons, our team is the runner-up in the national Hackathon hosted by IIITNR, we also won hackathon at IITBHILAI and also won a lot of contests too and i also completed the summer internship in my first year in NITR and then i joined the startup as a backend intern and so on .
Why Me:
I have been contributing to BookBrainz and ListenBrainz for the past few months. I am very familiar with the codebase. I have traced the full SSR pipeline from [generateProps()] /bookbrainz-site/src/server/helpers/props.ts through renderToString() to ReactDOM.hydrate(), which is exactly the pipeline this project modifies.
Working Prototype
I ran BookBrainz locally and wired i18next into the actual codebase on a public branch β not a separate demo app. The About page now renders in French end-to-end via Accept-Language detection.
While doing this I hit two bugs:
-
storing the i18next instance in
res.localsbreaks JSON serialization in [escapeProps()](/bookbrainz-site/src/server/helpers/props.ts#25-32 (the client receives{}). -
renderToString()needsI18nextProvideroruseTranslation()hooks return raw keys during SSR.
Both fixes are already reflected in the implementation below.
About page running on localhost β heading translated via Accept-Language: fr detection, remaining strings pending migration.
My Portfolio :
https://g-forge.vercel.app/ (currently Improving) Have a look at the project section thatβs freaking cool .
Personal and Team Projects :
-
CollabGPT : Live Demo source code
-
Post90 β A tool for developers who struggle to post consistently on LinkedIn or X. Reads your GitHub commits and generates post ideas you can publish directly. Live demo Β· GitHub
-
CampusX β A collaborative project I have been working on for a while. GitHub
-
NexusAI : NEXUS is a local AI automation agent that translates English task descriptions into step-by-step execution plans, then carries them out by controlling your browser, desktop apps, files powered by Groq (free) or other LLM providers. Github
My merged and Open PRs (10+) across MetaBrainz projects :
- BookBrainz PRs: Merged PRs Β· Open PRs
- ListenBrainz PRs: Merged PRs
My backend internship experience showcased designing databases, building system architecture, and delivering features end-to-end maps directly to this project. At my internship i also design the database , handles documenttion using swagger and also javascript and typescript is the primary language i worked on.
Project Summary:
Title: Set Up BookBrainz for Internationalization
Proposed Mentor: @mr_monkey
Project Length: 175 hours
Short Description: BookBrainz is currently only in English, which limits its reach to a global audience. The MusicBrainz internationalization documentation explicitly confirms this: βThe following official MetaBrainz projects arenβt translatable at the moment: BookBrainz, Cover Art Archive, and ListenBrainz.β
Expected Outcome: A full i18n infrastructure β i18next integrated with React SSR, locale detection middleware, a language selector dropdown in the navbar, English translation JSON files, ~600 strings migrated across ~60+ files, and a BookBrainz project on translations.metabrainz.org so volunteer translators can contribute immediately.
Implementation:
The user visits BookBrainz with their browser set to French. The Express server reads the Accept-Language header using the existing getAcceptedLanguageCodes() helper already in the codebase. It loads the French translation JSON, creates an i18next instance, and passes it to the React SSR renderer. The page renders in French on the server. The loaded translations are injected alongside the existing Redux props via [generateProps()]( /bookbrainz-site/src/server/helpers/props.ts#33-52) β so React hydration requires zero extra HTTP requests and the page stays in French with no flicker.
For new translations: a developer pushes a new English key to GitHub. A GitHub Action runs i18next-parser automatically and commits updated JSON. A webhook fires to translations.metabrainz.org, Weblate shows the key to volunteer translators, and Weblate automatically opens a PR with the translated JSON. A maintainer merges it and the translation is live.
Existing Codebase β Whatβs Already There
Before writing any code, I studied the repo carefully from the last december. The codebase already done some work:
src/server/helpers/i18n.ts β Already exports two functions:
// Parses Accept-Language header, returns sorted array of language objects
export function parseAcceptLanguage(acceptLanguage: string): AcceptedLanguage[]
// Wraps the above β takes the full Express request, returns ['fr', 'en', ...]
export function getAcceptedLanguageCodes(request: Request): string[]
src/server/helpers/props.ts β The generateProps() function already merges req.session, res.locals, and page-specific props into a single object that feeds both ReactDOMServer.renderToString() and the client hydration. I18n state (locale + resources) will be added here β in the same place all other shared state lives.
src/server/templates/target.js β The HTML template already injects Redux state as a <script id='props'> tag. The i18n resources travel through this same mechanism β no new injection pattern needed.
public/locales/ β Express already serves public/ as static files via app.use(express.static('public')) in app.js. No new route is needed for /locales/fr/common.json.
package.json β @cospired/i18n-iso-languages is already a dependency, confirming the codebase has been pointed toward i18n work before.
This means the infrastructure requires adding to existing patterns, not reinventing them.
The SSR Pipeline β How i18n Plugs In
Every BookBrainz page follows this exact 4-step flow:
Step 1: generateProps(req, res) merges session + res.locals + page props
Step 2: ReactDOMServer.renderToString(<Layout {...props}/>) β HTML string
Step 3: target.js injects markup + serialized props into full HTML page
Step 4: Client reads props from DOM β ReactDOM.hydrate()
I18n state (locale + resources) is injected at Step 1, so both renderToString (Step 2) and hydrate (Step 4) use the exact same language and translations. Hydration mismatches are prevented by design β not by patching.
File Structure
bookbrainz-site/
βββ public/
β βββ locales/
β βββ en/ β English source (developer creates these)
β β βββ common.json β buttons, nav, labels
β β βββ entityEditor.json β alias, name, relationship editors
β β βββ pages.json β About, Help, FAQ, static pages
β β βββ entities.json β entity display pages
β β βββ errors.json β error page messages
β βββ fr/ β seeded for testing
β βββ de/ β seeded for CSS stress-testing
β
βββ src/
β βββ common/
β β βββ i18n/
β β βββ i18n.ts β NEW: isomorphic initialiser
β βββ client/
β β βββ components/
β β βββ language-selector.tsx β NEW: navbar language dropdown
β βββ server/
β β βββ helpers/
β β βββ i18n.ts β EXISTS: unchanged
β β βββ props.ts β MODIFIED: add i18n to shared state
β β βββ middleware.ts β MODIFIED: add i18nMiddleware
β βββ templates/
β βββ target.js β MODIFIED: lang attr
β
βββ i18next-parser.config.js β NEW: string extraction config
βββ .github/workflows/
β βββ i18n-extract.yml β NEW: auto-extract on every push
βββ package.json β MODIFIED: i18next packages added
1. Install Packages
npm install i18next react-i18next i18next-http-backend i18next-browser-languagedetector
npm install --save-dev i18next-parser
2. Create the Isomorphic i18next Initialiser
File: src/common/i18n/i18n.ts (new file)
export function createI18n(locale = 'en', resources?) {
const instance = i18n.createInstance(); // fresh per request β no locale leaking
instance.use(initReactI18next);
const hasResources = resources && Object.keys(resources).length > 0;
if (!hasResources && !isServer) {
// Browser only enters this branch if server failed to inject resources.
// In normal flow this never runs β resources always arrive via #props.
instance.use(HttpBackend).use(LangDetector);
}
instance.init({
fallbackLng: 'en',
initImmediate: false, // synchronous init when resources are already in memory
lng: locale,
ns: ['common', 'entityEditor', 'pages', 'entities', 'errors'],
...(hasResources
? {resources} // server OR client with injected data
: {backend: {loadPath: '/locales/{{lng}}/{{ns}}.json'}} // client-only fallback
),
});
return instance;
}
3. Add Locale Detection Middleware
File: src/server/helpers/middleware.ts (add function)
import {getAcceptedLanguageCodes} from './i18n'; // already exists
const SUPPORTED = ['en', 'fr', 'de']; // for eg
export function i18nMiddleware(req, res, next) {
// Cookie takes priority - set when user explicitly picks a language from the dropdown.
// Falls back to Accept-language handles for the first-time visitors with no preference set.
const cookieLang = req.cookies?.bb_lang;
const [headerLang = 'en'] = getAcceptedLanguageCodes(req);
const preferred = cookieLang ?? headerLang;
const locale = SUPPORTED.includes(preferred) ? preferred : 'en';
const load = (ns: string) => {
try {
return JSON.parse(fs.readFileSync(
path.join(process.cwd(), 'public', 'locales', locale, `${ns}.json`), 'utf8'
));
} catch { return {}; } // missing file never breaks the site
};
const resources = {[locale]: {common: load('common'), entityEditor: load('entityEditor')}};
res.locals.locale = locale;
res.locals.i18nResources = resources;
// Note: do NOT store the i18next instance in res.locals β it is not serializable.
// The client re-creates it from the plain {locale, resources} object in props.
next();
}
Register in src/server/app.js:
app.use(i18nMiddleware);
4. Inject i18n State into the Shared Props Pipeline
File: src/server/helpers/props.ts (add two lines)
export function generateProps<T>(req: PassportRequest, res: Response, props?: T) {
const baseObject: Record<string, unknown> = {};
if (req.session?.mergeQueue) baseObject.mergeQueue = req.session.mergeQueue;
const merged = Object.assign(baseObject, res.locals, props);
// Set i18n AFTER Object.assign β res.locals spreads many keys and would overwrite
// anything set beforehand. Setting last guarantees {locale, resources} is never lost.
merged.i18n = {
locale: res.locals.locale || 'en',
resources: res.locals.i18nResources || {}
};
return merged;
}
The client reads this before ReactDOM.hydrate() β same locale, same translations, zero mismatch.
SSR note:
ReactDOMServer.renderToString()must be wrapped withI18nextProvidersouseTranslation()hooks have access to the i18next instance during server-side rendering. Without the provider, hooks return the raw key string instead of the translation.const markup = ReactDOMServer.renderToString( <I18nextProvider i18n={createI18n(locale, resources)}> <Layout {...props}><PageComponent /></Layout> </I18nextProvider> );
5. Update the HTML Template
File: src/server/templates/target.js
// Add locale to the parameter list, add lang attribute (WCAG 2.1 criterion 3.1.1):
export default ({title, markup, page, props, script, locale}) => `
<!doctype html>
<html lang="${locale || 'en'}">
...
6. Set Up Auto String Extraction (CI/CD)
File: i18next-parser.config.js
module.exports = {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'public/locales/$LOCALE/$NAMESPACE.json',
locales: ['en'],
defaultNamespace: 'common',
sort: true,
};
File: .github/workflows/i18n-extract.yml (new) β runs on every push to master, auto-commits updated JSON so Weblate always sees fresh keys:
name: Extract i18n Strings
on:
push:
branches: [master]
paths: ['src/**']
jobs:
extract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: {node-version: '18'}
- run: npm ci && npx i18next-parser
- run: |
git config user.name "github-actions[bot]"
git add public/locales/
git diff --cached --quiet || git commit -m "chore(i18n): update translation strings" && git push
7. Migrate Components (Bulk of the 175 Hours)
Pattern A β Function components (useTranslation hook):
alias-editor.js is the proof-of-concept β migrated first to verify the full pipeline:
// BEFORE:
<Modal.Title>Alias Editor</Modal.Title>
<Button onClick={onClose}>Close</Button>
// AFTER:
const {t} = useTranslation('entityEditor');
<Modal.Title>{t('aliasEditor.title')}</Modal.Title>
<Button onClick={onClose}>{t('button.close')}</Button>
Pattern B β Class components (withTranslation HOC, zero refactoring needed):
pager.js hooks unavailable, so withTranslation wraps it and injects t as a prop:
// BEFORE:
<Button>← Previous Page</Button>
<Button>Results {from + 1} β {from + size}</Button>
export default PagerElement;
// AFTER:
const {t} = this.props;
<Button>{t('pagination.previous')}</Button>
<Button>{t('pagination.results', {from: from + 1, to: from + size})}</Button>
export default withTranslation('common')(PagerElement);
{{from}} and {{to}} are i18next interpolation variables β translators keep the variable names and translate the surrounding words.
Migration scope:
| Area | Files | Strings |
|---|---|---|
| Layout, Nav & Footer | ~10 | ~65 |
| Static Pages (About, Help, FAQ, etc.) | ~25 | ~215 |
| Entity Editor | ~25 | ~150 |
| Entity Display | ~25 | ~80 |
| Search, Revisions, Collections | ~10 | ~60 |
| Errors & server-side titles | ~25 | ~50 |
| Total | ~120 | ~620 |
8. Language Selector Dropdown
A visible user-facing deliverable β a language dropdown in the navbar using react-bootstrap. Saves the userβs choice in a cookie so the server renders in the correct language on the next visit.
// src/client/components/language-selector.tsx (new)
function LanguageSelector(): JSX.Element {
const {i18n} = useTranslation();
const handleChange = (code: string) => {
document.cookie = `bb_lang=${code};path=/;max-age=31536000`;
window.location.reload(); // server re-renders page in new language
};
return (
<NavDropdown id="language-dropdown" title={<FontAwesomeIcon icon={faGlobe}/>}>
{SUPPORTED_LANGUAGES.map(lang => (
<NavDropdown.Item key={lang.code} onClick={() => handleChange(lang.code)}>
{lang.name}
</NavDropdown.Item>
))}
</NavDropdown>
);
}
9. Weblate Integration
After English JSON files exist on GitHub, the mentor creates the BookBrainz project on translations.metabrainz.org. From that point the GitHub Action keeps it in sync automatically:
GitHub Action extracts new strings β commits to public/locales/en/
β Weblate detects update via webhook
Volunteer translators work in Weblate UI (no code, no GitHub account needed as per my knowledge)
β Weblate opens a PR: public/locales/fr/common.json
Maintainer reviews + merges β French is live on BookBrainz
10. Developer Workflow for New Components
Four steps, documented in CONTRIBUTING.md:
Step 1 β Add the English string to the correct JSON file:
{ "myFeature.submitButton": "Submit Report" }
Step 2 β Use t() in the component:
const {t} = useTranslation('common'); // function component
const {t} = this.props; // class component via HOC
<Button>{t('myFeature.submitButton')}</Button>
Step 3 β Run the parser to verify:
npm run parse-i18n # myFeature.submitButton stored in common.json
Step 4 β Push to GitHub. The GitHub Action and Weblate handle everything else automatically.
Risks & Mitigations
| Risk | Mitigation |
|---|---|
| SSR/hydration mismatch | i18n state travels through generateProps() β server and client always use identical locale + resources |
| Missing locale file breaks the page | try/catch in load() returns {} β site silently falls back to English, never crashes |
| Long translations breaking CSS | German stress-test in Phase 7; apply overflow-wrap: break-word where needed |
| Translation keys going out of sync | GitHub Action auto-extracts on every push to master |
| React 16 compatibility | react-i18next v11+ supports React β₯ 16.8; withTranslation HOC works with any React version |
| Future contributors hardcoding strings | 4-step CONTRIBUTING.md guide; GitHub Action catches missing keys on every CI run |
Timeline:
Community Bonding (May):
- Set up dev environment, run BookBrainz locally
- Study props.ts, app.js, middleware.ts, target.js, alias-editor.js in depth
- Discuss approach, namespace structure, and file naming with mentor
Coding Phase 1 β Infrastructure (May 25 β May 31): ~15h
- Install packages, create
src/common/i18n/i18n.ts - Add
i18nMiddleware, wiregenerateProps(), update target.js (lang attribute) - Unit tests for
createI18n()and locale fallback - Set up GitHub Action for auto-extraction
Coding Phase 2 β Pipeline Verification (June 1 β June 7): ~12h
- Migrate alias-editor.js as proof-of-concept (full round-trip in one component)
- Coordinate with mentor to register on
translations.metabrainz.org - Verify Weblate auto-PR cycle end-to-end with 3 test strings
Coding Phase 3 β Language Selector + String Extraction (June 8 β June 14): ~15h
- Build
language-selector.tsx, integrate into navbar with cookie persistence - Run full parser scan, document total string count across all namespaces
Coding Phase 4 β Entity Editor Migration (June 15 β June 28): ~30h
- Week 1(June 15 - 21):
- Migrate alias-editor, alias-row, name-section, name-section-merge.
- Week 2 (June 22 - 28) :
- Migrate identifier-editor, relationship-editor, annotation, button-bar, submission-section .
- Populate
entityEditor.jsonnamespace .
Coding Phase 5 β Static Pages + Layout (June 29 β July 5): ~15h
- Migrate About, Help, FAQ, Contribute, Privacy, error, registration pages
- Migrate navigation (
layout.js), footer, editor container - Write first draft of
CONTRIBUTING.md
MidTerm Evaluation β (July 6 - July 10)
- infrastructure complete, language selector shipped, entity editor fully migrated, ~350/600 strings done .
Coding Phase 6 β Entity Display + Unified Form + Controllers (July 10 β July 30): ~45h
-
Week 1 (July 10 - 16) :
- Migrate entity display pages: Author, Work, Edition, Publisher, Series, Edition Group.
-
Week 2 (July 17-23) :
- Migrate shared display components: identifiers, relationships, annotations .
- search, revisions, collections pages and all 13
unified-form/components (cover, detail, content, submit tabs).
-
Week 3(July 24-30) :
- Migrate controllers with UI strings: search, editor, collections, statistics, deletion
- Server-side route titles via
req.t() - Full site coverage using
i18next-parseroutput shows zero unextracted keys
Coding Phase 7 β Edge Cases and Testing (July 31β August 6): ~20h
- Pluralization (t(βkeyβ, {count: n})), date/number formatting
- Pseudo-localization via
npx pseudolocaleβ 40% longer strings for 100% UI coverage without real translations - DOM overflow scan (
scrollWidth > clientWidth) and (scrollheight >clientheight) run across all main routes - French + German seed translations β full UI test in both languages
- CSS stress-test with long German words
Coding Phase 8 β Final Polish (August 7 β August 16): ~20h
- Zero-key sweep with GitHub Action
- Buffer for missed items
- Documentation, final report, demo video
Extended / Future Goals:
Translation completeness threshold: Only activate a language once it crosses a minimum coverage percentage , users never see a half-translated UI unless they explicitly choose an incomplete language
RTL language support (Arabic, Hebrew): Switch direction: rtl via i18next language metadata automatically.
Complex pluralization: Russian, Polish, Arabic have 3+ plural forms β i18nextβs CLDR resolver handles them automatically.
Apply to ListenBrainz website: The infrastructure built here is a direct blueprint.
Detailed information about yourself:
What type of music do you listen to?
I like soft, indie, and chill music that keeps my mind focused while building. Some of my favourites:
- PARO
- Shayrana
- The Rubberband Man (a classic)
What type of books do you read?
I mostly read tech and self-improvement books. Currently reading curtains .
What aspects of the project youβre applying for interest you the most?
The SSR hydration challenge interests me the most , making sure the server and client use identical locale and translation data so the page never flickers or mismatches. I hit this exact problem during my prototype build and had to trace the full generateProps() β renderToString() β hydrate() pipeline to understand where i18n state needed to live. That kind of problem, where you have to deeply understand the architecture before you can solve it cleanly, is what I enjoy most.
Have you ever used MusicBrainz Picard or any of our projects?
I use BookBrainz to look up editions and series while working on the codebase, and I contribute to ListenBrainz. I have not used Picard yet.
When did you first start programming?
I started programming in 2022. I was always fascinated by computers and that curiosity is what pulled me into software development.
Have you contributed to other Open Source projects? If so, which projects and can we see some of your code?
My primary open source contributions are to the MetaBrainz ecosystem (links in Why Me above).
Tell us about the computer(s) you have available for working on your SoC project!
Laptop: AMD Ryzen 5 7535HS with Radeon Graphics (3.30 GHz), 8 GB RAM, 64-bit Windows 11
How much time do you have available, and how would you plan to use it?
I have no other commitments during the summer and can dedicate 20β25 hours per week. The timeline above is planned around this, with buffer built into Phases 7 and 8.

