一键导入
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