fix(native/games): game-over modal — maxHeight 85%, KeyboardAvoidingView, Button comp, regenerate
Four issues from the screenshot review plus one new affordance: 1. Modal overflowing on small devices — capped at maxHeight: '85%'. Header (handle bar + Lyra avatar + title + subtitle) stays fixed above a ScrollView body; action buttons stay fixed below with a border separator. Stat cards, star rating, and TextInput now live inside the scrollable body. 2. Keyboard pushed the TextInput out of sight — replaced the bespoke Keyboard.addListener + Animated.multiply lift hack (Easing, keyboardLiftY, the whole apparatus) with a plain KeyboardAvoidingView wrapper (behavior="padding" iOS / "height" Android). ScrollView already had keyboardShouldPersistTaps="handled" so taps on Posten/Abbrechen still work while the keyboard is up. 3. All four action buttons (Nochmal, Beenden, Abbrechen, Posten) plus the inner Save-Rating CTA now route through components/Button.tsx — picks up the slimmer paddingVertical:12 default from the central component. Posten gets the paper-plane icon. Nochmal + Posten = primary, Beenden + Abbrechen = secondary. 4. New "Neuer Vorschlag" regenerate button (ghost variant, sm size, refresh-outline icon) sits between the TextInput and the Abbrechen/ Posten row. Reuses POST /api/games/share-text — no new endpoint. Tracks the last Lyra-generated text in a ref so we can detect user edits; if the user has modified the suggestion, taps go through an Alert.alert confirm before overwrite. Spinner during the regen call, Posten / Abbrechen stay active. i18n keys gameOver.regen_* across DE/EN/FR.
This commit is contained in:
parent
d28d1f145d
commit
964dc2b6e0
@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
Animated,
|
Animated,
|
||||||
Easing,
|
Easing,
|
||||||
Keyboard,
|
KeyboardAvoidingView,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
@ -18,6 +19,7 @@ import * as Haptics from 'expo-haptics';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { RiveAvatar } from '../RiveAvatar';
|
import { RiveAvatar } from '../RiveAvatar';
|
||||||
import { StarRating } from './StarRating';
|
import { StarRating } from './StarRating';
|
||||||
|
import { Button } from '../Button';
|
||||||
import { useColors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
import { apiFetch } from '../../lib/api';
|
import { apiFetch } from '../../lib/api';
|
||||||
|
|
||||||
@ -59,36 +61,7 @@ export function GameOverScreen({
|
|||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
// Slide-In Spring für den Sheet-Auftritt (eigene Bouncy-Animation behalten)
|
|
||||||
const slideAnim = useRef(new Animated.Value(500)).current;
|
const slideAnim = useRef(new Animated.Value(500)).current;
|
||||||
// Keyboard-Lift via plain RN Keyboard.addListener (funktioniert in Modals,
|
|
||||||
// anders als react-native-keyboard-controller's useKeyboardAnimation).
|
|
||||||
const keyboardLift = useRef(new Animated.Value(0)).current;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
||||||
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
||||||
const showSub = Keyboard.addListener(showEvent, (e) => {
|
|
||||||
Animated.timing(keyboardLift, {
|
|
||||||
toValue: e.endCoordinates.height,
|
|
||||||
duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220,
|
|
||||||
easing: Easing.out(Easing.cubic),
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
});
|
|
||||||
const hideSub = Keyboard.addListener(hideEvent, (e) => {
|
|
||||||
Animated.timing(keyboardLift, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: Platform.OS === 'ios' ? (e?.duration ?? 250) : 220,
|
|
||||||
easing: Easing.out(Easing.cubic),
|
|
||||||
useNativeDriver: true,
|
|
||||||
}).start();
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
showSub.remove();
|
|
||||||
hideSub.remove();
|
|
||||||
};
|
|
||||||
}, [keyboardLift]);
|
|
||||||
|
|
||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
const [feedback, setFeedback] = useState('');
|
const [feedback, setFeedback] = useState('');
|
||||||
@ -97,7 +70,9 @@ export function GameOverScreen({
|
|||||||
|
|
||||||
const [shareSectionOpen, setShareSectionOpen] = useState(false);
|
const [shareSectionOpen, setShareSectionOpen] = useState(false);
|
||||||
const [shareText, setShareText] = useState('');
|
const [shareText, setShareText] = useState('');
|
||||||
|
const lyraShareTextRef = useRef('');
|
||||||
const [shareTextLoading, setShareTextLoading] = useState(false);
|
const [shareTextLoading, setShareTextLoading] = useState(false);
|
||||||
|
const [regenLoading, setRegenLoading] = useState(false);
|
||||||
const [sharing, setSharing] = useState(false);
|
const [sharing, setSharing] = useState(false);
|
||||||
const [posted, setPosted] = useState(false);
|
const [posted, setPosted] = useState(false);
|
||||||
const [postError, setPostError] = useState(false);
|
const [postError, setPostError] = useState(false);
|
||||||
@ -118,14 +93,12 @@ export function GameOverScreen({
|
|||||||
}).start();
|
}).start();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Negativer Lift — translateY -keyboardHeight schiebt Sheet nach oben.
|
|
||||||
const keyboardLiftY = Animated.multiply(keyboardLift, -1);
|
|
||||||
|
|
||||||
function handleExit() {
|
function handleExit() {
|
||||||
Animated.timing(slideAnim, {
|
Animated.timing(slideAnim, {
|
||||||
toValue: 500,
|
toValue: 500,
|
||||||
duration: 220,
|
duration: 220,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
|
easing: Easing.in(Easing.cubic),
|
||||||
}).start(() => onExit());
|
}).start(() => onExit());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,29 +122,69 @@ export function GameOverScreen({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchShareText() {
|
||||||
|
const data = await apiFetch<{ text: string }>('/api/games/share-text', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
gameName: gameName.toLowerCase(),
|
||||||
|
score,
|
||||||
|
scoreLabel,
|
||||||
|
bestScore,
|
||||||
|
isNewRecord: score > bestScore,
|
||||||
|
mode: 'game',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data.text || `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function openShareSection() {
|
async function openShareSection() {
|
||||||
setShareTextLoading(true);
|
setShareTextLoading(true);
|
||||||
setShareSectionOpen(true);
|
setShareSectionOpen(true);
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch<{ text: string }>('/api/games/share-text', {
|
const text = await fetchShareText();
|
||||||
method: 'POST',
|
lyraShareTextRef.current = text;
|
||||||
body: {
|
setShareText(text);
|
||||||
gameName: gameName.toLowerCase(),
|
|
||||||
score,
|
|
||||||
scoreLabel,
|
|
||||||
bestScore,
|
|
||||||
isNewRecord: score > bestScore,
|
|
||||||
mode: 'game',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setShareText(data.text || `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`);
|
|
||||||
} catch {
|
} catch {
|
||||||
setShareText(`${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`);
|
const fallback = `${gameName}: ${score} ${scoreLabel ?? 'Punkte'}\n${t('gameOver.share_challenge')}`;
|
||||||
|
lyraShareTextRef.current = fallback;
|
||||||
|
setShareText(fallback);
|
||||||
} finally {
|
} finally {
|
||||||
setShareTextLoading(false);
|
setShareTextLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function regenerateShareText() {
|
||||||
|
if (regenLoading || shareTextLoading) return;
|
||||||
|
|
||||||
|
const doRegen = async () => {
|
||||||
|
setRegenLoading(true);
|
||||||
|
try {
|
||||||
|
const text = await fetchShareText();
|
||||||
|
lyraShareTextRef.current = text;
|
||||||
|
setShareText(text);
|
||||||
|
} catch {
|
||||||
|
// keep existing text on error
|
||||||
|
} finally {
|
||||||
|
setRegenLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const userModified = shareText.trim() !== lyraShareTextRef.current.trim();
|
||||||
|
|
||||||
|
if (userModified) {
|
||||||
|
Alert.alert(
|
||||||
|
t('gameOver.regen_confirm_title'),
|
||||||
|
t('gameOver.regen_confirm_body'),
|
||||||
|
[
|
||||||
|
{ text: t('common.cancel'), style: 'cancel' },
|
||||||
|
{ text: t('gameOver.regen_confirm_ok'), onPress: doRegen },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
doRegen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitCommunityPost() {
|
async function submitCommunityPost() {
|
||||||
if (!shareText.trim()) return;
|
if (!shareText.trim()) return;
|
||||||
setSharing(true);
|
setSharing(true);
|
||||||
@ -202,20 +215,19 @@ export function GameOverScreen({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible transparent animationType="none" onRequestClose={handleExit}>
|
<Modal visible transparent animationType="none" onRequestClose={handleExit}>
|
||||||
<View style={{ flex: 1, justifyContent: 'flex-end' }}>
|
<KeyboardAvoidingView
|
||||||
|
style={{ flex: 1, justifyContent: 'flex-end' }}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
<TouchableOpacity onPress={handleExit} activeOpacity={1} style={{ flex: 1 }} />
|
<TouchableOpacity onPress={handleExit} activeOpacity={1} style={{ flex: 1 }} />
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
transform: [
|
transform: [{ translateY: slideAnim }],
|
||||||
{ translateY: slideAnim },
|
|
||||||
{ translateY: keyboardLiftY },
|
|
||||||
],
|
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
borderTopLeftRadius: 28,
|
borderTopLeftRadius: 28,
|
||||||
borderTopRightRadius: 28,
|
borderTopRightRadius: 28,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
paddingHorizontal: 20,
|
maxHeight: '85%',
|
||||||
paddingBottom: insets.bottom + 24,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Grab-handle */}
|
{/* Grab-handle */}
|
||||||
@ -231,10 +243,11 @@ export function GameOverScreen({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Scrollable body */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={{ gap: 16, paddingBottom: 8 }}
|
contentContainerStyle={{ gap: 16, paddingHorizontal: 20, paddingBottom: 8 }}
|
||||||
>
|
>
|
||||||
{/* Lyra avatar + message */}
|
{/* Lyra avatar + message */}
|
||||||
<View style={{ alignItems: 'center', gap: 8 }}>
|
<View style={{ alignItems: 'center', gap: 8 }}>
|
||||||
@ -319,77 +332,16 @@ export function GameOverScreen({
|
|||||||
textAlignVertical: 'top',
|
textAlignVertical: 'top',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<Button
|
||||||
|
title={t('gameOver.save_rating')}
|
||||||
onPress={submitRating}
|
onPress={submitRating}
|
||||||
disabled={saving}
|
loading={saving}
|
||||||
activeOpacity={0.7}
|
variant="primary"
|
||||||
style={{
|
size="md"
|
||||||
backgroundColor: '#007AFF',
|
/>
|
||||||
borderRadius: 12,
|
|
||||||
minHeight: 40,
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
opacity: saving ? 0.65 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#ffffff' }}>
|
|
||||||
{saving ? t('common.loading') : t('gameOver.save_rating')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Primary action row */}
|
|
||||||
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
|
||||||
onRetry();
|
|
||||||
}}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#007AFF',
|
|
||||||
borderRadius: 12,
|
|
||||||
minHeight: 40,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#ffffff' }}>
|
|
||||||
{t('gameOver.retry')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
|
||||||
handleExit();
|
|
||||||
}}
|
|
||||||
activeOpacity={0.75}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#e5e7eb',
|
|
||||||
borderRadius: 12,
|
|
||||||
minHeight: 40,
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(0,0,0,0.08)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#374151' }}>
|
|
||||||
{t('gameOver.exit')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Share section */}
|
{/* Share section */}
|
||||||
{posted ? (
|
{posted ? (
|
||||||
<View style={{ alignItems: 'center', paddingVertical: 4, flexDirection: 'row', justifyContent: 'center', gap: 6 }}>
|
<View style={{ alignItems: 'center', paddingVertical: 4, flexDirection: 'row', justifyContent: 'center', gap: 6 }}>
|
||||||
@ -439,6 +391,21 @@ export function GameOverScreen({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Regenerate suggestion button */}
|
||||||
|
{!shareTextLoading ? (
|
||||||
|
<View style={{ alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
title={t('gameOver.regen_suggestion')}
|
||||||
|
onPress={regenerateShareText}
|
||||||
|
disabled={regenLoading || sharing}
|
||||||
|
loading={regenLoading}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon="refresh-outline"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{postError ? (
|
{postError ? (
|
||||||
<Text style={{ fontSize: 12, color: colors.error, fontFamily: 'Nunito_600SemiBold', textAlign: 'center' }}>
|
<Text style={{ fontSize: 12, color: colors.error, fontFamily: 'Nunito_600SemiBold', textAlign: 'center' }}>
|
||||||
{t('gameOver.post_error')}
|
{t('gameOver.post_error')}
|
||||||
@ -446,60 +413,63 @@ export function GameOverScreen({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', gap: 12 }}>
|
<View style={{ flexDirection: 'row', gap: 12 }}>
|
||||||
<TouchableOpacity
|
<Button
|
||||||
|
title={t('common.cancel')}
|
||||||
onPress={() => { setShareSectionOpen(false); setShareText(''); setPostError(false); }}
|
onPress={() => { setShareSectionOpen(false); setShareText(''); setPostError(false); }}
|
||||||
activeOpacity={0.7}
|
variant="secondary"
|
||||||
style={{
|
size="md"
|
||||||
flex: 1,
|
style={{ flex: 1 }}
|
||||||
backgroundColor: '#e5e7eb',
|
/>
|
||||||
borderRadius: 12,
|
<Button
|
||||||
minHeight: 40,
|
title={t('gameOver.post_to_community')}
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'rgba(0,0,0,0.08)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#374151' }}>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={submitCommunityPost}
|
onPress={submitCommunityPost}
|
||||||
disabled={!shareText.trim() || sharing || shareTextLoading}
|
disabled={!shareText.trim() || sharing || shareTextLoading}
|
||||||
activeOpacity={0.85}
|
loading={sharing}
|
||||||
style={{
|
variant="primary"
|
||||||
flex: 1,
|
size="md"
|
||||||
backgroundColor: '#007AFF',
|
icon="paper-plane-outline"
|
||||||
borderRadius: 12,
|
style={{ flex: 1 }}
|
||||||
minHeight: 40,
|
/>
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 6,
|
|
||||||
opacity: sharing || !shareText.trim() || shareTextLoading ? 0.55 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sharing ? (
|
|
||||||
<ActivityIndicator size="small" color="#ffffff" />
|
|
||||||
) : (
|
|
||||||
<Ionicons name="paper-plane-outline" size={16} color="#ffffff" />
|
|
||||||
)}
|
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#ffffff' }}>
|
|
||||||
{t('gameOver.post_to_community')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Fixed footer — primary action row */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 12,
|
||||||
|
paddingBottom: insets.bottom + 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
title={t('gameOver.retry')}
|
||||||
|
onPress={() => {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||||
|
onRetry();
|
||||||
|
}}
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={t('gameOver.exit')}
|
||||||
|
onPress={() => {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||||
|
handleExit();
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1011,7 +1011,11 @@
|
|||||||
"share_loading": "Lyra formuliert...",
|
"share_loading": "Lyra formuliert...",
|
||||||
"post_to_community": "Posten",
|
"post_to_community": "Posten",
|
||||||
"posted": "Im Community-Feed gepostet",
|
"posted": "Im Community-Feed gepostet",
|
||||||
"post_error": "Posten fehlgeschlagen, nochmal versuchen"
|
"post_error": "Posten fehlgeschlagen, nochmal versuchen",
|
||||||
|
"regen_suggestion": "Neuer Vorschlag",
|
||||||
|
"regen_confirm_title": "Text verwerfen?",
|
||||||
|
"regen_confirm_body": "Deinen aktuellen Text verwerfen und neuen Vorschlag holen?",
|
||||||
|
"regen_confirm_ok": "Verwerfen"
|
||||||
},
|
},
|
||||||
"alert": {
|
"alert": {
|
||||||
"error_generic": "Etwas ist schiefgelaufen — versuch es nochmal.",
|
"error_generic": "Etwas ist schiefgelaufen — versuch es nochmal.",
|
||||||
|
|||||||
@ -1011,7 +1011,11 @@
|
|||||||
"share_loading": "Lyra is writing...",
|
"share_loading": "Lyra is writing...",
|
||||||
"post_to_community": "Post",
|
"post_to_community": "Post",
|
||||||
"posted": "Posted to the community feed",
|
"posted": "Posted to the community feed",
|
||||||
"post_error": "Posting failed, please try again"
|
"post_error": "Posting failed, please try again",
|
||||||
|
"regen_suggestion": "New suggestion",
|
||||||
|
"regen_confirm_title": "Discard text?",
|
||||||
|
"regen_confirm_body": "Discard your current text and fetch a new suggestion?",
|
||||||
|
"regen_confirm_ok": "Discard"
|
||||||
},
|
},
|
||||||
"alert": {
|
"alert": {
|
||||||
"error_generic": "Something went wrong — please try again.",
|
"error_generic": "Something went wrong — please try again.",
|
||||||
|
|||||||
@ -1008,7 +1008,11 @@
|
|||||||
"share_loading": "Lyra rédige...",
|
"share_loading": "Lyra rédige...",
|
||||||
"post_to_community": "Publier",
|
"post_to_community": "Publier",
|
||||||
"posted": "Publié dans le fil communautaire",
|
"posted": "Publié dans le fil communautaire",
|
||||||
"post_error": "Publication échouée, veuillez réessayer"
|
"post_error": "Publication échouée, veuillez réessayer",
|
||||||
|
"regen_suggestion": "Nouvelle suggestion",
|
||||||
|
"regen_confirm_title": "Effacer le texte ?",
|
||||||
|
"regen_confirm_body": "Effacer votre texte actuel et obtenir une nouvelle suggestion ?",
|
||||||
|
"regen_confirm_ok": "Effacer"
|
||||||
},
|
},
|
||||||
"alert": {
|
"alert": {
|
||||||
"error_generic": "Une erreur est survenue — veuillez réessayer.",
|
"error_generic": "Une erreur est survenue — veuillez réessayer.",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user