Synk fra arbeidsrepoet

This commit is contained in:
Vaultnord
2026-08-31 21:40:43 +00:00
parent e0994317c6
commit 6a7315ba97
16 changed files with 612 additions and 247 deletions
+13 -2
View File
@@ -34,6 +34,18 @@ RUN set -eux; \
done; \
python3 /sprak.py --sjekk /locales/nb/messages.json
# E-postmalene bygges av maler.py, ett sett per språk. Serveren peker på ett av
# settene per kunde, så en engelsktalende bedrift kan få engelske e-poster uten
# at noen tekst skrives to steder.
FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d AS maler
RUN apk add --no-cache python3
COPY --from=kilde /vaultwarden /vaultwarden
ARG MAIL_DIR=vault-image/mail
COPY ${MAIL_DIR}/maler.py /maler.py
# Første kall sammenligner med malene serveren faktisk kan sende: kommer det en
# ny i en oppgradering, stopper bygget her i stedet for at den går ut på engelsk.
RUN python3 /maler.py --mot /vaultwarden && python3 /maler.py /maler
FROM vaultwarden/server:${VW_VERSION}@${VW_DIGEST}
COPY --from=sprak /locales /web-vault/locales
@@ -44,8 +56,7 @@ COPY ${BRAND_DIR}/ /web-vault/_vaultnord/
# Egne e-postmaler. Uten disse går invitasjonene ut med Vaultwardens navn og
# utseende - det er kundens første inntrykk av tjenesten, og det skal være vårt.
ARG MAIL_DIR=vault-image/mail
COPY ${MAIL_DIR}/ /vaultnord/templates/
COPY --from=maler /maler/ /vaultnord/templates/
RUN set -eux; \
cp /web-vault/_vaultnord/favicon.png /web-vault/images/favicon-32x32.png; \
-12
View File
@@ -1,12 +0,0 @@
{{{email}}} har takket ja til invitasjonen
<!---------------->
{{email}} har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.
Siste steg må gjøres av deg: logg inn på {{url}} og bekreft brukeren under
Medlemmer i organisasjonen. Først da får de tilgang til det som deles.
Bekreftelsen kan bare gjøres av deg - den krever organisasjonsnøkkelen, som
bare finnes i din egen innloggede klient. Det er derfor vi ikke kan gjøre det
for deg.
Vaultnord
-21
View File
@@ -1,21 +0,0 @@
{{{email}}} har takket ja til invitasjonen
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">{{email}} har takket ja</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6"><strong>{{email}}</strong> har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.</p>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Siste steg må gjøres av deg: logg inn på <a href="{{{url}}}" style="color:#17807d">hvelvet</a> og bekreft brukeren under <em>Medlemmer</em>. Først da får de tilgang til det som deles.</p>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Bekreftelsen krever organisasjonsnøkkelen, som bare finnes i din egen innloggede klient &mdash; derfor kan ikke vi gjøre den for deg.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-10
View File
@@ -1,10 +0,0 @@
Du har fått tilgang til {{{org_name}}}
<!---------------->
Du er nå bekreftet som medlem av {{org_name}}.
Passordene organisasjonen deler med deg dukker opp i hvelvet ditt: {{url}}
Bruker du Bitwarden-appen eller nettleserutvidelsen, kan du måtte synkronisere
en gang for å se dem.
Vaultnord
-20
View File
@@ -1,20 +0,0 @@
Du har fått tilgang til {{{org_name}}}
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Du har fått tilgang til {{org_name}}</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Du er nå bekreftet som medlem, og passordene organisasjonen deler med deg dukker opp i <a href="{{{url}}}" style="color:#17807d">hvelvet ditt</a>.</p>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Bruker du appen eller nettleserutvidelsen, kan du måtte synkronisere én gang for å se dem.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-16
View File
@@ -1,16 +0,0 @@
Ny innlogging fra {{{device_name}}}
<!---------------->
Hei,
Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.
Tidspunkt: {{datetime}}
IP-adresse: {{ip}}
Enhet: {{device_name}} ({{device_type}})
Var det deg, trenger du ikke gjøre noe.
Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle
enheter under Innstillinger i hvelvet ({{url}}).
Vaultnord
-25
View File
@@ -1,25 +0,0 @@
Ny innlogging fra {{{device_name}}}
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Ny innlogging på kontoen din</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.</p>
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 16px;font-size:14px;color:#55617a">
<tr><td style="padding:2px 12px 2px 0">Tidspunkt</td><td style="color:#2e3440">{{datetime}}</td></tr>
<tr><td style="padding:2px 12px 2px 0">IP-adresse</td><td style="color:#2e3440">{{ip}}</td></tr>
<tr><td style="padding:2px 12px 2px 0">Enhet</td><td style="color:#2e3440">{{device_name}} ({{device_type}})</td></tr>
</table>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Var det deg, trenger du ikke gjøre noe. Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle enheter under Innstillinger i <a href="{{{url}}}" style="color:#17807d">hvelvet</a>.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-18
View File
@@ -1,18 +0,0 @@
Invitasjon til passordhvelvet hos {{{org_name}}}
<!---------------->
Hei,
Du er invitert til passordhvelvet til {{org_name}}.
Åpne denne lenken for å komme i gang. Der setter du ditt eget hovedpassord —
det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre:
{{{url}}}
Lenken er gyldig i fem dager. Bruk hele adressen, også den lange tegnrekken
til slutt.
Var ikke dette ventet? Da kan du se bort fra e-posten.
Vaultnord
Passordhvelv driftet i Norden
-29
View File
@@ -1,29 +0,0 @@
Invitasjon til passordhvelvet hos {{{org_name}}}
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Du er invitert til passordhvelvet hos {{org_name}}</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Åpne lenken under for å komme i gang. Der setter du ditt eget hovedpassord &mdash; det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre.</p>
</td></tr>
<tr><td style="padding:8px 32px 24px">
<table role="presentation" cellpadding="0" cellspacing="0"><tr>
<td style="border-radius:8px;background:#17807d">
<a href="{{{url}}}" clicktracking=off target="_blank" style="display:inline-block;padding:12px 22px;color:#ffffff;font-size:15px;font-weight:600;text-decoration:none">Sett hovedpassord</a>
</td>
</tr></table>
</td></tr>
<tr><td style="padding:0 32px 20px;color:#55617a;font-size:13px;line-height:1.6">
Lenken er gyldig i fem dager. Var ikke dette ventet, kan du se bort fra e-posten.
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; passordhvelv driftet i Norden
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-9
View File
@@ -1,9 +0,0 @@
Innloggingskode: {{{token}}}
<!---------------->
Koden din er: {{token}}
Bruk den for å fullføre innloggingen. Den varer i noen få minutter.
Var det ikke du som logget inn, bør du bytte hovedpassord.
Vaultnord
-21
View File
@@ -1,21 +0,0 @@
Innloggingskode: {{{token}}}
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Innloggingskode</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Koden din er:</p>
<p style="margin:0 0 16px;font-size:30px;letter-spacing:.18em;font-weight:700;color:#17807d">{{token}}</p>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Den varer i noen få minutter. Var det ikke du som logget inn, bør du bytte hovedpassord.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-9
View File
@@ -1,9 +0,0 @@
Bekreft e-postadressen din
<!---------------->
Bekreft e-postadressen for kontoen din ved å åpne lenken under:
{{url}}/#/verify-email/?userId={{user_id}}&token={{token}}
Var det ikke du som ba om dette, kan du se bort fra e-posten.
Vaultnord
-24
View File
@@ -1,24 +0,0 @@
Bekreft e-postadressen din
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Bekreft e-postadressen din</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Åpne lenken under for å bekrefte adressen for kontoen din.</p>
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:8px 0 20px"><tr>
<td style="border-radius:8px;background:#17807d">
<a href="{{{url}}}/#/verify-email/?userId={{user_id}}&amp;token={{token}}" clicktracking=off target="_blank" style="display:inline-block;padding:12px 22px;color:#ffffff;font-size:15px;font-weight:600;text-decoration:none">Bekreft adressen</a>
</td></tr></table>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Var det ikke du som ba om dette, kan du se bort fra e-posten.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
-11
View File
@@ -1,11 +0,0 @@
Hvelvet ditt er klart til bruk
<!---------------->
Hei,
Hovedpassordet ditt er satt, og hvelvet er klart: {{url}}
Ett tips til slutt: installer Bitwarden-appen eller nettleserutvidelsen og oppgi
adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden
lastes på nytt, fordi nøkkelen bare finnes i minnet - utvidelsen holder økten.
Vaultnord
-20
View File
@@ -1,20 +0,0 @@
Hvelvet ditt er klart til bruk
<!---------------->
<!doctype html>
<html lang="no"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">Hvelvet ditt er klart</h1>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Hovedpassordet ditt er satt, og hvelvet er klart til bruk på <a href="{{{url}}}" style="color:#17807d">{{url}}</a>.</p>
<p style="margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6">Ett tips: installer Bitwarden-appen eller nettleserutvidelsen og oppgi adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden lastes på nytt, fordi nøkkelen bare finnes i minnet &mdash; utvidelsen holder økten.</p>
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.
</td></tr>
</table>
</td></tr>
</table>
</body></html>
+599
View File
@@ -0,0 +1,599 @@
"""Bygger e-postmalene til hvelvet, ett sett per språk.
Vaultwarden har 30 maler. Vi hadde skrevet sju av dem om til norsk; resten gikk
ut på engelsk med Bitwardens navn i teksten - altså midt i onboardingen. Å
håndskrive de 23 siste i to formater og to språk blir 92 filer som må holdes i
takt, så teksten ligger heller i tabellen under og formene genereres.
Kjøres under bygget: python3 maler.py <utmappe>
og legger igjen <utmappe>/<språk>/email/<navn>.hbs og <navn>.html.hbs.
Et nytt språk er en kolonne til i tabellen, ikke en ny mappe med filer.
"""
import re
import sys
import textwrap
from pathlib import Path
SPRÅK = ("nb", "en")
# Bunnteksten under strekend i hver e-post.
BUNN = {
"nb": "Vaultnord &middot; drift i Finland, backup i Tyskland &mdash; alt innenfor EU.",
"en": "Vaultnord &middot; hosted in Finland, backed up in Germany &mdash; all within the EU.",
}
BUNN_TEKST = {"nb": "Vaultnord", "en": "Vaultnord"}
# Navnet på appen kunden faktisk installerer. Den heter Bitwarden i butikkene,
# og å kalle den noe annet ville sende folk på et umulig søk.
APP = "Bitwarden"
# --- oppmerking ------------------------------------------------------------
# Teksten skrives én gang. [tekst](lenke) blir en lenke, *ord* blir uthevet, og
# linjer som begynner med "- " blir en liste. Alt annet er avsnitt.
LENKE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
UTHEVET = re.compile(r"\*([^*]+)\*")
BARE_LENKE = re.compile(r"^\[([^\]]+)\]\(([^)]+)\)$")
def _tekst(avsnitt: list[str]) -> str:
ut = []
for a in avsnitt:
if a.startswith("- "):
ut.append("\n".join(" " + linje[2:] for linje in a.split("\n")))
continue
# En lenke som står alene er en oppfordring: sett adressen på egen linje,
# uinnpakket, så den blir klikkbar i alle klienter.
alene = BARE_LENKE.match(a)
if alene:
ut.append(f"{alene.group(1)}:\n{alene.group(2)}")
continue
ut.append(textwrap.fill(LENKE.sub(_flat, a), 78,
break_long_words=False, break_on_hyphens=False))
return "\n\n".join(ut)
def _flat(m: re.Match) -> str:
"""Lenke midt i en setning. Er teksten adressen selv, skal den ikke stå to
ganger - da ble det «{{url}}: {{{url}}}» i innboksen."""
tekst, adresse = m.group(1), m.group(2)
if tekst.strip("{} ") == adresse.strip("{} "):
return tekst
return f"{tekst} ({adresse})"
def _html(avsnitt: list[str]) -> str:
p = ('margin:0 0 16px;color:#2e3440;font-size:15px;line-height:1.6')
ut = []
for a in avsnitt:
if a.startswith("- "):
punkter = "".join(
f'<li style="margin:0 0 4px">{_inline(linje[2:])}</li>'
for linje in a.split("\n"))
ut.append(f'<ul style="{p};padding-left:20px">{punkter}</ul>')
else:
ut.append(f'<p style="{p}">{_inline(a)}</p>')
return "\n ".join(ut)
def _inline(s: str) -> str:
s = LENKE.sub(r'<a href="\2" style="color:#17807d">\1</a>', s)
return UTHEVET.sub(r"<strong>\1</strong>", s)
RAMME = """<!doctype html>
<html lang="{lang}"><body style="margin:0;padding:0;background:#f3f6fa">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f6fa">
<tr><td align="center" style="padding:32px 16px">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:560px;background:#ffffff;border:1px solid #dbe2ec;border-radius:12px;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif">
<tr><td style="padding:28px 32px 4px;font-size:20px;font-weight:700;letter-spacing:-.02em;color:#2e3440">vaultnord</td></tr>
<tr><td style="padding:12px 32px 0">
<h1 style="margin:0 0 18px;font-size:20px;line-height:1.3;color:#2e3440;font-weight:650">{tittel}</h1>
{kropp}
</td></tr>
<tr><td style="padding:18px 32px 26px;border-top:1px solid #dbe2ec;color:#55617a;font-size:13px;line-height:1.6">
{bunn}
</td></tr>
</table>
</td></tr>
</table>
</body></html>"""
HTML_LANG = {"nb": "no", "en": "en"}
# --- teksten ---------------------------------------------------------------
# Per mal og språk: (emne, overskrift, avsnitt). Variabelnavnene er
# Vaultwardens egne - endres de her, kommer e-posten ut med tomme felter.
MALER: dict[str, dict[str, tuple[str, str, list[str]]]] = {
"welcome": {
"nb": ("Hvelvet ditt er klart til bruk", "Hvelvet ditt er klart", [
"Hovedpassordet ditt er satt, og hvelvet er klart til bruk på [{{url}}]({{{url}}}).",
f"Ett tips til slutt: installer {APP}-appen eller nettleserutvidelsen og oppgi "
"adressen over som serveradresse. Nettleserversjonen låser seg hver gang siden "
"lastes på nytt, fordi nøkkelen bare finnes i minnet — utvidelsen holder økten.",
]),
"en": ("Your vault is ready", "Your vault is ready", [
"Your master password is set, and your vault is ready at [{{url}}]({{{url}}}).",
f"One tip: install the {APP} app or browser extension and enter the address "
"above as the server address. The web version locks every time the page "
"reloads, because the key only lives in memory — the extension keeps the session.",
]),
},
"welcome_must_verify": {
"nb": ("Bekreft e-postadressen din", "Ett steg igjen", [
"Kontoen din på [{{url}}]({{{url}}}) er opprettet. Før du kan logge inn må du "
"bekrefte at adressen er din.",
"[Bekreft e-postadressen]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
"Var det ikke du som opprettet kontoen, kan du se bort fra e-posten.",
]),
"en": ("Verify your email address", "One step left", [
"Your account at [{{url}}]({{{url}}}) has been created. Before you can log in, "
"you need to confirm that this address is yours.",
"[Verify your email address]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
"If you did not create this account, you can ignore this email.",
]),
},
"verify_email": {
"nb": ("Bekreft e-postadressen din", "Bekreft e-postadressen", [
"Bekreft e-postadressen for kontoen din ved å åpne lenken under:",
"[Bekreft e-postadressen]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
"Var det ikke du som ba om dette, kan du se bort fra e-posten.",
]),
"en": ("Verify your email address", "Verify your email address", [
"Confirm the email address on your account by opening the link below:",
"[Verify your email address]({{url}}/#/verify-email/?userId={{user_id}}&token={{token}})",
"If you did not ask for this, you can ignore this email.",
]),
},
"register_verify_email": {
"nb": ("Bekreft e-postadressen din", "Bekreft e-postadressen", [
"Bekreft adressen for å fullføre opprettelsen av kontoen.",
"[Bekreft e-postadressen]({{{url}}})",
"Var det ikke du som ba om dette, kan du se bort fra e-posten.",
]),
"en": ("Verify your email address", "Verify your email address", [
"Confirm this address to finish creating your account.",
"[Verify your email address]({{{url}}})",
"If you did not ask for this, you can ignore this email.",
]),
},
"send_org_invite": {
"nb": ("Invitasjon til passordhvelvet hos {{{org_name}}}", "Du er invitert", [
"Du er invitert til passordhvelvet til {{org_name}}.",
"Åpne lenken under for å komme i gang. Der setter du ditt eget hovedpassord — "
"det er bare du som kjenner det, og det kan ikke gjenopprettes av noen andre.",
"[Bli med]({{{url}}})",
"Lenken er gyldig i fem dager. Bruk hele adressen, også den lange tegnrekken til slutt.",
"Var ikke dette ventet? Da kan du se bort fra e-posten.",
]),
"en": ("Invitation to the password vault at {{{org_name}}}", "You have been invited", [
"You have been invited to the password vault of {{org_name}}.",
"Open the link below to get started. There you set your own master password — "
"you are the only one who knows it, and nobody else can recover it.",
"[Join]({{{url}}})",
"The link is valid for five days. Use the whole address, including the long string at the end.",
"Were you not expecting this? Then you can ignore this email.",
]),
},
"invite_accepted": {
"nb": ("{{{email}}} har takket ja til invitasjonen", "Én bekreftelse gjenstår", [
"{{email}} har satt hovedpassordet sitt og takket ja til invitasjonen til {{org_name}}.",
"Siste steg må gjøres av deg: logg inn på [{{url}}]({{{url}}}) og bekreft brukeren "
"under Medlemmer i organisasjonen. Først da får de tilgang til det som deles.",
"Bekreftelsen kan bare gjøres av deg — den krever organisasjonsnøkkelen, som bare "
"finnes i din egen innloggede klient. Det er derfor vi ikke kan gjøre det for deg.",
]),
"en": ("{{{email}}} accepted the invitation", "One confirmation left", [
"{{email}} has set their master password and accepted the invitation to {{org_name}}.",
"The last step is yours: log in at [{{url}}]({{{url}}}) and confirm the user under "
"Members in the organisation. Only then do they get access to what is shared.",
"Only you can confirm — it requires the organisation key, which exists solely in "
"your own logged-in client. That is why we cannot do it for you.",
]),
},
"invite_confirmed": {
"nb": ("Du har fått tilgang til {{{org_name}}}", "Du er med", [
"Du er nå bekreftet som medlem av {{org_name}}.",
"Passordene organisasjonen deler med deg dukker opp i hvelvet ditt: [{{url}}]({{{url}}}).",
f"Bruker du {APP}-appen eller nettleserutvidelsen, kan du måtte synkronisere en gang for å se dem.",
]),
"en": ("You now have access to {{{org_name}}}", "You are in", [
"You have been confirmed as a member of {{org_name}}.",
"The passwords the organisation shares with you appear in your vault: [{{url}}]({{{url}}}).",
f"If you use the {APP} app or browser extension, you may need to sync once to see them.",
]),
},
"new_device_logged_in": {
"nb": ("Ny innlogging fra {{{device_name}}}", "Ny innlogging", [
"Kontoen din ble nettopp brukt til å logge inn fra en ny enhet.",
"- Tidspunkt: {{datetime}}\n- IP-adresse: {{ip}}\n- Enhet: {{device_name}} ({{device_type}})",
"Var det deg, trenger du ikke gjøre noe.",
"Kjenner du deg ikke igjen: bytt hovedpassord med en gang, og logg ut alle enheter "
"under Innstillinger i hvelvet ([{{url}}]({{{url}}})).",
]),
"en": ("New login from {{{device_name}}}", "New login", [
"Your account was just used to log in from a new device.",
"- Time: {{datetime}}\n- IP address: {{ip}}\n- Device: {{device_name}} ({{device_type}})",
"If this was you, there is nothing to do.",
"If you do not recognise it: change your master password right away, and sign out "
"all devices under Settings in the vault ([{{url}}]({{{url}}})).",
]),
},
"incomplete_2fa_login": {
"nb": ("Avbrutt innlogging fra {{{device_name}}}", "Noen kom halvveis inn", [
"Noen logget inn på kontoen din med riktig hovedpassord, men fullførte ikke "
"totrinnsbekreftelsen innen {{time_limit}} minutter.",
"- Tidspunkt: {{datetime}}\n- IP-adresse: {{ip}}\n- Enhet: {{device_name}} ({{device_type}})",
"Var det ikke du eller noen du kjenner: *bytt hovedpassord med en gang*. At passordet "
"var riktig betyr at det er på avveie.",
]),
"en": ("Interrupted login from {{{device_name}}}", "Someone got halfway in", [
"Someone logged into your account with the correct master password, but did not "
"complete two-step verification within {{time_limit}} minutes.",
"- Time: {{datetime}}\n- IP address: {{ip}}\n- Device: {{device_name}} ({{device_type}})",
"If this was not you or someone you know: *change your master password immediately*. "
"The password being correct means it has leaked.",
]),
},
"twofactor_email": {
"nb": ("Innloggingskode: {{{token}}}", "Innloggingskode", [
"Koden din er: *{{token}}*",
"Bruk den for å fullføre innloggingen. Den varer i noen få minutter.",
"Var det ikke du som logget inn, bør du bytte hovedpassord.",
]),
"en": ("Login code: {{{token}}}", "Login code", [
"Your code is: *{{token}}*",
"Use it to finish logging in. It lasts a few minutes.",
"If you were not the one logging in, you should change your master password.",
]),
},
"protected_action": {
"nb": ("Bekreftelseskode: {{{token}}}", "Bekreftelseskode", [
"Koden din er: *{{token}}*",
"Bruk den for å fullføre handlingen du startet i hvelvet.",
]),
"en": ("Verification code: {{{token}}}", "Verification code", [
"Your code is: *{{token}}*",
"Use it to complete the action you started in the vault.",
]),
},
"change_email": {
"nb": ("Bytte av e-postadresse", "Bekreft den nye adressen", [
"Skriv denne koden i hvelvet for å fullføre byttet av e-postadresse: *{{token}}*",
"Var det ikke du som ba om byttet, ta kontakt med administratoren din.",
]),
"en": ("Email address change", "Confirm the new address", [
"Enter this code in the vault to finish changing your email address: *{{token}}*",
"If you did not ask for the change, contact your administrator.",
]),
},
"change_email_existing": {
"nb": ("Noen forsøkte å bruke adressen din", "Adressen er allerede i bruk", [
"En bruker ({{ acting_address }}) forsøkte nettopp å flytte kontoen sin til denne "
"adressen ({{ existing_address }}). Det finnes allerede en konto her.",
"Var det ikke du som forsøkte, ta kontakt med administratoren din.",
]),
"en": ("Someone tried to use your address", "The address is already in use", [
"A user ({{ acting_address }}) just tried to move their account to this address "
"({{ existing_address }}). An account already exists here.",
"If it was not you, contact your administrator.",
]),
},
"change_email_invited": {
"nb": ("Noen forsøkte å bruke adressen din", "Adressen er allerede invitert", [
"En bruker ({{ acting_address }}) forsøkte nettopp å flytte kontoen sin til denne "
"adressen ({{ existing_address }}). Adressen er allerede invitert til et hvelv.",
"Skal adressen brukes til noe annet, må kontoen som hører til den slettes først: "
"[be om sletting]({{url}}/#/recover-delete). Invitasjonen må da sendes på nytt.",
"Var det ikke du som forsøkte, ta kontakt med administratoren din.",
]),
"en": ("Someone tried to use your address", "The address is already invited", [
"A user ({{ acting_address }}) just tried to move their account to this address "
"({{ existing_address }}). The address has already been invited to a vault.",
"To use the address for something else, the account tied to it must be deleted "
"first: [request deletion]({{url}}/#/recover-delete). The invitation then has to be resent.",
"If it was not you, contact your administrator.",
]),
},
"delete_account": {
"nb": ("Slett kontoen din", "Bekreft sletting", [
"Åpne lenken under for å slette kontoen din. *Alt innholdet i hvelvet forsvinner, "
"og det kan ikke hentes tilbake.*",
"[Slett kontoen]({{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}})",
"Var det ikke du som ba om dette, kan du se bort fra e-posten.",
]),
"en": ("Delete your account", "Confirm deletion", [
"Open the link below to delete your account. *Everything in the vault is lost, "
"and it cannot be brought back.*",
"[Delete the account]({{url}}/#/verify-recover-delete?userId={{user_id}}&token={{token}}&email={{email}})",
"If you did not ask for this, you can ignore this email.",
]),
},
"pw_hint_some": {
"nb": ("Hintet til hovedpassordet ditt", "Hintet ditt", [
"Noen ba om hintet til hovedpassordet ditt.",
"Hintet er: *{{hint}}*",
"[Logg inn i hvelvet]({{{url}}})",
"Husker du ikke hovedpassordet, finnes det ingen vei tilbake til innholdet — det er "
"kryptert med nøkkelen bare du har. Eneste utvei er å [slette kontoen]({{url}}/#/recover-delete) "
"og begynne på nytt. Alt innholdet forsvinner da.",
"Var det ikke du som ba om hintet, kan du se bort fra e-posten.",
]),
"en": ("Your master password hint", "Your hint", [
"Someone asked for your master password hint.",
"The hint is: *{{hint}}*",
"[Log in to the vault]({{{url}}})",
"If you cannot remember your master password, there is no way back to the contents — "
"they are encrypted with a key only you hold. The only way out is to "
"[delete the account]({{url}}/#/recover-delete) and start over. Everything in it is lost.",
"If you did not ask for the hint, you can ignore this email.",
]),
},
"pw_hint_none": {
"nb": ("Hintet til hovedpassordet ditt", "Du har ikke lagret noe hint", [
"Noen ba om hintet til hovedpassordet ditt, men kontoen din har ikke noe hint lagret.",
"Husker du ikke hovedpassordet, finnes det ingen vei tilbake til innholdet — det er "
"kryptert med nøkkelen bare du har. Eneste utvei er å [slette kontoen]({{url}}/#/recover-delete) "
"og begynne på nytt. Alt innholdet forsvinner da.",
"Var det ikke du som ba om hintet, kan du se bort fra e-posten.",
]),
"en": ("Your master password hint", "You have no hint saved", [
"Someone asked for your master password hint, but your account has none saved.",
"If you cannot remember your master password, there is no way back to the contents — "
"they are encrypted with a key only you hold. The only way out is to "
"[delete the account]({{url}}/#/recover-delete) and start over. Everything in it is lost.",
"If you did not ask for the hint, you can ignore this email.",
]),
},
"admin_reset_password": {
"nb": ("Hovedpassordet ditt er endret", "Hovedpassordet er endret", [
"Hovedpassordet til {{user_name}} er endret av en administrator i {{org_name}}.",
"Var det ikke avtalt, ta kontakt med administratoren din med en gang.",
]),
"en": ("Your master password has been changed", "Master password changed", [
"The master password for {{user_name}} was changed by an administrator in {{org_name}}.",
"If this was not agreed, contact your administrator immediately.",
]),
},
"send_2fa_removed_from_org": {
"nb": ("Tilgangen din til {{{org_name}}} er fjernet", "Tilgangen er fjernet", [
"Du er tatt ut av {{org_name}} fordi kontoen din ikke har totrinnsbekreftelse.",
"Slå på totrinnsbekreftelse under Innstillinger i hvelvet, så kan du bli med igjen.",
]),
"en": ("Your access to {{{org_name}}} has been removed", "Access removed", [
"You were removed from {{org_name}} because your account has no two-step verification.",
"Turn on two-step verification under Settings in the vault, and you can rejoin.",
]),
},
"send_single_org_removed_from_org": {
"nb": ("Tilgangen din til {{{org_name}}} er fjernet", "Tilgangen er fjernet", [
"Du er tatt ut av {{org_name}} fordi du også er medlem av en annen organisasjon. "
"{{org_name}} tillater ikke det.",
"Meld deg ut av de andre organisasjonene, eller bli med med en annen konto.",
]),
"en": ("Your access to {{{org_name}}} has been removed", "Access removed", [
"You were removed from {{org_name}} because you are also a member of another "
"organisation. {{org_name}} does not allow that.",
"Leave the other organisations, or join with a different account.",
]),
},
"sso_change_email": {
"nb": ("E-postadressen din er endret", "Adressen er endret hos påloggingstjenesten", [
"E-postadressen din ble endret hos påloggingstjenesten. Oppdater den også i hvelvet, "
"under Kontoinnstillinger: [{{url}}]({{{url}}}).",
]),
"en": ("Your email address has changed", "The address changed at your login provider", [
"Your email address was changed at your login provider. Update it in the vault too, "
"under Account settings: [{{url}}]({{{url}}}).",
]),
},
"smtp_test": {
"nb": ("Test av e-postoppsettet", "Testen kom fram", [
"Dette er en testmelding for e-postoppsettet til {{url}}.",
"Kan du lese den, virker oppsettet.",
]),
"en": ("Email configuration test", "The test arrived", [
"This is a test message for the email setup of {{url}}.",
"If you can read it, the setup works.",
]),
},
"send_emergency_access_invite": {
"nb": ("Nødtilgang for {{{grantor_name}}}", "Du er spurt om å være nødkontakt", [
"{{grantor_name}} har spurt om du vil være nødkontakt for hvelvet sitt.",
"[Ta imot]({{{url}}})",
"Vil du ikke, kan du se bort fra e-posten.",
]),
"en": ("Emergency access for {{{grantor_name}}}", "You have been asked to be an emergency contact", [
"{{grantor_name}} has asked you to be an emergency contact for their vault.",
"[Accept]({{{url}}})",
"If you would rather not, you can ignore this email.",
]),
},
"emergency_access_invite_accepted": {
"nb": ("{{{grantee_email}}} har takket ja", "Én bekreftelse gjenstår", [
"{{grantee_email}} har takket ja til å være nødkontakten din.",
"Logg inn i hvelvet ([{{url}}]({{{url}}})) og bekreft dem under Innstillinger. "
"Vil du likevel ikke, kan du fjerne dem samme sted.",
]),
"en": ("{{{grantee_email}}} accepted", "One confirmation left", [
"{{grantee_email}} has accepted being your emergency contact.",
"Log in to the vault ([{{url}}]({{{url}}})) and confirm them under Settings. "
"If you have changed your mind, you can remove them in the same place.",
]),
},
"emergency_access_invite_confirmed": {
"nb": ("Du er nødkontakt for {{{grantor_name}}}", "Du er bekreftet", [
"Du er bekreftet som nødkontakt for *{{grantor_name}}*.",
"Du kan be om nødtilgang fra hvelvet: [{{url}}]({{{url}}}).",
]),
"en": ("You are an emergency contact for {{{grantor_name}}}", "You are confirmed", [
"You have been confirmed as an emergency contact for *{{grantor_name}}*.",
"You can request emergency access from the vault: [{{url}}]({{{url}}}).",
]),
},
"emergency_access_recovery_initiated": {
"nb": ("{{{grantee_name}}} har bedt om nødtilgang", "Noen ber om nødtilgang", [
"{{grantee_name}} har bedt om nødtilgang for å {{atype}} kontoen din.",
"Logg inn i hvelvet ([{{url}}]({{{url}}})) for å godta eller avslå.",
"Gjør du ingenting, blir forespørselen *godtatt automatisk om {{wait_time_days}} dager*.",
]),
"en": ("{{{grantee_name}}} requested emergency access", "Someone is requesting emergency access", [
"{{grantee_name}} has requested emergency access to {{atype}} your account.",
"Log in to the vault ([{{url}}]({{{url}}})) to approve or reject it.",
"If you do nothing, the request is *approved automatically in {{wait_time_days}} days*.",
]),
},
"emergency_access_recovery_reminder": {
"nb": ("{{{grantee_name}}} venter fortsatt på nødtilgang", "Forespørselen ligger fortsatt der", [
"{{grantee_name}} har en ubesvart forespørsel om nødtilgang for å {{atype}} kontoen din.",
"Logg inn i hvelvet ([{{url}}]({{{url}}})) for å godta eller avslå.",
"Gjør du ingenting, blir forespørselen *godtatt automatisk om {{days_left}} dager*.",
]),
"en": ("{{{grantee_name}}} is still waiting for emergency access", "The request is still open", [
"{{grantee_name}} has a pending emergency access request to {{atype}} your account.",
"Log in to the vault ([{{url}}]({{{url}}})) to approve or reject it.",
"If you do nothing, the request is *approved automatically in {{days_left}} days*.",
]),
},
"emergency_access_recovery_approved": {
"nb": ("Nødtilgangen din er godtatt", "Godtatt", [
"{{grantor_name}} har godtatt forespørselen din om nødtilgang.",
"Du kan nå logge inn i hvelvet: [{{url}}]({{{url}}}).",
]),
"en": ("Your emergency access was approved", "Approved", [
"{{grantor_name}} approved your emergency access request.",
"You can now log in to the vault: [{{url}}]({{{url}}}).",
]),
},
"emergency_access_recovery_rejected": {
"nb": ("Nødtilgangen din er avslått", "Avslått", [
"{{grantor_name}} har avslått forespørselen din om nødtilgang.",
]),
"en": ("Your emergency access was rejected", "Rejected", [
"{{grantor_name}} rejected your emergency access request.",
]),
},
"emergency_access_recovery_timed_out": {
"nb": ("{{{grantee_name}}} har fått nødtilgang", "Fristen gikk ut", [
"{{grantee_name}} har fått nødtilgang til å {{atype}} kontoen din, fordi forespørselen "
"ble stående ubesvart til fristen gikk ut.",
"Var ikke dette meningen, kan du trekke tilgangen tilbake i hvelvet: [{{url}}]({{{url}}}).",
]),
"en": ("{{{grantee_name}}} was granted emergency access", "The deadline passed", [
"{{grantee_name}} has been granted emergency access to {{atype}} your account, because "
"the request went unanswered until the deadline passed.",
"If that was not intended, you can revoke the access in the vault: [{{url}}]({{{url}}}).",
]),
},
}
def mot_binær(sti: Path) -> None:
"""Sammenlign med malene serveren faktisk kan sende.
Navnene ligger som tekst i binæren. Kommer det en ny mal i en oppgradering,
ville den gått ut med serverens innebygde engelske tekst - og den sier
Bitwarden. Da skal bygget stoppe her i stedet for at en kunde oppdager det
i innboksen."""
data = sti.read_bytes()
finnes = {m.group(1).decode()
for m in re.finditer(rb"email/([a-z0-9_]+)(?=[A-Z{])", data)}
# Partialene har ingen egen tekst; de innebygde malene bruker dem, våre ikke.
finnes -= {"email_header", "email_footer", "email_footer_text"}
mangler = finnes - set(MALER)
if mangler:
raise SystemExit("maler.py: serveren har maler vi ikke har skrevet: "
+ ", ".join(sorted(mangler)))
def skriv(ut: Path) -> int:
n = 0
for navn, per_språk in MALER.items():
for språk, (emne, tittel, avsnitt) in per_språk.items():
mappe = ut / språk / "email"
mappe.mkdir(parents=True, exist_ok=True)
skille = "<!---------------->"
(mappe / f"{navn}.hbs").write_text(
f"{emne}\n{skille}\n{_tekst(avsnitt)}\n\n{BUNN_TEKST[språk]}\n",
encoding="utf-8")
(mappe / f"{navn}.html.hbs").write_text(
f"{emne}\n{skille}\n" + RAMME.format(
lang=HTML_LANG[språk], tittel=tittel,
kropp=_html(avsnitt), bunn=BUNN[språk]) + "\n",
encoding="utf-8")
n += 2
return n
def sjekk() -> None:
feil = []
for navn, per_språk in MALER.items():
if set(per_språk) != set(SPRÅK):
feil.append(f"{navn}: mangler språk {set(SPRÅK) - set(per_språk)}")
for språk, (emne, tittel, avsnitt) in per_språk.items():
alt = " ".join([emne, tittel, *avsnitt])
# Vaultwardens eget navn skal aldri stå i en e-post fra oss.
if "vaultwarden" in alt.lower():
feil.append(f"{navn}/{språk}: sier Vaultwarden")
# Bitwarden er navnet på appen kunden installerer, og skal bare
# stå der vi faktisk viser til appen eller utvidelsen.
for treff in re.finditer(r"Bitwarden(\S*)", alt):
if treff.group(1) not in ("-appen", "-utvidelsen", ""):
feil.append(f"{navn}/{språk}: uventet bruk av Bitwarden")
if alt.count("{{") != alt.count("}}"):
feil.append(f"{navn}/{språk}: ubalanserte klammer")
if not avsnitt:
feil.append(f"{navn}/{språk}: ingen tekst")
if feil:
raise SystemExit("maler.py: " + "; ".join(feil))
if __name__ == "__main__":
sjekk()
if len(sys.argv) > 2 and sys.argv[1] == "--mot":
mot_binær(Path(sys.argv[2]))
print("maler.py: alle malene serveren kan sende er dekket")
raise SystemExit(0)
if len(sys.argv) > 1:
antall = skriv(Path(sys.argv[1]))
print(f"maler.py: skrev {antall} filer i {len(SPRÅK)} språk")
else:
print(f"maler.py: {len(MALER)} maler, {len(SPRÅK)} språk - alt i orden")