618 lines
28 KiB
Python
618 lines
28 KiB
Python
"""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 · drift i Finland, backup i Tyskland — alt innenfor EU.",
|
|
"en": "Vaultnord · hosted in Finland, backed up in Germany — 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)}
|
|
mangler = _mangler(finnes)
|
|
if mangler:
|
|
raise SystemExit("maler.py: serveren har maler vi ikke har skrevet: "
|
|
+ ", ".join(sorted(mangler)))
|
|
|
|
|
|
# Partialene har ingen egen tekst; de innebygde malene bruker dem, våre ikke.
|
|
PARTIALER = {"email_header", "email_footer", "email_footer_text"}
|
|
|
|
|
|
def _mangler(finnes: set[str]) -> set[str]:
|
|
"""Hvilke av serverens maler vi ikke har skrevet.
|
|
|
|
Navnene plukkes ut av en binærfil, så treffene inneholder avkuttede biter -
|
|
«sm» der det står smtp_test, «we» der det står welcome. Et navn som bare er
|
|
starten på et annet navn er et slikt fragment, ikke en mal.
|
|
|
|
Prisen er at en oppstrøms mal som heter nøyaktig starten på en vi allerede
|
|
har, ville sluppet gjennom. Det har ikke skjedd, og alternativet - å la
|
|
fragmentene stoppe hvert eneste bygg - er verre."""
|
|
kjente = finnes | set(MALER)
|
|
ekte = {n for n in finnes - PARTIALER
|
|
if not any(k != n and k.startswith(n) for k in kjente)}
|
|
return ekte - set(MALER)
|
|
|
|
|
|
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")
|