Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/web/src/components/CrisisResources.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { CRISIS_RESOURCES, CRISIS_BLURB } from '@dsim/shared';
import { useTranslation } from 'react-i18next';

type CrisisResource = { label: string; detail: string };

/**
* Real-world crisis resources, shown to the PLAYER (not the fiction) — at the
* tragic-outcomes opt-in and on a memorialized character's page. Calm and plain.
*/
export function CrisisResources({ className = '' }: { className?: string }) {
const { t } = useTranslation('common');
const blurb = t('crisis.blurb');
const resources = t('crisis.resources', { returnObjects: true }) as CrisisResource[];
return (
<div className={`crisis-card ${className}`}>
<p className="crisis-blurb">{CRISIS_BLURB}</p>
<p className="crisis-blurb">{blurb}</p>
<ul className="crisis-list">
{CRISIS_RESOURCES.map((r) => (
{resources.map((r) => (
<li key={r.label}>
<strong>{r.label}</strong> — {r.detail}
</li>
Expand Down
11 changes: 5 additions & 6 deletions apps/web/src/components/phone/TogetherApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import type { ActivityDef, Character, TogetherResult } from '@dsim/shared';
import { fitLabel, togetherFit } from '@dsim/shared';
import { api } from '../../lib/api';
Expand All @@ -14,14 +15,12 @@ import { ResultCard, type ResultTone } from '../ResultCard';
import { Banner } from '../ui';
import './phone-life.css';

type TFn = (key: string, opts?: Record<string, unknown>) => string;

type TogetherNote = { tone: ResultTone; seal: string; summary: string };

/** How an outing landed, as a keepsake card: a tone + seal + the descriptive line.
* Warmth (spark/warm) reads rose; a flat/crowded afternoon reads moonlit; a
* misfire reads ember. */
function togetherNote(res: TogetherResult, name: string, t: TFn): TogetherNote {
function togetherNote(res: TogetherResult, name: string, t: TFunction<'phone'>): TogetherNote {
const stat = relationshipStatLabel(res.stat).toLowerCase();
const tension = res.tensionDelta > 0 ? t('together.tensionSuffix', { n: res.tensionDelta }) : '';
switch (res.outcome) {
Expand Down Expand Up @@ -137,7 +136,7 @@ export function TogetherApp() {
const res = await api.performActivity({ activityId: a.id, worldId: activeWorldId, characterId: target });
await Promise.all([reloadPlayer(), refreshWorldState()]);
const name = partner?.name.split(' ')[0] ?? t('together.they');
if (res.together) setNote(togetherNote(res.together, name, t as unknown as TFn));
if (res.together) setNote(togetherNote(res.together, name, t));
} catch (e) {
setError(errorMessage(e));
} finally {
Expand Down Expand Up @@ -202,8 +201,8 @@ export function TogetherApp() {
<div className="pl-tile pl-together" key={a.id}>
<div className="pl-tile-icon" aria-hidden="true">{a.icon ?? '☕'}</div>
<div className="pl-tile-body">
<div className="pl-tile-label">{a.label}</div>
<div className="pl-tile-desc">{a.description}</div>
<div className="pl-tile-label">{t(`together.activities.${a.id}.label` as never)}</div>
<div className="pl-tile-desc">{t(`together.activities.${a.id}.description` as never)}</div>
<div className="pl-meta">
{tier && <span className={`pl-fit ${FIT_CLS[tier]}`}>{t(`together.fit.${tier}`)}</span>}
{bold && <span className="pl-bold">{t('together.bolder')}</span>}
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/phone/WorkApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function WorkApp() {

/** Jobs that feed a trade, for the trade card's "earned at" line. */
const jobsFor = (s: string): string[] => [
...work.filter((a) => a.skill === s).map((a) => a.label),
...work.filter((a) => a.skill === s).map((a) => t(`work.activities.${a.id}.label` as never)),
...jobGames.filter((g) => g.skill === s).map((g) => g.title),
];

Expand Down Expand Up @@ -344,8 +344,8 @@ export function WorkApp() {
{a.icon ? <span className="pl-tile-glyph">{a.icon}</span> : <Icon name="work" size={18} />}
</div>
<div className="pl-tile-body">
<div className="pl-tile-label">{a.label}</div>
<div className="pl-tile-desc">{a.description}</div>
<div className="pl-tile-label">{t(`work.activities.${a.id}.label` as never)}</div>
<div className="pl-tile-desc">{t(`work.activities.${a.id}.description` as never)}</div>
<div className="pl-work-tags">
{isCareerSkill(a.skill) && <span className="pl-work-tag">{skillName(a.skill)}</span>}
{v > 0 && <span className="pl-work-tag">{t('work.payVaries')}</span>}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const PSEUDO_LOCALE = 'en-XA';

export const SUPPORTED_LOCALES: LocaleMeta[] = [
{ code: 'en', label: 'English', dir: 'ltr' },
// Add real locales here, e.g. { code: 'es', label: 'Español', dir: 'ltr' }.
{ code: 'zh-Hans', label: '简体中文', dir: 'ltr' },
...(import.meta.env.DEV ? [{ code: PSEUDO_LOCALE, label: 'Pseudo (QA)', dir: 'ltr' as const }] : []),
];

Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,5 +540,22 @@
"bag": "Bag",
"settings": "Settings"
}
},
"crisis": {
"blurb": "If you or someone you know is struggling, you are not alone — free, confidential help is available any time.",
"resources": [
{
"label": "988 Suicide & Crisis Lifeline (US)",
"detail": "Call or text 988, any time, day or night."
},
{
"label": "Crisis Text Line",
"detail": "Text HOME to 741741 (US & Canada), or 85258 (UK)."
},
{
"label": "Find a helpline anywhere",
"detail": "findahelpline.com lists free, confidential lines worldwide."
}
]
}
}
22 changes: 22 additions & 0 deletions apps/web/src/i18n/locales/en/phone.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@
"unavailableToday": "Unavailable today",
"isUnavailableToday": "is unavailable today",
"they": "They",
"unavailableReasons": {
"0": "is buried in work today",
"1": "is out of town until tomorrow",
"2": "needs some time alone to recharge",
"3": "is tied up with family business",
"4": "is feeling under the weather",
"5": "has a packed schedule today",
"6": "is off the grid, dealing with something personal",
"7": "asked for a quiet day to themselves"
},
"landlord": {
"title": "Property Management",
"notices": "Notices",
Expand Down Expand Up @@ -494,6 +504,11 @@
"work": {
"title": "Work",
"onShift": "on shift",
"activities": {
"work_shift": { "label": "Work a shift", "description": "Steady hours for steady pay. No surprises — and you get a little better at the work each time." },
"odd_jobs": { "label": "Hustle odd jobs", "description": "Grittier work, an uneven cut — clean up on a good day, scrape by on a bad one." },
"job_weatherwork": { "label": "Day-labor outdoors", "description": "Hard graft the almanac prices by the sky — storms pay best, fair days least. Costs two actions." }
},
"daysShifts": "the day's shifts",
"errWorld": "Pick an active world first.",
"errOnDate": "You're on a date with {name} — wrap it up on the Date tab first.",
Expand Down Expand Up @@ -581,6 +596,13 @@
"together": {
"title": "Together",
"kicker": "time, shared",
"activities": {
"tg_in": { "label": "A quiet one in", "description": "Cook, talk, let the hours go slow. The safe, easy kind of closeness." },
"tg_talk": { "label": "A long heart-to-heart", "description": "Open up and really listen — the kind of talk that earns trust, or asks for too much too soon." },
"tg_out": { "label": "Out for a run together", "description": "Sweat, banter, a little chemistry on the move." },
"tg_play": { "label": "Mess around, no agenda", "description": "An afternoon of games and nonsense. Costs a little; almost always lands light." },
"tg_culture": { "label": "Wander a gallery", "description": "Trade quiet opinions among the frames. Earns respect from the right sort of person." }
},
"errWorld": "Pick an active world first.",
"errOnDate": "You're on a date with {name} — wrap it up on the Date tab first.",
"errOnHangout": "You're hanging out with {name} — wrap it up on the Date tab first.",
Expand Down
Loading