mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
feat(native): add English and Spanish i18n support
This commit is contained in:
@@ -4,6 +4,7 @@ import { ClerkProvider, SignedIn, SignedOut, useAuth } from "@clerk/clerk-expo";
|
||||
import { tokenCache } from "@clerk/clerk-expo/token-cache";
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { I18nProvider, useI18n } from "./src/i18n/I18nProvider";
|
||||
import { useAppTheme } from "./src/lib/theme";
|
||||
import { SignInScreen } from "./src/screens/SignInScreen";
|
||||
import { StreamerScreen } from "./src/screens/StreamerScreen";
|
||||
@@ -35,13 +36,22 @@ function AuthReadyGate() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<AppContent />
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const theme = useAppTheme();
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!publishableKey) {
|
||||
return (
|
||||
<View style={[styles.loadingWrap, { backgroundColor: theme.background }]}>
|
||||
<Text style={[styles.errorText, { color: theme.destructive }]}>
|
||||
Missing EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY
|
||||
{t("missingClerkKey")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ Simple React Native streamer app that:
|
||||
- Captures Android screen with `getDisplayMedia()`
|
||||
- Hosts a room on `/ws/signaling` and streams to connected viewers
|
||||
- Uses matching light/dark palette semantics from the Helium web app
|
||||
- Includes built-in i18n for English and Spanish based on device locale
|
||||
|
||||
## Auth implementation notes (from Clerk docs via Context7)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "react-native-webrtc";
|
||||
|
||||
import { getSignalingUrl } from "../lib/signaling";
|
||||
import type { MessageKey } from "../i18n/messages";
|
||||
import type {
|
||||
IncomingSignalingMessage,
|
||||
NativeIceServer,
|
||||
@@ -23,7 +24,8 @@ interface PeerConnectionHandlers {
|
||||
}
|
||||
|
||||
interface UseHeliumStreamerResult {
|
||||
status: string;
|
||||
statusKey: MessageKey;
|
||||
statusParams?: Record<string, string | number>;
|
||||
roomCode: string;
|
||||
streamUrl: string | null;
|
||||
viewerCount: number;
|
||||
@@ -78,7 +80,10 @@ export function useHeliumStreamer(
|
||||
const heartbeatRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const peersRef = useRef<Record<string, RTCPeerConnection>>({});
|
||||
|
||||
const [status, setStatus] = useState<string>("idle");
|
||||
const [statusKey, setStatusKey] = useState<MessageKey>("statusIdle");
|
||||
const [statusParams, setStatusParams] = useState<
|
||||
Record<string, string | number> | undefined
|
||||
>(undefined);
|
||||
const [roomCode, setRoomCode] = useState<string>("");
|
||||
const [streamUrl, setStreamUrl] = useState<string | null>(null);
|
||||
const [viewerCount, setViewerCount] = useState<number>(0);
|
||||
@@ -126,7 +131,8 @@ export function useHeliumStreamer(
|
||||
setRoomCode("");
|
||||
setStreamUrl(null);
|
||||
setIsSharing(false);
|
||||
setStatus("stopped");
|
||||
setStatusKey("statusStopped");
|
||||
setStatusParams(undefined);
|
||||
}, [closeAllPeers]);
|
||||
|
||||
const handleViewerJoined = useCallback(
|
||||
@@ -169,7 +175,8 @@ export function useHeliumStreamer(
|
||||
};
|
||||
|
||||
peerWithHandlers.onconnectionstatechange = (): void => {
|
||||
setStatus(`viewer ${viewerId}: ${peer.connectionState}`);
|
||||
setStatusKey("statusPeerState");
|
||||
setStatusParams({ state: peer.connectionState });
|
||||
};
|
||||
|
||||
const offer = (await peer.createOffer()) as NativeSessionDescriptionInit;
|
||||
@@ -191,12 +198,14 @@ export function useHeliumStreamer(
|
||||
|
||||
if (message.event === "room-created") {
|
||||
setRoomCode(message.roomId);
|
||||
setStatus(`room code: ${message.roomId}`);
|
||||
setStatusKey("statusRoomCreated");
|
||||
setStatusParams({ roomId: message.roomId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.event === "viewer-joined") {
|
||||
setStatus(`viewer joined: ${message.viewerId}`);
|
||||
setStatusKey("statusViewerJoined");
|
||||
setStatusParams(undefined);
|
||||
await handleViewerJoined(message.viewerId);
|
||||
return;
|
||||
}
|
||||
@@ -232,7 +241,8 @@ export function useHeliumStreamer(
|
||||
}
|
||||
|
||||
if (message.event === "error") {
|
||||
setStatus(`error: ${message.message}`);
|
||||
setStatusKey("statusError");
|
||||
setStatusParams({ message: message.message });
|
||||
}
|
||||
},
|
||||
[handleViewerJoined],
|
||||
@@ -242,11 +252,13 @@ export function useHeliumStreamer(
|
||||
stopSharing();
|
||||
|
||||
if (!iceServers.length) {
|
||||
setStatus("no preset selected");
|
||||
setStatusKey("statusNoPreset");
|
||||
setStatusParams(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("requesting screen capture");
|
||||
setStatusKey("statusRequestingCapture");
|
||||
setStatusParams(undefined);
|
||||
const stream = await (mediaDevices as unknown as {
|
||||
getDisplayMedia: (constraints?: {
|
||||
video?: boolean;
|
||||
@@ -264,12 +276,14 @@ export function useHeliumStreamer(
|
||||
const audioTrackCount = stream.getAudioTracks().length;
|
||||
|
||||
if (!videoTrackCount) {
|
||||
setStatus("screen capture started without video track");
|
||||
setStatusKey("statusNoVideoTrack");
|
||||
setStatusParams(undefined);
|
||||
stopSharing();
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(`capturing ${videoTrackCount} video / ${audioTrackCount} audio tracks`);
|
||||
setStatusKey("statusCapturing");
|
||||
setStatusParams({ video: videoTrackCount, audio: audioTrackCount });
|
||||
|
||||
stream.getTracks().forEach((track) => {
|
||||
const streamTrack = track as unknown as MediaStreamTrack & {
|
||||
@@ -280,13 +294,15 @@ export function useHeliumStreamer(
|
||||
};
|
||||
});
|
||||
|
||||
setStatus("connecting signaling");
|
||||
setStatusKey("statusConnectingSignaling");
|
||||
setStatusParams(undefined);
|
||||
|
||||
const ws = new WebSocket(getSignalingUrl());
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = (): void => {
|
||||
setStatus("creating room");
|
||||
setStatusKey("statusCreatingRoom");
|
||||
setStatusParams(undefined);
|
||||
sendMessage({ event: "create-room" });
|
||||
|
||||
heartbeatRef.current = setInterval(() => {
|
||||
@@ -299,7 +315,8 @@ export function useHeliumStreamer(
|
||||
};
|
||||
|
||||
ws.onerror = (): void => {
|
||||
setStatus("websocket error");
|
||||
setStatusKey("statusWebsocketError");
|
||||
setStatusParams(undefined);
|
||||
};
|
||||
|
||||
ws.onclose = (): void => {
|
||||
@@ -308,7 +325,8 @@ export function useHeliumStreamer(
|
||||
heartbeatRef.current = null;
|
||||
}
|
||||
if (isSharing) {
|
||||
setStatus("websocket closed");
|
||||
setStatusKey("statusWebsocketClosed");
|
||||
setStatusParams(undefined);
|
||||
}
|
||||
};
|
||||
}, [handleIncomingMessage, iceServers, isSharing, sendMessage, stopSharing]);
|
||||
@@ -320,7 +338,8 @@ export function useHeliumStreamer(
|
||||
}, [stopSharing]);
|
||||
|
||||
return {
|
||||
status,
|
||||
statusKey,
|
||||
statusParams,
|
||||
roomCode,
|
||||
streamUrl,
|
||||
viewerCount,
|
||||
|
||||
54
native-app/src/i18n/I18nProvider.tsx
Normal file
54
native-app/src/i18n/I18nProvider.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
|
||||
import { messages, type Locale, type MessageKey } from "./messages";
|
||||
|
||||
interface I18nContextValue {
|
||||
locale: Locale;
|
||||
t: (key: MessageKey, params?: Record<string, string | number>) => string;
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
function resolveLocale(): Locale {
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase();
|
||||
return locale.startsWith("es") ? "es" : "en";
|
||||
}
|
||||
|
||||
function translate(
|
||||
locale: Locale,
|
||||
key: MessageKey,
|
||||
params?: Record<string, string | number>,
|
||||
): string {
|
||||
const template = messages[locale][key] ?? messages.en[key] ?? key;
|
||||
|
||||
if (!params) {
|
||||
return template;
|
||||
}
|
||||
|
||||
return Object.entries(params).reduce((result, [paramKey, value]) => {
|
||||
return result.replaceAll(`{${paramKey}}`, String(value));
|
||||
}, template);
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const locale = resolveLocale();
|
||||
|
||||
const value = useMemo<I18nContextValue>(() => {
|
||||
return {
|
||||
locale,
|
||||
t: (key, params) => translate(locale, key, params),
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
||||
}
|
||||
|
||||
export function useI18n(): I18nContextValue {
|
||||
const context = useContext(I18nContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useI18n must be used within I18nProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
138
native-app/src/i18n/messages.ts
Normal file
138
native-app/src/i18n/messages.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
export type Locale = "en" | "es";
|
||||
|
||||
export type MessageKey =
|
||||
| "missingClerkKey"
|
||||
| "appTitle"
|
||||
| "signInSubtitle"
|
||||
| "email"
|
||||
| "password"
|
||||
| "signIn"
|
||||
| "signingIn"
|
||||
| "signedIn"
|
||||
| "signInFailed"
|
||||
| "needsExtraStep"
|
||||
| "loadingPresets"
|
||||
| "couldNotReadToken"
|
||||
| "noPresetsFound"
|
||||
| "loadedIceServers"
|
||||
| "failedToLoadPresets"
|
||||
| "failedToParsePreset"
|
||||
| "streamerTitle"
|
||||
| "streamerSubtitle"
|
||||
| "preset"
|
||||
| "session"
|
||||
| "status"
|
||||
| "viewers"
|
||||
| "defaultLabel"
|
||||
| "startShare"
|
||||
| "stop"
|
||||
| "signOut"
|
||||
| "previewActive"
|
||||
| "previewIdle"
|
||||
| "statusIdle"
|
||||
| "statusStopped"
|
||||
| "statusNoPreset"
|
||||
| "statusRequestingCapture"
|
||||
| "statusNoVideoTrack"
|
||||
| "statusCapturing"
|
||||
| "statusConnectingSignaling"
|
||||
| "statusCreatingRoom"
|
||||
| "statusRoomCreated"
|
||||
| "statusViewerJoined"
|
||||
| "statusPeerState"
|
||||
| "statusWebsocketError"
|
||||
| "statusWebsocketClosed"
|
||||
| "statusError";
|
||||
|
||||
type MessageMap = Record<MessageKey, string>;
|
||||
|
||||
export const messages: Record<Locale, MessageMap> = {
|
||||
en: {
|
||||
missingClerkKey: "Missing EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
||||
appTitle: "Helium Native",
|
||||
signInSubtitle: "Sign in with Clerk",
|
||||
email: "Email",
|
||||
password: "Password",
|
||||
signIn: "Sign in",
|
||||
signingIn: "Signing in...",
|
||||
signedIn: "Signed in",
|
||||
signInFailed: "Sign-in failed",
|
||||
needsExtraStep: "Needs extra step: {status}",
|
||||
loadingPresets: "Loading presets",
|
||||
couldNotReadToken: "Could not read auth token",
|
||||
noPresetsFound: "No presets found",
|
||||
loadedIceServers: "Loaded {count} ICE server entries",
|
||||
failedToLoadPresets: "Failed to load presets: {message}",
|
||||
failedToParsePreset: "Failed to parse ICE servers from preset",
|
||||
streamerTitle: "Helium Streamer",
|
||||
streamerSubtitle: "Share your Android screen to Helium viewers",
|
||||
preset: "Preset",
|
||||
session: "Session",
|
||||
status: "Status",
|
||||
viewers: "Viewers",
|
||||
defaultLabel: "default",
|
||||
startShare: "Start screen share",
|
||||
stop: "Stop",
|
||||
signOut: "Sign out",
|
||||
previewActive: "Screen capture active. Preview disabled to reduce latency.",
|
||||
previewIdle: "Start sharing to broadcast this phone screen",
|
||||
statusIdle: "Idle",
|
||||
statusStopped: "Stopped",
|
||||
statusNoPreset: "No preset selected",
|
||||
statusRequestingCapture: "Requesting screen capture",
|
||||
statusNoVideoTrack: "Screen capture started without video track",
|
||||
statusCapturing: "Capturing {video} video / {audio} audio tracks",
|
||||
statusConnectingSignaling: "Connecting signaling",
|
||||
statusCreatingRoom: "Creating room",
|
||||
statusRoomCreated: "Room code: {roomId}",
|
||||
statusViewerJoined: "Viewer joined",
|
||||
statusPeerState: "Peer state: {state}",
|
||||
statusWebsocketError: "WebSocket error",
|
||||
statusWebsocketClosed: "WebSocket closed",
|
||||
statusError: "Error: {message}",
|
||||
},
|
||||
es: {
|
||||
missingClerkKey: "Falta EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
||||
appTitle: "Helium Nativo",
|
||||
signInSubtitle: "Inicia sesion con Clerk",
|
||||
email: "Correo",
|
||||
password: "Contrasena",
|
||||
signIn: "Iniciar sesion",
|
||||
signingIn: "Iniciando sesion...",
|
||||
signedIn: "Sesion iniciada",
|
||||
signInFailed: "Error al iniciar sesion",
|
||||
needsExtraStep: "Falta un paso adicional: {status}",
|
||||
loadingPresets: "Cargando presets",
|
||||
couldNotReadToken: "No se pudo leer el token",
|
||||
noPresetsFound: "No se encontraron presets",
|
||||
loadedIceServers: "Se cargaron {count} entradas ICE",
|
||||
failedToLoadPresets: "Error al cargar presets: {message}",
|
||||
failedToParsePreset: "Error al parsear ICE del preset",
|
||||
streamerTitle: "Helium Emisor",
|
||||
streamerSubtitle: "Comparte la pantalla de Android con Helium",
|
||||
preset: "Preset",
|
||||
session: "Sesion",
|
||||
status: "Estado",
|
||||
viewers: "Espectadores",
|
||||
defaultLabel: "predeterminado",
|
||||
startShare: "Iniciar pantalla",
|
||||
stop: "Detener",
|
||||
signOut: "Cerrar sesion",
|
||||
previewActive: "Captura activa. Vista previa desactivada para menor latencia.",
|
||||
previewIdle: "Inicia la captura para transmitir esta pantalla",
|
||||
statusIdle: "En espera",
|
||||
statusStopped: "Detenido",
|
||||
statusNoPreset: "No hay preset seleccionado",
|
||||
statusRequestingCapture: "Solicitando captura de pantalla",
|
||||
statusNoVideoTrack: "La captura inicio sin pista de video",
|
||||
statusCapturing: "Capturando {video} video / {audio} audio",
|
||||
statusConnectingSignaling: "Conectando senalizacion",
|
||||
statusCreatingRoom: "Creando sala",
|
||||
statusRoomCreated: "Codigo de sala: {roomId}",
|
||||
statusViewerJoined: "Se unio un espectador",
|
||||
statusPeerState: "Estado del peer: {state}",
|
||||
statusWebsocketError: "Error de WebSocket",
|
||||
statusWebsocketClosed: "WebSocket cerrado",
|
||||
statusError: "Error: {message}",
|
||||
},
|
||||
};
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
} from "react-native";
|
||||
|
||||
import { useAppTheme } from "../lib/theme";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
export function SignInScreen() {
|
||||
const theme = useAppTheme();
|
||||
const { t } = useI18n();
|
||||
const { isLoaded, signIn, setActive } = useSignIn();
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
@@ -24,7 +26,7 @@ export function SignInScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("Signing in...");
|
||||
setStatus(t("signingIn"));
|
||||
|
||||
try {
|
||||
const attempt = await signIn.create({
|
||||
@@ -34,33 +36,33 @@ export function SignInScreen() {
|
||||
|
||||
if (attempt.status === "complete") {
|
||||
await setActive({ session: attempt.createdSessionId });
|
||||
setStatus("Signed in");
|
||||
setStatus(t("signedIn"));
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(`Needs extra step: ${attempt.status}`);
|
||||
setStatus(t("needsExtraStep", { status: String(attempt.status) }));
|
||||
} catch {
|
||||
setStatus("Sign-in failed");
|
||||
setStatus(t("signInFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Helium Native</Text>
|
||||
<Text style={styles.subtitle}>Sign in with Clerk</Text>
|
||||
<Text style={styles.title}>{t("appTitle")}</Text>
|
||||
<Text style={styles.subtitle}>{t("signInSubtitle")}</Text>
|
||||
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="Email"
|
||||
placeholder={t("email")}
|
||||
placeholderTextColor="#7e8794"
|
||||
style={styles.input}
|
||||
value={email}
|
||||
/>
|
||||
<TextInput
|
||||
onChangeText={setPassword}
|
||||
placeholder="Password"
|
||||
placeholder={t("password")}
|
||||
placeholderTextColor="#7e8794"
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
@@ -75,7 +77,7 @@ export function SignInScreen() {
|
||||
style={styles.button}
|
||||
>
|
||||
{isLoaded ? (
|
||||
<Text style={styles.buttonText}>Sign in</Text>
|
||||
<Text style={styles.buttonText}>{t("signIn")}</Text>
|
||||
) : (
|
||||
<ActivityIndicator color={theme.primaryForeground} />
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "react-native";
|
||||
|
||||
import { useHeliumStreamer } from "../hooks/useHeliumStreamer";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import type { MessageKey } from "../i18n/messages";
|
||||
import { useAppTheme } from "../lib/theme";
|
||||
import { getPresets } from "../lib/presets";
|
||||
import type { NativeIceServer, PresetUser } from "../types/presets";
|
||||
@@ -17,16 +19,21 @@ import type { NativeIceServer, PresetUser } from "../types/presets";
|
||||
export function StreamerScreen() {
|
||||
const { getToken, signOut } = useAuth();
|
||||
const theme = useAppTheme();
|
||||
const { t } = useI18n();
|
||||
|
||||
const [presets, setPresets] = useState<PresetUser[]>([]);
|
||||
const [presetId, setPresetId] = useState<string>("");
|
||||
const [iceServers, setIceServers] = useState<NativeIceServer[]>([]);
|
||||
const [presetStatus, setPresetStatus] = useState<string>("loading presets");
|
||||
const [presetStatusKey, setPresetStatusKey] = useState<MessageKey>("loadingPresets");
|
||||
const [presetStatusParams, setPresetStatusParams] = useState<
|
||||
Record<string, string | number> | undefined
|
||||
>(undefined);
|
||||
|
||||
const styles = useMemo(() => createStyles(theme), [theme]);
|
||||
|
||||
const {
|
||||
status,
|
||||
statusKey,
|
||||
statusParams,
|
||||
roomCode,
|
||||
viewerCount,
|
||||
isSharing,
|
||||
@@ -43,7 +50,8 @@ export function StreamerScreen() {
|
||||
const token = await getToken();
|
||||
|
||||
if (!token) {
|
||||
setPresetStatus("could not read auth token");
|
||||
setPresetStatusKey("couldNotReadToken");
|
||||
setPresetStatusParams(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +60,8 @@ export function StreamerScreen() {
|
||||
setPresets(availablePresets);
|
||||
|
||||
if (!availablePresets.length) {
|
||||
setPresetStatus("no presets found");
|
||||
setPresetStatusKey("noPresetsFound");
|
||||
setPresetStatusParams(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,7 +70,8 @@ export function StreamerScreen() {
|
||||
|
||||
setPresetId(defaultPreset.presetId);
|
||||
} catch (error) {
|
||||
setPresetStatus(`failed to load presets: ${(error as Error).message}`);
|
||||
setPresetStatusKey("failedToLoadPresets");
|
||||
setPresetStatusParams({ message: (error as Error).message });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,22 +92,24 @@ export function StreamerScreen() {
|
||||
: rawIceServers;
|
||||
|
||||
setIceServers(parsedIceServers ?? []);
|
||||
setPresetStatus(`loaded ${(parsedIceServers ?? []).length} ICE server entries`);
|
||||
setPresetStatusKey("loadedIceServers");
|
||||
setPresetStatusParams({ count: (parsedIceServers ?? []).length });
|
||||
} catch {
|
||||
setIceServers([]);
|
||||
setPresetStatus("failed to parse ICE servers from preset");
|
||||
setPresetStatusKey("failedToParsePreset");
|
||||
setPresetStatusParams(undefined);
|
||||
}
|
||||
}, [selectedPreset]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<Text style={styles.title}>Helium Streamer</Text>
|
||||
<Text style={styles.subtitle}>Share your Android screen to Helium viewers</Text>
|
||||
<Text style={styles.title}>{t("streamerTitle")}</Text>
|
||||
<Text style={styles.subtitle}>{t("streamerSubtitle")}</Text>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Preset</Text>
|
||||
<Text style={styles.small}>{presetStatus}</Text>
|
||||
<Text style={styles.cardTitle}>{t("preset")}</Text>
|
||||
<Text style={styles.small}>{t(presetStatusKey, presetStatusParams)}</Text>
|
||||
|
||||
<View style={styles.presetList}>
|
||||
{presets.map((preset) => {
|
||||
@@ -117,7 +129,7 @@ export function StreamerScreen() {
|
||||
]}
|
||||
>
|
||||
{preset.preset.name}
|
||||
{preset.isDefault ? " (default)" : ""}
|
||||
{preset.isDefault ? ` (${t("defaultLabel")})` : ""}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -126,9 +138,9 @@ export function StreamerScreen() {
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Session</Text>
|
||||
<Text style={styles.small}>Status: {status}</Text>
|
||||
<Text style={styles.small}>Viewers: {viewerCount}</Text>
|
||||
<Text style={styles.cardTitle}>{t("session")}</Text>
|
||||
<Text style={styles.small}>{t("status")}: {t(statusKey, statusParams)}</Text>
|
||||
<Text style={styles.small}>{t("viewers")}: {viewerCount}</Text>
|
||||
<Text style={styles.roomCode}>{roomCode || "------"}</Text>
|
||||
|
||||
<View style={styles.actions}>
|
||||
@@ -138,11 +150,11 @@ export function StreamerScreen() {
|
||||
}}
|
||||
style={styles.primaryButton}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>Start screen share</Text>
|
||||
<Text style={styles.primaryButtonText}>{t("startShare")}</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable onPress={stopSharing} style={styles.secondaryButton}>
|
||||
<Text style={styles.secondaryButtonText}>Stop</Text>
|
||||
<Text style={styles.secondaryButtonText}>{t("stop")}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -150,10 +162,10 @@ export function StreamerScreen() {
|
||||
<View style={styles.preview}>
|
||||
{isSharing ? (
|
||||
<Text style={styles.previewPlaceholder}>
|
||||
Screen capture active. Preview disabled to reduce latency.
|
||||
{t("previewActive")}
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.previewPlaceholder}>Start sharing to broadcast this phone screen</Text>
|
||||
<Text style={styles.previewPlaceholder}>{t("previewIdle")}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -163,7 +175,7 @@ export function StreamerScreen() {
|
||||
}}
|
||||
style={styles.signOutButton}
|
||||
>
|
||||
<Text style={styles.signOutText}>Sign out</Text>
|
||||
<Text style={styles.signOutText}>{t("signOut")}</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
|
||||
Reference in New Issue
Block a user