بنقرة واحدة
sif-intl
Mønster for typesikker i18n (nb/nn) i apper og pakker — implementering, parametersjekk og meningssjekk.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Mønster for typesikker i18n (nb/nn) i apper og pakker — implementering, parametersjekk og meningssjekk.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Mønster for å kombinere flere API-hooks til én asynkron laster (useInitialData + InitialDataLoader) i sif-soknad-apper.
Legg til et nytt steg i en søknadsapp som bruker @sif/soknad og @sif/rhf — oppretter alle filer og oppdaterer config/routing/i18n.
Arkitekturreferanse og beslutningslogg for @sif/soknad-app — rammeverket for søknadsapper i sif-brukerdialog.
Sett opp OppsummeringSteg i en v2-app — DTO-basert oppsummering med FormSummary, bekreftelsescheckbox og i18n-nøkler.
Veiledning for oppsett av en ny søknadsapp med @sif/soknad-app — SøknadRouter, SøknadAppProvider, AppContext og routing shell.
Implementer et vedleggssteg i en v2-app — VedleggPanel, PersistedVedlegg, hydration og MSW-handlere.
| name | sif-intl |
| type | referanse |
| description | Mønster for typesikker i18n (nb/nn) i apper og pakker — implementering, parametersjekk og meningssjekk. |
i18n/nb.ts eller i18n/nn.ts.{param}-variabler er like i nb og nn.AppText, useAppIntl, useSifXxxIntl, applicationIntlMessages.nb.ts og nn.ts med korrekte nøkler og parametere{param} i nb og nn)i18n/index.tsxMønsteret er delt i to kontekster:
@sif/soknad-ui)src/
i18n/
index.tsx ← samler nb, nn; eksporterer hooks, komponent og messages
nb.ts ← (alternativt aggregert via side-spesifikke filer)
pages/
my-page/
i18n/
nb.ts ← kilde for meldinger i denne siden/komponenten
nn.ts ← typed mot nb-objektet, garanterer full dekning
Eksempel nb.ts (pakke, side-spesifikk):
export const stepPageMessages_nb = {
'@sifSoknadUi.stepFooter.fortsettSenere.trigger.label': 'Lagre og fortsett senere',
'@sifSoknadUi.stepFooter.slettSøknad.dialog.text.1':
'Informasjonen du har fylt ut blir slettet, og du kommer tilbake til velkomstsiden.',
};
Eksempel nn.ts (pakke):
import { stepPageMessages_nb } from './nb';
export const stepPageMessages_nn: Record<keyof typeof stepPageMessages_nb, string> = {
'@sifSoknadUi.stepFooter.fortsettSenere.trigger.label': 'Lagre og hald fram seinare',
'@sifSoknadUi.stepFooter.slettSøknad.dialog.text.1':
'Informasjonen du har fylt ut vert sletta, og du kjem tilbake til velkomstsida.',
};
i18n/index.tsx (pakke):
import { typedIntlHelper } from '@navikt/sif-common-utils';
import { FormattedMessage, useIntl } from 'react-intl';
import { stepPageMessages_nb } from '../pages/step-page/i18n/nb';
import { stepPageMessages_nn } from '../pages/step-page/i18n/nn';
const nb = {
...stepPageMessages_nb,
};
const nn: Record<keyof typeof nb, string> = {
...stepPageMessages_nn,
};
type SifSoknadUiMessageKeys = keyof typeof nb;
export const useSifSoknadUiIntl = () => {
const intl = useIntl();
return typedIntlHelper<SifSoknadUiMessageKeys>(intl);
};
interface SifSoknadUiTextProps {
id: SifSoknadUiMessageKeys;
values?: any;
}
export const SifSoknadUiText = (props: SifSoknadUiTextProps) => {
return <FormattedMessage {...props} />;
};
export const sifSoknadUiMessages = {
nb,
nn,
};
aktivitetspenger-soknad)src/app/
i18n/
index.tsx ← samler lib-meldinger + app-meldinger; eksporterer AppText, useAppIntl
nb/
appMessages.ts ← aggregerer steg- og domain-meldinger
nn/
appMessages.ts ← Record<keyof typeof appMessages_nb, string>
pages/
velkommen/
i18n/
nb.ts ← side-spesifikke nb-tekster
nn.ts ← Record<keyof typeof velkommenPageMessages_nb, string>
steps/
barn/
i18n/
nb.ts ← steg-spesifikke nb-tekster
nn.ts ← Record<keyof typeof barnStegMessages_nb, string>
Eksempel nb.ts (steg, med param og HTML-tag):
export const barnStegMessages_nb = {
'barnSteg.tittel': 'Barn',
'barnSteg.spørsmål.harBarn': 'Stemmer opplysningen om {antallBarn, plural, one {barnet} other {barna}}?',
'barnSteg.opplysninger.info.text':
'Du må være registrert som forelder ... <Lenke>kontakt med Skatteetaten</Lenke>.',
};
Eksempel nn.ts (steg):
import { barnStegMessages_nb } from './nb';
export const barnStegMessages_nn: Record<keyof typeof barnStegMessages_nb, string> = {
'barnSteg.tittel': 'Barn',
'barnSteg.spørsmål.harBarn': 'Stemmer opplysninga om {antallBarn, plural, one {barnet} other {barna}}?',
'barnSteg.opplysninger.info.text': 'Du må vere registrert som forelder ... <Lenke>kontakt Skatteetaten</Lenke>.',
};
i18n/nb/appMessages.ts:
import { barnStegMessages_nb } from '../../steps/barn/i18n/nb';
export const appMessages_nb = {
...barnStegMessages_nb,
'application.title': 'Søknad om aktivitetspenger',
};
i18n/nn/appMessages.ts:
import { barnStegMessages_nn } from '../../steps/barn/i18n/nn';
import { appMessages_nb } from '../nb/appMessages';
export const appMessages_nn: Record<keyof typeof appMessages_nb, string> = {
...barnStegMessages_nn,
'application.title': 'Søknad om aktivitetspengar',
};
i18n/index.tsx (app):
import { typedIntlHelper } from '@navikt/sif-common-utils';
import { sifSoknadUiMessages } from '@sif/soknad-ui/i18n';
import { FormattedMessage, useIntl } from 'react-intl';
import { velkommenPageMessages_nb } from '../pages/velkommen/i18n/nb';
import { velkommenPageMessages_nn } from '../pages/velkommen/i18n/nn';
import { appMessages_nb } from './nb/appMessages';
import { appMessages_nn } from './nn/appMessages';
const libMessages = {
nb: { ...sifSoknadUiMessages.nb },
nn: { ...sifSoknadUiMessages.nn },
};
const nb = {
...libMessages.nb,
...appMessages_nb,
...velkommenPageMessages_nb,
};
const nn: Record<keyof typeof nb, string> = {
...libMessages.nn,
...appMessages_nn,
...velkommenPageMessages_nn,
};
export type AppMessageKeys = keyof typeof nb;
export const useAppIntl = () => {
const intl = useIntl();
return typedIntlHelper<AppMessageKeys>(intl);
};
export type AppIntlShape = ReturnType<typeof useAppIntl>;
interface AppTextProps {
id: AppMessageKeys;
values?: any;
}
export const AppText = (props: AppTextProps) => {
return <FormattedMessage {...props} />;
};
export const applicationIntlMessages = {
nb,
nn,
};
preview.js eller stories er også brukervendte tekster og skal holde korrekt norsk rettskrivning.æ, ø og å når teksten er norsk, med mindre teksten eksplisitt kommer fra en kilde som allerede mangler disse tegnene.| Kontekst | Prefiks-konvensjon | Eksempel |
|---|---|---|
| Pakke | @pkgName. | @sifSoknadUi.stepFooter.slettSøknad.label |
| Side (app) | page.pageName. | page.velkommen.guide.tittel |
| Steg (app) | stepName. | barnSteg.tittel |
| Sub-komponent innen et steg | componentName. | tilsynsordningSøknadsperiode.leggTilEndring |
| App-nivå | fritt | application.title |
Grunnpattern for sub-komponenter: Bruk komponentnavnet (camelCase) som prefiks — ikke domenebetegnelsen for innholdet. Eksempel: nøkler i TilsynsordningSøknadsperiode.tsx bruker prefikset tilsynsordningSøknadsperiode..
nn skal alltid types mot nb med Record<keyof typeof ..._nb, string>. Dette sikrer:
Parametre i meldingsstrenger som {navn}, {dato}, {count} og custom tags er en del av kontrakten mellom meldingen og komponenten som sender values.
nb.ts, nn.ts og i komponentkoden som sender values.{param} og custom tags mellom nb, nn og kallstedet.Dette er en blokkerende regel. Mismatch i parameternavn gir ødelagt interpolering i UI og er ikke akseptabelt.
Hvis nynorsk ikke finnes eller ikke er levert av en oversetter, spread nb uten å oversette selv:
import { myMessages_nb } from './nb';
export const myMessages_nn: Record<keyof typeof myMessages_nb, string> = {
...myMessages_nb,
};
Lag nynorsk-tekster kun når brukeren eksplisitt ber om det. Ellers skal nynorsk komme fra et menneske eller en offisiell oversetter.
I nynorsk tekst skal me alltid brukes i staden for vi som subjektspronomen i første person fleirtal.
✅ «slik at me kan handsame saka di»
❌ «slik at vi kan handsame saka di»
Sjekk alle nn.ts-filer for vi (med mellomrom etter) og erstatt med me .
Velg basert på om teksten er en JSX-node (children) eller en string-prop:
| Kontekst | Bruk | Eksempel |
|---|---|---|
| JSX text node (children) | <AppText> / <SifSoknadUiText> | <Heading><AppText id="tittel" /></Heading> |
| String-prop (aria-label, title, label, placeholder o.l.) | text() fra hook | aria-label={text('lukk.label')} |
export const MyComponent = () => {
const { text } = useAppIntl();
return (
<Button aria-label={text('myBtn.ariaLabel')}>
<AppText id="myBtn.label" />
</Button>
);
};
react-intl støtter custom tags for å sende inn React-komponenter som values. Viktig gotcha:
// ❌ Virker ikke — react-intl parser ikke self-closing custom tags
'melding': 'Send oss en melding via <SkrivTilOssLink />.'
// ✅ Riktig
'melding': 'Send oss en melding via <SkrivTilOssLink></SkrivTilOssLink>.'
I komponenten sendes taggen inn som en values-funksjon:
<AppText id="melding" values={{ SkrivTilOssLink: () => <SkrivTilOssLink /> }} />
...) for å samle meldinger fra steg/sider inn i appMessages.ts.nn/appMessages.ts sprer ...stegMessages_nn og typecheckes mot keyof typeof appMessages_nb.index.tsx i appen sprer lib-meldinger først, så app-meldinger, så side-meldinger (senere spread vinner ved nøkkelkonflikt).En pakke eksporterer kun det aggregerte meldings-objektet — aldri de individuelle underfilene.
i18n/index.tsx eksporterer xyzMessages (aggregert { nb, nn }). Dette er det eneste konsumenter trenger.modules/oppgaver/avvik-registerinntekt/i18n/nb.ts skal ikke eksporteres fra pakkens index.ts — de er implementasjonsdetaljer.IntlProvider. De trenger ikke vite hvilke underfiler meldingene kom fra.// ✅ Riktig — pakken eksporterer kun aggregert
export { xyzMessages } from './i18n'; // { nb, nn }
// ❌ Feil — eksponerer intern struktur
export { avvikRegisterinntektMessages_nb } from './modules/oppgaver/avvik-registerinntekt/i18n/nb';
Filstiene er deterministiske og avledes direkte fra komponent- eller stegnavnet. Ikke søk bredt i kodebasen — les de kjente filene direkte i parallell.
Gitt en komponent i steps/<steg-mappenavn>/, les følgende fire filer i parallell:
| Fil | Formål |
|---|---|
steps/<steg-mappenavn>/<StegComponent>.tsx | Komponentfilen med hardkodede tekster |
steps/<steg-mappenavn>/i18n/nb.ts | Eksisterende nb-nøkler for steget |
steps/<steg-mappenavn>/i18n/nn.ts | Eksisterende nn-nøkler for steget |
i18n/nb/appMessages.ts | Bekrefter steg-modulen er registrert |
Eksempel: For AndreYtelserSteg.tsx i steps/andre-ytelser/, les:
steps/andre-ytelser/AndreYtelserSteg.tsxsteps/andre-ytelser/i18n/nb.tssteps/andre-ytelser/i18n/nn.tsi18n/nb/appMessages.tsGitt en komponent i pages/<side-mappenavn>/, les:
| Fil | Formål |
|---|---|
pages/<side-mappenavn>/<PageComponent>.tsx | Komponentfilen med hardkodede tekster |
pages/<side-mappenavn>/i18n/nb.ts | Eksisterende nb-nøkler for siden |
pages/<side-mappenavn>/i18n/nn.ts | Eksisterende nn-nøkler for siden |
i18n/index.tsx | Bekrefter siden er inkludert i app-meldingene |
Gitt en komponent i src/<type>/<komponent>/, les:
| Fil | Formål |
|---|---|
src/<type>/<komponent>/<Component>.tsx | Komponentfilen med hardkodede tekster |
src/<type>/<komponent>/i18n/nb.ts | Eksisterende nb-nøkler |
src/<type>/<komponent>/i18n/nn.ts | Eksisterende nn-nøkler |
src/i18n/index.tsx | Bekrefter komponenten er registrert i pakke-messages |
Regel: Hvis alle fire filene leses i parallell i én batch, har du all nødvendig kontekst. Ikke gjør ytterligere søk.
Når du skal opprette eller oppdatere i18n-filer:
nb.ts med eksisterende eller eksplisitt levert bokmålstekst, uten omskriving.nn.ts med Record<keyof typeof ..._nb, string> — ikke kopier nb som utgangspunkt, bruk heller spread ...nb. Da ser vi nå noen nn nøkler manglerappMessages.ts (nb og nn).index.tsx i appen inkluderer nn-versjon av alle kilder.applicationIntlMessages eksporterer { nb, nn }.Hvis oppgaven gjelder uttrekk fra hardkodede tekster i komponenter, skal nb.ts gjengi nøyaktig samme tekst som stod i komponenten før uttrekket.
Les alle aktuelle komponentfiler i én parallell read_file-batch. Formålet er å lage en komplett liste over inline tekster FØR du oppretter noen filer.
For hvert steg som skal ekstraheres, les hovedkomponenten (f.eks. BostedForm.tsx, OppsummeringSteg.tsx).
Fra hver fil, noter alle hardkodede tekster:
legend="...", aria-label="...", title="...", placeholder="..."legend={`Tekst ${x}?`} → i18n-nøkkel med {param}<Heading>Tekst</Heading>, <SifInfoCard>Tekst</SifInfoCard>Hva som IKKE skal ekstraheres:
variant="warning", size="small")text('nøkkel') eller <AppText id="..." />)Effektivisering: Bruk én parallell read_file-batch for alle steg. Samle alle funn i en mental tabell (steg → nøkkel → tekst) før du går videre til steg 2.
For hvert steg med inline tekster:
i18n/nb.ts og i18n/nn.ts i stegets folder (f.eks. steps/bosted/i18n/nb.ts).nb.ts med passende nøkler (f.eks. barnSteg.tittel, barnSteg.spørsmål.harBarn). Navnet på variabelen er komponentnavnet + "Messages" (f.eks. barnStegMessages_nb).nn.ts med Record<keyof typeof nb, string> og spread ...nb — ikke oversett tekstene til nynorsk. Nynorsk-oversettelse gjøres manuelt av utvikler i etterkant._nb-variabelen i pakkens i18n/index.tsx (i nb-objektet), slik at meldingene eksporteres via sifSoknadUiMessages / applicationIntlMessages. Uten dette steget er meldingene ikke tilgjengelige i konsumerende apper.
_nn-variabelen i nn-objektet bare hvis den inneholder faktiske nynorsk-oversettelser (dvs. ikke er kun { ...nb }). Hvis nn.ts bare er et spread av nb, er ...nb i nn allerede tilstrekkelig — ikke legg til redundant import.appMessages.ts ved å spre ...barnStegMessages_nb og ...barnStegMessages_nn.Bruk multi_replace_string_in_file for å oppdatere ALLE komponenter i én operasjon.
Regelen for hook vs. komponent:
text('nøkkel') fra hook<AppText id="nøkkel" /> komponentFor hver komponent som oppdateres:
import { AppText, useAppIntl } from '@app/i18n'; — utelat useAppIntl om kun children brukes, utelat AppText om kun string-props brukesconst { text } = useAppIntl(); kun hvis text() brukes// String-prop → hook
<YesOrNoQuestion
name={Field.erBosattITrondheim}
legend={text('bostedSteg.spørsmål.erBosattITrondheim')}
/>
// Template literal med param → hook
<YesOrNoQuestion
legend={text('kontonummerSteg.spørsmål.kontonummerErRiktig', { kontonummer: info.kontonummer })}
/>
// JSX children → komponent
<Heading><AppText id="bostedUtlandSteg.bosteder.tittel" /></Heading>
<Button><AppText id="bostedUtlandSteg.bosteder.leggTil" /></Button>
<Checkbox validate={...}>
<AppText id="oppsummeringSteg.bekrefterOpplysninger.label" />
</Checkbox>
Erstatt hardkodede tekster etter regelen:
<AppText id="..."> / <SifSoknadUiText id="..." />text('...') fra hook// JSX node → komponent
<Heading><SifSoknadUiText id="@ui.myComponent.heading" /></Heading>
// String-prop → hook
<Input aria-label={text('@ui.myComponent.inputLabel')} />
// Fallback i children → komponent (siden children er en node)
{propLabel || <SifSoknadUiText id="@ui.myComponent.defaultLabel" />}
Verifiserer at {param}-variabler er identiske mellom nb og nn for samme nøkkel.
Fremgangsmåte:
{...}-forekomster fra nb-verdien.PARAMETER MISMATCH
Nøkkel: barnSteg.spørsmål.harBarn
nb: {antallBarn, plural, one {barnet} other {barna}}
nn: {antallBarn} ← mangler plural-form
Hva som sjekkes:
{param} i nn som finnes i nb{param} i nn som ikke finnes i nbplural, select) som avviker<TagName>...</TagName> HTML-tags som mangler eller er feilstavet i nnTip: Bruk grep-mønsteret \{[^}]+\} for å finne alle parametre i en tekstverdi.
Semantisk vurdering av om nb og nn uttrykker det samme innholdet.
Fremgangsmåte:
MENINGSAVVIK
Nøkkel: personopplysninger.2
nb: "Vi innhenter og mottar opplysninger om deg når vi skal behandle saken din.
Det er nødvendig for at du skal få riktig tjeneste. Saken din kan behandles automatisk."
nn: "Vi hentar opplysningar om deg."
Problem: nn mangler "behandles automatisk" og "riktig tjeneste" — juridisk viktig informasjon.
Forslag: Legg til "Saka di kan handsamast automatisk." og "for at du skal få rett teneste."
Hva som sjekkes:
apps/aktivitetspenger-soknad/src/app/i18n/index.tsxapps/aktivitetspenger-soknad/src/app/i18n/nb/appMessages.tsapps/aktivitetspenger-soknad/src/app/pages/velkommen/i18n/nb.ts / nn.tsapps/aktivitetspenger-soknad/src/app/steps/barn/i18n/nb.tspackages/sif-soknad-ui/src/i18n/index.tsxpackages/sif-soknad-ui/src/pages/step-page/i18n/nb.ts