diff --git a/apps/web/e2e/language.spec.ts b/apps/web/e2e/language.spec.ts
new file mode 100644
index 0000000..1f7d640
--- /dev/null
+++ b/apps/web/e2e/language.spec.ts
@@ -0,0 +1,69 @@
+import AxeBuilder from "@axe-core/playwright";
+import { type Page, expect, test } from "@playwright/test";
+
+// The language of the account pages: ?lang= (the launcher passes its own), then the cookie, then
+// Accept-Language, English otherwise. The mock plays the cloud.
+
+const shots = process.env.SHOTS;
+async function shoot(page: Page, name: string) {
+ if (shots) await page.screenshot({ path: `${shots}/${name}-${page.viewportSize()!.width}.png`, fullPage: true });
+}
+
+async function noViolations(page: Page) {
+ // The flow loads its session first; the audit is of the page that stands afterwards.
+ await expect(page.getByRole("heading", { level: 1 }).first()).toBeVisible();
+ const { violations } = await new AxeBuilder({ page }).disableRules(["color-contrast"]).analyze();
+ expect(violations.map((v) => `${v.id}: ${v.help}`)).toEqual([]);
+}
+
+test("an explicit ?lang= wins, is remembered, and sets the document language", async ({ page }) => {
+ await page.goto("/sign-in?mock=signedout&lang=de");
+ await expect(page.locator("html")).toHaveAttribute("lang", "de");
+ await expect(page).toHaveTitle(/Anmelden/);
+ await shoot(page, "sign-in-de");
+ await noViolations(page);
+ // The next page, opened without the parameter, keeps German through the cookie.
+ await page.goto("/create?mock=signedout");
+ await expect(page.locator("html")).toHaveAttribute("lang", "de");
+ await expect(page).toHaveTitle(/Erstelle dein Konto/);
+});
+
+test("Turkish comes out whole, with its own letters", async ({ page }) => {
+ await page.goto("/sign-in?mock=signedout&lang=tr");
+ await expect(page.locator("html")).toHaveAttribute("lang", "tr");
+ await expect(page).toHaveTitle(/Giriş yap/);
+ await shoot(page, "sign-in-tr");
+ await noViolations(page);
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
+ expect(overflow).toBe(0);
+});
+
+test("without a parameter or cookie the browser's language decides, and English is the fallback", async ({ browser }) => {
+ const french = await browser.newContext({ locale: "fr-FR", extraHTTPHeaders: { "accept-language": "fr-FR,fr;q=0.9,en;q=0.5" } });
+ const page = await french.newPage();
+ await page.goto("/sign-in?mock=signedout");
+ await expect(page.locator("html")).toHaveAttribute("lang", "fr");
+ await french.close();
+
+ const japanese = await browser.newContext({ extraHTTPHeaders: { "accept-language": "ja" } });
+ const other = await japanese.newPage();
+ await other.goto("/sign-in?mock=signedout");
+ await expect(other.locator("html")).toHaveAttribute("lang", "en");
+ await expect(other).toHaveTitle(/Sign in/);
+ await japanese.close();
+});
+
+test("an unknown ?lang= is ignored", async ({ page }) => {
+ await page.goto("/sign-in?mock=signedout&lang=xx");
+ await expect(page.locator("html")).toHaveAttribute("lang", "en");
+});
+
+test("the account area speaks the language too, and the long words fit", async ({ page }) => {
+ await page.goto("/account?lang=de");
+ await expect(page.locator("html")).toHaveAttribute("lang", "de");
+ await expect(page.getByRole("heading", { level: 1 }).first()).toBeVisible();
+ await shoot(page, "account-de");
+ await noViolations(page);
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
+ expect(overflow).toBe(0);
+});
diff --git a/apps/web/messages/de/account.json b/apps/web/messages/de/account.json
new file mode 100644
index 0000000..c358d8e
--- /dev/null
+++ b/apps/web/messages/de/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "nie",
+ "on": "An",
+ "off": "Aus",
+ "cancel": "Abbrechen",
+ "working": "Wird ausgeführt…",
+ "typeToConfirm": "Gib zum Bestätigen {word} ein",
+ "audience": {
+ "everyone": "Alle",
+ "friends": "Freunde",
+ "nobody": "Nur ich"
+ },
+ "stepUp": {
+ "title": "Bestätige, dass du es bist",
+ "needed": "Für diese Änderung ist eine aktuelle Anmeldung nötig. Sie gilt 15 Minuten.",
+ "sent": "Wir haben einen 6-stelligen Code an {hint} gesendet.",
+ "passkey": "Passkey verwenden",
+ "sending": "Wird gesendet …",
+ "sendCode": "Code per E-Mail senden"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, Startseite",
+ "signOut": "Abmelden",
+ "nav": "Konto",
+ "nav_overview": "Übersicht",
+ "nav_profile": "Profil",
+ "nav_friends": "Freunde",
+ "nav_trophies": "Trophäen",
+ "nav_security": "Sicherheit",
+ "nav_notifications": "Benachrichtigungen",
+ "nav_privacy": "Datenschutz",
+ "nav_connections": "Verknüpfungen",
+ "nav_data": "Deine Daten",
+ "unreachable": "AnyPlay ist nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.",
+ "loading": "Lädt"
+ },
+ "level": {
+ "top": "Höchstes Level erreicht",
+ "toNext": "Noch {count, plural, one {# Punkt} other {# Punkte}} bis Level {level}",
+ "level": "Level {level}",
+ "points": "{count, plural, one {# Punkt} other {# Punkte}}",
+ "progress": "Level-Fortschritt",
+ "progressTo": "Fortschritt zu Level {level}"
+ },
+ "meta": {
+ "title": "Dein Konto",
+ "template": "%s · Dein Konto · AnyPlay"
+ },
+ "state": {
+ "loading": "Wird geladen",
+ "tryAgain": "Erneut versuchen"
+ },
+ "overview": {
+ "label": "Dein Konto",
+ "memberSince": "Mitglied seit {date}",
+ "appearingOffline": "Du erscheinst offline",
+ "deletion": "Dein Konto wird am {date} gelöscht. Mein Konto behalten",
+ "passkeys": "{count, plural, one {# Passkey} other {# Passkeys}}",
+ "noPasskey": "Noch kein Passkey",
+ "launchersBrowsers": "{launchers, plural, one {# Launcher} other {# Launcher}} · {browsers, plural, one {# Browser} other {# Browser}}",
+ "trophiesAudience": "Trophäen: {audience}",
+ "pictureAudience": "Profilbild: {audience}",
+ "audience": {
+ "everyone": "Alle",
+ "friends": "Freunde",
+ "nobody": "Nur du"
+ },
+ "audienceInline": {
+ "everyone": "alle",
+ "friends": "Freunde",
+ "nobody": "nur du"
+ },
+ "linked": "{count} verknüpft",
+ "nothingLinked": "Nichts verknüpft",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Exportieren oder löschen",
+ "dataDetail": "Alles, was wir über dich speichern",
+ "launcher": {
+ "title": "Hol dir den Launcher",
+ "description": "Melde dich mit diesem Konto im Launcher an, und deine Freunde, Partys und Trophäen sind da."
+ }
+ },
+ "profile": {
+ "lead": "Das sehen deine Freunde, wenn sie dein Profil öffnen. Wer welchen Teil sieht, bestimmst du auf der Seite Privatsphäre.",
+ "avatar": {
+ "title": "Bild",
+ "description": "PNG oder JPEG, bis zu 1 MB. Es wird rund angezeigt, halte das Gesicht also in der Mitte.",
+ "uploading": "Wird hochgeladen…",
+ "change": "Bild ändern",
+ "upload": "Bild hochladen",
+ "remove": "Entfernen"
+ },
+ "about": {
+ "title": "Über mich",
+ "description": "Ein bis zwei Zeilen über dich: was du spielst, wann du da bist.",
+ "placeholder": "Koop, wenn es geht, solo, wenn nicht.",
+ "saved": "Gespeichert",
+ "saving": "Wird gespeichert…",
+ "save": "Speichern"
+ },
+ "favourites": {
+ "title": "Lieblingsspiele",
+ "description": "Bis zu {max}, per Titel-ID. Diese Seite zeigt die ID. Der Launcher zeigt den Namen jedes Spiels aus deiner eigenen Kopie.",
+ "empty": "Noch keine Lieblingsspiele. Füge unten eine Titel-ID hinzu.",
+ "listLabel": "Deine Lieblingsspiele",
+ "remove": "{id} entfernen",
+ "fieldLabel": "Titel-ID",
+ "full": "Du hast {max} Spiele. Entferne eins, um ein weiteres hinzuzufügen.",
+ "add": "Spiel hinzufügen",
+ "saved": "Gespeichert",
+ "saving": "Wird gespeichert…",
+ "save": "Speichern",
+ "badId": "Verwende nur Buchstaben, Zahlen, Bindestriche und Unterstriche.",
+ "duplicate": "{id} steht schon auf der Liste."
+ }
+ },
+ "security": {
+ "lead": "Wie du dich anmeldest, und jeder Launcher und Browser, der bei deinem Konto angemeldet ist.",
+ "email": {
+ "title": "E-Mail",
+ "description": "Dorthin gehen deine Anmeldecodes. Zum Ändern brauchst du einen Code, der an die neue Adresse geschickt wird.",
+ "change": "Ändern",
+ "invalid": "Gib die neue Adresse ein, zum Beispiel name@beispiel.de.",
+ "done": "Deine Anmeldeadresse ist jetzt {email}.",
+ "newAddress": "Neue Adresse",
+ "sending": "Wird gesendet…",
+ "send": "Code senden",
+ "cancel": "Abbrechen",
+ "sentTo": "Wir haben einen 6-stelligen Code an {email} gesendet."
+ },
+ "passkeys": {
+ "title": "Passkeys",
+ "description": "Melde dich mit Fingerabdruck, Gesicht oder Geräte-PIN statt mit einem Code an. Passkeys lassen sich nicht phishen.",
+ "add": "Passkey hinzufügen",
+ "waiting": "Warte auf dein Gerät…",
+ "detail": "Hinzugefügt {added} · Zuletzt verwendet: {used}",
+ "remove": "Entfernen",
+ "empty": "Noch kein Passkey. Füge einen hinzu, und die Anmeldung geht mit einer Berührung statt mit einem E-Mail-Code.",
+ "full": "Du hast die meisten Passkeys, die ein Konto behalten kann ({max}). Entferne einen, um einen weiteren hinzuzufügen.",
+ "confirmTitle": "Diesen Passkey entfernen?",
+ "confirmDescription": "Dieses Gerät kann dich damit nicht mehr anmelden. Mit einem Code per E-Mail kannst du dich immer anmelden.",
+ "confirmLabel": "Passkey entfernen",
+ "defaultName": "Passkey auf {system}",
+ "thisDevice": "diesem Gerät"
+ },
+ "devices": {
+ "title": "Launcher",
+ "description": "Jeder PC, auf dem der Launcher bei deinem Konto angemeldet ist.",
+ "detail": "{platform} · Zuletzt verwendet: {used}",
+ "signOut": "Abmelden",
+ "empty": "Noch kein Launcher angemeldet.",
+ "confirmTitle": "Diesen Launcher abmelden?",
+ "confirmDescription": "Er geht offline, bis sich jemand auf diesem PC wieder anmeldet.",
+ "confirmLabel": "Launcher abmelden"
+ },
+ "browsers": {
+ "title": "Browser",
+ "description": "Jeder Browser, der auf dieser Kontoseite angemeldet ist. Wir speichern über sie nicht mehr als den Zeitpunkt der Nutzung.",
+ "thisBrowser": "Dieser Browser",
+ "here": "Du bist hier",
+ "other": "Anderer Browser",
+ "detail": "Angemeldet {created} · Zuletzt verwendet: {used}",
+ "signOut": "Abmelden",
+ "confirmThisTitle": "Von diesem Browser abmelden?",
+ "confirmOtherTitle": "Diesen anderen Browser abmelden?",
+ "confirmThisDescription": "Du kommst zurück zum Start und meldest dich beim nächsten Mal neu an.",
+ "confirmOtherDescription": "Er muss sich neu anmelden, um dein Konto zu öffnen.",
+ "confirmLabel": "Abmelden"
+ },
+ "everywhere": {
+ "title": "Überall abmelden",
+ "description": "Jeder Launcher und jeder Browser, auch dieser. Nutze das, wenn ein Gerät verloren ging oder sich jemand anderes angemeldet haben könnte.",
+ "button": "Überall abmelden",
+ "confirmTitle": "Überall abmelden?",
+ "confirmDescription": "Jeder Launcher und Browser wird sofort abgemeldet, auch dieser.",
+ "confirmLabel": "Überall abmelden"
+ }
+ },
+ "privacy": {
+ "lead": "Wer dein Profil und deine Aktivität sieht. Dein Name ist immer sichtbar, damit Freunde dich finden.",
+ "profile": {
+ "title": "Dein Profil",
+ "description": "Änderungen gelten sofort, auch im Launcher."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Trophäen",
+ "detail": "Deine Trophäenzahlen, Punkte und dein Level"
+ },
+ "playtime": {
+ "title": "Spielzeit",
+ "detail": "Wie lange du gespielt hast, pro Spiel"
+ },
+ "about": {
+ "title": "Über mich",
+ "detail": "Die Zeile in deinem Profil"
+ },
+ "favourites": {
+ "title": "Lieblingsspiele",
+ "detail": "Bis zu drei Spiele, die du anheftest"
+ },
+ "avatar": {
+ "title": "Bild",
+ "detail": "Dein Profilbild"
+ }
+ },
+ "whoSees": "Wer sieht: {part}",
+ "activity": {
+ "title": "Deine Aktivität",
+ "description": "Was du auf AnyPlay tust, so wie deine Freunde es in ihrem Feed sehen.",
+ "row": "Aktivität",
+ "detail": "Freunde sehen sie in ihrem Feed. Fremde sehen keinen Feed. Wähle „{nobody}“, um zu verbergen, was dort steht, und neue Einträge zu stoppen."
+ },
+ "appearOffline": {
+ "title": "Online-Status",
+ "description": "Gilt für jeden Launcher und Browser, der bei deinem Konto angemeldet ist.",
+ "label": "Offline erscheinen",
+ "detail": "Freunde sehen dich als offline. Du kannst trotzdem spielen, chatten und beitreten."
+ },
+ "publicProfile": {
+ "title": "Öffentliches Profil",
+ "description": "Aus, bis du es einschaltest. Gilt für jeden Launcher und Browser, der bei deinem Konto angemeldet ist.",
+ "label": "Öffentliches Profil anzeigen",
+ "detail": "Jeder mit dem Link sieht dein Level, deine Trophäen und gespielte Spiele. Ob du online bist, zeigt es nie.",
+ "page": "Deine Seite: {url}"
+ },
+ "playedWith": {
+ "title": "Zuletzt gespielt mit",
+ "description": "Wenn du es ausschaltest, kommt niemand Neues dazu. Die Liste bleibt, bis du sie leerst.",
+ "label": "Mitspieler merken",
+ "detail": "Merkt sich die Leute, mit denen du eine Party oder Spielsitzung geteilt hast, 30 Tage lang, bis zu 50. Nur du siehst die Liste.",
+ "clear": "Liste leeren",
+ "cleared": "Die Liste ist leer.",
+ "confirmTitle": "Liste leeren?",
+ "confirmDescription": "Alle auf deiner Liste der Mitspieler werden entfernt. Die Listen anderer ändern sich nicht, und du kannst das nicht rückgängig machen."
+ }
+ },
+ "connections": {
+ "lead": "Verknüpfe die Konten, mit denen du schon spielst, damit Freunde dich finden und sehen, wo du sonst noch bist.",
+ "result": {
+ "linked": "{name} ist mit deinem Konto verknüpft.",
+ "denied": "{name} wurde nicht verknüpft: Die Anfrage wurde dort abgelehnt.",
+ "expired": "Das Verknüpfen von {name} hat zu lange gedauert. Starte neu.",
+ "fresh": "Zum Verknüpfen von {name} brauchst du eine frische Anmeldung. Starte neu und bestätige, dass du es bist.",
+ "taken": "Dieses {name}-Konto ist schon mit einem anderen AnyPlay-Konto verknüpft.",
+ "exists": "Du hast schon ein {name}-Konto verknüpft. Hebe zuerst diese Verknüpfung auf.",
+ "failed": "Das Verknüpfen von {name} ist fehlgeschlagen. Versuche es in einer Minute erneut."
+ },
+ "whoSees": "Wer dein {name}-Konto sieht",
+ "linkedAgo": "Verknüpft {ago}",
+ "unlink": "Verknüpfung aufheben",
+ "link": "{name} verknüpfen",
+ "opening": "Wird geöffnet…",
+ "soon": "Bald",
+ "notLinked": "Nicht verknüpft",
+ "arrivesSoon": "Verknüpfung kommt bald",
+ "confirmTitle": "Verknüpfung mit {name} aufheben?",
+ "confirmDescription": "Dein {name}-Konto erscheint nicht mehr in deinem Profil. Du kannst es jederzeit wieder verknüpfen.",
+ "confirmLabel": "Verknüpfung mit {name} aufheben"
+ },
+ "data": {
+ "lead": "Alles, was AnyPlay über dich speichert, gehört dir: Hol dir eine Kopie oder lösche das Konto.",
+ "export": {
+ "title": "Deine Daten herunterladen",
+ "description": "Dein Konto, dein Profil, Freunde, Trophäen, Spielzeit und verknüpfte Konten als eine JSON-Datei.",
+ "preparing": "Wird vorbereitet…",
+ "button": "Herunterladen"
+ },
+ "scheduled": {
+ "title": "Löschung geplant",
+ "description": "Dein Konto und alles darin wird am {date} gelöscht. Bis dahin kannst du es behalten.",
+ "keep": "Mein Konto behalten"
+ },
+ "delete": {
+ "title": "Dein Konto löschen",
+ "description": "Dein Konto wird nach 30 Tagen gelöscht; bis dahin kannst du es dir anders überlegen. Danach ist es weg, samt Freunden, Trophäen und Spielzeit.",
+ "button": "Konto löschen"
+ },
+ "confirm": {
+ "title": "Dein Konto löschen?",
+ "description": "Es wird in 30 Tagen gelöscht. Jeder Launcher und Browser wird jetzt abgemeldet."
+ }
+ },
+ "compare": {
+ "title": "Trophäen vergleichen",
+ "description": "Deine Trophäen neben denen von {handle}.",
+ "descriptionShared": "Du und {handle}, in einem Spiel, das ihr beide habt.",
+ "signInDescription": "Sieh, wie deine Trophäen im Vergleich zu denen von {handle} aussehen.",
+ "signIn": "Anmelden, um Trophäen zu vergleichen",
+ "hidden": "Die Trophäen von {handle} sind nicht zum Vergleichen verfügbar.",
+ "loading": "Wird geladen",
+ "loadingComparison": "Der Vergleich wird geladen",
+ "empty": "Du und {handle} habt noch kein gemeinsames Spiel. Spiel ein Spiel, das {handle} gespielt hat, dann erscheint es hier.",
+ "game": "Spiel",
+ "gameNamed": "Spiel {title}",
+ "verdictNone": "Keiner von euch hat in diesem Spiel eine Trophäe erhalten.",
+ "verdictLevel": "In diesem Spiel steht ihr punktgleich.",
+ "verdictAhead": "Du liegst in diesem Spiel {count, plural, one {# Punkt} other {# Punkte}} vorn.",
+ "verdictBehind": "{handle} liegt in diesem Spiel {count, plural, one {# Punkt} other {# Punkte}} vorn.",
+ "caption": "Trophäen in {titleId}: du neben {handle}",
+ "stat": "Kategorie",
+ "you": "Du",
+ "ahead": "(vorn)",
+ "rowTrophies": "Trophäen",
+ "rowPoints": "Punkte in diesem Spiel",
+ "rowLevel": "Level insgesamt",
+ "grade": {
+ "platinum": "Platin",
+ "gold": "Gold",
+ "silver": "Silber",
+ "bronze": "Bronze"
+ },
+ "both": "Von beiden erhalten",
+ "onlyYou": "Nur du",
+ "onlyThem": "Nur {handle}"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, Startseite",
+ "yourAccount": "Dein Konto",
+ "signIn": "Anmelden",
+ "loading": "Wird geladen",
+ "missingTitle": "Kein Spieler namens {handle}",
+ "missingBody": "Prüfe die Schreibweise des Namens oder bitte den Spieler, ihn noch einmal zu schicken.",
+ "goHome": "Zu AnyPlay",
+ "player": "Spieler",
+ "trophies": "Trophäen",
+ "trophiesEarned": "{count, number} erhalten.",
+ "noTrophies": "{handle} hat noch keine Trophäe erhalten.",
+ "favourites": "Lieblingsspiele",
+ "favouritesNote": "Angezeigt wird die Titel-ID.",
+ "game": "Spiel",
+ "connections": "Verknüpfungen",
+ "playtime": "Gespielte Spiele",
+ "playtimeNote": "{count, plural, one {# Spiel} other {# Spiele}}, insgesamt {total}.",
+ "nothing": "Von {handle} gibt es noch nichts weiter zu zeigen.",
+ "openProfile": "Profil öffnen",
+ "openProfileOn": "auf {brand} (öffnet in einem neuen Tab)",
+ "signInToAdd": "Anmelden, um {handle} hinzuzufügen",
+ "editProfile": "Profil bearbeiten",
+ "blocked": "Blockiert",
+ "friends": "Freunde",
+ "requestSent": "Anfrage gesendet",
+ "accepting": "Wird angenommen…",
+ "accept": "Anfrage annehmen",
+ "sending": "Wird gesendet…",
+ "addFriend": "Freund hinzufügen",
+ "addInAnyPlay": "Freund in AnyPlay hinzufügen",
+ "minutes": "{n} Min.",
+ "hours": "{n} Std."
+ },
+ "friends": {
+ "title": "Freunde",
+ "lead": "Füge die Leute hinzu, mit denen du spielst, beantworte Anfragen und entscheide, wer draußen bleibt.",
+ "add": {
+ "title": "Freund hinzufügen",
+ "description": "Gib den Namen eines Spielers ein, um ihm eine Anfrage zu senden.",
+ "handle": "Name",
+ "hint": "Sein Name, mit oder ohne @.",
+ "send": "Anfrage senden",
+ "sending": "Wird gesendet…",
+ "enterHandle": "Gib den Namen des Spielers ein, den du hinzufügen möchtest.",
+ "nowFriends": "Du und @{handle} seid jetzt Freunde.",
+ "sent": "Anfrage an @{handle} gesendet. Ihr seid Freunde, sobald die Anfrage angenommen wird."
+ },
+ "requests": {
+ "title": "Anfragen",
+ "description": "Spieler, die mit dir befreundet sein möchten, und die Anfragen, die du gesendet hast.",
+ "empty": "Keine offenen Anfragen. Wenn dich jemand hinzufügt, erscheint die Anfrage hier.",
+ "received": "Erhalten",
+ "sent": "Gesendet",
+ "receivedAgo": "Erhalten: {when}",
+ "sentAgo": "Gesendet: {when}",
+ "decline": "Ablehnen",
+ "accept": "Annehmen",
+ "withdraw": "Zurückziehen"
+ },
+ "list": {
+ "title": "Deine Freunde",
+ "description": "Sieh, wer online ist und was gespielt wird.",
+ "empty": "Noch keine Freunde. Füge oben jemanden über den Namen hinzu; sobald die Anfrage angenommen wird, erscheint er hier.",
+ "online": "Online",
+ "offline": "Offline",
+ "playing": "Spielt {title}",
+ "block": "Blockieren",
+ "remove": "Entfernen"
+ },
+ "blocked": {
+ "title": "Blockiert",
+ "description": "Blockierte Spieler werden aus deinen Freunden und Anfragen entfernt.",
+ "empty": "Du hast niemanden blockiert. Blockiere einen Spieler in deiner Freundesliste, wenn du es brauchst.",
+ "unblock": "Blockierung aufheben"
+ },
+ "confirm": {
+ "blockTitle": "@{handle} blockieren?",
+ "removeTitle": "@{handle} als Freund entfernen?",
+ "blockText": "Ihr seid dann keine Freunde mehr, und offene Anfragen werden gelöscht. Du kannst die Blockierung jederzeit aufheben.",
+ "removeText": "Ihr seht nicht mehr, wann der andere online ist. Später kannst du eine neue Anfrage senden.",
+ "blockLabel": "Spieler blockieren",
+ "removeLabel": "Freund entfernen"
+ }
+ },
+ "trophies": {
+ "title": "Trophäen",
+ "lead": "Was du in deinen Spielen verdient hast. AnyPlay speichert nur Anzahlen pro Spiel und Stufe, nie Trophäennamen oder Grafiken.",
+ "totals": {
+ "title": "Gesamt",
+ "description": "{total} verdient in {games, plural, one {# Spiel} other {# Spielen}}.",
+ "empty": "Noch keine Trophäen. Spiele ein Spiel über AnyPlay, dann werden deine Trophäen hier gezählt."
+ },
+ "byGame": {
+ "title": "Nach Spiel",
+ "description": "Die Spiele sind nach Titel-ID sortiert, die neuesten zuerst.",
+ "detail": "{earned, plural, one {# Trophäe} other {# Trophäen}} · zuletzt verdient: {when}"
+ },
+ "grades": {
+ "platinum": "Platin",
+ "gold": "Gold",
+ "silver": "Silber",
+ "bronze": "Bronze"
+ }
+ },
+ "notifications": {
+ "title": "Benachrichtigungen",
+ "lead": "Erhalte auf diesem Gerät eine Benachrichtigung, wenn dir ein Freund eine Anfrage sendet, dich in eine Party einlädt oder dir schreibt.",
+ "device": {
+ "title": "Dieses Gerät",
+ "description": "Jeder Browser und jedes Handy wird einzeln eingerichtet. Schalte das dort ein, wo du benachrichtigt werden möchtest.",
+ "notEnabled": "Diese Cloud sendet keine Benachrichtigungen.",
+ "checking": "Dieses Gerät wird geprüft…",
+ "unsupported": "Dieser Browser kann keine Benachrichtigungen empfangen. Nutze ein aktuelles Chrome, Edge, Firefox oder Safari.",
+ "needsInstall": "Auf dem iPhone und iPad braucht AnyPlay für Benachrichtigungen einen Platz auf dem Home-Bildschirm. Tippe auf Teilen, wähle „Zum Home-Bildschirm“ und öffne AnyPlay von dort.",
+ "blocked": "Benachrichtigungen sind für diese Seite blockiert. Erlaube sie in den Website-Einstellungen deines Browsers und komm dann zurück.",
+ "switch": "Dieses Gerät benachrichtigen",
+ "others": "{count, plural, one {Ein weiteres Gerät erhält ebenfalls Benachrichtigungen. Schalte sie auf diesem Gerät aus.} other {# weitere Geräte erhalten ebenfalls Benachrichtigungen. Schalte sie auf diesen Geräten aus.}}"
+ },
+ "kinds": {
+ "title": "Worüber benachrichtigt werden soll",
+ "description": "Nur für dieses Gerät.",
+ "friend_request": {
+ "label": "Freundschaftsanfragen",
+ "detail": "Jemand möchte mit dir befreundet sein."
+ },
+ "party_invite": {
+ "label": "Party-Einladungen",
+ "detail": "Ein Freund lädt dich in eine Party ein."
+ },
+ "message": {
+ "label": "Nachrichten",
+ "detail": "Ein Freund schreibt dir. Die Nachricht selbst steht nicht in der Benachrichtigung."
+ }
+ },
+ "errors": {
+ "timeout": "Der Benachrichtigungsdienst deines Browsers hat nicht geantwortet. Prüfe deine Verbindung und versuche es erneut.",
+ "noKey": "Diese Cloud hat keinen Schlüssel zum Senden von Benachrichtigungen.",
+ "lostSubscription": "Dieser Browser hat sein Abonnement verloren. Schalte Benachrichtigungen aus und wieder ein.",
+ "notAllowed": "Dieser Browser hat Benachrichtigungen nicht erlaubt. In einem privaten Fenster funktionieren sie nicht; prüfe sonst die Website-Einstellungen."
+ }
+ }
+}
diff --git a/apps/web/messages/de/auth.json b/apps/web/messages/de/auth.json
new file mode 100644
index 0000000..9fe0d1f
--- /dev/null
+++ b/apps/web/messages/de/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Anmelden",
+ "description": "Melde dich bei AnyPlay mit einem Passkey oder einem Code per E-Mail an."
+ },
+ "create": {
+ "title": "Erstelle dein Konto",
+ "description": "Erstelle dein AnyPlay-Konto: eine E-Mail-Adresse und ein Name genügen."
+ },
+ "link": {
+ "title": "Öffne dein Konto",
+ "description": "Öffne dein AnyPlay-Konto aus dem Launcher in diesem Browser."
+ },
+ "join": {
+ "title": "Einladung",
+ "description": "Eine Einladung, gemeinsam auf AnyPlay zu spielen."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, Startseite",
+ "back": "Zurück zum Anfang",
+ "promise": "Spiele deine Spiele gemeinsam.",
+ "toastOnline": "mira ist online",
+ "toastParty": "kit ist deiner Party beigetreten",
+ "toastTrophy": "Trophäe erhalten · Into the Ember",
+ "help": "Brauchst du Hilfe?",
+ "helpLink": "Frag auf Discord",
+ "legalNav": "Rechtliches",
+ "legalNotice": "Impressum",
+ "privacyNotice": "Datenschutzhinweis",
+ "termsOfUse": "Nutzungsbedingungen"
+ },
+ "step": {
+ "loading": "Wird geladen",
+ "unreachable": "AnyPlay ist nicht erreichbar",
+ "tryAgain": "Erneut versuchen",
+ "newTab": "(öffnet in einem neuen Tab)",
+ "backToStart": "Zurück zum Anfang",
+ "signIn": "Anmelden",
+ "createAccount": "Konto erstellen",
+ "getApp": "AnyPlay holen",
+ "continue": "Weiter"
+ },
+ "fields": {
+ "code": "Code"
+ },
+ "flow": {
+ "authorize": "AnyPlay auf {device} wartet auf diese Anmeldung."
+ },
+ "email": {
+ "create": {
+ "title": "Erstelle dein Konto",
+ "lead": "Beginne mit deiner E-Mail-Adresse. Wir senden dir einen Code zur Bestätigung."
+ },
+ "signIn": {
+ "title": "Anmelden",
+ "lead": "Nutze deinen Passkey oder lass dir einen Code per E-Mail schicken."
+ },
+ "passkey": "Mit Passkey anmelden",
+ "or": "oder",
+ "label": "E-Mail-Adresse",
+ "placeholder": "name@example.com",
+ "invalid": "Gib deine E-Mail-Adresse ein, zum Beispiel name@example.com.",
+ "sending": "Wird gesendet…",
+ "send": "Code senden",
+ "haveAccount": "Du hast schon ein Konto? Anmelden",
+ "newHere": "Neu bei AnyPlay? Konto erstellen"
+ },
+ "code": {
+ "title": "Prüfe deine E-Mails",
+ "lead": "Wir haben einen 6-stelligen Code an {email} gesendet. Er gilt 10 Minuten.",
+ "incomplete": "Gib die 6 Ziffern aus der E-Mail ein.",
+ "checking": "Wird geprüft…",
+ "resent": "Ein neuer Code ist unterwegs.",
+ "noMail": "Keine E-Mail da?",
+ "resendIn": "Neuen Code in {seconds} s senden",
+ "resend": "Neuen Code senden"
+ },
+ "details": {
+ "title": "Wähle einen Namen",
+ "lead": "Deine Adresse {email} ist bestätigt. Andere Spieler sehen deinen Namen.",
+ "handle": "Name",
+ "handleHint": "Verwende 3 bis 16 Buchstaben, Ziffern, Bindestriche oder Unterstriche. Beginne mit einem Buchstaben.",
+ "ageHint": "Das heißt: 16 oder älter.",
+ "age": "Ich bin alt genug, um den Bedingungen selbst zuzustimmen.",
+ "terms": "Ich akzeptiere die Nutzungsbedingungen (Version {version}) und habe den Datenschutzhinweis gelesen.",
+ "creating": "Wird erstellt…",
+ "submit": "Konto erstellen"
+ },
+ "terms": {
+ "title": "Bedingungen ansehen",
+ "lead": "Die Bedingungen haben sich geändert. Lies die Nutzungsbedingungen und akzeptiere Version {version}, um fortzufahren.",
+ "accept": "Ich akzeptiere die Nutzungsbedingungen (Version {version}).",
+ "required": "Akzeptiere die Bedingungen, um fortzufahren.",
+ "submit": "Akzeptieren und fortfahren"
+ },
+ "banned": {
+ "title": "Dieses Konto ist gesperrt",
+ "leadUntil": "Die Sperre endet am {date}.",
+ "leadForever": "Die Sperre endet nicht von selbst.",
+ "rule": "Regel",
+ "reason": "Grund",
+ "appeal": "Stellungnahme lesen und Einspruch einlegen"
+ },
+ "handoff": {
+ "title": "Angemeldet",
+ "lead": "Du wirst zu AnyPlay zurückgebracht. Passiert nichts, nutze die Schaltfläche.",
+ "open": "AnyPlay öffnen",
+ "note": "Du kannst diesen Tab schließen, sobald AnyPlay dich als angemeldet zeigt."
+ },
+ "done": {
+ "title": "Du bist angemeldet",
+ "inviteLead": "Du wirst zur Einladung zurückgebracht.",
+ "welcome": "Willkommen, {handle}. Deine Freunde, Partys und Trophäen warten im Launcher.",
+ "account": "Zu deinem Konto"
+ },
+ "join": {
+ "expiredTitle": "Diese Einladung ist abgelaufen",
+ "expiredLead": "Der Link ist vielleicht abgelaufen, oder die Person spielt nicht mehr. Bitte um einen neuen.",
+ "title": "{handle} hat dich eingeladen",
+ "leadSignedIn": "Öffne AnyPlay auf deinem PC, um die Einladung zu sehen. Dort entscheidest du, ob du beitrittst.",
+ "leadSignedOut": "Melde dich zuerst an oder erstelle ein Konto. Danach kommst du hierher zurück, um die Einladung zu öffnen.",
+ "party": "Party",
+ "game": "Spiel",
+ "partyName": "Eine AnyPlay-Party",
+ "gameFallback": "Ein Spiel",
+ "joined": "Beigetreten",
+ "players": "{members} von {max, plural, one {# Spieler} other {# Spielern}}",
+ "open": "In AnyPlay öffnen",
+ "noApp": "Noch kein AnyPlay auf diesem PC? Hol es dir zuerst und öffne diese Seite dann erneut."
+ },
+ "link": {
+ "expiredTitle": "Dieser Link ist abgelaufen",
+ "expiredLead": "Öffne deine Kontoseite noch einmal aus AnyPlay.",
+ "close": "Du kannst diesen Tab schließen.",
+ "title": "Öffne dein Konto",
+ "lead": "AnyPlay auf deinem PC möchte das Konto von {handle} in diesem Browser öffnen.",
+ "open": "Konto öffnen",
+ "opening": "Wird geöffnet…",
+ "notYou": "Das warst nicht du? Schließe den Tab."
+ }
+}
diff --git a/apps/web/messages/de/common.json b/apps/web/messages/de/common.json
new file mode 100644
index 0000000..e689a5c
--- /dev/null
+++ b/apps/web/messages/de/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Etwas ist schiefgelaufen. Versuche es erneut.",
+ "network": "AnyPlay ist nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.",
+ "internal": "Bei AnyPlay ist ein Problem aufgetreten. Versuche es in einer Minute erneut.",
+ "unauthorized": "Deine Sitzung ist abgelaufen. Melde dich erneut an.",
+ "forbidden": "Das kannst du mit diesem Konto nicht tun.",
+ "banned": "Dieses Konto ist gesperrt.",
+ "not_found": "Das gibt es nicht mehr.",
+ "limit": "Du hast das Limit dafür erreicht. Entferne zuerst eines.",
+ "too_large": "Das ist zu groß zum Senden.",
+ "code_wrong": "Dieser Code ist falsch. Prüfe ihn und versuche es erneut.",
+ "code_expired": "Dieser Code ist abgelaufen. Fordere einen neuen an.",
+ "handle_taken": "Dieser Name ist vergeben. Versuche einen anderen.",
+ "handle_invalid": "Verwende 3 bis 16 Buchstaben, Ziffern, Bindestriche oder Unterstriche. Beginne mit einem Buchstaben.",
+ "rate_limited": "Zu viele Versuche. Warte einen Moment und versuche es erneut.",
+ "rate_limited_wait": "Zu viele Versuche. Warte {seconds, plural, one {# Sekunde} other {# Sekunden}} und versuche es erneut.",
+ "passkey_unsupported": "Dieser Browser unterstützt keine Passkeys. Verwende stattdessen den E-Mail-Code."
+ }
+}
diff --git a/apps/web/messages/en/account.json b/apps/web/messages/en/account.json
new file mode 100644
index 0000000..33ba1e5
--- /dev/null
+++ b/apps/web/messages/en/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "never",
+ "on": "On",
+ "off": "Off",
+ "cancel": "Cancel",
+ "working": "Working…",
+ "typeToConfirm": "Type {word} to confirm",
+ "audience": {
+ "everyone": "Everyone",
+ "friends": "Friends",
+ "nobody": "Only me"
+ },
+ "stepUp": {
+ "title": "Confirm it’s you",
+ "needed": "This change needs a recent sign-in. It lasts 15 minutes.",
+ "sent": "We sent a 6-digit code to {hint}.",
+ "passkey": "Use your passkey",
+ "sending": "Sending…",
+ "sendCode": "Send a code by e-mail"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, home",
+ "signOut": "Sign out",
+ "nav": "Account",
+ "nav_overview": "Overview",
+ "nav_profile": "Profile",
+ "nav_friends": "Friends",
+ "nav_trophies": "Trophies",
+ "nav_security": "Security",
+ "nav_notifications": "Notifications",
+ "nav_privacy": "Privacy",
+ "nav_connections": "Connections",
+ "nav_data": "Your data",
+ "unreachable": "Can’t reach AnyPlay. Check your connection and try again.",
+ "loading": "Loading"
+ },
+ "level": {
+ "top": "Top level reached",
+ "toNext": "{count, plural, one {# point} other {# points}} to level {level}",
+ "level": "Level {level}",
+ "points": "{count, plural, one {# point} other {# points}}",
+ "progress": "Level progress",
+ "progressTo": "Progress to level {level}"
+ },
+ "meta": {
+ "title": "Your account",
+ "template": "%s · Your account · AnyPlay"
+ },
+ "state": {
+ "loading": "Loading",
+ "tryAgain": "Try again"
+ },
+ "overview": {
+ "label": "Your account",
+ "memberSince": "Member since {date}",
+ "appearingOffline": "Appearing offline",
+ "deletion": "Your account will be deleted on {date}. Keep my account",
+ "passkeys": "{count, plural, one {# passkey} other {# passkeys}}",
+ "noPasskey": "No passkey yet",
+ "launchersBrowsers": "{launchers, plural, one {# launcher} other {# launchers}} · {browsers, plural, one {# browser} other {# browsers}}",
+ "trophiesAudience": "Trophies: {audience}",
+ "pictureAudience": "Profile picture: {audience}",
+ "audience": {
+ "everyone": "Everyone",
+ "friends": "Friends",
+ "nobody": "Only you"
+ },
+ "audienceInline": {
+ "everyone": "everyone",
+ "friends": "friends",
+ "nobody": "only you"
+ },
+ "linked": "{count} linked",
+ "nothingLinked": "Nothing linked",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Export or delete",
+ "dataDetail": "Everything we keep about you",
+ "launcher": {
+ "title": "Get the launcher",
+ "description": "Sign in to the launcher with this account, and your friends, parties and trophies are there."
+ }
+ },
+ "profile": {
+ "lead": "What your friends see when they open your profile. Who sees each part is up to you on the Privacy page.",
+ "avatar": {
+ "title": "Picture",
+ "description": "PNG or JPEG, up to 1 MB. It shows round, so keep the face in the middle.",
+ "uploading": "Uploading…",
+ "change": "Change picture",
+ "upload": "Upload picture",
+ "remove": "Remove"
+ },
+ "about": {
+ "title": "About",
+ "description": "A line or two about you: what you play, when you're around.",
+ "placeholder": "Co-op when I can, solo when I can't.",
+ "saved": "Saved",
+ "saving": "Saving…",
+ "save": "Save"
+ },
+ "favourites": {
+ "title": "Favourite games",
+ "description": "Up to {max}, by title ID. This site shows the ID. The launcher shows each game's name from your own copy.",
+ "empty": "No favourite games yet. Add a title ID below.",
+ "listLabel": "Your favourite games",
+ "remove": "Remove {id}",
+ "fieldLabel": "Title ID",
+ "full": "You have {max} games. Remove one to add another.",
+ "add": "Add game",
+ "saved": "Saved",
+ "saving": "Saving…",
+ "save": "Save",
+ "badId": "Use only letters, numbers, hyphens and underscores.",
+ "duplicate": "{id} is on the list already."
+ }
+ },
+ "security": {
+ "lead": "How you sign in, and every launcher and browser that is signed in to your account.",
+ "email": {
+ "title": "E-mail",
+ "description": "Where your sign-in codes go. Changing it needs a code sent to the new address.",
+ "change": "Change",
+ "invalid": "Enter the new address, like name@example.com.",
+ "done": "Your sign-in address is now {email}.",
+ "newAddress": "New address",
+ "sending": "Sending…",
+ "send": "Send code",
+ "cancel": "Cancel",
+ "sentTo": "We sent a 6-digit code to {email}."
+ },
+ "passkeys": {
+ "title": "Passkeys",
+ "description": "Sign in with your fingerprint, face or device PIN instead of a code. Nobody can phish a passkey.",
+ "add": "Add a passkey",
+ "waiting": "Waiting for your device…",
+ "detail": "Added {added} · Last used {used}",
+ "remove": "Remove",
+ "empty": "No passkey yet. Add one and signing in takes a touch instead of an e-mail code.",
+ "full": "You have the most passkeys an account can keep ({max}). Remove one to add another.",
+ "confirmTitle": "Remove this passkey?",
+ "confirmDescription": "That device can no longer sign you in with it. You can always sign in with a code by e-mail.",
+ "confirmLabel": "Remove passkey",
+ "defaultName": "Passkey on {system}",
+ "thisDevice": "this device"
+ },
+ "devices": {
+ "title": "Launchers",
+ "description": "Every PC where the launcher is signed in to your account.",
+ "detail": "{platform} · Last used {used}",
+ "signOut": "Sign out",
+ "empty": "No launcher is signed in yet.",
+ "confirmTitle": "Sign out this launcher?",
+ "confirmDescription": "It goes offline until someone signs in on that PC again.",
+ "confirmLabel": "Sign out launcher"
+ },
+ "browsers": {
+ "title": "Browsers",
+ "description": "Every browser signed in to this account page. We keep no more about them than when they were used.",
+ "thisBrowser": "This browser",
+ "here": "You are here",
+ "other": "Other browser",
+ "detail": "Signed in {created} · Last used {used}",
+ "signOut": "Sign out",
+ "confirmThisTitle": "Sign out of this browser?",
+ "confirmOtherTitle": "Sign out that browser?",
+ "confirmThisDescription": "You go back to the start and sign in again next time.",
+ "confirmOtherDescription": "It has to sign in again to open your account.",
+ "confirmLabel": "Sign out"
+ },
+ "everywhere": {
+ "title": "Sign out everywhere",
+ "description": "Every launcher and every browser, this one too. Use it when a device was lost or someone else might have signed in.",
+ "button": "Sign out everywhere",
+ "confirmTitle": "Sign out everywhere?",
+ "confirmDescription": "Every launcher and browser is signed out at once, this one too.",
+ "confirmLabel": "Sign out everywhere"
+ }
+ },
+ "privacy": {
+ "lead": "Who sees your profile and your activity. Your handle is always visible, so friends can find you.",
+ "profile": {
+ "title": "Your profile",
+ "description": "Changes count at once, in the launcher too."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Trophies",
+ "detail": "Your trophy counts, points and level"
+ },
+ "playtime": {
+ "title": "Playtime",
+ "detail": "How long you played, per game"
+ },
+ "about": {
+ "title": "About",
+ "detail": "The line on your profile"
+ },
+ "favourites": {
+ "title": "Favourite games",
+ "detail": "Up to three games you pin"
+ },
+ "avatar": {
+ "title": "Picture",
+ "detail": "Your profile picture"
+ }
+ },
+ "whoSees": "Who sees: {part}",
+ "activity": {
+ "title": "Your activity",
+ "description": "What you do on AnyPlay, as your friends see it in their feed.",
+ "row": "Activity",
+ "detail": "Friends see it in their feed. Strangers see no feed. Choose “{nobody}” to hide what is there and stop new items."
+ },
+ "appearOffline": {
+ "title": "Online status",
+ "description": "Applies to every launcher and browser signed in to your account.",
+ "label": "Appear offline",
+ "detail": "Friends see you as offline. You can still play, chat and join."
+ },
+ "publicProfile": {
+ "title": "Public profile",
+ "description": "Off until you turn it on. Applies to every launcher and browser signed in to your account.",
+ "label": "Show a public profile",
+ "detail": "Anyone with the link sees your level, trophies and games played. It never shows whether you are online.",
+ "page": "Your page: {url}"
+ },
+ "playedWith": {
+ "title": "People you played with",
+ "description": "Turning it off adds no one new. The list stays until you clear it.",
+ "label": "Remember people I play with",
+ "detail": "Keeps the people you shared a party or game session with for 30 days, up to 50. Only you see the list.",
+ "clear": "Clear the list",
+ "cleared": "The list is empty.",
+ "confirmTitle": "Clear the list?",
+ "confirmDescription": "Everyone on your list of people you played with is removed. It does not change anyone else’s list, and you can’t undo it."
+ }
+ },
+ "connections": {
+ "lead": "Link the accounts you already play with, so friends can find you and see where else you are.",
+ "result": {
+ "linked": "{name} is linked to your account.",
+ "denied": "{name} was not linked: the request was declined there.",
+ "expired": "Linking {name} took too long. Start again.",
+ "fresh": "Linking {name} needs a recent sign-in. Start again and confirm it's you.",
+ "taken": "That {name} account is already linked to another AnyPlay account.",
+ "exists": "You already have a {name} account linked. Unlink it first.",
+ "failed": "Linking {name} failed. Try again in a minute."
+ },
+ "whoSees": "Who sees your {name} account",
+ "linkedAgo": "Linked {ago}",
+ "unlink": "Unlink",
+ "link": "Link {name}",
+ "opening": "Opening…",
+ "soon": "Soon",
+ "notLinked": "Not linked",
+ "arrivesSoon": "Linking arrives soon",
+ "confirmTitle": "Unlink {name}?",
+ "confirmDescription": "Your {name} account no longer shows on your profile. You can link it again any time.",
+ "confirmLabel": "Unlink {name}"
+ },
+ "data": {
+ "lead": "Everything AnyPlay keeps about you is yours: take a copy, or delete the account.",
+ "export": {
+ "title": "Download your data",
+ "description": "Your account, profile, friends, trophies, playtime and linked accounts, as one JSON file.",
+ "preparing": "Preparing…",
+ "button": "Download"
+ },
+ "scheduled": {
+ "title": "Deletion scheduled",
+ "description": "Your account and everything in it will be deleted on {date}. Until then you can keep it.",
+ "keep": "Keep my account"
+ },
+ "delete": {
+ "title": "Delete your account",
+ "description": "Your account is deleted after 30 days; until then you can change your mind. After that it is gone, with your friends, trophies and playtime.",
+ "button": "Delete account"
+ },
+ "confirm": {
+ "title": "Delete your account?",
+ "description": "It is deleted in 30 days. Every launcher and browser is signed out now."
+ }
+ },
+ "compare": {
+ "title": "Compare trophies",
+ "description": "Your trophies next to {handle}’s.",
+ "descriptionShared": "You and {handle}, in a game you both have.",
+ "signInDescription": "See how your trophies stack up against {handle}.",
+ "signIn": "Sign in to compare trophies",
+ "hidden": "{handle}’s trophies aren’t available to compare.",
+ "loading": "Loading",
+ "loadingComparison": "Loading the comparison",
+ "empty": "You and {handle} have no game in common yet. Play a game {handle} has played and it shows up here.",
+ "game": "Game",
+ "gameNamed": "Game {title}",
+ "verdictNone": "Neither of you has earned a trophy in this game.",
+ "verdictLevel": "You are level on points in this game.",
+ "verdictAhead": "You are ahead by {count, plural, one {# point} other {# points}} in this game.",
+ "verdictBehind": "{handle} is ahead by {count, plural, one {# point} other {# points}} in this game.",
+ "caption": "Trophies in {titleId}, you next to {handle}",
+ "stat": "Stat",
+ "you": "You",
+ "ahead": "(ahead)",
+ "rowTrophies": "Trophies",
+ "rowPoints": "Points in this game",
+ "rowLevel": "Level overall",
+ "grade": {
+ "platinum": "Platinum",
+ "gold": "Gold",
+ "silver": "Silver",
+ "bronze": "Bronze"
+ },
+ "both": "Earned by both",
+ "onlyYou": "Only you",
+ "onlyThem": "Only {handle}"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, home",
+ "yourAccount": "Your account",
+ "signIn": "Sign in",
+ "loading": "Loading",
+ "missingTitle": "No player named {handle}",
+ "missingBody": "Check the spelling of the handle, or ask the player to send it again.",
+ "goHome": "Go to AnyPlay",
+ "player": "Player",
+ "trophies": "Trophies",
+ "trophiesEarned": "{count, number} earned.",
+ "noTrophies": "{handle} has not earned a trophy yet.",
+ "favourites": "Favourite games",
+ "favouritesNote": "Shown by title ID.",
+ "game": "Game",
+ "connections": "Connections",
+ "playtime": "Games played",
+ "playtimeNote": "{count, plural, one {# game} other {# games}}, {total} in total.",
+ "nothing": "There is nothing more to show of {handle} yet.",
+ "openProfile": "Open profile",
+ "openProfileOn": "on {brand} (opens in a new tab)",
+ "signInToAdd": "Sign in to add {handle}",
+ "editProfile": "Edit your profile",
+ "blocked": "Blocked",
+ "friends": "Friends",
+ "requestSent": "Request sent",
+ "accepting": "Accepting…",
+ "accept": "Accept request",
+ "sending": "Sending…",
+ "addFriend": "Add friend",
+ "addInAnyPlay": "Add friend in AnyPlay",
+ "minutes": "{n} min",
+ "hours": "{n} h"
+ },
+ "friends": {
+ "title": "Friends",
+ "lead": "Add the people you play with, answer requests and decide who stays out.",
+ "add": {
+ "title": "Add a friend",
+ "description": "Enter a player’s handle to send them a request.",
+ "handle": "Handle",
+ "hint": "Their handle, with or without the @.",
+ "send": "Send request",
+ "sending": "Sending…",
+ "enterHandle": "Enter the handle of the player you want to add.",
+ "nowFriends": "You and @{handle} are now friends.",
+ "sent": "Request sent to @{handle}. You are friends once they accept."
+ },
+ "requests": {
+ "title": "Requests",
+ "description": "Players who want to be your friend, and the requests you sent.",
+ "empty": "No open requests. When someone adds you, the request shows up here.",
+ "received": "Received",
+ "sent": "Sent",
+ "receivedAgo": "Received {when}",
+ "sentAgo": "Sent {when}",
+ "decline": "Decline",
+ "accept": "Accept",
+ "withdraw": "Withdraw"
+ },
+ "list": {
+ "title": "Your friends",
+ "description": "See who is online and what they are playing.",
+ "empty": "No friends yet. Add someone by their handle above; they show up here once they accept.",
+ "online": "Online",
+ "offline": "Offline",
+ "playing": "Playing {title}",
+ "block": "Block",
+ "remove": "Remove"
+ },
+ "blocked": {
+ "title": "Blocked",
+ "description": "Blocked players are removed from your friends and requests.",
+ "empty": "You have not blocked anyone. Block a player from your friends list when you need to.",
+ "unblock": "Unblock"
+ },
+ "confirm": {
+ "blockTitle": "Block @{handle}?",
+ "removeTitle": "Remove @{handle} from your friends?",
+ "blockText": "You are no longer friends and any open request is dropped. You can unblock them any time.",
+ "removeText": "You stop seeing when each other is online. You can send a new request later.",
+ "blockLabel": "Block player",
+ "removeLabel": "Remove friend"
+ }
+ },
+ "trophies": {
+ "title": "Trophies",
+ "lead": "What you earned in your games. AnyPlay keeps counts per game and grade, never trophy names or art.",
+ "totals": {
+ "title": "Totals",
+ "description": "{total} earned across {games, plural, one {# game} other {# games}}.",
+ "empty": "No trophies yet. Play a game through AnyPlay and the ones you earn are counted here."
+ },
+ "byGame": {
+ "title": "By game",
+ "description": "Games are listed by title id, the most recent first.",
+ "detail": "{earned, plural, one {# trophy} other {# trophies}} · last earned {when}"
+ },
+ "grades": {
+ "platinum": "Platinum",
+ "gold": "Gold",
+ "silver": "Silver",
+ "bronze": "Bronze"
+ }
+ },
+ "notifications": {
+ "title": "Notifications",
+ "lead": "Get a notification on this device when a friend asks to be your friend, invites you to a party or writes to you.",
+ "device": {
+ "title": "This device",
+ "description": "Each browser or phone is set up on its own. Turn this on where you want to be notified.",
+ "notEnabled": "This cloud does not send notifications.",
+ "checking": "Checking this device…",
+ "unsupported": "This browser can’t receive notifications. Use a current Chrome, Edge, Firefox or Safari.",
+ "needsInstall": "On iPhone and iPad, notifications need AnyPlay on your Home Screen. Tap Share, choose Add to Home Screen, then open AnyPlay from there.",
+ "blocked": "Notifications are blocked for this site. Allow them in your browser’s site settings, then come back.",
+ "switch": "Notify this device",
+ "others": "{count, plural, one {One other device also gets notifications. Turn them off on that device.} other {# other devices also get notifications. Turn them off on that device.}}"
+ },
+ "kinds": {
+ "title": "What to notify about",
+ "description": "For this device only.",
+ "friend_request": {
+ "label": "Friend requests",
+ "detail": "Someone asks to be your friend."
+ },
+ "party_invite": {
+ "label": "Party invitations",
+ "detail": "A friend invites you to a party."
+ },
+ "message": {
+ "label": "Messages",
+ "detail": "A friend writes to you. The message itself stays out of the notification."
+ }
+ },
+ "errors": {
+ "timeout": "Your browser’s notification service did not answer. Check your connection and try again.",
+ "noKey": "This cloud has no key to send notifications with.",
+ "lostSubscription": "This browser lost its subscription. Turn notifications off and on again.",
+ "notAllowed": "This browser did not allow notifications. They don’t work in a private window; otherwise check the site settings."
+ }
+ }
+}
diff --git a/apps/web/messages/en/auth.json b/apps/web/messages/en/auth.json
new file mode 100644
index 0000000..fb8324e
--- /dev/null
+++ b/apps/web/messages/en/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Sign in",
+ "description": "Sign in to AnyPlay with a passkey or a code by e-mail."
+ },
+ "create": {
+ "title": "Create your account",
+ "description": "Create your AnyPlay account: an e-mail address and a handle."
+ },
+ "link": {
+ "title": "Open your account",
+ "description": "Open your AnyPlay account in this browser, from the launcher."
+ },
+ "join": {
+ "title": "Invitation",
+ "description": "An invitation to play together on AnyPlay."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, home",
+ "back": "Back to the start",
+ "promise": "Play your games together.",
+ "toastOnline": "mira is online",
+ "toastParty": "kit joined your party",
+ "toastTrophy": "Trophy earned · Into the Ember",
+ "help": "Need help?",
+ "helpLink": "Ask on Discord",
+ "legalNav": "Legal",
+ "legalNotice": "Legal notice",
+ "privacyNotice": "Privacy notice",
+ "termsOfUse": "Terms of use"
+ },
+ "step": {
+ "loading": "Loading",
+ "unreachable": "Can’t reach AnyPlay",
+ "tryAgain": "Try again",
+ "newTab": "(opens in a new tab)",
+ "backToStart": "Back to the start",
+ "signIn": "Sign in",
+ "createAccount": "Create an account",
+ "getApp": "Get AnyPlay",
+ "continue": "Continue"
+ },
+ "fields": {
+ "code": "Code"
+ },
+ "flow": {
+ "authorize": "AnyPlay on {device} is waiting for this sign-in."
+ },
+ "email": {
+ "create": {
+ "title": "Create your account",
+ "lead": "Start with your e-mail address. We send a code to confirm it."
+ },
+ "signIn": {
+ "title": "Sign in",
+ "lead": "Use your passkey, or get a code by e-mail."
+ },
+ "passkey": "Sign in with a passkey",
+ "or": "or",
+ "label": "E-mail address",
+ "placeholder": "name@example.com",
+ "invalid": "Enter your e-mail address, like name@example.com.",
+ "sending": "Sending…",
+ "send": "Send code",
+ "haveAccount": "Already have an account? Sign in",
+ "newHere": "New to AnyPlay? Create an account"
+ },
+ "code": {
+ "title": "Check your e-mail",
+ "lead": "We sent a 6-digit code to {email}. It works for 10 minutes.",
+ "incomplete": "Enter the 6 digits from the e-mail.",
+ "checking": "Checking…",
+ "resent": "A new code is on its way.",
+ "noMail": "No e-mail?",
+ "resendIn": "Send a new code in {seconds} s",
+ "resend": "Send a new code"
+ },
+ "details": {
+ "title": "Choose a handle",
+ "lead": "Your address {email} is confirmed. Other players see your handle.",
+ "handle": "Handle",
+ "handleHint": "Use 3 to 16 letters, digits, hyphens or underscores. Start with a letter.",
+ "ageHint": "That means 16 or older.",
+ "age": "I am old enough to agree to the terms on my own.",
+ "terms": "I accept the terms (version {version}) and have read the privacy notice.",
+ "creating": "Creating…",
+ "submit": "Create account"
+ },
+ "terms": {
+ "title": "Review the terms",
+ "lead": "The terms changed. Read the terms and accept version {version} to continue.",
+ "accept": "I accept the terms (version {version}).",
+ "required": "Accept the terms to continue.",
+ "submit": "Accept and continue"
+ },
+ "banned": {
+ "title": "This account is banned",
+ "leadUntil": "The ban ends on {date}.",
+ "leadForever": "The ban does not end on its own.",
+ "rule": "Rule",
+ "reason": "Reason",
+ "appeal": "Read the statement and appeal"
+ },
+ "handoff": {
+ "title": "Signed in",
+ "lead": "Taking you back to AnyPlay. If nothing happens, use the button.",
+ "open": "Open AnyPlay",
+ "note": "You can close this tab once AnyPlay shows you as signed in."
+ },
+ "done": {
+ "title": "You’re signed in",
+ "inviteLead": "Taking you back to the invitation.",
+ "welcome": "Welcome, {handle}. Your friends, parties and trophies are waiting in the launcher.",
+ "account": "Go to your account"
+ },
+ "join": {
+ "expiredTitle": "This invitation has expired",
+ "expiredLead": "The link may have run out, or the player has stopped playing. Ask them for a new one.",
+ "title": "{handle} invited you",
+ "leadSignedIn": "Open AnyPlay on your PC to see the invitation. You choose there whether to join.",
+ "leadSignedOut": "Sign in or create an account first. You come back here to open the invitation.",
+ "party": "Party",
+ "game": "Game",
+ "partyName": "An AnyPlay party",
+ "gameFallback": "A game",
+ "joined": "Joined",
+ "players": "{members} of {max, plural, one {# player} other {# players}}",
+ "open": "Open in AnyPlay",
+ "noApp": "No AnyPlay on this PC yet? Get it first, then open this page again."
+ },
+ "link": {
+ "expiredTitle": "This link has expired",
+ "expiredLead": "Open your account page from AnyPlay again.",
+ "close": "You can close this tab.",
+ "title": "Open your account",
+ "lead": "AnyPlay on your PC asked to open the account of {handle} in this browser.",
+ "open": "Open account",
+ "opening": "Opening…",
+ "notYou": "Did not ask for this? Close the tab."
+ }
+}
diff --git a/apps/web/messages/en/common.json b/apps/web/messages/en/common.json
new file mode 100644
index 0000000..e7c6bd5
--- /dev/null
+++ b/apps/web/messages/en/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Something went wrong. Try again.",
+ "network": "Can’t reach AnyPlay. Check your connection and try again.",
+ "internal": "AnyPlay had a problem on its side. Try again in a minute.",
+ "unauthorized": "Your session ended. Sign in again.",
+ "forbidden": "You can’t do that with this account.",
+ "banned": "This account is banned.",
+ "not_found": "That doesn’t exist any more.",
+ "limit": "You’ve reached the limit for this. Remove one first.",
+ "too_large": "That is too large to send.",
+ "code_wrong": "That code is wrong. Check it and try again.",
+ "code_expired": "That code has expired. Request a new one.",
+ "handle_taken": "That handle is taken. Try another.",
+ "handle_invalid": "Use 3 to 16 letters, digits, hyphens or underscores. Start with a letter.",
+ "rate_limited": "Too many tries. Wait a moment and try again.",
+ "rate_limited_wait": "Too many tries. Wait {seconds, plural, one {# second} other {# seconds}} and try again.",
+ "passkey_unsupported": "This browser can’t use passkeys. Use the e-mail code instead."
+ }
+}
diff --git a/apps/web/messages/es/account.json b/apps/web/messages/es/account.json
new file mode 100644
index 0000000..da0d603
--- /dev/null
+++ b/apps/web/messages/es/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "nunca",
+ "on": "Activado",
+ "off": "Desactivado",
+ "cancel": "Cancelar",
+ "working": "Procesando…",
+ "typeToConfirm": "Escribe {word} para confirmar",
+ "audience": {
+ "everyone": "Todos",
+ "friends": "Amigos",
+ "nobody": "Solo yo"
+ },
+ "stepUp": {
+ "title": "Confirma que eres tú",
+ "needed": "Este cambio requiere un inicio de sesión reciente. Dura 15 minutos.",
+ "sent": "Hemos enviado un código de 6 dígitos a {hint}.",
+ "passkey": "Usa tu llave de acceso",
+ "sending": "Enviando…",
+ "sendCode": "Enviar un código por correo"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, inicio",
+ "signOut": "Cerrar sesión",
+ "nav": "Cuenta",
+ "nav_overview": "Resumen",
+ "nav_profile": "Perfil",
+ "nav_friends": "Amigos",
+ "nav_trophies": "Trofeos",
+ "nav_security": "Seguridad",
+ "nav_notifications": "Notificaciones",
+ "nav_privacy": "Privacidad",
+ "nav_connections": "Conexiones",
+ "nav_data": "Tus datos",
+ "unreachable": "No se puede acceder a AnyPlay. Revisa tu conexión e inténtalo de nuevo.",
+ "loading": "Cargando"
+ },
+ "level": {
+ "top": "Nivel máximo alcanzado",
+ "toNext": "{count, plural, one {# punto} other {# puntos}} para el nivel {level}",
+ "level": "Nivel {level}",
+ "points": "{count, plural, one {# punto} other {# puntos}}",
+ "progress": "Progreso de nivel",
+ "progressTo": "Progreso hacia el nivel {level}"
+ },
+ "meta": {
+ "title": "Tu cuenta",
+ "template": "%s · Tu cuenta · AnyPlay"
+ },
+ "state": {
+ "loading": "Cargando",
+ "tryAgain": "Reintentar"
+ },
+ "overview": {
+ "label": "Tu cuenta",
+ "memberSince": "Miembro desde {date}",
+ "appearingOffline": "Apareces sin conexión",
+ "deletion": "Tu cuenta se eliminará el {date}. Conservar mi cuenta",
+ "passkeys": "{count, plural, one {# llave de acceso} other {# llaves de acceso}}",
+ "noPasskey": "Aún no hay llaves de acceso",
+ "launchersBrowsers": "{launchers, plural, one {# launcher} other {# launchers}} · {browsers, plural, one {# navegador} other {# navegadores}}",
+ "trophiesAudience": "Trofeos: {audience}",
+ "pictureAudience": "Foto de perfil: {audience}",
+ "audience": {
+ "everyone": "Todos",
+ "friends": "Amigos",
+ "nobody": "Solo tú"
+ },
+ "audienceInline": {
+ "everyone": "todos",
+ "friends": "amigos",
+ "nobody": "solo tú"
+ },
+ "linked": "{count} vinculadas",
+ "nothingLinked": "Nada vinculado",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Exportar o eliminar",
+ "dataDetail": "Todo lo que guardamos sobre ti",
+ "launcher": {
+ "title": "Consigue el launcher",
+ "description": "Inicia sesión en el launcher con esta cuenta y tus amigos, parties y trofeos estarán ahí."
+ }
+ },
+ "profile": {
+ "lead": "Lo que ven tus amigos al abrir tu perfil. Quién ve cada parte lo decides tú en la página de Privacidad.",
+ "avatar": {
+ "title": "Foto",
+ "description": "PNG o JPEG, de hasta 1 MB. Se muestra redonda, así que deja la cara en el centro.",
+ "uploading": "Subiendo…",
+ "change": "Cambiar foto",
+ "upload": "Subir foto",
+ "remove": "Quitar"
+ },
+ "about": {
+ "title": "Sobre mí",
+ "description": "Una o dos líneas sobre ti: a qué juegas, cuándo estás por aquí.",
+ "placeholder": "Cooperativo cuando puedo, solo cuando no.",
+ "saved": "Guardado",
+ "saving": "Guardando…",
+ "save": "Guardar"
+ },
+ "favourites": {
+ "title": "Juegos favoritos",
+ "description": "Hasta {max}, por ID de título. Esta web muestra el ID. El launcher muestra el nombre de cada juego desde tu propia copia.",
+ "empty": "Aún no hay juegos favoritos. Añade un ID de título abajo.",
+ "listLabel": "Tus juegos favoritos",
+ "remove": "Quitar {id}",
+ "fieldLabel": "ID de título",
+ "full": "Tienes {max} juegos. Quita uno para añadir otro.",
+ "add": "Añadir juego",
+ "saved": "Guardado",
+ "saving": "Guardando…",
+ "save": "Guardar",
+ "badId": "Usa solo letras, números, guiones y guiones bajos.",
+ "duplicate": "{id} ya está en la lista."
+ }
+ },
+ "security": {
+ "lead": "Cómo inicias sesión y todos los launchers y navegadores con la sesión iniciada en tu cuenta.",
+ "email": {
+ "title": "Correo electrónico",
+ "description": "Adónde van tus códigos de inicio de sesión. Para cambiarlo hace falta un código enviado a la nueva dirección.",
+ "change": "Cambiar",
+ "invalid": "Escribe la nueva dirección, por ejemplo nombre@ejemplo.com.",
+ "done": "Tu dirección de inicio de sesión ahora es {email}.",
+ "newAddress": "Nueva dirección",
+ "sending": "Enviando…",
+ "send": "Enviar código",
+ "cancel": "Cancelar",
+ "sentTo": "Hemos enviado un código de 6 dígitos a {email}."
+ },
+ "passkeys": {
+ "title": "Llaves de acceso",
+ "description": "Inicia sesión con tu huella, tu cara o el PIN del dispositivo en lugar de un código. Nadie puede suplantar una llave de acceso.",
+ "add": "Añadir una llave de acceso",
+ "waiting": "Esperando a tu dispositivo…",
+ "detail": "Añadida {added} · Último uso: {used}",
+ "remove": "Quitar",
+ "empty": "Aún no hay llaves de acceso. Añade una y iniciar sesión será un toque en lugar de un código por correo.",
+ "full": "Tienes el máximo de llaves de acceso que puede tener una cuenta ({max}). Quita una para añadir otra.",
+ "confirmTitle": "¿Quitar esta llave de acceso?",
+ "confirmDescription": "Ese dispositivo ya no podrá iniciar tu sesión con ella. Siempre puedes iniciar sesión con un código por correo.",
+ "confirmLabel": "Quitar llave de acceso",
+ "defaultName": "Llave de acceso en {system}",
+ "thisDevice": "este dispositivo"
+ },
+ "devices": {
+ "title": "Launchers",
+ "description": "Todos los PC donde el launcher tiene la sesión iniciada en tu cuenta.",
+ "detail": "{platform} · Último uso: {used}",
+ "signOut": "Cerrar sesión",
+ "empty": "Aún no hay ningún launcher con la sesión iniciada.",
+ "confirmTitle": "¿Cerrar sesión en este launcher?",
+ "confirmDescription": "Se quedará sin conexión hasta que alguien inicie sesión de nuevo en ese PC.",
+ "confirmLabel": "Cerrar sesión del launcher"
+ },
+ "browsers": {
+ "title": "Navegadores",
+ "description": "Todos los navegadores con la sesión iniciada en esta página de cuenta. De ellos solo guardamos cuándo se usaron.",
+ "thisBrowser": "Este navegador",
+ "here": "Estás aquí",
+ "other": "Otro navegador",
+ "detail": "Sesión iniciada {created} · Último uso: {used}",
+ "signOut": "Cerrar sesión",
+ "confirmThisTitle": "¿Cerrar sesión en este navegador?",
+ "confirmOtherTitle": "¿Cerrar sesión en ese navegador?",
+ "confirmThisDescription": "Vuelves al inicio y tendrás que iniciar sesión de nuevo la próxima vez.",
+ "confirmOtherDescription": "Tendrá que iniciar sesión de nuevo para abrir tu cuenta.",
+ "confirmLabel": "Cerrar sesión"
+ },
+ "everywhere": {
+ "title": "Cerrar sesión en todas partes",
+ "description": "Todos los launchers y todos los navegadores, también este. Úsalo si has perdido un dispositivo o alguien más pudo iniciar sesión.",
+ "button": "Cerrar sesión en todas partes",
+ "confirmTitle": "¿Cerrar sesión en todas partes?",
+ "confirmDescription": "Se cierra la sesión de todos los launchers y navegadores a la vez, también este.",
+ "confirmLabel": "Cerrar sesión en todas partes"
+ }
+ },
+ "privacy": {
+ "lead": "Quién ve tu perfil y tu actividad. Tu nombre siempre es visible, para que tus amigos te encuentren.",
+ "profile": {
+ "title": "Tu perfil",
+ "description": "Los cambios cuentan al instante, también en el launcher."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Trofeos",
+ "detail": "Tu número de trofeos, tus puntos y tu nivel"
+ },
+ "playtime": {
+ "title": "Tiempo de juego",
+ "detail": "Cuánto has jugado, por juego"
+ },
+ "about": {
+ "title": "Sobre mí",
+ "detail": "La línea de tu perfil"
+ },
+ "favourites": {
+ "title": "Juegos favoritos",
+ "detail": "Hasta tres juegos que fijas"
+ },
+ "avatar": {
+ "title": "Foto",
+ "detail": "Tu foto de perfil"
+ }
+ },
+ "whoSees": "Quién ve: {part}",
+ "activity": {
+ "title": "Tu actividad",
+ "description": "Lo que haces en AnyPlay, tal como lo ven tus amigos en su feed.",
+ "row": "Actividad",
+ "detail": "Tus amigos la ven en su feed. Los desconocidos no ven ningún feed. Elige «{nobody}» para ocultar lo que hay y detener los elementos nuevos."
+ },
+ "appearOffline": {
+ "title": "Estado en línea",
+ "description": "Se aplica a todos los launchers y navegadores con la sesión iniciada en tu cuenta.",
+ "label": "Aparecer sin conexión",
+ "detail": "Tus amigos te ven sin conexión. Aun así puedes jugar, chatear y unirte."
+ },
+ "publicProfile": {
+ "title": "Perfil público",
+ "description": "Desactivado hasta que lo actives. Se aplica a todos los launchers y navegadores con la sesión iniciada en tu cuenta.",
+ "label": "Mostrar un perfil público",
+ "detail": "Cualquiera con el enlace ve tu nivel, tus trofeos y los juegos a los que has jugado. Nunca muestra si estás en línea.",
+ "page": "Tu página: {url}"
+ },
+ "playedWith": {
+ "title": "Jugadores recientes",
+ "description": "Si lo desactivas, no se añade a nadie nuevo. La lista se queda hasta que la vacíes.",
+ "label": "Recordar con quién juego",
+ "detail": "Guarda durante 30 días a las personas con quienes compartiste una party o una sesión de juego, hasta 50. Solo tú ves la lista.",
+ "clear": "Vaciar la lista",
+ "cleared": "La lista está vacía.",
+ "confirmTitle": "¿Vaciar la lista?",
+ "confirmDescription": "Se quita a todos de tu lista de personas con las que has jugado. No cambia la lista de nadie más y no se puede deshacer."
+ }
+ },
+ "connections": {
+ "lead": "Vincula las cuentas con las que ya juegas, para que tus amigos te encuentren y vean dónde más estás.",
+ "result": {
+ "linked": "{name} está vinculado a tu cuenta.",
+ "denied": "{name} no se vinculó: la solicitud fue rechazada allí.",
+ "expired": "Vincular {name} tardó demasiado. Empieza de nuevo.",
+ "fresh": "Vincular {name} requiere un inicio de sesión reciente. Empieza de nuevo y confirma que eres tú.",
+ "taken": "Esa cuenta de {name} ya está vinculada a otra cuenta de AnyPlay.",
+ "exists": "Ya tienes una cuenta de {name} vinculada. Desvincúlala primero.",
+ "failed": "No se pudo vincular {name}. Inténtalo de nuevo en un minuto."
+ },
+ "whoSees": "Quién ve tu cuenta de {name}",
+ "linkedAgo": "Vinculada {ago}",
+ "unlink": "Desvincular",
+ "link": "Vincular {name}",
+ "opening": "Abriendo…",
+ "soon": "Próximamente",
+ "notLinked": "Sin vincular",
+ "arrivesSoon": "La vinculación llega pronto",
+ "confirmTitle": "¿Desvincular {name}?",
+ "confirmDescription": "Tu cuenta de {name} ya no aparece en tu perfil. Puedes volver a vincularla cuando quieras.",
+ "confirmLabel": "Desvincular {name}"
+ },
+ "data": {
+ "lead": "Todo lo que AnyPlay guarda sobre ti es tuyo: llévate una copia o elimina la cuenta.",
+ "export": {
+ "title": "Descargar tus datos",
+ "description": "Tu cuenta, perfil, amigos, trofeos, tiempo de juego y cuentas vinculadas, en un solo archivo JSON.",
+ "preparing": "Preparando…",
+ "button": "Descargar"
+ },
+ "scheduled": {
+ "title": "Eliminación programada",
+ "description": "Tu cuenta y todo lo que contiene se eliminarán el {date}. Hasta entonces puedes conservarla.",
+ "keep": "Conservar mi cuenta"
+ },
+ "delete": {
+ "title": "Eliminar tu cuenta",
+ "description": "Tu cuenta se elimina a los 30 días; hasta entonces puedes cambiar de opinión. Después desaparece, junto con tus amigos, trofeos y tiempo de juego.",
+ "button": "Eliminar cuenta"
+ },
+ "confirm": {
+ "title": "¿Eliminar tu cuenta?",
+ "description": "Se elimina en 30 días. Se cierra ahora la sesión de todos los launchers y navegadores."
+ }
+ },
+ "compare": {
+ "title": "Comparar trofeos",
+ "description": "Tus trofeos junto a los de {handle}.",
+ "descriptionShared": "Tú y {handle}, en un juego que tenéis los dos.",
+ "signInDescription": "Mira cómo se comparan tus trofeos con los de {handle}.",
+ "signIn": "Inicia sesión para comparar trofeos",
+ "hidden": "Los trofeos de {handle} no están disponibles para comparar.",
+ "loading": "Cargando",
+ "loadingComparison": "Cargando la comparación",
+ "empty": "Tú y {handle} aún no tenéis ningún juego en común. Juega a un juego que {handle} haya jugado y aparecerá aquí.",
+ "game": "Juego",
+ "gameNamed": "Juego {title}",
+ "verdictNone": "Ninguno de los dos ha conseguido un trofeo en este juego.",
+ "verdictLevel": "Estáis igualados en puntos en este juego.",
+ "verdictAhead": "Vas {count, plural, one {# punto} other {# puntos}} por delante en este juego.",
+ "verdictBehind": "{handle} va {count, plural, one {# punto} other {# puntos}} por delante en este juego.",
+ "caption": "Trofeos en {titleId}: tú junto a {handle}",
+ "stat": "Categoría",
+ "you": "Tú",
+ "ahead": "(por delante)",
+ "rowTrophies": "Trofeos",
+ "rowPoints": "Puntos en este juego",
+ "rowLevel": "Nivel general",
+ "grade": {
+ "platinum": "Platino",
+ "gold": "Oro",
+ "silver": "Plata",
+ "bronze": "Bronce"
+ },
+ "both": "Conseguidos por ambos",
+ "onlyYou": "Solo tú",
+ "onlyThem": "Solo {handle}"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, inicio",
+ "yourAccount": "Tu cuenta",
+ "signIn": "Iniciar sesión",
+ "loading": "Cargando",
+ "missingTitle": "No hay ningún jugador llamado {handle}",
+ "missingBody": "Revisa cómo está escrito el nombre o pídele al jugador que lo envíe de nuevo.",
+ "goHome": "Ir a AnyPlay",
+ "player": "Jugador",
+ "trophies": "Trofeos",
+ "trophiesEarned": "{count, number} conseguidos.",
+ "noTrophies": "{handle} aún no ha conseguido ningún trofeo.",
+ "favourites": "Juegos favoritos",
+ "favouritesNote": "Se muestran por ID del título.",
+ "game": "Juego",
+ "connections": "Conexiones",
+ "playtime": "Juegos jugados",
+ "playtimeNote": "{count, plural, one {# juego} other {# juegos}}, {total} en total.",
+ "nothing": "Aún no hay nada más que mostrar de {handle}.",
+ "openProfile": "Abrir perfil",
+ "openProfileOn": "en {brand} (se abre en una pestaña nueva)",
+ "signInToAdd": "Inicia sesión para añadir a {handle}",
+ "editProfile": "Editar tu perfil",
+ "blocked": "Bloqueado",
+ "friends": "Amigos",
+ "requestSent": "Solicitud enviada",
+ "accepting": "Aceptando…",
+ "accept": "Aceptar solicitud",
+ "sending": "Enviando…",
+ "addFriend": "Añadir amigo",
+ "addInAnyPlay": "Añadir amigo en AnyPlay",
+ "minutes": "{n} min",
+ "hours": "{n} h"
+ },
+ "friends": {
+ "title": "Amigos",
+ "lead": "Añade a la gente con la que juegas, responde solicitudes y decide quién se queda fuera.",
+ "add": {
+ "title": "Añadir un amigo",
+ "description": "Escribe el nombre de un jugador para enviarle una solicitud.",
+ "handle": "Nombre",
+ "hint": "Su nombre, con o sin la @.",
+ "send": "Enviar solicitud",
+ "sending": "Enviando…",
+ "enterHandle": "Escribe el nombre del jugador que quieres añadir.",
+ "nowFriends": "Tú y @{handle} ya sois amigos.",
+ "sent": "Solicitud enviada a @{handle}. Seréis amigos cuando la acepte."
+ },
+ "requests": {
+ "title": "Solicitudes",
+ "description": "Jugadores que quieren ser tus amigos y las solicitudes que has enviado.",
+ "empty": "No hay solicitudes abiertas. Cuando alguien te añada, la solicitud aparecerá aquí.",
+ "received": "Recibidas",
+ "sent": "Enviadas",
+ "receivedAgo": "Recibida {when}",
+ "sentAgo": "Enviada {when}",
+ "decline": "Rechazar",
+ "accept": "Aceptar",
+ "withdraw": "Retirar"
+ },
+ "list": {
+ "title": "Tus amigos",
+ "description": "Mira quién está en línea y a qué juega.",
+ "empty": "Aún no tienes amigos. Añade a alguien por su nombre arriba; aparecerá aquí cuando acepte.",
+ "online": "En línea",
+ "offline": "Sin conexión",
+ "playing": "Jugando a {title}",
+ "block": "Bloquear",
+ "remove": "Quitar"
+ },
+ "blocked": {
+ "title": "Bloqueados",
+ "description": "Los jugadores bloqueados se quitan de tus amigos y solicitudes.",
+ "empty": "No has bloqueado a nadie. Bloquea a un jugador desde tu lista de amigos cuando lo necesites.",
+ "unblock": "Desbloquear"
+ },
+ "confirm": {
+ "blockTitle": "¿Bloquear a @{handle}?",
+ "removeTitle": "¿Quitar a @{handle} de tus amigos?",
+ "blockText": "Dejáis de ser amigos y se descarta cualquier solicitud abierta. Puedes desbloquearlo cuando quieras.",
+ "removeText": "Dejáis de ver cuándo está en línea el otro. Puedes enviar una solicitud nueva más tarde.",
+ "blockLabel": "Bloquear jugador",
+ "removeLabel": "Quitar amigo"
+ }
+ },
+ "trophies": {
+ "title": "Trofeos",
+ "lead": "Lo que has conseguido en tus juegos. AnyPlay guarda recuentos por juego y categoría, nunca nombres ni imágenes de trofeos.",
+ "totals": {
+ "title": "Totales",
+ "description": "{total} conseguidos en {games, plural, one {# juego} other {# juegos}}.",
+ "empty": "Aún no hay trofeos. Juega a algo con AnyPlay y los que consigas se contarán aquí."
+ },
+ "byGame": {
+ "title": "Por juego",
+ "description": "Los juegos se ordenan por ID de título, los más recientes primero.",
+ "detail": "{earned, plural, one {# trofeo} other {# trofeos}} · último conseguido: {when}"
+ },
+ "grades": {
+ "platinum": "Platino",
+ "gold": "Oro",
+ "silver": "Plata",
+ "bronze": "Bronce"
+ }
+ },
+ "notifications": {
+ "title": "Notificaciones",
+ "lead": "Recibe una notificación en este dispositivo cuando un amigo te envíe una solicitud, te invite a una party o te escriba.",
+ "device": {
+ "title": "Este dispositivo",
+ "description": "Cada navegador o móvil se configura por separado. Actívalo donde quieras recibir avisos.",
+ "notEnabled": "Esta nube no envía notificaciones.",
+ "checking": "Comprobando este dispositivo…",
+ "unsupported": "Este navegador no puede recibir notificaciones. Usa una versión actual de Chrome, Edge, Firefox o Safari.",
+ "needsInstall": "En iPhone y iPad, las notificaciones necesitan AnyPlay en tu pantalla de inicio. Toca Compartir, elige Añadir a pantalla de inicio y abre AnyPlay desde ahí.",
+ "blocked": "Las notificaciones están bloqueadas para este sitio. Permítelas en los ajustes del sitio de tu navegador y vuelve.",
+ "switch": "Notificar a este dispositivo",
+ "others": "{count, plural, one {Otro dispositivo también recibe notificaciones. Desactívalas en ese dispositivo.} other {Otros # dispositivos también reciben notificaciones. Desactívalas en esos dispositivos.}}"
+ },
+ "kinds": {
+ "title": "Sobre qué avisar",
+ "description": "Solo para este dispositivo.",
+ "friend_request": {
+ "label": "Solicitudes de amistad",
+ "detail": "Alguien quiere ser tu amigo."
+ },
+ "party_invite": {
+ "label": "Invitaciones a party",
+ "detail": "Un amigo te invita a una party."
+ },
+ "message": {
+ "label": "Mensajes",
+ "detail": "Un amigo te escribe. El mensaje en sí no aparece en la notificación."
+ }
+ },
+ "errors": {
+ "timeout": "El servicio de notificaciones de tu navegador no ha respondido. Comprueba tu conexión e inténtalo de nuevo.",
+ "noKey": "Esta nube no tiene una clave para enviar notificaciones.",
+ "lostSubscription": "Este navegador ha perdido su suscripción. Desactiva las notificaciones y vuelve a activarlas.",
+ "notAllowed": "Este navegador no ha permitido las notificaciones. No funcionan en una ventana privada; si no, revisa los ajustes del sitio."
+ }
+ }
+}
diff --git a/apps/web/messages/es/auth.json b/apps/web/messages/es/auth.json
new file mode 100644
index 0000000..0c87adf
--- /dev/null
+++ b/apps/web/messages/es/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Iniciar sesión",
+ "description": "Inicia sesión en AnyPlay con una llave de acceso o con un código por correo."
+ },
+ "create": {
+ "title": "Crea tu cuenta",
+ "description": "Crea tu cuenta de AnyPlay: un correo electrónico y un nombre."
+ },
+ "link": {
+ "title": "Abre tu cuenta",
+ "description": "Abre tu cuenta de AnyPlay en este navegador desde el launcher."
+ },
+ "join": {
+ "title": "Invitación",
+ "description": "Una invitación para jugar juntos en AnyPlay."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, inicio",
+ "back": "Volver al inicio",
+ "promise": "Juega tus juegos juntos.",
+ "toastOnline": "mira está en línea",
+ "toastParty": "kit se unió a tu party",
+ "toastTrophy": "Trofeo conseguido · Into the Ember",
+ "help": "¿Necesitas ayuda?",
+ "helpLink": "Pregunta en Discord",
+ "legalNav": "Legal",
+ "legalNotice": "Aviso legal",
+ "privacyNotice": "Aviso de privacidad",
+ "termsOfUse": "Condiciones de uso"
+ },
+ "step": {
+ "loading": "Cargando",
+ "unreachable": "No se puede conectar con AnyPlay",
+ "tryAgain": "Reintentar",
+ "newTab": "(se abre en una pestaña nueva)",
+ "backToStart": "Volver al inicio",
+ "signIn": "Iniciar sesión",
+ "createAccount": "Crear cuenta",
+ "getApp": "Obtener AnyPlay",
+ "continue": "Continuar"
+ },
+ "fields": {
+ "code": "Código"
+ },
+ "flow": {
+ "authorize": "AnyPlay en {device} está esperando este inicio de sesión."
+ },
+ "email": {
+ "create": {
+ "title": "Crea tu cuenta",
+ "lead": "Empieza con tu correo electrónico. Te enviamos un código para confirmarlo."
+ },
+ "signIn": {
+ "title": "Iniciar sesión",
+ "lead": "Usa tu llave de acceso o recibe un código por correo."
+ },
+ "passkey": "Iniciar sesión con una llave de acceso",
+ "or": "o",
+ "label": "Correo electrónico",
+ "placeholder": "name@example.com",
+ "invalid": "Escribe tu correo electrónico, por ejemplo name@example.com.",
+ "sending": "Enviando…",
+ "send": "Enviar código",
+ "haveAccount": "¿Ya tienes una cuenta? Iniciar sesión",
+ "newHere": "¿Eres nuevo en AnyPlay? Crear cuenta"
+ },
+ "code": {
+ "title": "Revisa tu correo",
+ "lead": "Enviamos un código de 6 dígitos a {email}. Es válido durante 10 minutos.",
+ "incomplete": "Escribe los 6 dígitos del correo.",
+ "checking": "Comprobando…",
+ "resent": "Un código nuevo está en camino.",
+ "noMail": "¿No llega el correo?",
+ "resendIn": "Enviar un código nuevo en {seconds} s",
+ "resend": "Enviar un código nuevo"
+ },
+ "details": {
+ "title": "Elige un nombre",
+ "lead": "Tu dirección {email} está confirmada. Los demás jugadores ven tu nombre.",
+ "handle": "Nombre",
+ "handleHint": "Usa de 3 a 16 letras, dígitos, guiones o guiones bajos. Empieza con una letra.",
+ "ageHint": "Es decir, 16 años o más.",
+ "age": "Tengo edad suficiente para aceptar las condiciones por mi cuenta.",
+ "terms": "Acepto las condiciones (versión {version}) y he leído el aviso de privacidad.",
+ "creating": "Creando…",
+ "submit": "Crear cuenta"
+ },
+ "terms": {
+ "title": "Revisa las condiciones",
+ "lead": "Las condiciones han cambiado. Lee las condiciones y acepta la versión {version} para continuar.",
+ "accept": "Acepto las condiciones (versión {version}).",
+ "required": "Acepta las condiciones para continuar.",
+ "submit": "Aceptar y continuar"
+ },
+ "banned": {
+ "title": "Esta cuenta está suspendida",
+ "leadUntil": "La suspensión termina el {date}.",
+ "leadForever": "La suspensión no termina por sí sola.",
+ "rule": "Norma",
+ "reason": "Motivo",
+ "appeal": "Leer la declaración y apelar"
+ },
+ "handoff": {
+ "title": "Sesión iniciada",
+ "lead": "Te llevamos de vuelta a AnyPlay. Si no pasa nada, usa el botón.",
+ "open": "Abrir AnyPlay",
+ "note": "Puedes cerrar esta pestaña cuando AnyPlay te muestre con la sesión iniciada."
+ },
+ "done": {
+ "title": "Has iniciado sesión",
+ "inviteLead": "Te llevamos de vuelta a la invitación.",
+ "welcome": "Te damos la bienvenida, {handle}. Tus amigos, partys y trofeos te esperan en el launcher.",
+ "account": "Ir a tu cuenta"
+ },
+ "join": {
+ "expiredTitle": "Esta invitación ha caducado",
+ "expiredLead": "Puede que el enlace haya caducado o que el jugador haya dejado de jugar. Pide uno nuevo.",
+ "title": "{handle} te ha invitado",
+ "leadSignedIn": "Abre AnyPlay en tu PC para ver la invitación. Allí decides si te unes.",
+ "leadSignedOut": "Primero inicia sesión o crea una cuenta. Después vuelves aquí para abrir la invitación.",
+ "party": "Party",
+ "game": "Juego",
+ "partyName": "Una party de AnyPlay",
+ "gameFallback": "Un juego",
+ "joined": "Se han unido",
+ "players": "{members} de {max, plural, one {# jugador} other {# jugadores}}",
+ "open": "Abrir en AnyPlay",
+ "noApp": "¿Aún no tienes AnyPlay en este PC? Consíguelo primero y vuelve a abrir esta página."
+ },
+ "link": {
+ "expiredTitle": "Este enlace ha caducado",
+ "expiredLead": "Vuelve a abrir tu página de cuenta desde AnyPlay.",
+ "close": "Puedes cerrar esta pestaña.",
+ "title": "Abre tu cuenta",
+ "lead": "AnyPlay en tu PC ha pedido abrir la cuenta de {handle} en este navegador.",
+ "open": "Abrir cuenta",
+ "opening": "Abriendo…",
+ "notYou": "¿No lo has pedido tú? Cierra la pestaña."
+ }
+}
diff --git a/apps/web/messages/es/common.json b/apps/web/messages/es/common.json
new file mode 100644
index 0000000..8fad371
--- /dev/null
+++ b/apps/web/messages/es/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Algo salió mal. Inténtalo de nuevo.",
+ "network": "No se puede conectar con AnyPlay. Comprueba tu conexión e inténtalo de nuevo.",
+ "internal": "AnyPlay tuvo un problema. Inténtalo de nuevo en un minuto.",
+ "unauthorized": "Tu sesión ha terminado. Inicia sesión de nuevo.",
+ "forbidden": "No puedes hacer eso con esta cuenta.",
+ "banned": "Esta cuenta está bloqueada.",
+ "not_found": "Eso ya no existe.",
+ "limit": "Has llegado al límite. Quita uno primero.",
+ "too_large": "Eso es demasiado grande para enviarlo.",
+ "code_wrong": "Ese código es incorrecto. Revísalo e inténtalo de nuevo.",
+ "code_expired": "Ese código ha caducado. Solicita uno nuevo.",
+ "handle_taken": "Ese nombre de usuario ya está en uso. Prueba con otro.",
+ "handle_invalid": "Usa de 3 a 16 letras, dígitos, guiones o guiones bajos. Empieza con una letra.",
+ "rate_limited": "Demasiados intentos. Espera un momento e inténtalo de nuevo.",
+ "rate_limited_wait": "Demasiados intentos. Espera {seconds, plural, one {# segundo} other {# segundos}} e inténtalo de nuevo.",
+ "passkey_unsupported": "Este navegador no admite passkeys. Usa el código por correo electrónico."
+ }
+}
diff --git a/apps/web/messages/fr/account.json b/apps/web/messages/fr/account.json
new file mode 100644
index 0000000..fc41a8b
--- /dev/null
+++ b/apps/web/messages/fr/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "jamais",
+ "on": "Activé",
+ "off": "Désactivé",
+ "cancel": "Annuler",
+ "working": "En cours…",
+ "typeToConfirm": "Saisis {word} pour confirmer",
+ "audience": {
+ "everyone": "Tout le monde",
+ "friends": "Amis",
+ "nobody": "Moi seul"
+ },
+ "stepUp": {
+ "title": "Confirme que c’est toi",
+ "needed": "Ce changement demande une connexion récente. Elle reste valable 15 minutes.",
+ "sent": "Nous avons envoyé un code à 6 chiffres à {hint}.",
+ "passkey": "Utilise ta clé d’accès",
+ "sending": "Envoi…",
+ "sendCode": "Envoyer un code par e-mail"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, accueil",
+ "signOut": "Se déconnecter",
+ "nav": "Compte",
+ "nav_overview": "Aperçu",
+ "nav_profile": "Profil",
+ "nav_friends": "Amis",
+ "nav_trophies": "Trophées",
+ "nav_security": "Sécurité",
+ "nav_notifications": "Notifications",
+ "nav_privacy": "Confidentialité",
+ "nav_connections": "Connexions",
+ "nav_data": "Tes données",
+ "unreachable": "Impossible de joindre AnyPlay. Vérifie ta connexion et réessaie.",
+ "loading": "Chargement"
+ },
+ "level": {
+ "top": "Niveau maximal atteint",
+ "toNext": "{count, plural, one {# point} other {# points}} pour le niveau {level}",
+ "level": "Niveau {level}",
+ "points": "{count, plural, one {# point} other {# points}}",
+ "progress": "Progression du niveau",
+ "progressTo": "Progression vers le niveau {level}"
+ },
+ "meta": {
+ "title": "Ton compte",
+ "template": "%s · Ton compte · AnyPlay"
+ },
+ "state": {
+ "loading": "Chargement",
+ "tryAgain": "Réessayer"
+ },
+ "overview": {
+ "label": "Ton compte",
+ "memberSince": "Membre depuis le {date}",
+ "appearingOffline": "Tu apparais hors ligne",
+ "deletion": "Ton compte sera supprimé le {date}. Garder mon compte",
+ "passkeys": "{count, plural, one {# clé d’accès} other {# clés d’accès}}",
+ "noPasskey": "Pas encore de clé d’accès",
+ "launchersBrowsers": "{launchers, plural, one {# launcher} other {# launchers}} · {browsers, plural, one {# navigateur} other {# navigateurs}}",
+ "trophiesAudience": "Trophées : {audience}",
+ "pictureAudience": "Photo de profil : {audience}",
+ "audience": {
+ "everyone": "Tout le monde",
+ "friends": "Amis",
+ "nobody": "Toi seul"
+ },
+ "audienceInline": {
+ "everyone": "tout le monde",
+ "friends": "amis",
+ "nobody": "toi seul"
+ },
+ "linked": "{count} liés",
+ "nothingLinked": "Rien de lié",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Exporter ou supprimer",
+ "dataDetail": "Tout ce que nous gardons sur toi",
+ "launcher": {
+ "title": "Récupère le launcher",
+ "description": "Connecte-toi au launcher avec ce compte, et tes amis, tes parties et tes trophées y sont."
+ }
+ },
+ "profile": {
+ "lead": "Ce que tes amis voient quand ils ouvrent ton profil. Qui voit chaque partie, tu le choisis sur la page Confidentialité.",
+ "avatar": {
+ "title": "Photo",
+ "description": "PNG ou JPEG, jusqu’à 1 Mo. Elle s’affiche en rond, alors garde le visage au centre.",
+ "uploading": "Envoi…",
+ "change": "Changer la photo",
+ "upload": "Envoyer une photo",
+ "remove": "Retirer"
+ },
+ "about": {
+ "title": "À propos",
+ "description": "Une ou deux lignes sur toi : à quoi tu joues, quand tu es là.",
+ "placeholder": "En coop quand je peux, en solo sinon.",
+ "saved": "Enregistré",
+ "saving": "Enregistrement…",
+ "save": "Enregistrer"
+ },
+ "favourites": {
+ "title": "Jeux favoris",
+ "description": "Jusqu’à {max}, par ID de titre. Ce site affiche l’ID. Le launcher affiche le nom de chaque jeu à partir de ta propre copie.",
+ "empty": "Pas encore de jeux favoris. Ajoute un ID de titre ci-dessous.",
+ "listLabel": "Tes jeux favoris",
+ "remove": "Retirer {id}",
+ "fieldLabel": "ID de titre",
+ "full": "Tu as {max} jeux. Retire-en un pour en ajouter un autre.",
+ "add": "Ajouter un jeu",
+ "saved": "Enregistré",
+ "saving": "Enregistrement…",
+ "save": "Enregistrer",
+ "badId": "Utilise uniquement des lettres, des chiffres, des traits d’union et des tirets bas.",
+ "duplicate": "{id} est déjà dans la liste."
+ }
+ },
+ "security": {
+ "lead": "Comment tu te connectes, et chaque launcher et navigateur connecté à ton compte.",
+ "email": {
+ "title": "E-mail",
+ "description": "L’adresse où arrivent tes codes de connexion. Pour la changer, il faut un code envoyé à la nouvelle adresse.",
+ "change": "Changer",
+ "invalid": "Saisis la nouvelle adresse, par exemple nom@exemple.com.",
+ "done": "Ton adresse de connexion est maintenant {email}.",
+ "newAddress": "Nouvelle adresse",
+ "sending": "Envoi…",
+ "send": "Envoyer le code",
+ "cancel": "Annuler",
+ "sentTo": "Nous avons envoyé un code à 6 chiffres à {email}."
+ },
+ "passkeys": {
+ "title": "Clés d’accès",
+ "description": "Connecte-toi avec ton empreinte, ton visage ou le code PIN de l’appareil au lieu d’un code. Personne ne peut hameçonner une clé d’accès.",
+ "add": "Ajouter une clé d’accès",
+ "waiting": "En attente de ton appareil…",
+ "detail": "Ajoutée {added} · Dernière utilisation : {used}",
+ "remove": "Retirer",
+ "empty": "Pas encore de clé d’accès. Ajoutes-en une et te connecter ne prend qu’une touche, au lieu d’un code par e-mail.",
+ "full": "Tu as le maximum de clés d’accès qu’un compte peut garder ({max}). Retires-en une pour en ajouter une autre.",
+ "confirmTitle": "Retirer cette clé d’accès ?",
+ "confirmDescription": "Cet appareil ne pourra plus te connecter avec elle. Tu peux toujours te connecter avec un code par e-mail.",
+ "confirmLabel": "Retirer la clé d’accès",
+ "defaultName": "Clé d’accès sur {system}",
+ "thisDevice": "cet appareil"
+ },
+ "devices": {
+ "title": "Launchers",
+ "description": "Chaque PC où le launcher est connecté à ton compte.",
+ "detail": "{platform} · Dernière utilisation : {used}",
+ "signOut": "Se déconnecter",
+ "empty": "Aucun launcher n’est encore connecté.",
+ "confirmTitle": "Déconnecter ce launcher ?",
+ "confirmDescription": "Il passe hors ligne jusqu’à ce que quelqu’un se reconnecte sur ce PC.",
+ "confirmLabel": "Déconnecter le launcher"
+ },
+ "browsers": {
+ "title": "Navigateurs",
+ "description": "Chaque navigateur connecté à cette page de compte. Nous ne gardons rien d’autre à leur sujet que le moment où ils ont servi.",
+ "thisBrowser": "Ce navigateur",
+ "here": "Tu es ici",
+ "other": "Autre navigateur",
+ "detail": "Connecté {created} · Dernière utilisation : {used}",
+ "signOut": "Se déconnecter",
+ "confirmThisTitle": "Se déconnecter de ce navigateur ?",
+ "confirmOtherTitle": "Déconnecter ce navigateur ?",
+ "confirmThisDescription": "Tu reviens à l’accueil et tu te reconnectes la prochaine fois.",
+ "confirmOtherDescription": "Il doit se reconnecter pour ouvrir ton compte.",
+ "confirmLabel": "Se déconnecter"
+ },
+ "everywhere": {
+ "title": "Se déconnecter partout",
+ "description": "Chaque launcher et chaque navigateur, celui-ci compris. Utilise-le si un appareil est perdu ou si quelqu’un d’autre a pu se connecter.",
+ "button": "Se déconnecter partout",
+ "confirmTitle": "Se déconnecter partout ?",
+ "confirmDescription": "Chaque launcher et chaque navigateur est déconnecté d’un coup, celui-ci compris.",
+ "confirmLabel": "Se déconnecter partout"
+ }
+ },
+ "privacy": {
+ "lead": "Qui voit ton profil et ton activité. Ton nom est toujours visible, pour que tes amis puissent te trouver.",
+ "profile": {
+ "title": "Ton profil",
+ "description": "Les changements comptent tout de suite, dans le launcher aussi."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Trophées",
+ "detail": "Ton nombre de trophées, tes points et ton niveau"
+ },
+ "playtime": {
+ "title": "Temps de jeu",
+ "detail": "Combien de temps tu as joué, par jeu"
+ },
+ "about": {
+ "title": "À propos",
+ "detail": "La ligne de ton profil"
+ },
+ "favourites": {
+ "title": "Jeux favoris",
+ "detail": "Jusqu’à trois jeux que tu épingles"
+ },
+ "avatar": {
+ "title": "Photo",
+ "detail": "Ta photo de profil"
+ }
+ },
+ "whoSees": "Qui voit : {part}",
+ "activity": {
+ "title": "Ton activité",
+ "description": "Ce que tu fais sur AnyPlay, tel que tes amis le voient dans leur fil.",
+ "row": "Activité",
+ "detail": "Tes amis la voient dans leur fil. Les inconnus ne voient aucun fil. Choisis « {nobody} » pour masquer ce qui s’y trouve et arrêter les nouveaux éléments."
+ },
+ "appearOffline": {
+ "title": "Statut en ligne",
+ "description": "S’applique à chaque launcher et navigateur connecté à ton compte.",
+ "label": "Apparaître hors ligne",
+ "detail": "Tes amis te voient hors ligne. Tu peux quand même jouer, discuter et rejoindre."
+ },
+ "publicProfile": {
+ "title": "Profil public",
+ "description": "Désactivé tant que tu ne l’actives pas. S’applique à chaque launcher et navigateur connecté à ton compte.",
+ "label": "Afficher un profil public",
+ "detail": "Toute personne qui a le lien voit ton niveau, tes trophées et les jeux auxquels tu as joué. Il n’indique jamais si tu es en ligne.",
+ "page": "Ta page : {url}"
+ },
+ "playedWith": {
+ "title": "Joué récemment avec",
+ "description": "Si tu le désactives, aucune nouvelle personne n’est ajoutée. La liste reste jusqu’à ce que tu la vides.",
+ "label": "Retenir avec qui je joue",
+ "detail": "Garde pendant 30 jours les personnes avec qui tu as partagé une party ou une session de jeu, jusqu’à 50. Toi seul vois la liste.",
+ "clear": "Vider la liste",
+ "cleared": "La liste est vide.",
+ "confirmTitle": "Vider la liste ?",
+ "confirmDescription": "Tout le monde est retiré de ta liste des personnes avec qui tu as joué. La liste des autres ne change pas, et tu ne peux pas annuler."
+ }
+ },
+ "connections": {
+ "lead": "Lie les comptes avec lesquels tu joues déjà, pour que tes amis te trouvent et voient où tu es aussi.",
+ "result": {
+ "linked": "{name} est lié à ton compte.",
+ "denied": "{name} n’a pas été lié : la demande a été refusée là-bas.",
+ "expired": "La liaison de {name} a pris trop de temps. Recommence.",
+ "fresh": "La liaison de {name} demande une connexion récente. Recommence et confirme que c’est bien toi.",
+ "taken": "Ce compte {name} est déjà lié à un autre compte AnyPlay.",
+ "exists": "Tu as déjà un compte {name} lié. Dissocie-le d’abord.",
+ "failed": "La liaison de {name} a échoué. Réessaie dans une minute."
+ },
+ "whoSees": "Qui voit ton compte {name}",
+ "linkedAgo": "Lié {ago}",
+ "unlink": "Dissocier",
+ "link": "Lier {name}",
+ "opening": "Ouverture…",
+ "soon": "Bientôt",
+ "notLinked": "Non lié",
+ "arrivesSoon": "La liaison arrive bientôt",
+ "confirmTitle": "Dissocier {name} ?",
+ "confirmDescription": "Ton compte {name} n’apparaît plus sur ton profil. Tu peux le lier de nouveau quand tu veux.",
+ "confirmLabel": "Dissocier {name}"
+ },
+ "data": {
+ "lead": "Tout ce qu’AnyPlay garde sur toi est à toi : prends-en une copie, ou supprime le compte.",
+ "export": {
+ "title": "Télécharger tes données",
+ "description": "Ton compte, ton profil, tes amis, tes trophées, ton temps de jeu et tes comptes liés, dans un seul fichier JSON.",
+ "preparing": "Préparation…",
+ "button": "Télécharger"
+ },
+ "scheduled": {
+ "title": "Suppression programmée",
+ "description": "Ton compte et tout ce qu’il contient seront supprimés le {date}. D’ici là, tu peux le garder.",
+ "keep": "Garder mon compte"
+ },
+ "delete": {
+ "title": "Supprimer ton compte",
+ "description": "Ton compte est supprimé au bout de 30 jours ; d’ici là, tu peux changer d’avis. Ensuite, il a disparu, avec tes amis, tes trophées et ton temps de jeu.",
+ "button": "Supprimer le compte"
+ },
+ "confirm": {
+ "title": "Supprimer ton compte ?",
+ "description": "Il est supprimé dans 30 jours. Chaque launcher et chaque navigateur est déconnecté maintenant."
+ }
+ },
+ "compare": {
+ "title": "Comparer les trophées",
+ "description": "Tes trophées à côté de ceux de {handle}.",
+ "descriptionShared": "Toi et {handle}, dans un jeu que vous avez tous les deux.",
+ "signInDescription": "Vois comment tes trophées se comparent à ceux de {handle}.",
+ "signIn": "Connecte-toi pour comparer les trophées",
+ "hidden": "Les trophées de {handle} ne sont pas disponibles pour la comparaison.",
+ "loading": "Chargement",
+ "loadingComparison": "Chargement de la comparaison",
+ "empty": "Toi et {handle}, vous n’avez pas encore de jeu en commun. Joue à un jeu auquel {handle} a joué et il apparaîtra ici.",
+ "game": "Jeu",
+ "gameNamed": "Jeu {title}",
+ "verdictNone": "Aucun de vous n’a encore obtenu de trophée dans ce jeu.",
+ "verdictLevel": "Vous êtes à égalité de points dans ce jeu.",
+ "verdictAhead": "Tu as {count, plural, one {# point} other {# points}} d’avance dans ce jeu.",
+ "verdictBehind": "{handle} a {count, plural, one {# point} other {# points}} d’avance dans ce jeu.",
+ "caption": "Trophées dans {titleId} : toi à côté de {handle}",
+ "stat": "Catégorie",
+ "you": "Toi",
+ "ahead": "(devant)",
+ "rowTrophies": "Trophées",
+ "rowPoints": "Points dans ce jeu",
+ "rowLevel": "Niveau global",
+ "grade": {
+ "platinum": "Platine",
+ "gold": "Or",
+ "silver": "Argent",
+ "bronze": "Bronze"
+ },
+ "both": "Obtenus par les deux",
+ "onlyYou": "Toi seul",
+ "onlyThem": "{handle} seul"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, accueil",
+ "yourAccount": "Ton compte",
+ "signIn": "Se connecter",
+ "loading": "Chargement",
+ "missingTitle": "Aucun joueur nommé {handle}",
+ "missingBody": "Vérifie l’orthographe du pseudo ou demande au joueur de te le renvoyer.",
+ "goHome": "Aller sur AnyPlay",
+ "player": "Joueur",
+ "trophies": "Trophées",
+ "trophiesEarned": "{count, number} obtenus.",
+ "noTrophies": "{handle} n’a pas encore obtenu de trophée.",
+ "favourites": "Jeux favoris",
+ "favouritesNote": "Affichés par ID de titre.",
+ "game": "Jeu",
+ "connections": "Connexions",
+ "playtime": "Jeux joués",
+ "playtimeNote": "{count, plural, one {# jeu} other {# jeux}}, {total} au total.",
+ "nothing": "Il n’y a encore rien d’autre à montrer de {handle}.",
+ "openProfile": "Ouvrir le profil",
+ "openProfileOn": "sur {brand} (s’ouvre dans un nouvel onglet)",
+ "signInToAdd": "Connecte-toi pour ajouter {handle}",
+ "editProfile": "Modifier ton profil",
+ "blocked": "Bloqué",
+ "friends": "Amis",
+ "requestSent": "Demande envoyée",
+ "accepting": "Acceptation…",
+ "accept": "Accepter la demande",
+ "sending": "Envoi…",
+ "addFriend": "Ajouter un ami",
+ "addInAnyPlay": "Ajouter un ami dans AnyPlay",
+ "minutes": "{n} min",
+ "hours": "{n} h"
+ },
+ "friends": {
+ "title": "Amis",
+ "lead": "Ajoute les personnes avec qui tu joues, réponds aux demandes et décide qui reste dehors.",
+ "add": {
+ "title": "Ajouter un ami",
+ "description": "Saisis le nom d’un joueur pour lui envoyer une demande.",
+ "handle": "Nom",
+ "hint": "Son nom, avec ou sans le @.",
+ "send": "Envoyer la demande",
+ "sending": "Envoi…",
+ "enterHandle": "Saisis le nom du joueur que tu veux ajouter.",
+ "nowFriends": "Toi et @{handle} êtes maintenant amis.",
+ "sent": "Demande envoyée à @{handle}. Vous serez amis dès qu’elle sera acceptée."
+ },
+ "requests": {
+ "title": "Demandes",
+ "description": "Les joueurs qui veulent devenir tes amis et les demandes que tu as envoyées.",
+ "empty": "Aucune demande en cours. Quand quelqu’un t’ajoute, la demande apparaît ici.",
+ "received": "Reçues",
+ "sent": "Envoyées",
+ "receivedAgo": "Reçue {when}",
+ "sentAgo": "Envoyée {when}",
+ "decline": "Refuser",
+ "accept": "Accepter",
+ "withdraw": "Retirer"
+ },
+ "list": {
+ "title": "Tes amis",
+ "description": "Vois qui est en ligne et à quoi la personne joue.",
+ "empty": "Pas encore d’amis. Ajoute quelqu’un par son nom ci-dessus ; la personne apparaît ici une fois la demande acceptée.",
+ "online": "En ligne",
+ "offline": "Hors ligne",
+ "playing": "En train de jouer à {title}",
+ "block": "Bloquer",
+ "remove": "Retirer"
+ },
+ "blocked": {
+ "title": "Bloqués",
+ "description": "Les joueurs bloqués sont retirés de tes amis et de tes demandes.",
+ "empty": "Tu n’as bloqué personne. Bloque un joueur depuis ta liste d’amis quand tu en as besoin.",
+ "unblock": "Débloquer"
+ },
+ "confirm": {
+ "blockTitle": "Bloquer @{handle} ?",
+ "removeTitle": "Retirer @{handle} de tes amis ?",
+ "blockText": "Vous n’êtes plus amis et toute demande en cours est supprimée. Tu peux débloquer cette personne à tout moment.",
+ "removeText": "Vous ne verrez plus quand l’autre est en ligne. Tu pourras envoyer une nouvelle demande plus tard.",
+ "blockLabel": "Bloquer le joueur",
+ "removeLabel": "Retirer l’ami"
+ }
+ },
+ "trophies": {
+ "title": "Trophées",
+ "lead": "Ce que tu as obtenu dans tes jeux. AnyPlay garde des totaux par jeu et par niveau, jamais les noms ni les visuels des trophées.",
+ "totals": {
+ "title": "Totaux",
+ "description": "{total} obtenus dans {games, plural, one {# jeu} other {# jeux}}.",
+ "empty": "Pas encore de trophées. Joue à un jeu avec AnyPlay et ceux que tu obtiens sont comptés ici."
+ },
+ "byGame": {
+ "title": "Par jeu",
+ "description": "Les jeux sont classés par ID de titre, les plus récents d’abord.",
+ "detail": "{earned, plural, one {# trophée} other {# trophées}} · dernier obtenu : {when}"
+ },
+ "grades": {
+ "platinum": "Platine",
+ "gold": "Or",
+ "silver": "Argent",
+ "bronze": "Bronze"
+ }
+ },
+ "notifications": {
+ "title": "Notifications",
+ "lead": "Reçois une notification sur cet appareil quand un ami t’envoie une demande, t’invite dans une party ou t’écrit.",
+ "device": {
+ "title": "Cet appareil",
+ "description": "Chaque navigateur ou téléphone se règle séparément. Active l’option là où tu veux être prévenu.",
+ "notEnabled": "Ce cloud n’envoie pas de notifications.",
+ "checking": "Vérification de cet appareil…",
+ "unsupported": "Ce navigateur ne peut pas recevoir de notifications. Utilise une version récente de Chrome, Edge, Firefox ou Safari.",
+ "needsInstall": "Sur iPhone et iPad, les notifications demandent qu’AnyPlay soit sur ton écran d’accueil. Touche Partager, choisis Sur l’écran d’accueil, puis ouvre AnyPlay depuis là.",
+ "blocked": "Les notifications sont bloquées pour ce site. Autorise-les dans les réglages du site de ton navigateur, puis reviens.",
+ "switch": "Notifier cet appareil",
+ "others": "{count, plural, one {Un autre appareil reçoit aussi les notifications. Désactive-les sur cet appareil.} other {# autres appareils reçoivent aussi les notifications. Désactive-les sur ces appareils.}}"
+ },
+ "kinds": {
+ "title": "Sujets des notifications",
+ "description": "Pour cet appareil uniquement.",
+ "friend_request": {
+ "label": "Demandes d’ami",
+ "detail": "Quelqu’un veut devenir ton ami."
+ },
+ "party_invite": {
+ "label": "Invitations à une party",
+ "detail": "Un ami t’invite dans une party."
+ },
+ "message": {
+ "label": "Messages",
+ "detail": "Un ami t’écrit. Le message lui-même n’apparaît pas dans la notification."
+ }
+ },
+ "errors": {
+ "timeout": "Le service de notifications de ton navigateur n’a pas répondu. Vérifie ta connexion et réessaie.",
+ "noKey": "Ce cloud n’a pas de clé pour envoyer des notifications.",
+ "lostSubscription": "Ce navigateur a perdu son abonnement. Désactive les notifications puis réactive-les.",
+ "notAllowed": "Ce navigateur n’a pas autorisé les notifications. Elles ne fonctionnent pas en navigation privée ; sinon, vérifie les réglages du site."
+ }
+ }
+}
diff --git a/apps/web/messages/fr/auth.json b/apps/web/messages/fr/auth.json
new file mode 100644
index 0000000..b56c31d
--- /dev/null
+++ b/apps/web/messages/fr/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Se connecter",
+ "description": "Connecte-toi à AnyPlay avec une clé d’accès ou un code reçu par e-mail."
+ },
+ "create": {
+ "title": "Crée ton compte",
+ "description": "Crée ton compte AnyPlay : une adresse e-mail et un nom."
+ },
+ "link": {
+ "title": "Ouvre ton compte",
+ "description": "Ouvre ton compte AnyPlay dans ce navigateur, depuis le launcher."
+ },
+ "join": {
+ "title": "Invitation",
+ "description": "Une invitation à jouer ensemble sur AnyPlay."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, accueil",
+ "back": "Retour à l’accueil",
+ "promise": "Joue à tes jeux ensemble.",
+ "toastOnline": "mira est en ligne",
+ "toastParty": "kit a rejoint ta party",
+ "toastTrophy": "Trophée obtenu · Into the Ember",
+ "help": "Besoin d’aide ?",
+ "helpLink": "Demande sur Discord",
+ "legalNav": "Mentions légales",
+ "legalNotice": "Mentions légales",
+ "privacyNotice": "Avis de confidentialité",
+ "termsOfUse": "Conditions d’utilisation"
+ },
+ "step": {
+ "loading": "Chargement",
+ "unreachable": "Impossible de joindre AnyPlay",
+ "tryAgain": "Réessayer",
+ "newTab": "(s’ouvre dans un nouvel onglet)",
+ "backToStart": "Retour à l’accueil",
+ "signIn": "Se connecter",
+ "createAccount": "Créer un compte",
+ "getApp": "Obtenir AnyPlay",
+ "continue": "Continuer"
+ },
+ "fields": {
+ "code": "Code"
+ },
+ "flow": {
+ "authorize": "AnyPlay sur {device} attend cette connexion."
+ },
+ "email": {
+ "create": {
+ "title": "Crée ton compte",
+ "lead": "Commence par ton adresse e-mail. Nous t’envoyons un code pour la confirmer."
+ },
+ "signIn": {
+ "title": "Se connecter",
+ "lead": "Utilise ta clé d’accès ou reçois un code par e-mail."
+ },
+ "passkey": "Se connecter avec une clé d’accès",
+ "or": "ou",
+ "label": "Adresse e-mail",
+ "placeholder": "name@example.com",
+ "invalid": "Saisis ton adresse e-mail, par exemple name@example.com.",
+ "sending": "Envoi…",
+ "send": "Envoyer le code",
+ "haveAccount": "Tu as déjà un compte ? Se connecter",
+ "newHere": "Nouveau sur AnyPlay ? Créer un compte"
+ },
+ "code": {
+ "title": "Regarde tes e-mails",
+ "lead": "Nous avons envoyé un code à 6 chiffres à {email}. Il est valable 10 minutes.",
+ "incomplete": "Saisis les 6 chiffres de l’e-mail.",
+ "checking": "Vérification…",
+ "resent": "Un nouveau code est en route.",
+ "noMail": "Pas d’e-mail ?",
+ "resendIn": "Envoyer un nouveau code dans {seconds} s",
+ "resend": "Envoyer un nouveau code"
+ },
+ "details": {
+ "title": "Choisis un nom",
+ "lead": "Ton adresse {email} est confirmée. Les autres joueurs voient ton nom.",
+ "handle": "Nom",
+ "handleHint": "Utilise de 3 à 16 lettres, chiffres, tirets ou tirets bas. Commence par une lettre.",
+ "ageHint": "C’est-à-dire 16 ans ou plus.",
+ "age": "J’ai l’âge d’accepter les conditions par moi-même.",
+ "terms": "J’accepte les conditions (version {version}) et j’ai lu l’avis de confidentialité.",
+ "creating": "Création…",
+ "submit": "Créer un compte"
+ },
+ "terms": {
+ "title": "Relis les conditions",
+ "lead": "Les conditions ont changé. Lis les conditions et accepte la version {version} pour continuer.",
+ "accept": "J’accepte les conditions (version {version}).",
+ "required": "Accepte les conditions pour continuer.",
+ "submit": "Accepter et continuer"
+ },
+ "banned": {
+ "title": "Ce compte est banni",
+ "leadUntil": "Le bannissement prend fin le {date}.",
+ "leadForever": "Le bannissement ne prend pas fin de lui-même.",
+ "rule": "Règle",
+ "reason": "Motif",
+ "appeal": "Lire la décision et faire appel"
+ },
+ "handoff": {
+ "title": "Connecté",
+ "lead": "Nous te ramenons vers AnyPlay. Si rien ne se passe, utilise le bouton.",
+ "open": "Ouvrir AnyPlay",
+ "note": "Tu peux fermer cet onglet dès qu’AnyPlay t’affiche comme connecté."
+ },
+ "done": {
+ "title": "Tu es connecté",
+ "inviteLead": "Nous te ramenons à l’invitation.",
+ "welcome": "Bienvenue, {handle}. Tes amis, tes partys et tes trophées t’attendent dans le launcher.",
+ "account": "Aller à ton compte"
+ },
+ "join": {
+ "expiredTitle": "Cette invitation a expiré",
+ "expiredLead": "Le lien a peut-être expiré, ou le joueur a arrêté de jouer. Demande-en un nouveau.",
+ "title": "{handle} t’a envoyé une invitation",
+ "leadSignedIn": "Ouvre AnyPlay sur ton PC pour voir l’invitation. C’est là que tu choisis de rejoindre ou non.",
+ "leadSignedOut": "Connecte-toi ou crée un compte d’abord. Tu reviens ensuite ici pour ouvrir l’invitation.",
+ "party": "Party",
+ "game": "Jeu",
+ "partyName": "Une party AnyPlay",
+ "gameFallback": "Un jeu",
+ "joined": "Ont rejoint",
+ "players": "{members} sur {max, plural, one {# joueur} other {# joueurs}}",
+ "open": "Ouvrir dans AnyPlay",
+ "noApp": "Pas encore d’AnyPlay sur ce PC ? Obtiens-le d’abord, puis rouvre cette page."
+ },
+ "link": {
+ "expiredTitle": "Ce lien a expiré",
+ "expiredLead": "Rouvre ta page de compte depuis AnyPlay.",
+ "close": "Tu peux fermer cet onglet.",
+ "title": "Ouvre ton compte",
+ "lead": "AnyPlay sur ton PC demande d’ouvrir le compte de {handle} dans ce navigateur.",
+ "open": "Ouvrir le compte",
+ "opening": "Ouverture…",
+ "notYou": "Ce n’est pas toi ? Ferme l’onglet."
+ }
+}
diff --git a/apps/web/messages/fr/common.json b/apps/web/messages/fr/common.json
new file mode 100644
index 0000000..214b826
--- /dev/null
+++ b/apps/web/messages/fr/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Une erreur s’est produite. Réessaie.",
+ "network": "Impossible de joindre AnyPlay. Vérifie ta connexion et réessaie.",
+ "internal": "AnyPlay a rencontré un problème. Réessaie dans une minute.",
+ "unauthorized": "Ta session est terminée. Reconnecte-toi.",
+ "forbidden": "Tu ne peux pas faire cela avec ce compte.",
+ "banned": "Ce compte est banni.",
+ "not_found": "Cela n’existe plus.",
+ "limit": "Tu as atteint la limite. Supprime-en un d’abord.",
+ "too_large": "C’est trop volumineux pour être envoyé.",
+ "code_wrong": "Ce code est incorrect. Vérifie-le et réessaie.",
+ "code_expired": "Ce code a expiré. Demande-en un nouveau.",
+ "handle_taken": "Ce pseudo est déjà pris. Essaie-en un autre.",
+ "handle_invalid": "Utilise de 3 à 16 lettres, chiffres, tirets ou tirets bas. Commence par une lettre.",
+ "rate_limited": "Trop d’essais. Attends un instant et réessaie.",
+ "rate_limited_wait": "Trop d’essais. Attends {seconds, plural, one {# seconde} other {# secondes}} et réessaie.",
+ "passkey_unsupported": "Ce navigateur ne gère pas les clés d’accès. Utilise plutôt le code reçu par e-mail."
+ }
+}
diff --git a/apps/web/messages/pt/account.json b/apps/web/messages/pt/account.json
new file mode 100644
index 0000000..8f80d5e
--- /dev/null
+++ b/apps/web/messages/pt/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "nunca",
+ "on": "Ativado",
+ "off": "Desativado",
+ "cancel": "Cancelar",
+ "working": "Processando…",
+ "typeToConfirm": "Digite {word} para confirmar",
+ "audience": {
+ "everyone": "Todos",
+ "friends": "Amigos",
+ "nobody": "Só eu"
+ },
+ "stepUp": {
+ "title": "Confirme que é você",
+ "needed": "Esta alteração exige um login recente. Ele vale por 15 minutos.",
+ "sent": "Enviamos um código de 6 dígitos para {hint}.",
+ "passkey": "Use sua chave de acesso",
+ "sending": "Enviando…",
+ "sendCode": "Enviar um código por e-mail"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, início",
+ "signOut": "Sair",
+ "nav": "Conta",
+ "nav_overview": "Visão geral",
+ "nav_profile": "Perfil",
+ "nav_friends": "Amigos",
+ "nav_trophies": "Troféus",
+ "nav_security": "Segurança",
+ "nav_notifications": "Notificações",
+ "nav_privacy": "Privacidade",
+ "nav_connections": "Conexões",
+ "nav_data": "Seus dados",
+ "unreachable": "Não foi possível acessar o AnyPlay. Verifique sua conexão e tente de novo.",
+ "loading": "Carregando"
+ },
+ "level": {
+ "top": "Nível máximo alcançado",
+ "toNext": "{count, plural, one {# ponto} other {# pontos}} para o nível {level}",
+ "level": "Nível {level}",
+ "points": "{count, plural, one {# ponto} other {# pontos}}",
+ "progress": "Progresso de nível",
+ "progressTo": "Progresso para o nível {level}"
+ },
+ "meta": {
+ "title": "Sua conta",
+ "template": "%s · Sua conta · AnyPlay"
+ },
+ "state": {
+ "loading": "Carregando",
+ "tryAgain": "Tentar de novo"
+ },
+ "overview": {
+ "label": "Sua conta",
+ "memberSince": "Membro desde {date}",
+ "appearingOffline": "Você aparece offline",
+ "deletion": "Sua conta será excluída em {date}. Manter minha conta",
+ "passkeys": "{count, plural, one {# chave de acesso} other {# chaves de acesso}}",
+ "noPasskey": "Nenhuma chave de acesso ainda",
+ "launchersBrowsers": "{launchers, plural, one {# launcher} other {# launchers}} · {browsers, plural, one {# navegador} other {# navegadores}}",
+ "trophiesAudience": "Troféus: {audience}",
+ "pictureAudience": "Foto de perfil: {audience}",
+ "audience": {
+ "everyone": "Todos",
+ "friends": "Amigos",
+ "nobody": "Só você"
+ },
+ "audienceInline": {
+ "everyone": "todos",
+ "friends": "amigos",
+ "nobody": "só você"
+ },
+ "linked": "{count} vinculadas",
+ "nothingLinked": "Nada vinculado",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Exportar ou excluir",
+ "dataDetail": "Tudo o que guardamos sobre você",
+ "launcher": {
+ "title": "Baixe o launcher",
+ "description": "Entre no launcher com esta conta, e seus amigos, parties e troféus estarão lá."
+ }
+ },
+ "profile": {
+ "lead": "O que seus amigos veem quando abrem seu perfil. Quem vê cada parte é você quem decide, na página Privacidade.",
+ "avatar": {
+ "title": "Foto",
+ "description": "PNG ou JPEG, de até 1 MB. Ela aparece redonda, então deixe o rosto no meio.",
+ "uploading": "Enviando…",
+ "change": "Alterar foto",
+ "upload": "Enviar foto",
+ "remove": "Remover"
+ },
+ "about": {
+ "title": "Sobre",
+ "description": "Uma ou duas linhas sobre você: o que você joga, quando está por aqui.",
+ "placeholder": "Em co-op quando dá, sozinho quando não dá.",
+ "saved": "Salvo",
+ "saving": "Salvando…",
+ "save": "Salvar"
+ },
+ "favourites": {
+ "title": "Jogos favoritos",
+ "description": "Até {max}, por ID do título. Este site mostra o ID. O launcher mostra o nome de cada jogo a partir da sua própria cópia.",
+ "empty": "Nenhum jogo favorito ainda. Adicione um ID de título abaixo.",
+ "listLabel": "Seus jogos favoritos",
+ "remove": "Remover {id}",
+ "fieldLabel": "ID do título",
+ "full": "Você tem {max} jogos. Remova um para adicionar outro.",
+ "add": "Adicionar jogo",
+ "saved": "Salvo",
+ "saving": "Salvando…",
+ "save": "Salvar",
+ "badId": "Use apenas letras, números, hifens e sublinhados.",
+ "duplicate": "{id} já está na lista."
+ }
+ },
+ "security": {
+ "lead": "Como você entra e todos os launchers e navegadores conectados à sua conta.",
+ "email": {
+ "title": "E-mail",
+ "description": "Para onde vão seus códigos de entrada. Para mudar, é preciso um código enviado ao novo endereço.",
+ "change": "Alterar",
+ "invalid": "Digite o novo endereço, como nome@exemplo.com.",
+ "done": "Seu endereço de entrada agora é {email}.",
+ "newAddress": "Novo endereço",
+ "sending": "Enviando…",
+ "send": "Enviar código",
+ "cancel": "Cancelar",
+ "sentTo": "Enviamos um código de 6 dígitos para {email}."
+ },
+ "passkeys": {
+ "title": "Chaves de acesso",
+ "description": "Entre com sua digital, seu rosto ou o PIN do dispositivo, em vez de um código. Ninguém consegue aplicar phishing em uma chave de acesso.",
+ "add": "Adicionar uma chave de acesso",
+ "waiting": "Aguardando seu dispositivo…",
+ "detail": "Adicionada {added} · Último uso: {used}",
+ "remove": "Remover",
+ "empty": "Nenhuma chave de acesso ainda. Adicione uma e entrar vira um toque, em vez de um código por e-mail.",
+ "full": "Você tem o máximo de chaves de acesso que uma conta pode guardar ({max}). Remova uma para adicionar outra.",
+ "confirmTitle": "Remover esta chave de acesso?",
+ "confirmDescription": "Esse dispositivo não poderá mais entrar na sua conta com ela. Você sempre pode entrar com um código por e-mail.",
+ "confirmLabel": "Remover chave de acesso",
+ "defaultName": "Chave de acesso em {system}",
+ "thisDevice": "este dispositivo"
+ },
+ "devices": {
+ "title": "Launchers",
+ "description": "Todo PC em que o launcher está conectado à sua conta.",
+ "detail": "{platform} · Último uso: {used}",
+ "signOut": "Sair",
+ "empty": "Nenhum launcher conectado ainda.",
+ "confirmTitle": "Desconectar este launcher?",
+ "confirmDescription": "Ele fica offline até alguém entrar de novo nesse PC.",
+ "confirmLabel": "Desconectar launcher"
+ },
+ "browsers": {
+ "title": "Navegadores",
+ "description": "Todo navegador conectado a esta página da conta. Guardamos sobre eles apenas quando foram usados.",
+ "thisBrowser": "Este navegador",
+ "here": "Você está aqui",
+ "other": "Outro navegador",
+ "detail": "Entrou {created} · Último uso: {used}",
+ "signOut": "Sair",
+ "confirmThisTitle": "Sair deste navegador?",
+ "confirmOtherTitle": "Desconectar aquele navegador?",
+ "confirmThisDescription": "Você volta ao início e entra de novo na próxima vez.",
+ "confirmOtherDescription": "Ele precisa entrar de novo para abrir sua conta.",
+ "confirmLabel": "Sair"
+ },
+ "everywhere": {
+ "title": "Sair em todos os lugares",
+ "description": "Todo launcher e todo navegador, este também. Use quando um dispositivo for perdido ou alguém mais puder ter entrado.",
+ "button": "Sair em todos os lugares",
+ "confirmTitle": "Sair em todos os lugares?",
+ "confirmDescription": "Todo launcher e navegador é desconectado de uma vez, este também.",
+ "confirmLabel": "Sair em todos os lugares"
+ }
+ },
+ "privacy": {
+ "lead": "Quem vê seu perfil e sua atividade. Seu nome sempre fica visível, para que seus amigos encontrem você.",
+ "profile": {
+ "title": "Seu perfil",
+ "description": "As mudanças valem na hora, no launcher também."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Troféus",
+ "detail": "Sua contagem de troféus, seus pontos e seu nível"
+ },
+ "playtime": {
+ "title": "Tempo de jogo",
+ "detail": "Quanto você jogou, por jogo"
+ },
+ "about": {
+ "title": "Sobre",
+ "detail": "A linha do seu perfil"
+ },
+ "favourites": {
+ "title": "Jogos favoritos",
+ "detail": "Até três jogos que você fixa"
+ },
+ "avatar": {
+ "title": "Foto",
+ "detail": "Sua foto de perfil"
+ }
+ },
+ "whoSees": "Quem vê: {part}",
+ "activity": {
+ "title": "Sua atividade",
+ "description": "O que você faz no AnyPlay, como seus amigos veem no feed deles.",
+ "row": "Atividade",
+ "detail": "Seus amigos veem no feed deles. Desconhecidos não veem feed. Escolha “{nobody}” para esconder o que está lá e parar novos itens."
+ },
+ "appearOffline": {
+ "title": "Status online",
+ "description": "Vale para todo launcher e navegador conectado à sua conta.",
+ "label": "Aparecer offline",
+ "detail": "Seus amigos veem você offline. Você ainda pode jogar, conversar e entrar."
+ },
+ "publicProfile": {
+ "title": "Perfil público",
+ "description": "Desativado até você ativar. Vale para todo launcher e navegador conectado à sua conta.",
+ "label": "Mostrar um perfil público",
+ "detail": "Qualquer pessoa com o link vê seu nível, seus troféus e os jogos que você jogou. Nunca mostra se você está online.",
+ "page": "Sua página: {url}"
+ },
+ "playedWith": {
+ "title": "Jogadores recentes",
+ "description": "Se você desativar, ninguém novo é adicionado. A lista fica até você esvaziá-la.",
+ "label": "Lembrar com quem eu jogo",
+ "detail": "Guarda por 30 dias as pessoas com quem você dividiu uma party ou uma sessão de jogo, até 50. Só você vê a lista.",
+ "clear": "Esvaziar a lista",
+ "cleared": "A lista está vazia.",
+ "confirmTitle": "Esvaziar a lista?",
+ "confirmDescription": "Todos saem da sua lista de pessoas com quem você jogou. A lista de outras pessoas não muda, e você não pode desfazer isso."
+ }
+ },
+ "connections": {
+ "lead": "Vincule as contas com que você já joga, para que seus amigos encontrem você e vejam onde mais você está.",
+ "result": {
+ "linked": "{name} está vinculado à sua conta.",
+ "denied": "{name} não foi vinculado: o pedido foi recusado lá.",
+ "expired": "Vincular {name} demorou demais. Comece de novo.",
+ "fresh": "Vincular {name} exige uma entrada recente. Comece de novo e confirme que é você.",
+ "taken": "Essa conta {name} já está vinculada a outra conta do AnyPlay.",
+ "exists": "Você já tem uma conta {name} vinculada. Desvincule-a primeiro.",
+ "failed": "Não foi possível vincular {name}. Tente de novo em um minuto."
+ },
+ "whoSees": "Quem vê sua conta {name}",
+ "linkedAgo": "Vinculada {ago}",
+ "unlink": "Desvincular",
+ "link": "Vincular {name}",
+ "opening": "Abrindo…",
+ "soon": "Em breve",
+ "notLinked": "Não vinculada",
+ "arrivesSoon": "A vinculação chega em breve",
+ "confirmTitle": "Desvincular {name}?",
+ "confirmDescription": "Sua conta {name} deixa de aparecer no seu perfil. Você pode vinculá-la de novo quando quiser.",
+ "confirmLabel": "Desvincular {name}"
+ },
+ "data": {
+ "lead": "Tudo o que o AnyPlay guarda sobre você é seu: leve uma cópia ou exclua a conta.",
+ "export": {
+ "title": "Baixar seus dados",
+ "description": "Sua conta, seu perfil, amigos, troféus, tempo de jogo e contas vinculadas, em um único arquivo JSON.",
+ "preparing": "Preparando…",
+ "button": "Baixar"
+ },
+ "scheduled": {
+ "title": "Exclusão agendada",
+ "description": "Sua conta e tudo que há nela serão excluídos em {date}. Até lá, você pode mantê-la.",
+ "keep": "Manter minha conta"
+ },
+ "delete": {
+ "title": "Excluir sua conta",
+ "description": "Sua conta é excluída após 30 dias; até lá, você pode mudar de ideia. Depois disso ela some, junto com seus amigos, troféus e tempo de jogo.",
+ "button": "Excluir conta"
+ },
+ "confirm": {
+ "title": "Excluir sua conta?",
+ "description": "Ela é excluída em 30 dias. Todo launcher e navegador é desconectado agora."
+ }
+ },
+ "compare": {
+ "title": "Comparar troféus",
+ "description": "Seus troféus ao lado dos de {handle}.",
+ "descriptionShared": "Você e {handle}, em um jogo que vocês dois têm.",
+ "signInDescription": "Veja como seus troféus se comparam aos de {handle}.",
+ "signIn": "Entre para comparar troféus",
+ "hidden": "Os troféus de {handle} não estão disponíveis para comparação.",
+ "loading": "Carregando",
+ "loadingComparison": "Carregando a comparação",
+ "empty": "Você e {handle} ainda não têm nenhum jogo em comum. Jogue um jogo que {handle} já jogou e ele aparece aqui.",
+ "game": "Jogo",
+ "gameNamed": "Jogo {title}",
+ "verdictNone": "Nenhum dos dois conquistou um troféu neste jogo.",
+ "verdictLevel": "Vocês estão empatados em pontos neste jogo.",
+ "verdictAhead": "Você está {count, plural, one {# ponto} other {# pontos}} à frente neste jogo.",
+ "verdictBehind": "{handle} está {count, plural, one {# ponto} other {# pontos}} à frente neste jogo.",
+ "caption": "Troféus em {titleId}: você ao lado de {handle}",
+ "stat": "Categoria",
+ "you": "Você",
+ "ahead": "(à frente)",
+ "rowTrophies": "Troféus",
+ "rowPoints": "Pontos neste jogo",
+ "rowLevel": "Nível geral",
+ "grade": {
+ "platinum": "Platina",
+ "gold": "Ouro",
+ "silver": "Prata",
+ "bronze": "Bronze"
+ },
+ "both": "Conquistados por ambos",
+ "onlyYou": "Só você",
+ "onlyThem": "Só {handle}"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, início",
+ "yourAccount": "Sua conta",
+ "signIn": "Entrar",
+ "loading": "Carregando",
+ "missingTitle": "Nenhum jogador chamado {handle}",
+ "missingBody": "Verifique a grafia do nome ou peça ao jogador que o envie de novo.",
+ "goHome": "Ir para o AnyPlay",
+ "player": "Jogador",
+ "trophies": "Troféus",
+ "trophiesEarned": "{count, number} conquistados.",
+ "noTrophies": "{handle} ainda não conquistou nenhum troféu.",
+ "favourites": "Jogos favoritos",
+ "favouritesNote": "Exibidos pelo ID do título.",
+ "game": "Jogo",
+ "connections": "Conexões",
+ "playtime": "Jogos jogados",
+ "playtimeNote": "{count, plural, one {# jogo} other {# jogos}}, {total} no total.",
+ "nothing": "Ainda não há mais nada para mostrar de {handle}.",
+ "openProfile": "Abrir perfil",
+ "openProfileOn": "no {brand} (abre em uma nova guia)",
+ "signInToAdd": "Entre para adicionar {handle}",
+ "editProfile": "Editar seu perfil",
+ "blocked": "Bloqueado",
+ "friends": "Amigos",
+ "requestSent": "Solicitação enviada",
+ "accepting": "Aceitando…",
+ "accept": "Aceitar solicitação",
+ "sending": "Enviando…",
+ "addFriend": "Adicionar amigo",
+ "addInAnyPlay": "Adicionar amigo no AnyPlay",
+ "minutes": "{n} min",
+ "hours": "{n} h"
+ },
+ "friends": {
+ "title": "Amigos",
+ "lead": "Adicione as pessoas com quem você joga, responda a pedidos e decida quem fica de fora.",
+ "add": {
+ "title": "Adicionar um amigo",
+ "description": "Digite o nome de um jogador para enviar um pedido a ele.",
+ "handle": "Nome",
+ "hint": "O nome dele, com ou sem o @.",
+ "send": "Enviar pedido",
+ "sending": "Enviando…",
+ "enterHandle": "Digite o nome do jogador que você quer adicionar.",
+ "nowFriends": "Você e @{handle} agora são amigos.",
+ "sent": "Pedido enviado para @{handle}. Vocês serão amigos assim que ele for aceito."
+ },
+ "requests": {
+ "title": "Pedidos",
+ "description": "Jogadores que querem ser seus amigos e os pedidos que você enviou.",
+ "empty": "Nenhum pedido em aberto. Quando alguém adicionar você, o pedido aparece aqui.",
+ "received": "Recebidos",
+ "sent": "Enviados",
+ "receivedAgo": "Recebido {when}",
+ "sentAgo": "Enviado {when}",
+ "decline": "Recusar",
+ "accept": "Aceitar",
+ "withdraw": "Retirar"
+ },
+ "list": {
+ "title": "Seus amigos",
+ "description": "Veja quem está online e o que está jogando.",
+ "empty": "Você ainda não tem amigos. Adicione alguém pelo nome acima; a pessoa aparece aqui quando aceitar.",
+ "online": "Online",
+ "offline": "Offline",
+ "playing": "Jogando {title}",
+ "block": "Bloquear",
+ "remove": "Remover"
+ },
+ "blocked": {
+ "title": "Bloqueados",
+ "description": "Jogadores bloqueados são removidos dos seus amigos e pedidos.",
+ "empty": "Você não bloqueou ninguém. Bloqueie um jogador pela sua lista de amigos quando precisar.",
+ "unblock": "Desbloquear"
+ },
+ "confirm": {
+ "blockTitle": "Bloquear @{handle}?",
+ "removeTitle": "Remover @{handle} dos amigos?",
+ "blockText": "Vocês deixam de ser amigos e qualquer pedido em aberto é descartado. Você pode desbloquear a qualquer momento.",
+ "removeText": "Vocês deixam de ver quando o outro está online. Você pode enviar um novo pedido depois.",
+ "blockLabel": "Bloquear jogador",
+ "removeLabel": "Remover amigo"
+ }
+ },
+ "trophies": {
+ "title": "Troféus",
+ "lead": "O que você conquistou nos seus jogos. O AnyPlay guarda contagens por jogo e nível, nunca nomes ou imagens de troféus.",
+ "totals": {
+ "title": "Totais",
+ "description": "{total} conquistados em {games, plural, one {# jogo} other {# jogos}}.",
+ "empty": "Ainda não há troféus. Jogue algo pelo AnyPlay e os que você conquistar serão contados aqui."
+ },
+ "byGame": {
+ "title": "Por jogo",
+ "description": "Os jogos são listados por ID do título, os mais recentes primeiro.",
+ "detail": "{earned, plural, one {# troféu} other {# troféus}} · última conquista: {when}"
+ },
+ "grades": {
+ "platinum": "Platina",
+ "gold": "Ouro",
+ "silver": "Prata",
+ "bronze": "Bronze"
+ }
+ },
+ "notifications": {
+ "title": "Notificações",
+ "lead": "Receba uma notificação neste dispositivo quando um amigo enviar um pedido, convidar você para uma party ou escrever para você.",
+ "device": {
+ "title": "Este dispositivo",
+ "description": "Cada navegador ou celular é configurado separadamente. Ative onde você quiser ser avisado.",
+ "notEnabled": "Esta nuvem não envia notificações.",
+ "checking": "Verificando este dispositivo…",
+ "unsupported": "Este navegador não pode receber notificações. Use uma versão atual do Chrome, Edge, Firefox ou Safari.",
+ "needsInstall": "No iPhone e no iPad, as notificações precisam do AnyPlay na sua tela de início. Toque em Compartilhar, escolha Adicionar à Tela de Início e abra o AnyPlay por lá.",
+ "blocked": "As notificações estão bloqueadas para este site. Permita-as nas configurações do site no seu navegador e volte.",
+ "switch": "Notificar este dispositivo",
+ "others": "{count, plural, one {Outro dispositivo também recebe notificações. Desative-as nesse dispositivo.} other {Outros # dispositivos também recebem notificações. Desative-as nesses dispositivos.}}"
+ },
+ "kinds": {
+ "title": "Sobre o que avisar",
+ "description": "Apenas para este dispositivo.",
+ "friend_request": {
+ "label": "Pedidos de amizade",
+ "detail": "Alguém quer ser seu amigo."
+ },
+ "party_invite": {
+ "label": "Convites para party",
+ "detail": "Um amigo convida você para uma party."
+ },
+ "message": {
+ "label": "Mensagens",
+ "detail": "Um amigo escreve para você. A mensagem em si não aparece na notificação."
+ }
+ },
+ "errors": {
+ "timeout": "O serviço de notificações do seu navegador não respondeu. Verifique sua conexão e tente de novo.",
+ "noKey": "Esta nuvem não tem uma chave para enviar notificações.",
+ "lostSubscription": "Este navegador perdeu a assinatura. Desative as notificações e ative de novo.",
+ "notAllowed": "Este navegador não permitiu notificações. Elas não funcionam em uma janela privada; caso contrário, verifique as configurações do site."
+ }
+ }
+}
diff --git a/apps/web/messages/pt/auth.json b/apps/web/messages/pt/auth.json
new file mode 100644
index 0000000..3d5089f
--- /dev/null
+++ b/apps/web/messages/pt/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Entrar",
+ "description": "Entre no AnyPlay com uma chave de acesso ou um código por e-mail."
+ },
+ "create": {
+ "title": "Crie sua conta",
+ "description": "Crie sua conta do AnyPlay: um e-mail e um nome."
+ },
+ "link": {
+ "title": "Abra sua conta",
+ "description": "Abra sua conta do AnyPlay neste navegador, a partir do launcher."
+ },
+ "join": {
+ "title": "Convite",
+ "description": "Um convite para jogar junto no AnyPlay."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, início",
+ "back": "Voltar ao início",
+ "promise": "Jogue seus jogos juntos.",
+ "toastOnline": "mira está online",
+ "toastParty": "kit entrou na sua party",
+ "toastTrophy": "Troféu conquistado · Into the Ember",
+ "help": "Precisa de ajuda?",
+ "helpLink": "Pergunte no Discord",
+ "legalNav": "Legal",
+ "legalNotice": "Aviso legal",
+ "privacyNotice": "Aviso de privacidade",
+ "termsOfUse": "Termos de uso"
+ },
+ "step": {
+ "loading": "Carregando",
+ "unreachable": "Não foi possível conectar ao AnyPlay",
+ "tryAgain": "Tentar de novo",
+ "newTab": "(abre em uma nova aba)",
+ "backToStart": "Voltar ao início",
+ "signIn": "Entrar",
+ "createAccount": "Criar conta",
+ "getApp": "Baixar o AnyPlay",
+ "continue": "Continuar"
+ },
+ "fields": {
+ "code": "Código"
+ },
+ "flow": {
+ "authorize": "O AnyPlay em {device} está esperando você entrar."
+ },
+ "email": {
+ "create": {
+ "title": "Crie sua conta",
+ "lead": "Comece com seu e-mail. Enviamos um código para confirmá-lo."
+ },
+ "signIn": {
+ "title": "Entrar",
+ "lead": "Use sua chave de acesso ou receba um código por e-mail."
+ },
+ "passkey": "Entrar com uma chave de acesso",
+ "or": "ou",
+ "label": "E-mail",
+ "placeholder": "name@example.com",
+ "invalid": "Digite seu e-mail, por exemplo name@example.com.",
+ "sending": "Enviando…",
+ "send": "Enviar código",
+ "haveAccount": "Já tem uma conta? Entrar",
+ "newHere": "Novo no AnyPlay? Criar conta"
+ },
+ "code": {
+ "title": "Confira seu e-mail",
+ "lead": "Enviamos um código de 6 dígitos para {email}. Ele vale por 10 minutos.",
+ "incomplete": "Digite os 6 dígitos do e-mail.",
+ "checking": "Verificando…",
+ "resent": "Um novo código está a caminho.",
+ "noMail": "Não recebeu o e-mail?",
+ "resendIn": "Enviar novo código em {seconds} s",
+ "resend": "Enviar novo código"
+ },
+ "details": {
+ "title": "Escolha um nome",
+ "lead": "Seu endereço {email} foi confirmado. Os outros jogadores veem seu nome.",
+ "handle": "Nome",
+ "handleHint": "Use de 3 a 16 letras, dígitos, hífens ou sublinhados. Comece com uma letra.",
+ "ageHint": "Ou seja, 16 anos ou mais.",
+ "age": "Tenho idade suficiente para aceitar os termos por conta própria.",
+ "terms": "Aceito os termos (versão {version}) e li o aviso de privacidade.",
+ "creating": "Criando…",
+ "submit": "Criar conta"
+ },
+ "terms": {
+ "title": "Revise os termos",
+ "lead": "Os termos mudaram. Leia os termos e aceite a versão {version} para continuar.",
+ "accept": "Aceito os termos (versão {version}).",
+ "required": "Aceite os termos para continuar.",
+ "submit": "Aceitar e continuar"
+ },
+ "banned": {
+ "title": "Esta conta foi banida",
+ "leadUntil": "O banimento termina em {date}.",
+ "leadForever": "O banimento não termina por conta própria.",
+ "rule": "Regra",
+ "reason": "Motivo",
+ "appeal": "Ler a declaração e apelar"
+ },
+ "handoff": {
+ "title": "Conectado",
+ "lead": "Estamos levando você de volta ao AnyPlay. Se nada acontecer, use o botão.",
+ "open": "Abrir o AnyPlay",
+ "note": "Você pode fechar esta aba quando o AnyPlay mostrar que você está conectado."
+ },
+ "done": {
+ "title": "Você está conectado",
+ "inviteLead": "Estamos levando você de volta ao convite.",
+ "welcome": "Boas-vindas, {handle}. Seus amigos, partys e troféus estão esperando no launcher.",
+ "account": "Ir para sua conta"
+ },
+ "join": {
+ "expiredTitle": "Este convite expirou",
+ "expiredLead": "O link pode ter expirado, ou o jogador parou de jogar. Peça um novo.",
+ "title": "{handle} convidou você",
+ "leadSignedIn": "Abra o AnyPlay no seu PC para ver o convite. Lá você decide se quer entrar.",
+ "leadSignedOut": "Entre ou crie uma conta primeiro. Depois você volta aqui para abrir o convite.",
+ "party": "Party",
+ "game": "Jogo",
+ "partyName": "Uma party do AnyPlay",
+ "gameFallback": "Um jogo",
+ "joined": "Entraram",
+ "players": "{members} de {max, plural, one {# jogador} other {# jogadores}}",
+ "open": "Abrir no AnyPlay",
+ "noApp": "Ainda não tem o AnyPlay neste PC? Baixe primeiro e abra esta página de novo."
+ },
+ "link": {
+ "expiredTitle": "Este link expirou",
+ "expiredLead": "Abra a página da sua conta pelo AnyPlay de novo.",
+ "close": "Você pode fechar esta aba.",
+ "title": "Abra sua conta",
+ "lead": "O AnyPlay no seu PC pediu para abrir a conta de {handle} neste navegador.",
+ "open": "Abrir conta",
+ "opening": "Abrindo…",
+ "notYou": "Não foi você? Feche a aba."
+ }
+}
diff --git a/apps/web/messages/pt/common.json b/apps/web/messages/pt/common.json
new file mode 100644
index 0000000..d4ab3b6
--- /dev/null
+++ b/apps/web/messages/pt/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Algo deu errado. Tente de novo.",
+ "network": "Não foi possível conectar ao AnyPlay. Verifique sua conexão e tente de novo.",
+ "internal": "O AnyPlay teve um problema. Tente de novo em um minuto.",
+ "unauthorized": "Sua sessão terminou. Entre de novo.",
+ "forbidden": "Você não pode fazer isso com esta conta.",
+ "banned": "Esta conta está banida.",
+ "not_found": "Isso não existe mais.",
+ "limit": "Você atingiu o limite. Remova um primeiro.",
+ "too_large": "Isso é grande demais para enviar.",
+ "code_wrong": "Esse código está errado. Confira e tente de novo.",
+ "code_expired": "Esse código expirou. Peça um novo.",
+ "handle_taken": "Esse nome de usuário já está em uso. Tente outro.",
+ "handle_invalid": "Use de 3 a 16 letras, dígitos, hífens ou sublinhados. Comece com uma letra.",
+ "rate_limited": "Muitas tentativas. Aguarde um momento e tente de novo.",
+ "rate_limited_wait": "Muitas tentativas. Aguarde {seconds, plural, one {# segundo} other {# segundos}} e tente de novo.",
+ "passkey_unsupported": "Este navegador não oferece suporte a passkeys. Use o código por e-mail."
+ }
+}
diff --git a/apps/web/messages/tr/account.json b/apps/web/messages/tr/account.json
new file mode 100644
index 0000000..880d855
--- /dev/null
+++ b/apps/web/messages/tr/account.json
@@ -0,0 +1,461 @@
+{
+ "ui": {
+ "never": "hiç",
+ "on": "Açık",
+ "off": "Kapalı",
+ "cancel": "Vazgeç",
+ "working": "İşleniyor…",
+ "typeToConfirm": "Onaylamak için {word} yaz",
+ "audience": {
+ "everyone": "Herkes",
+ "friends": "Arkadaşlar",
+ "nobody": "Sadece ben"
+ },
+ "stepUp": {
+ "title": "Sen olduğunu doğrula",
+ "needed": "Bu değişiklik için yakın zamanda giriş yapmış olman gerekir. 15 dakika geçerli kalır.",
+ "sent": "{hint} adresine 6 haneli bir kod gönderdik.",
+ "passkey": "Geçiş anahtarını kullan",
+ "sending": "Gönderiliyor…",
+ "sendCode": "E-postayla kod gönder"
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, ana sayfa",
+ "signOut": "Çıkış yap",
+ "nav": "Hesap",
+ "nav_overview": "Genel bakış",
+ "nav_profile": "Profil",
+ "nav_friends": "Arkadaşlar",
+ "nav_trophies": "Kupalar",
+ "nav_security": "Güvenlik",
+ "nav_notifications": "Bildirimler",
+ "nav_privacy": "Gizlilik",
+ "nav_connections": "Bağlantılar",
+ "nav_data": "Verilerin",
+ "unreachable": "AnyPlay’e ulaşılamıyor. Bağlantını kontrol et ve tekrar dene.",
+ "loading": "Yükleniyor"
+ },
+ "level": {
+ "top": "En üst seviyeye ulaşıldı",
+ "toNext": "{level}. seviyeye {count, plural, one {# puan} other {# puan}} kaldı",
+ "level": "Seviye {level}",
+ "points": "{count, plural, one {# puan} other {# puan}}",
+ "progress": "Seviye ilerlemesi",
+ "progressTo": "Seviye {level} ilerlemesi"
+ },
+ "meta": {
+ "title": "Hesabın",
+ "template": "%s · Hesabın · AnyPlay"
+ },
+ "state": {
+ "loading": "Yükleniyor",
+ "tryAgain": "Tekrar dene"
+ },
+ "overview": {
+ "label": "Hesabın",
+ "memberSince": "Üyelik başlangıcı: {date}",
+ "appearingOffline": "Çevrimdışı görünüyorsun",
+ "deletion": "Hesabın {date} tarihinde silinecek. Hesabımı koru",
+ "passkeys": "{count, plural, one {# geçiş anahtarı} other {# geçiş anahtarı}}",
+ "noPasskey": "Henüz geçiş anahtarı yok",
+ "launchersBrowsers": "{launchers, plural, one {# Launcher} other {# Launcher}} · {browsers, plural, one {# tarayıcı} other {# tarayıcı}}",
+ "trophiesAudience": "Kupalar: {audience}",
+ "pictureAudience": "Profil resmi: {audience}",
+ "audience": {
+ "everyone": "Herkes",
+ "friends": "Arkadaşlar",
+ "nobody": "Yalnızca sen"
+ },
+ "audienceInline": {
+ "everyone": "herkes",
+ "friends": "arkadaşlar",
+ "nobody": "yalnızca sen"
+ },
+ "linked": "{count} bağlı",
+ "nothingLinked": "Hiçbir şey bağlı değil",
+ "connectionsDetail": "Discord, Steam",
+ "dataValue": "Dışa aktar ya da sil",
+ "dataDetail": "Hakkında tuttuğumuz her şey",
+ "launcher": {
+ "title": "Launcher’ı indir",
+ "description": "Bu hesapla Launcher’a giriş yap; arkadaşların, partilerin ve kupaların orada olur."
+ }
+ },
+ "profile": {
+ "lead": "Profilini açan arkadaşlarının gördükleri. Her bölümü kimin göreceğini Gizlilik sayfasından sen seçersin.",
+ "avatar": {
+ "title": "Resim",
+ "description": "PNG ya da JPEG, en fazla 1 MB. Yuvarlak görünür; bu yüzden yüzünü ortada tut.",
+ "uploading": "Yükleniyor…",
+ "change": "Resmi değiştir",
+ "upload": "Resim yükle",
+ "remove": "Kaldır"
+ },
+ "about": {
+ "title": "Hakkında",
+ "description": "Kendin hakkında bir iki satır: ne oynadığın, ne zaman çevrimiçi olduğun.",
+ "placeholder": "Olursa ortak oyun, olmazsa tek başıma.",
+ "saved": "Kaydedildi",
+ "saving": "Kaydediliyor…",
+ "save": "Kaydet"
+ },
+ "favourites": {
+ "title": "Favori oyunlar",
+ "description": "En fazla {max}, oyun kimliğiyle. Bu site kimliği gösterir. Launcher her oyunun adını kendi kopyandan gösterir.",
+ "empty": "Henüz favori oyun yok. Aşağıdan bir oyun kimliği ekle.",
+ "listLabel": "Favori oyunların",
+ "remove": "{id} kaldır",
+ "fieldLabel": "Oyun kimliği",
+ "full": "{max} oyunun var. Yenisini eklemek için birini kaldır.",
+ "add": "Oyun ekle",
+ "saved": "Kaydedildi",
+ "saving": "Kaydediliyor…",
+ "save": "Kaydet",
+ "badId": "Yalnızca harf, rakam, tire ve alt çizgi kullan.",
+ "duplicate": "{id} zaten listede."
+ }
+ },
+ "security": {
+ "lead": "Nasıl giriş yaptığın ve hesabına giriş yapmış her Launcher ile tarayıcı.",
+ "email": {
+ "title": "E-posta",
+ "description": "Giriş kodlarının gittiği adres. Değiştirmek için yeni adrese gönderilen bir kod gerekir.",
+ "change": "Değiştir",
+ "invalid": "Yeni adresi gir, örneğin ad@ornek.com.",
+ "done": "Giriş adresin artık {email}.",
+ "newAddress": "Yeni adres",
+ "sending": "Gönderiliyor…",
+ "send": "Kod gönder",
+ "cancel": "Vazgeç",
+ "sentTo": "{email} adresine 6 haneli bir kod gönderdik."
+ },
+ "passkeys": {
+ "title": "Geçiş anahtarları",
+ "description": "Kod yerine parmak izin, yüzün ya da cihaz PIN’inle giriş yap. Geçiş anahtarını kimse oltalayamaz.",
+ "add": "Geçiş anahtarı ekle",
+ "waiting": "Cihazın bekleniyor…",
+ "detail": "Eklenme: {added} · Son kullanım: {used}",
+ "remove": "Kaldır",
+ "empty": "Henüz geçiş anahtarı yok. Bir tane ekle; e-posta kodu yerine tek dokunuşla giriş yaparsın.",
+ "full": "Bir hesabın tutabileceği en fazla geçiş anahtarına sahipsin ({max}). Yenisini eklemek için birini kaldır.",
+ "confirmTitle": "Bu geçiş anahtarı kaldırılsın mı?",
+ "confirmDescription": "Bu cihaz artık onunla giriş yapamaz. E-postayla gelen kodla her zaman giriş yapabilirsin.",
+ "confirmLabel": "Geçiş anahtarını kaldır",
+ "defaultName": "{system} geçiş anahtarı",
+ "thisDevice": "bu cihaz"
+ },
+ "devices": {
+ "title": "Launcher’lar",
+ "description": "Launcher’ın hesabınla giriş yaptığı tüm bilgisayarlar.",
+ "detail": "{platform} · Son kullanım: {used}",
+ "signOut": "Çıkış yap",
+ "empty": "Henüz hiçbir Launcher’da giriş yapılmadı.",
+ "confirmTitle": "Bu Launcher’dan çıkış yapılsın mı?",
+ "confirmDescription": "O bilgisayarda biri tekrar giriş yapana kadar çevrimdışı olur.",
+ "confirmLabel": "Launcher’dan çıkış yap"
+ },
+ "browsers": {
+ "title": "Tarayıcılar",
+ "description": "Bu hesap sayfasına giriş yapmış her tarayıcı. Onlar hakkında, ne zaman kullanıldıklarından fazlasını tutmuyoruz.",
+ "thisBrowser": "Bu tarayıcı",
+ "here": "Şu an buradasın",
+ "other": "Başka tarayıcı",
+ "detail": "Giriş: {created} · Son kullanım: {used}",
+ "signOut": "Çıkış yap",
+ "confirmThisTitle": "Bu tarayıcıdan çıkış yapılsın mı?",
+ "confirmOtherTitle": "O tarayıcıdan çıkış yapılsın mı?",
+ "confirmThisDescription": "Başa dönersin ve bir sonraki sefer yeniden giriş yaparsın.",
+ "confirmOtherDescription": "Hesabını açmak için yeniden giriş yapması gerekir.",
+ "confirmLabel": "Çıkış yap"
+ },
+ "everywhere": {
+ "title": "Her yerden çıkış yap",
+ "description": "Bu dahil her Launcher ve her tarayıcı. Bir cihaz kaybolduysa ya da başkası giriş yapmış olabilirse kullan.",
+ "button": "Her yerden çıkış yap",
+ "confirmTitle": "Her yerden çıkış yapılsın mı?",
+ "confirmDescription": "Bu dahil her Launcher ve tarayıcıdan aynı anda çıkış yapılır.",
+ "confirmLabel": "Her yerden çıkış yap"
+ }
+ },
+ "privacy": {
+ "lead": "Profilini ve etkinliğini kimin gördüğü. Kullanıcı adın her zaman görünür; böylece arkadaşların seni bulabilir.",
+ "profile": {
+ "title": "Profilin",
+ "description": "Değişiklikler hemen geçerli olur, Launcher’da da."
+ },
+ "parts": {
+ "trophies": {
+ "title": "Kupalar",
+ "detail": "Kupa sayıların, puanların ve seviyen"
+ },
+ "playtime": {
+ "title": "Oynama süresi",
+ "detail": "Her oyunu ne kadar oynadığın"
+ },
+ "about": {
+ "title": "Hakkında",
+ "detail": "Profilindeki satır"
+ },
+ "favourites": {
+ "title": "Favori oyunlar",
+ "detail": "Sabitlediğin en fazla üç oyun"
+ },
+ "avatar": {
+ "title": "Resim",
+ "detail": "Profil resmin"
+ }
+ },
+ "whoSees": "Kim görür: {part}",
+ "activity": {
+ "title": "Etkinliğin",
+ "description": "AnyPlay’de yaptıkların, arkadaşlarının akışında göründüğü gibi.",
+ "row": "Etkinlik",
+ "detail": "Arkadaşların akışlarında görür. Yabancılar akış görmez. Oradakini gizlemek ve yeni öğeleri durdurmak için “{nobody}” seç."
+ },
+ "appearOffline": {
+ "title": "Çevrimiçi durumu",
+ "description": "Hesabına giriş yapmış her Launcher ve tarayıcı için geçerlidir.",
+ "label": "Çevrimdışı görün",
+ "detail": "Arkadaşların seni çevrimdışı görür. Yine de oynayabilir, sohbet edebilir ve katılabilirsin."
+ },
+ "publicProfile": {
+ "title": "Herkese açık profil",
+ "description": "Sen açana kadar kapalıdır. Hesabına giriş yapmış her Launcher ve tarayıcı için geçerlidir.",
+ "label": "Herkese açık profil göster",
+ "detail": "Bağlantıya sahip herkes seviyeni, kupalarını ve oynadığın oyunları görür. Çevrimiçi olup olmadığını asla göstermez.",
+ "page": "Sayfan: {url}"
+ },
+ "playedWith": {
+ "title": "Son oynadıkların",
+ "description": "Kapatırsan yeni kimse eklenmez. Liste, sen temizleyene kadar kalır.",
+ "label": "Oynadığım kişileri hatırla",
+ "detail": "Birlikte parti ya da oyun oturumu paylaştığın kişileri 30 gün boyunca, en fazla 50 kişi tutar. Listeyi yalnızca sen görürsün.",
+ "clear": "Listeyi temizle",
+ "cleared": "Liste boş.",
+ "confirmTitle": "Liste temizlensin mi?",
+ "confirmDescription": "Birlikte oynadıklarının listesindeki herkes kaldırılır. Başkalarının listesi değişmez ve bunu geri alamazsın."
+ }
+ },
+ "connections": {
+ "lead": "Zaten oynadığın hesapları bağla; arkadaşların seni bulsun ve başka nerede olduğunu görsün.",
+ "result": {
+ "linked": "{name} hesabına bağlandı.",
+ "denied": "{name} bağlanmadı: istek orada reddedildi.",
+ "expired": "{name} bağlama çok uzun sürdü. Baştan başla.",
+ "fresh": "{name} bağlamak için yakın zamanda giriş yapmış olman gerekir. Baştan başla ve senin olduğunu doğrula.",
+ "taken": "Bu {name} hesabı başka bir AnyPlay hesabına zaten bağlı.",
+ "exists": "Zaten bir {name} hesabın bağlı. Önce onun bağlantısını kaldır.",
+ "failed": "{name} bağlanamadı. Bir dakika sonra tekrar dene."
+ },
+ "whoSees": "{name} hesabını kim görür",
+ "linkedAgo": "Bağlanma: {ago}",
+ "unlink": "Bağlantıyı kaldır",
+ "link": "{name} bağla",
+ "opening": "Açılıyor…",
+ "soon": "Yakında",
+ "notLinked": "Bağlı değil",
+ "arrivesSoon": "Bağlama yakında geliyor",
+ "confirmTitle": "{name} bağlantısı kaldırılsın mı?",
+ "confirmDescription": "{name} hesabın artık profilinde görünmez. İstediğin zaman yeniden bağlayabilirsin.",
+ "confirmLabel": "{name} bağlantısını kaldır"
+ },
+ "data": {
+ "lead": "AnyPlay’in hakkında tuttuğu her şey senindir: bir kopyasını al ya da hesabı sil.",
+ "export": {
+ "title": "Verilerini indir",
+ "description": "Hesabın, profilin, arkadaşların, kupaların, oynama sürelerin ve bağlı hesapların tek bir JSON dosyası olarak.",
+ "preparing": "Hazırlanıyor…",
+ "button": "İndir"
+ },
+ "scheduled": {
+ "title": "Silme planlandı",
+ "description": "Hesabın ve içindeki her şey {date} tarihinde silinecek. O zamana kadar hesabını koruyabilirsin.",
+ "keep": "Hesabımı koru"
+ },
+ "delete": {
+ "title": "Hesabını sil",
+ "description": "Hesabın 30 gün sonra silinir; o zamana kadar fikrini değiştirebilirsin. Sonrasında arkadaşların, kupaların ve oynama sürelerinle birlikte tamamen gider.",
+ "button": "Hesabı sil"
+ },
+ "confirm": {
+ "title": "Hesabın silinsin mi?",
+ "description": "Hesap 30 gün içinde silinir. Her Launcher ve tarayıcıdan şimdi çıkış yapılır."
+ }
+ },
+ "compare": {
+ "title": "Kupaları karşılaştır",
+ "description": "Kupaların, {handle} ile yan yana.",
+ "descriptionShared": "Sen ve {handle}, ikinizin de sahip olduğu bir oyunda.",
+ "signInDescription": "Kupalarının {handle} ile nasıl kıyaslandığını gör.",
+ "signIn": "Kupaları karşılaştırmak için giriş yap",
+ "hidden": "{handle} kullanıcısının kupaları karşılaştırmaya uygun değil.",
+ "loading": "Yükleniyor",
+ "loadingComparison": "Karşılaştırma yükleniyor",
+ "empty": "Sen ve {handle} için henüz ortak bir oyun yok. {handle} oynadığı bir oyunu oynarsan burada görünür.",
+ "game": "Oyun",
+ "gameNamed": "Oyun {title}",
+ "verdictNone": "İkinizin de bu oyunda kazandığı bir kupa yok.",
+ "verdictLevel": "Bu oyunda puanlarınız eşit.",
+ "verdictAhead": "Bu oyunda {count, number} puan öndesin.",
+ "verdictBehind": "Bu oyunda {handle}, {count, number} puan önde.",
+ "caption": "{titleId} oyunundaki kupalar: sen ve {handle}",
+ "stat": "Kategori",
+ "you": "Sen",
+ "ahead": "(önde)",
+ "rowTrophies": "Kupalar",
+ "rowPoints": "Bu oyunda puan",
+ "rowLevel": "Genel seviye",
+ "grade": {
+ "platinum": "Platin",
+ "gold": "Altın",
+ "silver": "Gümüş",
+ "bronze": "Bronz"
+ },
+ "both": "İkinizin de kazandığı",
+ "onlyYou": "Sadece sen",
+ "onlyThem": "Sadece {handle}"
+ },
+ "publicProfile": {
+ "home": "AnyPlay, ana sayfa",
+ "yourAccount": "Hesabın",
+ "signIn": "Giriş yap",
+ "loading": "Yükleniyor",
+ "missingTitle": "{handle} adında bir oyuncu yok",
+ "missingBody": "Kullanıcı adının yazımını kontrol et ya da oyuncudan tekrar göndermesini iste.",
+ "goHome": "AnyPlay’e git",
+ "player": "Oyuncu",
+ "trophies": "Kupalar",
+ "trophiesEarned": "{count, number} kazanıldı.",
+ "noTrophies": "{handle} henüz kupa kazanmadı.",
+ "favourites": "Favori oyunlar",
+ "favouritesNote": "Başlık kimliğiyle gösterilir.",
+ "game": "Oyun",
+ "connections": "Bağlantılar",
+ "playtime": "Oynanan oyunlar",
+ "playtimeNote": "{count, number} oyun, toplam {total}.",
+ "nothing": "{handle} hakkında gösterilecek başka bir şey yok.",
+ "openProfile": "Profili aç",
+ "openProfileOn": "{brand} üzerinde (yeni sekmede açılır)",
+ "signInToAdd": "{handle} kullanıcısını eklemek için giriş yap",
+ "editProfile": "Profilini düzenle",
+ "blocked": "Engellendi",
+ "friends": "Arkadaşlar",
+ "requestSent": "İstek gönderildi",
+ "accepting": "Kabul ediliyor…",
+ "accept": "İsteği kabul et",
+ "sending": "Gönderiliyor…",
+ "addFriend": "Arkadaş ekle",
+ "addInAnyPlay": "AnyPlay’de arkadaş ekle",
+ "minutes": "{n} dk",
+ "hours": "{n} sa"
+ },
+ "friends": {
+ "title": "Arkadaşlar",
+ "lead": "Birlikte oynadığın kişileri ekle, isteklere yanıt ver ve kimin dışarıda kalacağına karar ver.",
+ "add": {
+ "title": "Arkadaş ekle",
+ "description": "İstek göndermek için bir oyuncunun adını gir.",
+ "handle": "Ad",
+ "hint": "Oyuncunun adı, @ ile ya da @ olmadan.",
+ "send": "İstek gönder",
+ "sending": "Gönderiliyor…",
+ "enterHandle": "Eklemek istediğin oyuncunun adını gir.",
+ "nowFriends": "Sen ve @{handle} artık arkadaşsınız.",
+ "sent": "İstek @{handle} kişisine gönderildi. Kabul ettiğinde arkadaş olursunuz."
+ },
+ "requests": {
+ "title": "İstekler",
+ "description": "Arkadaşın olmak isteyen oyuncular ve gönderdiğin istekler.",
+ "empty": "Açık istek yok. Biri seni eklediğinde istek burada görünür.",
+ "received": "Gelen",
+ "sent": "Giden",
+ "receivedAgo": "Alındı: {when}",
+ "sentAgo": "Gönderildi: {when}",
+ "decline": "Reddet",
+ "accept": "Kabul et",
+ "withdraw": "Geri çek"
+ },
+ "list": {
+ "title": "Arkadaşların",
+ "description": "Kimlerin çevrimiçi olduğunu ve ne oynadıklarını gör.",
+ "empty": "Henüz arkadaşın yok. Yukarıdan birini adıyla ekle; kabul ettiğinde burada görünür.",
+ "online": "Çevrimiçi",
+ "offline": "Çevrimdışı",
+ "playing": "{title} oynuyor",
+ "block": "Engelle",
+ "remove": "Çıkar"
+ },
+ "blocked": {
+ "title": "Engellenenler",
+ "description": "Engellenen oyuncular arkadaşlarından ve isteklerinden çıkarılır.",
+ "empty": "Kimseyi engellemedin. Gerektiğinde arkadaş listenden bir oyuncuyu engelleyebilirsin.",
+ "unblock": "Engeli kaldır"
+ },
+ "confirm": {
+ "blockTitle": "@{handle} engellensin mi?",
+ "removeTitle": "@{handle} arkadaşlıktan çıkarılsın mı?",
+ "blockText": "Artık arkadaş olmazsınız ve açık istekler silinir. Engeli istediğin zaman kaldırabilirsin.",
+ "removeText": "Birbirinizin çevrimiçi olup olmadığını görmezsiniz. Sonra yeni istek gönderebilirsin.",
+ "blockLabel": "Oyuncuyu engelle",
+ "removeLabel": "Arkadaşı çıkar"
+ }
+ },
+ "trophies": {
+ "title": "Kupalar",
+ "lead": "Oyunlarında kazandıkların. AnyPlay oyun ve derece başına sayıları tutar, kupa adlarını ya da görsellerini asla tutmaz.",
+ "totals": {
+ "title": "Toplam",
+ "description": "{games} oyunda toplam {total} kupa kazanıldı.",
+ "empty": "Henüz kupa yok. AnyPlay üzerinden bir oyun oyna, kazandıkların burada sayılır."
+ },
+ "byGame": {
+ "title": "Oyuna göre",
+ "description": "Oyunlar başlık kimliğine göre, en yenisi başta listelenir.",
+ "detail": "{earned} kupa · son kazanılan: {when}"
+ },
+ "grades": {
+ "platinum": "Platin",
+ "gold": "Altın",
+ "silver": "Gümüş",
+ "bronze": "Bronz"
+ }
+ },
+ "notifications": {
+ "title": "Bildirimler",
+ "lead": "Bir arkadaşın seni arkadaş olarak eklemek istediğinde, seni bir partiye davet ettiğinde ya da sana yazdığında bu cihazda bildirim al.",
+ "device": {
+ "title": "Bu cihaz",
+ "description": "Her tarayıcı ya da telefon ayrı ayarlanır. Bildirim almak istediğin yerde bunu aç.",
+ "notEnabled": "Bu bulut bildirim göndermiyor.",
+ "checking": "Bu cihaz kontrol ediliyor…",
+ "unsupported": "Bu tarayıcı bildirim alamıyor. Güncel bir Chrome, Edge, Firefox ya da Safari kullan.",
+ "needsInstall": "iPhone ve iPad’de bildirimler için AnyPlay’in Ana Ekran’da olması gerekir. Paylaş’a dokun, Ana Ekrana Ekle’yi seç, sonra AnyPlay’i oradan aç.",
+ "blocked": "Bu site için bildirimler engellenmiş. Tarayıcının site ayarlarından izin ver, sonra geri dön.",
+ "switch": "Bu cihaza bildirim gönder",
+ "others": "{count, plural, one {Başka bir cihaz da bildirim alıyor. Bildirimleri o cihazda kapat.} other {Başka # cihaz da bildirim alıyor. Bildirimleri o cihazlarda kapat.}}"
+ },
+ "kinds": {
+ "title": "Hangi bildirimler gelsin",
+ "description": "Yalnızca bu cihaz için.",
+ "friend_request": {
+ "label": "Arkadaşlık istekleri",
+ "detail": "Biri seninle arkadaş olmak istiyor."
+ },
+ "party_invite": {
+ "label": "Parti davetleri",
+ "detail": "Bir arkadaşın seni bir partiye davet ediyor."
+ },
+ "message": {
+ "label": "Mesajlar",
+ "detail": "Bir arkadaşın sana yazıyor. Mesajın kendisi bildirimde görünmez."
+ }
+ },
+ "errors": {
+ "timeout": "Tarayıcının bildirim hizmeti yanıt vermedi. Bağlantını kontrol edip tekrar dene.",
+ "noKey": "Bu bulutta bildirim göndermek için anahtar yok.",
+ "lostSubscription": "Bu tarayıcı aboneliğini kaybetti. Bildirimleri kapatıp yeniden aç.",
+ "notAllowed": "Bu tarayıcı bildirimlere izin vermedi. Gizli pencerede çalışmazlar; değilse site ayarlarına bak."
+ }
+ }
+}
diff --git a/apps/web/messages/tr/auth.json b/apps/web/messages/tr/auth.json
new file mode 100644
index 0000000..bdcb551
--- /dev/null
+++ b/apps/web/messages/tr/auth.json
@@ -0,0 +1,143 @@
+{
+ "meta": {
+ "signIn": {
+ "title": "Giriş yap",
+ "description": "AnyPlay’e geçiş anahtarıyla ya da e-postayla gelen kodla giriş yap."
+ },
+ "create": {
+ "title": "Hesabını oluştur",
+ "description": "AnyPlay hesabını oluştur: bir e-posta adresi ve bir ad yeterli."
+ },
+ "link": {
+ "title": "Hesabını aç",
+ "description": "AnyPlay hesabını başlatıcıdan bu tarayıcıda aç."
+ },
+ "join": {
+ "title": "Davet",
+ "description": "AnyPlay’de birlikte oynamak için bir davet."
+ }
+ },
+ "shell": {
+ "home": "AnyPlay, ana sayfa",
+ "back": "Başa dön",
+ "promise": "Oyunlarını birlikte oyna.",
+ "toastOnline": "mira çevrimiçi",
+ "toastParty": "kit partine katıldı",
+ "toastTrophy": "Kupa kazanıldı · Into the Ember",
+ "help": "Yardım mı lazım?",
+ "helpLink": "Discord’da sor",
+ "legalNav": "Yasal",
+ "legalNotice": "Yasal bildirim",
+ "privacyNotice": "Gizlilik bildirimi",
+ "termsOfUse": "Kullanım koşulları"
+ },
+ "step": {
+ "loading": "Yükleniyor",
+ "unreachable": "AnyPlay’e ulaşılamıyor",
+ "tryAgain": "Tekrar dene",
+ "newTab": "(yeni sekmede açılır)",
+ "backToStart": "Başa dön",
+ "signIn": "Giriş yap",
+ "createAccount": "Hesap oluştur",
+ "getApp": "AnyPlay’i indir",
+ "continue": "Devam"
+ },
+ "fields": {
+ "code": "Kod"
+ },
+ "flow": {
+ "authorize": "{device} cihazındaki AnyPlay bu girişi bekliyor."
+ },
+ "email": {
+ "create": {
+ "title": "Hesabını oluştur",
+ "lead": "E-posta adresinle başla. Onaylamak için bir kod gönderiyoruz."
+ },
+ "signIn": {
+ "title": "Giriş yap",
+ "lead": "Geçiş anahtarını kullan ya da e-postayla bir kod al."
+ },
+ "passkey": "Geçiş anahtarıyla giriş yap",
+ "or": "ya da",
+ "label": "E-posta adresi",
+ "placeholder": "name@example.com",
+ "invalid": "E-posta adresini yaz, örneğin name@example.com.",
+ "sending": "Gönderiliyor…",
+ "send": "Kodu gönder",
+ "haveAccount": "Zaten hesabın var mı? Giriş yap",
+ "newHere": "AnyPlay’de yeni misin? Hesap oluştur"
+ },
+ "code": {
+ "title": "E-postana bak",
+ "lead": "{email} adresine 6 haneli bir kod gönderdik. 10 dakika geçerli.",
+ "incomplete": "E-postadaki 6 haneyi gir.",
+ "checking": "Denetleniyor…",
+ "resent": "Yeni bir kod yolda.",
+ "noMail": "E-posta gelmedi mi?",
+ "resendIn": "Yeni kodu {seconds} sn sonra gönder",
+ "resend": "Yeni kod gönder"
+ },
+ "details": {
+ "title": "Bir ad seç",
+ "lead": "{email} adresin onaylandı. Diğer oyuncular adını görür.",
+ "handle": "Ad",
+ "handleHint": "3 ile 16 arasında harf, rakam, tire veya alt çizgi kullan. Bir harfle başla.",
+ "ageHint": "Yani 16 yaşında ya da daha büyüksün.",
+ "age": "Koşulları tek başıma kabul edecek yaştayım.",
+ "terms": "Koşulları (sürüm {version}) kabul ediyorum ve gizlilik bildirimini okudum.",
+ "creating": "Oluşturuluyor…",
+ "submit": "Hesap oluştur"
+ },
+ "terms": {
+ "title": "Koşulları gözden geçir",
+ "lead": "Koşullar değişti. Koşulları oku ve devam etmek için {version} sürümünü kabul et.",
+ "accept": "Koşulları kabul ediyorum (sürüm {version}).",
+ "required": "Devam etmek için koşulları kabul et.",
+ "submit": "Kabul et ve devam et"
+ },
+ "banned": {
+ "title": "Bu hesap engellendi",
+ "leadUntil": "Engel {date} tarihinde bitiyor.",
+ "leadForever": "Engel kendiliğinden bitmez.",
+ "rule": "Kural",
+ "reason": "Sebep",
+ "appeal": "Açıklamayı oku ve itiraz et"
+ },
+ "handoff": {
+ "title": "Giriş yapıldı",
+ "lead": "Seni AnyPlay’e geri götürüyoruz. Bir şey olmazsa düğmeyi kullan.",
+ "open": "AnyPlay’i aç",
+ "note": "AnyPlay seni giriş yapmış olarak gösterdiğinde bu sekmeyi kapatabilirsin."
+ },
+ "done": {
+ "title": "Giriş yaptın",
+ "inviteLead": "Seni davete geri götürüyoruz.",
+ "welcome": "Hoş geldin, {handle}. Arkadaşların, partilerin ve kupaların başlatıcıda seni bekliyor.",
+ "account": "Hesabına git"
+ },
+ "join": {
+ "expiredTitle": "Bu davetin süresi doldu",
+ "expiredLead": "Bağlantının süresi dolmuş ya da oyuncu oynamayı bırakmış olabilir. Yenisini iste.",
+ "title": "{handle} seni davet etti",
+ "leadSignedIn": "Daveti görmek için PC’nde AnyPlay’i aç. Katılıp katılmayacağına orada sen karar verirsin.",
+ "leadSignedOut": "Önce giriş yap ya da hesap oluştur. Daveti açmak için buraya geri dönersin.",
+ "party": "Parti",
+ "game": "Oyun",
+ "partyName": "Bir AnyPlay partisi",
+ "gameFallback": "Bir oyun",
+ "joined": "Katılanlar",
+ "players": "{members} / {max, plural, other {# oyuncu}}",
+ "open": "AnyPlay’de aç",
+ "noApp": "Bu PC’de henüz AnyPlay yok mu? Önce indir, sonra bu sayfayı yeniden aç."
+ },
+ "link": {
+ "expiredTitle": "Bu bağlantının süresi doldu",
+ "expiredLead": "Hesap sayfanı AnyPlay’den yeniden aç.",
+ "close": "Bu sekmeyi kapatabilirsin.",
+ "title": "Hesabını aç",
+ "lead": "PC’ndeki AnyPlay, {handle} hesabının bu tarayıcıda açılmasını istedi.",
+ "open": "Hesabı aç",
+ "opening": "Açılıyor…",
+ "notYou": "Bunu sen istemedin mi? Sekmeyi kapat."
+ }
+}
diff --git a/apps/web/messages/tr/common.json b/apps/web/messages/tr/common.json
new file mode 100644
index 0000000..3418d30
--- /dev/null
+++ b/apps/web/messages/tr/common.json
@@ -0,0 +1,20 @@
+{
+ "errors": {
+ "generic": "Bir şeyler ters gitti. Yeniden dene.",
+ "network": "AnyPlay’e ulaşılamıyor. Bağlantını kontrol edip yeniden dene.",
+ "internal": "AnyPlay’in tarafında bir sorun oldu. Bir dakika sonra yeniden dene.",
+ "unauthorized": "Oturumun sona erdi. Yeniden giriş yap.",
+ "forbidden": "Bunu bu hesapla yapamazsın.",
+ "banned": "Bu hesap engellendi.",
+ "not_found": "Bu artık yok.",
+ "limit": "Bunun için sınıra ulaştın. Önce birini kaldır.",
+ "too_large": "Bu göndermek için fazla büyük.",
+ "code_wrong": "Bu kod yanlış. Kontrol edip yeniden dene.",
+ "code_expired": "Bu kodun süresi doldu. Yeni bir kod iste.",
+ "handle_taken": "Bu kullanıcı adı alınmış. Başka bir tane dene.",
+ "handle_invalid": "3 ile 16 arasında harf, rakam, tire veya alt çizgi kullan. Bir harfle başla.",
+ "rate_limited": "Çok fazla deneme. Biraz bekleyip yeniden dene.",
+ "rate_limited_wait": "Çok fazla deneme. {seconds, plural, one {# saniye} other {# saniye}} bekleyip yeniden dene.",
+ "passkey_unsupported": "Bu tarayıcı geçiş anahtarlarını kullanamıyor. Bunun yerine e-posta kodunu kullan."
+ }
+}
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
index 5e33406..8f0c33d 100644
--- a/apps/web/next.config.ts
+++ b/apps/web/next.config.ts
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { NextConfig } from "next";
+import createNextIntlPlugin from "next-intl/plugin";
/**
* Hardening, because the 2025-26 Next.js advisories are real and every self-hoster would have to
@@ -47,4 +48,7 @@ const config: NextConfig = {
},
};
-export default config;
+// The words come from messages// (src/i18n); no language in the path, the proxy picks one.
+const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
+
+export default withNextIntl(config);
diff --git a/apps/web/package.json b/apps/web/package.json
index 9c294ad..2b54515 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -23,6 +23,7 @@
"@tanstack/react-query": "5.104.4",
"lenis": "1.3.26",
"next": "16.4.2",
+ "next-intl": "4.14.7",
"react": "19.3.1",
"react-dom": "19.3.1"
},
diff --git a/apps/web/src/app/(account)/account/connections/page.tsx b/apps/web/src/app/(account)/account/connections/page.tsx
index 00f5ab2..95a6417 100644
--- a/apps/web/src/app/(account)/account/connections/page.tsx
+++ b/apps/web/src/app/(account)/account/connections/page.tsx
@@ -1,9 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { Suspense } from "react";
import { ConnectionsPage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Connections" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_connections") };
+}
export default function Page() {
return (
diff --git a/apps/web/src/app/(account)/account/data/page.tsx b/apps/web/src/app/(account)/account/data/page.tsx
index 0cbbc38..20b6347 100644
--- a/apps/web/src/app/(account)/account/data/page.tsx
+++ b/apps/web/src/app/(account)/account/data/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { DataPage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Your data" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_data") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/friends/page.tsx b/apps/web/src/app/(account)/account/friends/page.tsx
index a9ace41..5138986 100644
--- a/apps/web/src/app/(account)/account/friends/page.tsx
+++ b/apps/web/src/app/(account)/account/friends/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { FriendsPage } from "@/components/account/social";
-export const metadata: Metadata = { title: "Friends" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_friends") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/layout.tsx b/apps/web/src/app/(account)/account/layout.tsx
index c2cb2e3..329a0fb 100644
--- a/apps/web/src/app/(account)/account/layout.tsx
+++ b/apps/web/src/app/(account)/account/layout.tsx
@@ -1,16 +1,27 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages, getTranslations } from "next-intl/server";
import { connection } from "next/server";
import type { ReactNode } from "react";
import { AccountShell } from "@/components/account/shell";
-export const metadata: Metadata = {
- title: { default: "Your account", template: "%s · Your account · AnyPlay" },
- robots: { index: false },
-};
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.meta");
+ return {
+ title: { default: t("title"), template: t("template") },
+ robots: { index: false },
+ };
+}
export default async function AccountLayout({ children }: { children: ReactNode }) {
// Per request, so Next's inline scripts carry the nonce of the script policy (src/proxy.ts).
await connection();
- return {children};
+ // The account pages add the auth words too: a sensitive change asks for a code or a passkey.
+ const { common, account, auth } = (await getMessages()) as Record>;
+ return (
+
+ {children}
+
+ );
}
diff --git a/apps/web/src/app/(account)/account/notifications/page.tsx b/apps/web/src/app/(account)/account/notifications/page.tsx
index f8a9a01..cd1d399 100644
--- a/apps/web/src/app/(account)/account/notifications/page.tsx
+++ b/apps/web/src/app/(account)/account/notifications/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { NotificationsPage } from "@/components/account/notifications";
-export const metadata: Metadata = { title: "Notifications" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_notifications") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/page.tsx b/apps/web/src/app/(account)/account/page.tsx
index 87b23f5..7508016 100644
--- a/apps/web/src/app/(account)/account/page.tsx
+++ b/apps/web/src/app/(account)/account/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { OverviewPage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Overview" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_overview") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/privacy/page.tsx b/apps/web/src/app/(account)/account/privacy/page.tsx
index dfee064..5b715f5 100644
--- a/apps/web/src/app/(account)/account/privacy/page.tsx
+++ b/apps/web/src/app/(account)/account/privacy/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { PrivacyPage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Privacy" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_privacy") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/profile/page.tsx b/apps/web/src/app/(account)/account/profile/page.tsx
index 6cbee78..be3c1e2 100644
--- a/apps/web/src/app/(account)/account/profile/page.tsx
+++ b/apps/web/src/app/(account)/account/profile/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { ProfilePage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Profile" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_profile") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/security/page.tsx b/apps/web/src/app/(account)/account/security/page.tsx
index 775352b..3ac4aa8 100644
--- a/apps/web/src/app/(account)/account/security/page.tsx
+++ b/apps/web/src/app/(account)/account/security/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { SecurityPage } from "@/components/account/pages";
-export const metadata: Metadata = { title: "Security" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_security") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(account)/account/trophies/page.tsx b/apps/web/src/app/(account)/account/trophies/page.tsx
index 11c5196..df50e6c 100644
--- a/apps/web/src/app/(account)/account/trophies/page.tsx
+++ b/apps/web/src/app/(account)/account/trophies/page.tsx
@@ -1,8 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { TrophiesPage } from "@/components/account/social";
-export const metadata: Metadata = { title: "Trophies" };
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("account.shell");
+ return { title: t("nav_trophies") };
+}
export default function Page() {
return ;
diff --git a/apps/web/src/app/(auth)/create/page.tsx b/apps/web/src/app/(auth)/create/page.tsx
index d60038d..43fb314 100644
--- a/apps/web/src/app/(auth)/create/page.tsx
+++ b/apps/web/src/app/(auth)/create/page.tsx
@@ -1,15 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { connection } from "next/server";
import { AuthFlow } from "@/components/auth/auth-flow";
import { AuthShell } from "@/components/auth/auth-shell";
import { safeNext } from "@/lib/join";
-export const metadata: Metadata = {
- title: "Create your account",
- description: "Create your AnyPlay account: an e-mail address and a handle.",
- robots: { index: false },
-};
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("auth.meta.create");
+ return {
+ title: t("title"),
+ description: t("description"),
+ robots: { index: false },
+ };
+}
export default async function Page({ searchParams }: { searchParams: Promise<{ next?: string | string[] }> }) {
// Per request, so Next's inline scripts carry the nonce of the script policy (src/proxy.ts).
diff --git a/apps/web/src/app/(auth)/join/[code]/page.tsx b/apps/web/src/app/(auth)/join/[code]/page.tsx
index b0bfd61..698eebd 100644
--- a/apps/web/src/app/(auth)/join/[code]/page.tsx
+++ b/apps/web/src/app/(auth)/join/[code]/page.tsx
@@ -1,17 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { connection } from "next/server";
import { AuthShell } from "@/components/auth/auth-shell";
import { JoinFlow } from "@/components/auth/join-flow";
// The address holds a join code. It stays out of search results, and out of the Referer of
// everything this page links to or loads.
-export const metadata: Metadata = {
- title: "Invitation",
- description: "An invitation to play together on AnyPlay.",
- robots: { index: false, follow: false },
- referrer: "no-referrer",
-};
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("auth.meta.join");
+ return {
+ title: t("title"),
+ description: t("description"),
+ robots: { index: false, follow: false },
+ referrer: "no-referrer",
+ };
+}
export default async function Page({ params }: { params: Promise<{ code: string }> }) {
// Per request, so Next's inline scripts carry the nonce of the script policy (src/proxy.ts).
diff --git a/apps/web/src/app/(auth)/layout.tsx b/apps/web/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..eda9809
--- /dev/null
+++ b/apps/web/src/app/(auth)/layout.tsx
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages } from "next-intl/server";
+import type { ReactNode } from "react";
+
+/** Sign-in, sign-up, device link and join: the shared words and the auth words, in the visitor's language. */
+export default async function AuthLayout({ children }: { children: ReactNode }) {
+ const { common, auth } = (await getMessages()) as Record>;
+ return {children};
+}
diff --git a/apps/web/src/app/(auth)/link/page.tsx b/apps/web/src/app/(auth)/link/page.tsx
index fb37341..5bd8ada 100644
--- a/apps/web/src/app/(auth)/link/page.tsx
+++ b/apps/web/src/app/(auth)/link/page.tsx
@@ -1,14 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { connection } from "next/server";
import { AuthShell } from "@/components/auth/auth-shell";
import { LinkFlow } from "@/components/auth/link-flow";
-export const metadata: Metadata = {
- title: "Open your account",
- description: "Open your AnyPlay account in this browser, from the launcher.",
- robots: { index: false },
-};
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("auth.meta.link");
+ return {
+ title: t("title"),
+ description: t("description"),
+ robots: { index: false },
+ };
+}
export default async function Page({ searchParams }: { searchParams: Promise<{ c?: string | string[] }> }) {
// Per request, so Next's inline scripts carry the nonce of the script policy (src/proxy.ts).
diff --git a/apps/web/src/app/(auth)/sign-in/page.tsx b/apps/web/src/app/(auth)/sign-in/page.tsx
index c3ad816..0597832 100644
--- a/apps/web/src/app/(auth)/sign-in/page.tsx
+++ b/apps/web/src/app/(auth)/sign-in/page.tsx
@@ -1,15 +1,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { Metadata } from "next";
+import { getTranslations } from "next-intl/server";
import { connection } from "next/server";
import { AuthFlow } from "@/components/auth/auth-flow";
import { AuthShell } from "@/components/auth/auth-shell";
import { safeNext } from "@/lib/join";
-export const metadata: Metadata = {
- title: "Sign in",
- description: "Sign in to AnyPlay with a passkey or a code by e-mail.",
- robots: { index: false },
-};
+export async function generateMetadata(): Promise {
+ const t = await getTranslations("auth.meta.signIn");
+ return {
+ title: t("title"),
+ description: t("description"),
+ robots: { index: false },
+ };
+}
export default async function Page({ searchParams }: { searchParams: Promise<{ next?: string | string[] }> }) {
// Per request, so Next's inline scripts carry the nonce of the script policy (src/proxy.ts).
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index ad689c2..d696311 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,4 +1,6 @@
import type { Metadata, Viewport } from "next";
+import { NextIntlClientProvider } from "next-intl";
+import { getLocale, getMessages } from "next-intl/server";
import { connection } from "next/server";
import { Monitoring } from "@/components/monitoring";
import { SiteProvider } from "@/components/site-context";
@@ -27,10 +29,15 @@ export const viewport: Viewport = { themeColor: "#0b0a09", colorScheme: "dark li
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const site = readSite();
+ const locale = await getLocale();
+ // Only the shared words go to every page; the account flows add theirs in their own layouts.
+ const { common } = (await getMessages()) as { common: Record };
return (
-
+
- {children}
+
+ {children}
+
diff --git a/apps/web/src/app/u/layout.tsx b/apps/web/src/app/u/layout.tsx
new file mode 100644
index 0000000..82e0e56
--- /dev/null
+++ b/apps/web/src/app/u/layout.tsx
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages } from "next-intl/server";
+import type { ReactNode } from "react";
+
+/** Public profiles: the shared words and the account words, in the visitor's language. */
+export default async function ProfileLayout({ children }: { children: ReactNode }) {
+ const { common, account } = (await getMessages()) as Record>;
+ return {children};
+}
diff --git a/apps/web/src/components/account/compare.tsx b/apps/web/src/components/account/compare.tsx
index 23c8b5d..7e90d62 100644
--- a/apps/web/src/components/account/compare.tsx
+++ b/apps/web/src/components/account/compare.tsx
@@ -1,12 +1,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
"use client";
-import { type CompareSide, type PublicUser, type TrophyCompare, describeError, isApiError } from "@anyplay/api-client";
+import { type CompareSide, type PublicUser, type TrophyCompare, isApiError } from "@anyplay/api-client";
+import { useFormatter, useTranslations } from "next-intl";
import Link from "next/link";
import { type ReactNode, useEffect, useId, useState } from "react";
import { Failed } from "./pages";
import { GRADES } from "./social";
import { Card, Empty, Notice, smallButton } from "./ui";
+import { useDescribeError } from "@/i18n/errors";
import { loadApi } from "@/lib/api";
/**
@@ -16,8 +18,6 @@ import { loadApi } from "@/lib/api";
* would be a column of digits that says nothing without the names.
*/
-const num = (n: number) => n.toLocaleString("en");
-
type Shared =
| { status: "loading" }
| { status: "ready"; titles: string[] }
@@ -30,10 +30,10 @@ type Result =
| { status: "hidden" }
| { status: "failed"; message: string };
-const HIDDEN = (handle: string) => `${handle}’s trophies aren’t available to compare.`;
-
/** The compare card of a profile; the page shows it only when the player's trophies are visible. */
export function CompareCard({ user }: { user: PublicUser }) {
+ const t = useTranslations("account.compare");
+ const describeError = useDescribeError();
const { handle } = user.account;
const [shared, setShared] = useState({ status: "loading" });
const [attempt, setAttempt] = useState(0);
@@ -61,14 +61,14 @@ export function CompareCard({ user }: { user: PublicUser }) {
return () => {
alive = false;
};
- }, [handle, attempt]);
+ }, [handle, attempt, describeError]);
- const description = shared.status === "ready" && shared.titles.length > 0 ? `You and ${handle}, in a game you both have.` : `Your trophies next to ${handle}’s.`;
+ const description = shared.status === "ready" && shared.titles.length > 0 ? t("descriptionShared", { handle }) : t("description", { handle });
let body: ReactNode;
if (shared.status === "loading") {
body = (
- Loading
+ {t("loading")}
@@ -84,16 +84,16 @@ export function CompareCard({ user }: { user: PublicUser }) {
/>
);
} else if (shared.status === "hidden") {
- body = {HIDDEN(handle)};
+ body = {t("hidden", { handle })};
} else if (shared.titles.length === 0 || !title) {
- body = You and {handle} have no game in common yet. Play a game {handle} has played and it shows up here.;
+ body = {t("empty", { handle })};
} else {
body = (