Teamet vårt hadde nylig problemer med serverside-rendret tekst med internasjonalisering.
Alt fungerte lokalt, men bare klientside-rendret tekst fungerte i skymiljøene.
Dette er historien om vår feilsøking og mysterieløsning med i18next, React Router v7 og Vercel.
Første oppdagelse
Det hele startet da en av designerne våre kikket opp fra skjermen på den motsatte pulten og spurte:
“Hei, har du lagt merke til at teksten blinker med noe placeholder et sekund i prod-miljøet?”

Da vi tok en titt, innså vi raskt at dette ikke bare var et problem i produksjon. Det samme problemet skjedde i staging og i alle våre sky-testmiljøer.
Når du lastet siden, ville du i et lite øyeblikk se rå oversettelsesnøkler blinke før den faktiske oversatte teksten dukket opp. Ikke akkurat den polerte brukeropplevelsen vi siktet mot.
Vi hoppet umiddelbart inn i de ulike miljøene for å få en bedre forståelse av hva som skjedde. Blinkingen var konsistent på tvers av hver eneste skydeploy på Vercel, men merkelig nok fungerte våre lokale utviklingsmiljøer helt fint. Ingen blinking i det hele tatt.
Forståelse av problemet
Litt kontekst: vi bruker i18next for å håndtere oversettelsene våre. Til tross for at det har “next” i navnet, har i18next ingenting med Next.js å gjøre — det er bare et frittstående internasjonaliseringsrammeverk for JavaScript.
Vi hadde nylig gått fra hardkodede statiske tekster til denne internasjonaliseringstilnærmingen, noe som gjorde dette problemet spesielt urovekkende.

Tanu tok ned produksjon: «Vanskeligste jeg har opplevd»
Vårt første instinkt var å dobbeltsjekke arbeidet vårt. Vi gikk tilbake til dokumentasjonen og sporet opp stegene våre. Hadde vi konfigurert alt riktig? Fulgte vi de anbefalte mønstrene? Alt så ut til å stemme ifølge dokumentasjonen, noe som bare forsterket mysteriet.
Etter noe manuell testing fant vi ut at alt fungerte lokalt, både serversidestekster, klientsidestekster og hydration. Vi fant også et forslag i i18nexts FAQ om å aktivere feilsøkingsmodus, som kanskje kunne gi oss flere ledetråder.
i18n.init({
debug: true,
// andre konfigurasjoner
});
Med feilsøkingsmodus aktivert kunne vi bekrefte det vi så med egne øyne. Lokalt ble alle tekster løst som de skulle, selv på serversiden. Så hvorfor skulle den samme koden oppføre seg så forskjellig når den ble deployet?
Dypere undersøkelse
Vi måtte få et lokalt bygg som var nærmere produksjonsmiljøet. Så vi bygde en produksjonsversjon lokalt i stedet for å bruke Vite-utviklingsserveren som React Router v7 bruker. Nå så vi at den samme tekstflimringen skjedde på maskinene våre. Fremgang!
Vi åpnet umiddelbart byggresultatet for å se etter åpenbare feil, som at oversettelsene ikke var inkludert i bygget. Der oppdaget vi et sti-problem. På serversiden brukte vi i18next-fs-backend for å lese oversettelsesfiler fra:
./public/locales/{{language}}/{{namespace}}/translation.json
Men i produksjonsbygget var stien faktisk:
/build/client/locales/{{language}}/{{namespace}}
Når vi oppdaterte stikonfigurasjonen, begynte vårt lokale produksjonsbygg å fungere riktig. Vi følte at vi gjorde fremgang. Bare å pushe fiksen og gå videre, ikke sant?
Falskt håp
Neida, det hadde vært for lett. Etter å ha deployert vår “fiks” til Vercel, var blinkingen fortsatt der. Tilbake til start.
Vi brukte den neste timen på å søke på nettet og fant andre utviklere som møtte samme problem. I en GitHub-diskusjon beskrev noen akkurat vårt problem. De ble videresent til i18next-fs-backend-repositoriet for å opprette et issue der i stedet. I dette issuet nevnte noen å legge til noe som kalles localePath for å peke til oversettelsenes rotmappe:
i18n.init({
backend: {
localePath: path.resolve("./public/locale"),
loadPath: path.resolve("./public/locales/{{lng}}/{{ns}}.json")
},
// andre konfigurasjoner
});
Vi kunne ikke umiddelbart finne noe om localePath når vi prøvde å lese hva konfigurasjonen skulle gjøre. Så vi var skeptiske til om det faktisk kunne være løsningen.
Det ville ikke være første gang jeg leste noe på Github som noen sa fikset et problem for dem, men som ikke hjalp. Altså hvor det ble raskt tydelig at det var noe annet de hadde gjort mens de feilsøkte som var den faktiske løsningen.
Noe mer googling avslørte ikke noen umiddelbare andre alternativer, så vi implementerte endringen, deployet igjen, og det virket! What?! Oversettelsene ble rendret riktig på serveren. Ingen mer blinking.
Skepsisen øker
Men noe føltes feil. Som utviklere er vi naturlig mistenksomme mot fikser som fungerer uten en klar grunn til hvorfor.
Vi gravde i i18next og i18next-fs-backend-dokumentasjonen og lette etter enhver omtale av denne localePath-egenskapen. Ingenting.
Vi gikk til og med gjennom kildekoden deres. Fortsatt ingenting.
Likevel ble denne udokumenterte egenskapen nevnt i andre GitHub-problemer som en løsning for Next.js-brukere med lignende problemer.
Hvis denne egenskapen ikke var dokumentert eller engang implementert i koden, hvordan fikset den problemet vårt?

Advarer: – Lett å få panikk når vi har gjort noe galt
Testing av teorien vår
Var det virkelig denne egenskapen som løste problemet vårt? Hva om vi endret property-navnet til noe oppdiktet tull? Da ville det sikkert feile igjen, sant?
i18n.init({
backend: {
interPathy: path.resolve("./public/locale"), // Helt oppfunnet navn
loadPath: path.resolve("./public/locales/{{lng}}/{{ns}}.json")
},
// andre konfigurasjoner
});
Vi deployet denne endringen og, overraskende nok, fungerte serverside-rendringen fortsatt. Dette bekreftet vår mistanke — egenskapsnavnet var irrelevant.
Det var verdien, path.resolve(“./public/locale”), som gjorde noe. Dette føltes imidlertid som galskap. Funksjonen path.resolve() har ikke sideeffekter — den normaliserer bare stier. Den burde ikke gjøre noe bare ved å bli kalt.
Eller burde den..? Nei, Google-resultatene mine så ut til å være enige med meg.
Vi tok eksperimentet vårt et skritt videre, vi fjernet egenskapen fra konfigurasjonsobjektet helt og bare satte kodelinjen et annet sted i filen:
path.resolve("./public/locale"); // Bare flyter her, ikke tilordnet til noe
Hvis denne path.resolve-en var magisk, ville den fortsatt fungere. Men det ville innebære at å løse filstien faktisk gjorde noe? Og gjett hva, det fungerte fortsatt!
På dette tidspunktet begynte jeg å tenke på historien om universitetet som ikke kunne sende e-poster lengre enn 500 miles. Dette begynte å føles like crazy.
Undersøkelsen fortsetter
Nå, hva var magisk her? Kunne vi løse hvilken som helst sti og det ville funke?
Vi bestemte oss for å teste forskjellige stier systematisk:
path.resolve("./public") // Fungerte
path.resolve("./public/locales") // Fungerte
path.resolve("./") // Feilet
path.resolve("/public/locales/{{lng}}") // Feilet
Et mønster begynte å dukke opp. Stien måtte være spesifikk nok til å peke til oversettelsesmappen vår, men ikke så spesifikk at den inkluderte i18next’s sti-parametere.
Gjennombruddet
Vi delte opp undersøkelsen vår: en av oss fortsatte å søke etter svar på nettet mens den andre begynte å teste de fungerende og ikke-fungerende stiene i bygg med Vercel CLI, og så etter forskjeller i hvordan de ble behandlet.
Heldigvis var Vercel CLI-byggene faktisk forskjellige. Hadde de ikke vært det, kunne jeg ha brutt mentalt sammen.
Ved å ta en titt i .vercel/output-mappen, fant jeg en fil med navnet .vc-config.json. Denne filen spesifiserer hvordan Serverless-funksjonen vil bli opprettet av Vercel. Når stier som /public eller /public/locale ble brukt, var det en ekstra oppføring i .vc-config.json-filen. Oversettelsesfilene våre dukket opp i noe som kalles filePathMap-objektet.
«Heldigvis var Vercel CLI-byggene faktisk forskjellige. Hadde de ikke vært det, kunne jeg ha brutt mentalt sammen.»
Omtrent samtidig fant vi et blogginnlegg med et avgjørende hint:
«path.resolve(...) instructs Vercel to include the scanned path in the serverless lambda.»
Men hvor lærte denne bloggeren det? Hvor var det dokumentert? Uansett var det en bekreftelse på det vi hadde lært — path.resolve påvirket faktisk hvilke filer som skulle inkluderes.
Med denne kunnskapen ganske godt forankret, googlet vi videre og fant Vercel Node File Trace — et system laget og brukt av Vercel, som bestemmer hvilke filer som skal inkluderes i en deployert serverless-funksjon. For å spare plass og optimalisere ytelsen skanner den koden din etter imports, requires og resolve-kall for å finne ut hvilke filer den trenger å inkludere.
Mysteriet løst?
Alt falt på plass. Når vi kalte path.resolve() på oversettelseskatalogen vår, oppdaget Vercel Node File Trace (NFT) det og la til oversettelsesfilene våre i deployeringspakken. Uten dette signalet ble oversettelsesfilene våre etterlatt, noe som forårsaket at serverside-rendringen feilet.
Løsningen hadde ingenting med i18next-konfigurasjon å gjøre! Det handlet om å signalisere til Vercels deploysystem hvilke filer det trengte å inkludere.
Hadde vi visst fra starten av at filen rett og slett ble droppet fra bygget, kunne vi bare ha lest guiden om hvordan man inkluderer filer i en serverless-funksjon, som vi fant først etter å ha dykket helt ned i kildekoden til Vercel NFT.
Sliter du med at en avhengighet ikke får fatt i en ressurs i Vercel, er dette altså løsningen for deg.

Beklager til 1.000 kunder: Får andre familiers bilder
Imidlertid gjenstår ennå noen spørsmål:
- Kunne vi inkludere disse filene på noen annen måte enn enten å importere dem eller ha den magiske path.resolve?
- Hva er begrensningene på hva stien kan være for at Vercel Node File Trace skal finne filen? Den ser åpenbart i mappen vi inkluderte for den faktiske filen siden stien inkludert i bygget var den faktiske, fulle filstien, men den nektet å skanne hele katalogen.
For spørsmål 1, kunne vi ikke bruke vercel.config-løsningen da det så ut til å kreve at funksjoner må være enten i pages eller api, og ikke matchet våre React Router v7-funksjoner som ble automatisk laget av Vercel Preset.
Å bare ha en “dinglende” path.resolve(...) som må være med i koden er ikke ideelt for fremtidig vedlikeholdbarhet. Hadde jeg personlig kommet inn i en kodebase med en slik linje hadde det vært svært fristende å fjerne den — den skal jo ikke gjøre noe etter all logikk.
Selv om brukeropplevelsen er god er ikke all ting godt. Vi er dermed fremdeles interessert i en bedre løsning.
Hvis du vet hvordan man løser dette med å inkludere en fil i alle funksjoner bygget gjennom @vercel/react-router, gi meg beskjed!
