mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 17:23:25 +00:00
Add the Weather phone experience with live widgets, configuration, and localized status details.\n\nProvide server-authorized, rate-limited remote weather camera sessions with configurable camera locations and safe client cleanup. Include test-server support and weather store coverage.
3291 lines
122 KiB
TypeScript
3291 lines
122 KiB
TypeScript
import { defineStore } from 'pinia'
|
||
|
||
import type { AppLaunchOrigin, LaunchablePhoneAppId } from '@/types/apps'
|
||
import type {
|
||
DeviceBootstrap,
|
||
DeviceSecurity,
|
||
PhoneDevice,
|
||
} from '@/types/device'
|
||
import { clampPage } from '@/utils/pages'
|
||
import { cloneJsonData } from '@/utils/clone'
|
||
import { nuiCall } from '@/utils/nui'
|
||
import type { NuiResponse } from '@/utils/nui'
|
||
import {
|
||
DEFAULT_PHONE_PREFERENCES,
|
||
parsePhonePreferences,
|
||
type AppNotificationPreferences,
|
||
type PhonePreferencesV1,
|
||
type WallpaperId,
|
||
} from '@/utils/preferences'
|
||
|
||
type LocaleTree = Record<string, unknown>
|
||
|
||
export type PasscodeResponseData = {
|
||
attemptsRemaining?: number
|
||
retryAfter?: number
|
||
security?: DeviceSecurity
|
||
}
|
||
|
||
export type PhoneOpenPayload = {
|
||
account?: DeviceBootstrap['account']
|
||
device?: PhoneDevice
|
||
lang?: string
|
||
locales?: LocaleTree
|
||
notes?: DeviceBootstrap['notes']
|
||
security?: DeviceSecurity
|
||
token?: string
|
||
}
|
||
|
||
const namespaceQueues = new Map<string, Promise<void>>()
|
||
|
||
const defaultLocales: LocaleTree = {
|
||
Apps: {
|
||
crewlink: {
|
||
name: 'CrewLink',
|
||
connecting: 'Connecting your crew...',
|
||
privateNetwork: 'Private location network',
|
||
signInTitle: 'Connect your iFruit Account',
|
||
signInBody:
|
||
'CrewLink uses your private iFruit identity to keep groups and roles available across your phones.',
|
||
openSettings: 'Open iFruit Settings',
|
||
welcomeEyebrow: 'Your crew. One signal.',
|
||
welcomeTitle: 'Find your people',
|
||
welcomeBody:
|
||
'Choose a private CrewLink username. Only confirmed group members can see your presence and shared location.',
|
||
username: 'CrewLink username',
|
||
usernamePlaceholder: 'e.g. Skyline',
|
||
createProfile: 'Create CrewLink Profile',
|
||
profileCreated: 'Your CrewLink profile is ready.',
|
||
privacyNote: 'Private by design. No public player search.',
|
||
noGroupEyebrow: 'No active crew',
|
||
noGroupTitle: 'Build your private network',
|
||
noGroupBody:
|
||
'Create a crew or join friends with a private invitation code.',
|
||
createGroup: 'Create Group',
|
||
createGroupBody:
|
||
'Give your crew a recognizable name and signal colour.',
|
||
joinGroup: 'Join Group',
|
||
joinWithCode: 'Join with Code',
|
||
joinWithCodeBody:
|
||
'Enter the private eight-character code shared by a coordinator.',
|
||
groupName: 'Group name',
|
||
groupNamePlaceholder: 'e.g. Night Shift',
|
||
groupColour: 'Signal colour',
|
||
createCrew: 'Create Crew',
|
||
joinCrew: 'Join Crew',
|
||
inviteCode: 'Invitation code',
|
||
inviteCodePlaceholder: 'AB12CD34',
|
||
groupCreated: 'Crew created.',
|
||
joinedGroup: 'You joined the crew.',
|
||
activeGroupChanged: 'Active crew changed.',
|
||
pendingInvitations: 'Invitations',
|
||
invitedBy: 'Invited by @{username}',
|
||
inviteAccepted: 'Invitation accepted.',
|
||
inviteDeclined: 'Invitation declined.',
|
||
navigation: 'CrewLink navigation',
|
||
map: 'Map',
|
||
crew: 'Crew',
|
||
pings: 'Pings',
|
||
profile: 'Profile',
|
||
onlineNow: 'online now',
|
||
zoomIn: 'Zoom in',
|
||
zoomOut: 'Zoom out',
|
||
myLocation: 'Center my location',
|
||
member: 'Member',
|
||
members: 'Members',
|
||
openCrew: 'View team',
|
||
newPing: 'New Ping',
|
||
shareLocation: 'Mark a place',
|
||
locationUnavailable: 'Your live location is currently hidden.',
|
||
activeCrew: 'Active private crew',
|
||
groupSummarySingle: '{online} of {total} member online',
|
||
groupSummary: '{online} of {total} members online',
|
||
private: 'Private',
|
||
nearby: 'Nearby',
|
||
copyCode: 'Copy code',
|
||
manage: 'Manage',
|
||
refresh: 'Refresh',
|
||
codeCopied: 'Invitation code copied.',
|
||
codeRotated: 'A new invitation code is active.',
|
||
peopleNearby: 'People Nearby',
|
||
peopleNearbyBody:
|
||
'Only CrewLink users within {distance} meters appear here.',
|
||
metersAway: '{distance} m away',
|
||
invite: 'Invite',
|
||
inviteSent: 'Invitation sent to @{username}.',
|
||
nobodyNearby: 'Nobody in range',
|
||
nobodyNearbyBody:
|
||
'Move closer to another CrewLink user and scan again.',
|
||
scanAgain: 'Scan Again',
|
||
liveCoordination: 'Live coordination',
|
||
noPings: 'No active pings',
|
||
noPingsBody: 'Share a meeting point, warning, or target with your crew.',
|
||
expiresMinutes: '{count} min left',
|
||
expiresSeconds: '{count} sec left',
|
||
sharedBy: 'Shared by @{username}',
|
||
setRoute: 'Set Route',
|
||
routeSet: 'GPS route set.',
|
||
pingCreated: 'Ping shared with your crew.',
|
||
pingRemoved: 'Ping removed.',
|
||
newPingBody:
|
||
'Share your current position or place the ping at the center of the map.',
|
||
pingLabel: 'Ping label',
|
||
pingLabelPlaceholder: 'e.g. Meet at the garage',
|
||
placeOnMap: 'Use map center',
|
||
placeOnMapBody: 'Otherwise your current position is used.',
|
||
sharePing: 'Share Ping',
|
||
pingTypes: {
|
||
meeting: 'Meet',
|
||
danger: 'Danger',
|
||
help: 'Help',
|
||
target: 'Target',
|
||
info: 'Info',
|
||
},
|
||
status: { live: 'Live', hidden: 'Location hidden', offline: 'Offline' },
|
||
roles: {
|
||
owner: 'Owner',
|
||
coordinator: 'Coordinator',
|
||
moderator: 'Moderator',
|
||
member: 'Member',
|
||
guest: 'Guest',
|
||
},
|
||
roleDescriptions: {
|
||
coordinator: 'Manage invitations, settings, roles, and pings.',
|
||
moderator: 'Invite nearby users and moderate group pings.',
|
||
member: 'Share presence and create pings when enabled.',
|
||
guest: 'View the active crew without coordination permissions.',
|
||
},
|
||
manageCrew: 'Manage Crew',
|
||
manageCrewBody: 'Update identity, permissions, and invitations.',
|
||
memberPings: 'Member pings',
|
||
memberPingsBody: 'Allow members and guests to create pings.',
|
||
allowOverhead: 'Overhead labels',
|
||
allowOverheadBody: 'Permit nearby opt-in member labels in the world.',
|
||
groupSaved: 'Crew settings saved.',
|
||
assignRole: 'Assign Role',
|
||
roleUpdated: 'Member role updated.',
|
||
transferOwnership: 'Transfer Ownership',
|
||
removeMember: 'Remove from Crew',
|
||
yourCrewLinkId: 'Your CrewLink ID',
|
||
privacyVisibility: 'Privacy & Visibility',
|
||
shareOnMap: 'Share on group map',
|
||
shareOnMapBody: 'Active members can see your live position.',
|
||
overheadLabels: 'Nearby overhead label',
|
||
overheadLabelsBody: 'Show your CrewLink name to nearby opted-in members.',
|
||
yourGroups: 'Your Groups',
|
||
createAnotherGroup: 'Create another group',
|
||
account: 'CrewLink Account',
|
||
externalApi: 'Connected resources',
|
||
externalApiBody: 'Approved scripts may add temporary group pings.',
|
||
editUsername: 'Edit Username',
|
||
editUsernameBody: 'Your username is unique across CrewLink.',
|
||
profileSaved: 'CrewLink profile saved.',
|
||
deleteGroup: 'Delete Group',
|
||
leaveGroup: 'Leave Group',
|
||
confirmAction: 'Confirm',
|
||
confirm: {
|
||
'delete-group': {
|
||
title: 'Delete this crew?',
|
||
body: 'The group, memberships, invitations, and active pings will be permanently removed.',
|
||
},
|
||
'leave-group': {
|
||
title: 'Leave this crew?',
|
||
body: 'You will lose access to its members, map, and pings.',
|
||
},
|
||
'remove-member': {
|
||
title: 'Remove this member?',
|
||
body: 'They will immediately lose access to this crew.',
|
||
},
|
||
'transfer-owner': {
|
||
title: 'Transfer ownership?',
|
||
body: 'The selected member becomes owner and you become coordinator.',
|
||
},
|
||
},
|
||
'delete-groupDone': 'Crew deleted.',
|
||
'leave-groupDone': 'You left the crew.',
|
||
'remove-memberDone': 'Member removed.',
|
||
'transfer-ownerDone': 'Ownership transferred.',
|
||
notifications: {
|
||
invite: '{actor} invited you to {group}.',
|
||
member_joined: '{actor} joined {group}.',
|
||
ping: '{actor} shared “{ping}” with {group}.',
|
||
role: 'Your CrewLink role changed in {group}.',
|
||
removed: 'You were removed from {group}.',
|
||
default: 'Your CrewLink group has an update.',
|
||
},
|
||
errors: {
|
||
not_authenticated: 'Sign in to your iFruit Account first.',
|
||
profile_required: 'Create your CrewLink profile first.',
|
||
invalid_username: 'Use 3–20 letters, numbers, dots, or underscores.',
|
||
invalid_profile: 'Check your profile details.',
|
||
username_taken: 'That CrewLink username is already taken.',
|
||
invalid_group: 'Choose a valid group name and signal colour.',
|
||
group_limit: 'You have reached your group limit.',
|
||
member_limit: 'This group has reached its member limit.',
|
||
group_not_found: 'This group is no longer available.',
|
||
invalid_code: 'That invitation code is invalid.',
|
||
already_member: 'You are already a member of this group.',
|
||
forbidden: 'Your current role cannot perform this action.',
|
||
player_not_nearby: 'That player is no longer nearby.',
|
||
player_unavailable: 'That player cannot receive a CrewLink invitation.',
|
||
invalid_invitation: 'This invitation is invalid.',
|
||
invitation_expired: 'This invitation has expired.',
|
||
invalid_role: 'That role cannot be assigned.',
|
||
owner_must_transfer: 'Transfer ownership before leaving this group.',
|
||
invalid_ping: 'Choose a valid ping type, label, and position.',
|
||
ping_limit: 'This crew already has too many active pings.',
|
||
ping_not_found: 'That ping is no longer active.',
|
||
rate_limited: 'Too many CrewLink actions. Try again shortly.',
|
||
request_failed: 'CrewLink is temporarily unavailable.',
|
||
},
|
||
},
|
||
flare: {
|
||
name: 'Flare',
|
||
signInTitle: 'Sign in to Flare',
|
||
signInBody:
|
||
'Flare profiles and matches are linked to your private iFruit account.',
|
||
createProfile: 'Create your profile',
|
||
welcome: 'Find your spark',
|
||
welcomeBody: 'Build a profile and meet people across Los Santos.',
|
||
photo: 'Profile photo',
|
||
profilePhotos: 'Profile photos',
|
||
profilePhotosBody:
|
||
'Choose up to six photos from Gallery. Your first photo is shown first.',
|
||
addPhotos: 'Add photos',
|
||
choosePhotos: 'Choose from Gallery',
|
||
primaryPhoto: 'Main',
|
||
removePhoto: 'Remove photo {number}',
|
||
yourName: 'Your name',
|
||
namePlaceholder: 'What should people call you?',
|
||
age: 'Age',
|
||
bio: 'About you',
|
||
bioPlaceholder: 'A short line that starts a conversation...',
|
||
gender: 'I am',
|
||
showMe: 'Show me',
|
||
woman: 'Woman',
|
||
man: 'Man',
|
||
nonbinary: 'Non-binary',
|
||
women: 'Women',
|
||
men: 'Men',
|
||
nonbinaryPeople: 'Non-binary people',
|
||
everyone: 'Everyone',
|
||
minimumAge: 'Minimum age',
|
||
maximumAge: 'Maximum age',
|
||
start: 'Start exploring',
|
||
saveProfile: 'Save profile',
|
||
editProfileBody: 'Keep your profile fresh and unmistakably you.',
|
||
navigation: 'Flare navigation',
|
||
discover: 'Discover',
|
||
matches: 'Matches',
|
||
profile: 'Profile',
|
||
tabs: {
|
||
discover: 'Swipe',
|
||
explore: 'Explore',
|
||
likes: 'Likes',
|
||
matches: 'Chat',
|
||
profile: 'Profile',
|
||
},
|
||
filters: 'Discovery settings',
|
||
nearby: 'Nearby',
|
||
rewind: 'Rewind',
|
||
superLike: 'Super Like',
|
||
explore: 'Explore',
|
||
clearExploreFilter: 'Show all profiles',
|
||
exploreTitle: 'Explore',
|
||
exploreBody: 'Find people who are into the same things.',
|
||
exploreModes: {
|
||
forYou: 'For You',
|
||
dateNight: 'Date Night',
|
||
freeTonight: 'Free Tonight',
|
||
longTerm: 'Long-term thing',
|
||
newFriends: 'New Friends',
|
||
weekend: 'Weekend plans',
|
||
},
|
||
likesTitle: 'Likes You',
|
||
likesBody: 'See who already likes you, then make your choice.',
|
||
matched: 'Matched',
|
||
newMatches: 'New Matches',
|
||
messages: 'Messages',
|
||
settings: 'Settings',
|
||
editProfile: 'Edit profile',
|
||
profileGoalBody: 'Shown on your discovery card.',
|
||
relationshipGoal: 'Relationship goal',
|
||
interests: 'Interests',
|
||
interestsPlaceholder: 'Music, travel, coffee',
|
||
interestsHint: 'Separate up to five interests with commas.',
|
||
discoverySettings: 'Discovery',
|
||
discoveryPreferences: 'Who you want to meet',
|
||
showProfile: 'Show me on Flare',
|
||
showProfileBody: 'Let new people discover your profile.',
|
||
discoveryPrivacyNote:
|
||
'Turning Discovery off hides you from new people. Your existing matches and chats stay available.',
|
||
discoveryOffTitle: 'Discovery is off',
|
||
discoveryOffBody:
|
||
'Your profile is hidden from new people. Your existing matches and chats stay available.',
|
||
discoveryOffShort: 'Only existing matches can still reach you.',
|
||
enableDiscovery: 'Enable Discovery',
|
||
saveSettings: 'Save settings',
|
||
likedYou: 'Likes you',
|
||
superLikedYou: 'Super Liked you',
|
||
noLikes: 'No new likes',
|
||
noLikesBody: 'People who like you before you decide will appear here.',
|
||
matchActions: 'Match actions',
|
||
messagePhoto: 'Photo',
|
||
gif: 'GIF',
|
||
video: 'Video',
|
||
attachPhoto: 'Attach Photo',
|
||
takePhoto: 'Take Photo',
|
||
emoji: 'Emoji',
|
||
attachGif: 'Attach GIF',
|
||
attachVideo: 'Attach Video',
|
||
gifs: 'GIFs',
|
||
searchGifs: 'Search GIPHY',
|
||
loadMore: 'Load More',
|
||
retryGifs: 'Try Again',
|
||
moreActions: 'More Actions',
|
||
unmatch: 'Unmatch',
|
||
unmatchTitle: 'Unmatch this person?',
|
||
unmatchBody:
|
||
'You and {name} will disappear from each other’s match lists. This cannot be undone.',
|
||
like: 'Like',
|
||
pass: 'Pass',
|
||
noProfiles: "You're all caught up",
|
||
noProfilesBody: 'Come back later when new people join Flare.',
|
||
yourMatches: 'Your matches',
|
||
matchesBody: 'A spark goes both ways.',
|
||
newMatch: 'You matched — say hello!',
|
||
noMatches: 'No sparks yet',
|
||
noMatchesBody: 'When someone likes you back, they will appear here.',
|
||
messagePlaceholder: 'Write a message',
|
||
itsAMatch: "It's a spark!",
|
||
matchBody: 'You and {name} liked each other.',
|
||
sayHello: 'Say hello',
|
||
keepSwiping: 'Keep exploring',
|
||
newMatchNotification: 'You and {sender} found a spark!',
|
||
newMessageNotification: 'New message from {sender}',
|
||
lookingFor: {
|
||
longTerm: 'Long-term connection',
|
||
dates: 'Good dates',
|
||
friends: 'New friends',
|
||
},
|
||
errors: {
|
||
invalid_profile: 'Check your name, age and profile text.',
|
||
invalid_profile_photos:
|
||
'Choose up to six photos from your own Gallery.',
|
||
request_failed: 'Flare could not save those changes. Try again.',
|
||
invalid_target: 'This profile is no longer available.',
|
||
invalid_choice: 'That swipe could not be saved.',
|
||
invalid_discovery: 'That Discovery setting is invalid.',
|
||
discovery_disabled: 'Enable Discovery before swiping.',
|
||
nothing_to_rewind: 'There is no recent swipe to rewind.',
|
||
cannot_rewind_match: 'A swipe that created a match cannot be rewound.',
|
||
match_not_found: 'This match is no longer available.',
|
||
invalid_message: 'Write a message before sending.',
|
||
invalid_attachment: 'This attachment is unavailable.',
|
||
gif_provider_unconfigured: 'GIF search is not configured.',
|
||
gif_provider_unauthorized: 'The GIF provider key is invalid.',
|
||
gif_provider_rate_limited: 'GIF search is busy. Try again shortly.',
|
||
gif_provider_failed: 'GIFs are temporarily unavailable.',
|
||
rate_limited: 'Slow down for a moment and try again.',
|
||
default: 'Flare could not complete the request.',
|
||
},
|
||
},
|
||
picstagram: {
|
||
name: 'Picstagram',
|
||
loading: 'Loading Picstagram',
|
||
home: 'Home',
|
||
explore: 'Explore',
|
||
create: 'Create',
|
||
activity: 'Activity',
|
||
profile: 'Profile',
|
||
following: 'Following',
|
||
city: 'City',
|
||
posts: 'Posts',
|
||
followers: 'Followers',
|
||
verified: 'Verified profile',
|
||
editProfile: 'Edit profile',
|
||
saved: 'Saved',
|
||
photos: 'Photos',
|
||
emptyBio: 'No bio yet.',
|
||
authTitle: 'Your Picstagram profile',
|
||
login: 'Sign In',
|
||
register: 'Register',
|
||
createAccount: 'Create Profile',
|
||
logout: 'Sign Out',
|
||
signingOut: 'Signing Out...',
|
||
loginBody:
|
||
'Sign in to continue with your photos, stories, follows, and saved posts.',
|
||
registerBody: 'Create a private Picstagram login for your photo profile.',
|
||
registrationHint:
|
||
'An iFruit account is required once to create and own a Picstagram profile.',
|
||
displayName: 'Name',
|
||
username: 'Username',
|
||
password: 'Password',
|
||
confirmPassword: 'Confirm password',
|
||
passwordsMismatch: 'The passwords do not match.',
|
||
displayNamePlaceholder: 'Your name',
|
||
usernamePlaceholder: 'username',
|
||
passwordPlaceholder: 'At least 8 characters',
|
||
confirmPasswordPlaceholder: 'Enter password again',
|
||
signOutTitle: 'Sign out of Picstagram?',
|
||
signOutBody:
|
||
'Your profile and posts stay online. This phone returns to the sign-in screen.',
|
||
stories: 'Stories',
|
||
yourStory: 'Your story',
|
||
noStories: 'No active stories',
|
||
storyViews: '{count} views',
|
||
storyViewers: 'Story viewers',
|
||
seenBy: 'Seen by {count}',
|
||
nextStory: 'Next story',
|
||
closeStory: 'Close story',
|
||
removeStory: 'Remove story',
|
||
storyRemoved: 'Story removed.',
|
||
emptyFeed: 'Your feed is quiet',
|
||
emptyFeedBody: 'Discover profiles or share the first photo.',
|
||
discoverPeople: 'Discover people',
|
||
searchPlaceholder: 'Search profiles, captions, places, or tags',
|
||
noResults: 'No results',
|
||
noResultsBody: 'Try another username, caption, place, or hashtag.',
|
||
newPost: 'New Post',
|
||
newStory: 'New Story',
|
||
post: 'Post',
|
||
story: 'Story',
|
||
choosePhotos: 'Choose photos',
|
||
choosePhoto: 'Choose photo',
|
||
choosePhotosHint: 'Select up to five photos from Gallery',
|
||
chooseStoryHint: 'Select one photo from Gallery',
|
||
selectedPhotos: '{count} selected',
|
||
changePhotos: 'Change selection',
|
||
caption: 'Caption',
|
||
captionPlaceholder: 'Write a caption...',
|
||
storyTextPlaceholder: 'Add a short story text...',
|
||
location: 'Location',
|
||
locationPlaceholder: 'Add a place',
|
||
allowComments: 'Allow comments',
|
||
publishing: 'Publishing...',
|
||
published: 'Your post is live.',
|
||
storyPublished: 'Your story is live.',
|
||
comments: 'Comments',
|
||
comment: 'Comment',
|
||
noComments: 'No comments yet',
|
||
addComment: 'Add a comment...',
|
||
reply: 'Reply',
|
||
removeComment: 'Remove comment',
|
||
likes: '{count} likes',
|
||
viewComments: 'View all {count} comments',
|
||
like: 'Like',
|
||
unlike: 'Unlike',
|
||
save: 'Save',
|
||
unsave: 'Remove from saved',
|
||
more: 'More',
|
||
archive: 'Archive',
|
||
restore: 'Restore',
|
||
deletePost: 'Delete post',
|
||
deletePostTitle: 'Delete this post?',
|
||
deletePostBody: 'The post and its reactions will no longer be available.',
|
||
privateProfile: 'This profile is private',
|
||
privateProfileBody: 'Follow this profile to see its posts and stories.',
|
||
follow: 'Follow',
|
||
requested: 'Requested',
|
||
unfollow: 'Following',
|
||
accept: 'Accept',
|
||
decline: 'Decline',
|
||
followRequests: 'Follow requests',
|
||
noActivity: 'No activity yet',
|
||
markRead: 'Mark as read',
|
||
publicProfile: 'Public profile',
|
||
privateProfileSetting: 'Private profile',
|
||
privacyHint: 'Only accepted followers can see your posts and stories.',
|
||
bio: 'Bio',
|
||
avatar: 'Profile photo',
|
||
chooseAvatar: 'Choose profile photo',
|
||
removeAvatar: 'Remove profile photo',
|
||
saveProfile: 'Save profile',
|
||
profileSaved: 'Profile updated.',
|
||
cancel: 'Cancel',
|
||
done: 'Done',
|
||
report: 'Report',
|
||
reportTarget: 'Report {target}',
|
||
reportReason: 'Reason',
|
||
reportDetails: 'Additional details (optional)',
|
||
submitReport: 'Submit report',
|
||
reported: 'Report submitted.',
|
||
block: 'Block profile',
|
||
unblock: 'Unblock profile',
|
||
blocked: 'Profile blocked.',
|
||
blockTitle: 'Block @{handle}?',
|
||
blockBody:
|
||
"You will no longer see or interact with each other's Picstagram content.",
|
||
moderation: 'Moderation',
|
||
reports: 'Open reports',
|
||
noReports: 'No open reports',
|
||
noDetails: 'No additional details',
|
||
hide: 'Hide',
|
||
remove: 'Remove',
|
||
restoreAction: 'Restore',
|
||
dismiss: 'Dismiss',
|
||
reportTargets: {
|
||
post: 'Post',
|
||
profile: 'Profile',
|
||
comment: 'Comment',
|
||
story: 'Story',
|
||
},
|
||
reportReasons: {
|
||
spam: 'Spam or misleading',
|
||
harassment: 'Harassment or bullying',
|
||
dangerous: 'Dangerous activity',
|
||
illegal: 'Illegal content',
|
||
other: 'Something else',
|
||
},
|
||
activityKinds: {
|
||
follow_request: 'requested to follow you',
|
||
follow: 'started following you',
|
||
request_accepted: 'accepted your follow request',
|
||
like: 'liked your post',
|
||
comment: 'commented on your post',
|
||
verified: 'verification changed',
|
||
},
|
||
notifications: {
|
||
follow_request: '{actor} requested to follow you.',
|
||
follow: '{actor} started following you.',
|
||
request_accepted: '{actor} accepted your follow request.',
|
||
like: '{actor} liked your post.',
|
||
comment: '{actor} commented on your post.',
|
||
verified: 'Your Picstagram profile is now verified.',
|
||
default: 'You have new Picstagram activity.',
|
||
},
|
||
errors: {
|
||
invalid_post: 'Check the post details.',
|
||
invalid_story: 'Check the story details.',
|
||
invalid_media: 'Choose photos owned by this phone.',
|
||
invalid_comment: 'Enter a valid comment.',
|
||
comments_disabled: 'Comments are disabled.',
|
||
invalid_profile: 'Check your profile details.',
|
||
invalid_handle: 'Use 3–24 letters, numbers, dots, or underscores.',
|
||
invalid_display_name: 'Enter a display name.',
|
||
invalid_password: 'Password must be 8–72 characters.',
|
||
invalid_credentials: 'Username or password is incorrect.',
|
||
already_registered:
|
||
'This iFruit account already owns a Picstagram profile.',
|
||
handle_taken: 'This username is already taken.',
|
||
post_not_found: 'This post is unavailable.',
|
||
story_not_found: 'This story is unavailable.',
|
||
profile_not_found: 'This profile is unavailable.',
|
||
request_not_found: 'This follow request is no longer open.',
|
||
profile_unavailable: 'This profile is unavailable.',
|
||
blocked: 'This profile is blocked.',
|
||
already_reported: 'You already reported this content.',
|
||
invalid_report: 'Choose a valid report reason.',
|
||
invalid_search: 'Enter a valid search.',
|
||
invalid_cursor: 'The feed changed. Refresh and try again.',
|
||
invalid_status: 'This action is not available.',
|
||
not_authorized: 'You are not allowed to do that.',
|
||
report_not_found: 'This report is no longer open.',
|
||
rate_limited: 'Too many actions. Try again shortly.',
|
||
not_authenticated: 'Sign in to iFruit first.',
|
||
picstagram_not_authenticated: 'Sign in to Picstagram first.',
|
||
request_failed: 'The request failed.',
|
||
default: 'Picstagram could not complete the request.',
|
||
},
|
||
},
|
||
feather: {
|
||
name: 'Feather',
|
||
loading: 'Loading Feather',
|
||
home: 'Home',
|
||
explore: 'Explore',
|
||
activity: 'Notifications',
|
||
activityNav: 'Alerts',
|
||
profile: 'Profile',
|
||
back: 'Back',
|
||
settings: 'Settings',
|
||
settingsEyebrow: 'Feather preferences',
|
||
compactMode: 'Compact timeline',
|
||
compactModeBody: 'Show more conversation on the screen.',
|
||
showSuggestions: 'Profile suggestions',
|
||
showSuggestionsBody: 'Show people to follow on your profile.',
|
||
forYou: 'For you',
|
||
following: 'Following',
|
||
add: 'Add',
|
||
network: 'Network',
|
||
networkTitle: 'Find your people',
|
||
networkBody:
|
||
'Follow voices from across Los Santos and build a timeline that feels like yours.',
|
||
networkSearchPlaceholder: 'Search names or @usernames',
|
||
postSearchPlaceholder: 'Search posts or #hashtags',
|
||
searchResults: 'Search results',
|
||
suggestedPeople: 'Suggested for you',
|
||
noPeopleFound: 'No people found',
|
||
noPeopleFoundBody: 'Try another name or username.',
|
||
noSuggestions: 'No suggestions yet',
|
||
noSuggestionsBody: 'New Feather accounts will appear here.',
|
||
all: 'All',
|
||
mentions: 'Mentions',
|
||
media: 'Media',
|
||
trending: 'Trending',
|
||
peopleTab: 'People',
|
||
searchPlaceholder: 'Search Feather',
|
||
search: 'Search',
|
||
cancel: 'Cancel',
|
||
done: 'Done',
|
||
authEyebrow: 'Feather network',
|
||
authWelcome: 'Welcome to Feather',
|
||
authBody: 'One iFruit account. Every conversation, wherever you sign in.',
|
||
login: 'Log in',
|
||
register: 'Register',
|
||
loginTitle: 'Good to see you again',
|
||
loginBody: 'Log in with your iFruit address to continue to Feather.',
|
||
registerTitle: 'Create your iFruit account',
|
||
registerBody:
|
||
'Choose an address and secure it with a password. Your Feather profile comes next.',
|
||
email: 'iFruit address',
|
||
emailPlaceholder: 'yourname',
|
||
password: 'Password',
|
||
passwordPlaceholder: '6–64 characters',
|
||
confirmPassword: 'Confirm password',
|
||
confirmPasswordPlaceholder: 'Enter it again',
|
||
showPassword: 'Show password',
|
||
hidePassword: 'Hide password',
|
||
loginAction: 'Continue to Feather',
|
||
registerAction: 'Create account',
|
||
noAccount: 'New to Feather?',
|
||
haveAccount: 'Already registered?',
|
||
registerNow: 'Create an account',
|
||
loginNow: 'Log in',
|
||
authTrust:
|
||
'Your credentials are verified by the server and this phone is linked to your iFruit account.',
|
||
profileStep: 'Step 2 of 2',
|
||
accountConnected: 'iFruit account connected',
|
||
authErrors: {
|
||
invalid_email: 'Enter a valid 3–32 character iFruit address.',
|
||
invalid_password: 'Password must be 6–64 characters.',
|
||
password_mismatch: 'The passwords do not match.',
|
||
invalid_credentials: 'The iFruit address or password is incorrect.',
|
||
email_taken: 'That iFruit address is already registered.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
default: 'The account request failed. Please try again.',
|
||
},
|
||
welcome: 'Find your voice',
|
||
welcomeBody:
|
||
'Join the live conversation in Los Santos. Your Feather profile stays linked to your iFruit account.',
|
||
createProfile: 'Create Feather profile',
|
||
profileDetailsHint: 'Choose how people will recognize you.',
|
||
displayName: 'Display name',
|
||
displayNamePlaceholder: 'Your name',
|
||
handle: 'Username',
|
||
handlePlaceholder: 'your_username',
|
||
handleHint: '3-30 letters, numbers or underscores',
|
||
bio: 'Bio',
|
||
bioPlaceholder: 'What should people know about you?',
|
||
start: 'Join Feather',
|
||
chooseAvatar: 'Choose profile photo',
|
||
changeAvatar: 'Change photo',
|
||
newPost: 'New feather',
|
||
reply: 'Reply',
|
||
replyingTo: 'Replying to @{handle}',
|
||
quote: 'Quote',
|
||
post: 'Post',
|
||
posting: 'Posting...',
|
||
composerPlaceholder: "What's happening?",
|
||
replyPlaceholder: 'Post your reply',
|
||
addPhotos: 'Add photos',
|
||
removePhoto: 'Remove photo {number}',
|
||
charactersLeft: '{count} left',
|
||
composerAudience: 'Everyone can join the conversation',
|
||
mediaTitle: 'Add to your feather',
|
||
mediaBody: 'Share up to four photos from your phone or take a new one.',
|
||
chooseGallery: 'Gallery',
|
||
chooseGalleryBody: 'Choose one or more saved photos',
|
||
takePhoto: 'Camera',
|
||
takePhotoBody: 'Take a new photo now',
|
||
selectedPhotos: 'Selected photos',
|
||
photoLimit: 'You can add up to four photos.',
|
||
emptyFeed: 'Your timeline is quiet',
|
||
emptyFeedBody: 'Follow people or post the first feather.',
|
||
emptyExplore: 'No results found',
|
||
emptyExploreBody: 'Try another search or explore a topic.',
|
||
noTrendingHashtags: 'No hashtags yet',
|
||
noTrendingHashtagsBody: 'Hashtags from published posts will appear here.',
|
||
happeningNow: "What's happening",
|
||
trends: 'Trends for you',
|
||
trendingIn: 'Trending in Los Santos',
|
||
openTopic: 'View conversation',
|
||
postsCount: '{count} posts',
|
||
exploreTabs: {
|
||
explore: 'Explore',
|
||
trending: 'Trends',
|
||
news: 'News',
|
||
},
|
||
trendKinds: {
|
||
explore: 'Trending in Los Santos',
|
||
trending: 'Popular right now',
|
||
news: 'News · Trending',
|
||
sports: 'Sports · Trending',
|
||
entertainment: 'Entertainment · Trending',
|
||
},
|
||
people: 'Who to follow',
|
||
posts: 'Posts',
|
||
followers: 'Followers',
|
||
followingCount: 'Following',
|
||
removeConnection: 'Remove',
|
||
followerRemoved: 'Follower removed.',
|
||
followingRemoved: 'Account unfollowed.',
|
||
noConnections: 'Nobody here yet',
|
||
noConnectionsBody: 'Profiles will appear here as connections are made.',
|
||
follow: 'Follow',
|
||
unfollow: 'Following',
|
||
editProfile: 'Edit profile',
|
||
saveProfile: 'Save',
|
||
joined: 'Joined Feather',
|
||
joinedDate: 'Joined August 2026',
|
||
shareProfile: 'Share profile',
|
||
profileCopied: 'Profile handle copied.',
|
||
postCopied: 'Post copied.',
|
||
verified: 'Verified profile',
|
||
noBio: 'No bio yet.',
|
||
showMore: 'Show more',
|
||
replies: 'Replies',
|
||
noReplies: 'No replies yet. Start the conversation.',
|
||
likes: 'Likes',
|
||
bookmarks: 'Bookmarks',
|
||
noBookmarks: 'No saved posts yet',
|
||
noBookmarksBody: 'Posts you bookmark will appear here.',
|
||
imagePreview: 'Image preview',
|
||
openImage: 'Open image {number}',
|
||
postImage: 'Post image {number}',
|
||
previousImage: 'Previous image',
|
||
nextImage: 'Next image',
|
||
delete: 'Delete',
|
||
report: 'Report',
|
||
block: 'Block @{handle}',
|
||
reportTitle: 'Report feather',
|
||
reportBody: 'Tell us why this feather should be reviewed.',
|
||
reportSubmit: 'Submit report',
|
||
reported: 'Report submitted.',
|
||
blocked: 'Profile blocked.',
|
||
deleted: 'Feather deleted.',
|
||
reasons: {
|
||
spam: 'Spam',
|
||
harassment: 'Harassment',
|
||
dangerous: 'Dangerous content',
|
||
illegal: 'Illegal content',
|
||
other: 'Other',
|
||
},
|
||
noActivity: 'No activity yet',
|
||
activityBody: 'Likes, replies and new followers will appear here.',
|
||
activityKinds: {
|
||
like: 'liked your feather',
|
||
reply: 'replied to your feather',
|
||
follow: 'followed you',
|
||
quote: 'quoted your feather',
|
||
},
|
||
errors: {
|
||
accountRequired: 'Sign in to iFruit in Settings first.',
|
||
invalidHandle: 'Use 3–30 letters, numbers or underscores.',
|
||
handleTaken: 'That handle is already taken.',
|
||
invalidPost: 'Write something or attach a photo.',
|
||
generic: 'Something went wrong. Please try again.',
|
||
},
|
||
notifications: {
|
||
default: 'You have new activity.',
|
||
like: '{actor} liked your feather.',
|
||
reply: '{actor} replied to your feather.',
|
||
follow: '{actor} followed you.',
|
||
quote: '{actor} quoted your feather.',
|
||
},
|
||
},
|
||
fliptok: {
|
||
name: 'FlipTok',
|
||
loading: 'Loading FlipTok',
|
||
following: 'Following',
|
||
forYou: 'For You',
|
||
verified: 'Verified account',
|
||
originalSound: 'original sound',
|
||
save: 'Save',
|
||
home: 'Home',
|
||
discover: 'Discover',
|
||
create: 'Create',
|
||
activity: 'Activity',
|
||
profile: 'Profile',
|
||
emptyFeed: 'No videos yet',
|
||
emptyFeedBody: 'Follow creators or post the first FlipTok.',
|
||
searchPlaceholder: 'Search creators and videos',
|
||
noActivity: 'No activity yet',
|
||
followers: 'Followers',
|
||
videos: 'Videos',
|
||
emptyBio: 'No bio yet.',
|
||
editProfile: 'Edit profile',
|
||
newVideo: 'New FlipTok',
|
||
chooseVideo: 'Choose a video',
|
||
chooseVideoHint: 'Select one from Gallery',
|
||
changeVideo: 'Change',
|
||
captionPlaceholder: 'Write a caption...',
|
||
location: 'Add location',
|
||
whoCanWatch: 'Who can watch',
|
||
public: 'Everyone',
|
||
followersOnly: 'Followers',
|
||
private: 'Only me',
|
||
allowComments: 'Allow comments',
|
||
saveDraft: 'Drafts',
|
||
publishing: 'Posting...',
|
||
post: 'Post',
|
||
draftSaved: 'Draft saved.',
|
||
published: 'Your FlipTok is live.',
|
||
linkCopied: 'Video link copied.',
|
||
reported: 'Report submitted.',
|
||
blocked: 'Creator blocked.',
|
||
comments: 'Comments',
|
||
noComments: 'No comments yet',
|
||
addComment: 'Add comment...',
|
||
report: 'Report video',
|
||
reportReason: 'Reason',
|
||
reportDetails: 'Additional details (optional)',
|
||
submitReport: 'Submit report',
|
||
reportReasons: {
|
||
spam: 'Spam or misleading',
|
||
harassment: 'Harassment or bullying',
|
||
dangerous: 'Dangerous activity',
|
||
illegal: 'Illegal content',
|
||
other: 'Something else',
|
||
},
|
||
block: 'Block creator',
|
||
follow: 'Follow',
|
||
unfollow: 'Following',
|
||
backToProfile: 'Back',
|
||
sounds: 'Sound',
|
||
chooseSound: 'Choose music',
|
||
originalOnly: 'Original sound only',
|
||
noMusic: 'No music tracks are configured.',
|
||
trimAndCover: 'Trim & cover',
|
||
trimStart: 'Start',
|
||
trimEnd: 'End',
|
||
coverFrame: 'Cover',
|
||
originalVolume: 'Original sound',
|
||
musicVolume: 'Music',
|
||
moderation: 'Moderation',
|
||
reports: 'Open reports',
|
||
noReports: 'No open reports',
|
||
removeVideo: 'Remove video',
|
||
dismissReport: 'Dismiss',
|
||
cancel: 'Cancel',
|
||
done: 'Done',
|
||
displayName: 'Name',
|
||
username: 'Username',
|
||
bio: 'Bio',
|
||
accountType: 'Account type',
|
||
authTitle: 'Your FlipTok account',
|
||
login: 'Sign In',
|
||
register: 'Register',
|
||
createAccount: 'Create Account',
|
||
logout: 'Sign Out',
|
||
loginBody:
|
||
'Sign in to continue with your videos, follows, and saved posts.',
|
||
registerBody: 'Create a private FlipTok login for this profile.',
|
||
password: 'Password',
|
||
confirmPassword: 'Confirm password',
|
||
passwordsMismatch: 'The passwords do not match.',
|
||
displayNamePlaceholder: 'Your name',
|
||
usernamePlaceholder: 'username',
|
||
passwordPlaceholder: 'At least 8 characters',
|
||
confirmPasswordPlaceholder: 'Enter password again',
|
||
registrationHint:
|
||
'An iFruit account is required once to create and own a FlipTok profile.',
|
||
accountDetails: 'Account details',
|
||
profileDetails: 'Profile details',
|
||
account: 'Account',
|
||
signOutTitle: 'Sign out of FlipTok?',
|
||
signOutBody:
|
||
'Your profile and videos stay online. This phone will return to the FlipTok sign-in screen.',
|
||
signingOut: 'Signing Out...',
|
||
accountTypes: {
|
||
person: 'Person',
|
||
business: 'Business',
|
||
organization: 'Organization',
|
||
media: 'Media',
|
||
event: 'Event',
|
||
},
|
||
activityKinds: {
|
||
like: 'liked your video',
|
||
comment: 'commented on your video',
|
||
follow: 'started following you',
|
||
verified: 'verification changed',
|
||
},
|
||
notifications: {
|
||
like: '{actor} liked your video.',
|
||
comment: '{actor} commented on your video.',
|
||
follow: '{actor} started following you.',
|
||
verified: 'Your FlipTok account is now verified.',
|
||
default: 'You have new FlipTok activity.',
|
||
},
|
||
errors: {
|
||
invalid_video: 'Check the video details.',
|
||
invalid_media: 'Choose a video from this phone.',
|
||
invalid_comment: 'Enter a valid comment.',
|
||
comments_disabled: 'Comments are disabled.',
|
||
invalid_profile: 'Check your profile details.',
|
||
invalid_handle: 'Use 3–24 letters, numbers, dots, or underscores.',
|
||
invalid_display_name: 'Enter a display name.',
|
||
invalid_password: 'Password must be 8–72 characters.',
|
||
invalid_credentials: 'Username or password is incorrect.',
|
||
already_registered:
|
||
'This iFruit account already owns a registered FlipTok profile.',
|
||
handle_taken: 'This username is already taken.',
|
||
video_not_found: 'This video is unavailable.',
|
||
rate_limited: 'Too many actions. Try again shortly.',
|
||
not_authenticated: 'Sign in to iFruit first.',
|
||
blocked: 'This account is blocked.',
|
||
not_authorized: 'You do not have moderation access.',
|
||
report_not_found: 'This report is no longer open.',
|
||
default: 'FlipTok could not complete the request.',
|
||
},
|
||
},
|
||
darkchat: {
|
||
name: 'DarkChat',
|
||
newMessage: 'New DarkChat message from {sender}',
|
||
privateNotification: 'New DarkChat message',
|
||
signInBody:
|
||
'DarkChat identities are linked to your private iFruit account.',
|
||
signInHint: 'Sign in through Settings to continue.',
|
||
security: 'Security',
|
||
privateNetwork: 'Private invitation-only network',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try another alias or Dark-ID.',
|
||
noChats: 'No Private Chats',
|
||
noChatsBody:
|
||
'Connect with a Dark-ID or invitation code. There is no public search.',
|
||
newChat: 'New Chat',
|
||
connectPrivately: 'Connect privately',
|
||
newChatBody:
|
||
'Enter an exact Dark-ID or invitation code. Unknown identities require confirmation.',
|
||
darkIdOrInvite: 'Dark-ID or invitation code',
|
||
continue: 'Continue',
|
||
contacts: 'DarkChat Contacts',
|
||
shareIdentity: 'Tap to share your private identity',
|
||
message: 'Dark message',
|
||
activeNow: 'Activity shared',
|
||
encryptedSession: 'Private session',
|
||
serverPrivate: 'Private server-stored conversation',
|
||
emoji: 'Emoji',
|
||
gif: 'GIF',
|
||
gifs: 'GIFs',
|
||
photo: 'Photo',
|
||
video: 'Video',
|
||
attachPhoto: 'Attach Photo',
|
||
takePhoto: 'Take Photo',
|
||
attachGif: 'Attach GIF',
|
||
attachVideo: 'Attach Video',
|
||
searchGifs: 'Search GIFs',
|
||
loadMore: 'Load More',
|
||
voiceMessage: 'Voice message',
|
||
sending: 'Sending',
|
||
failed: 'Not delivered',
|
||
delivered: 'Delivered',
|
||
read: 'Read',
|
||
replying: 'Replying to',
|
||
messageDeleted: 'Message deleted',
|
||
securityUpdate: 'Security settings updated',
|
||
timerChanged: 'Disappearing messages: {timer}',
|
||
timerOff: 'Off',
|
||
timerAfterRead: 'After reading',
|
||
timerMinute: '1 minute',
|
||
timerFiveMinutes: '5 minutes',
|
||
timerHour: '1 hour',
|
||
timerDay: '24 hours',
|
||
timerWeek: '7 days',
|
||
copied: 'Copied',
|
||
reply: 'Reply',
|
||
copy: 'Copy',
|
||
deleteForMe: 'Delete for me',
|
||
deleteForBoth: 'Delete for both',
|
||
report: 'Report',
|
||
contactSecurity: 'Contact & Security',
|
||
chatSince: 'Private chat since {date}',
|
||
notifications: 'Notifications',
|
||
readReceipts: 'Read receipts',
|
||
disappearing: 'Disappearing messages',
|
||
contactAlias: 'Contact alias',
|
||
saveContact: 'Save Contact',
|
||
addContact: 'Add Contact',
|
||
contactSaved: 'Contact saved',
|
||
removeContact: 'Remove Contact',
|
||
block: 'Block User',
|
||
unblock: 'Unblock User',
|
||
clearChat: 'Clear Chat',
|
||
chatCleared: 'Chat cleared',
|
||
myIdentity: 'My Dark Identity',
|
||
alias: 'Alias',
|
||
notificationPrivacy: 'Notification privacy',
|
||
notificationFull: 'Full · alias and message',
|
||
notificationPrivate: 'Private · generic message',
|
||
notificationHidden: 'Invisible · badge only',
|
||
shareActivity: 'Share activity status',
|
||
inviteCode: 'Private invitation code',
|
||
copyInvite: 'Copy Invite',
|
||
privacyDisclaimer:
|
||
'DarkChat stores messages on the server and does not claim end-to-end encryption.',
|
||
unknownIdentity: 'Unknown Identity',
|
||
unknownIdentityBody:
|
||
'Only continue if you expected this identity. The account is not discoverable through public search.',
|
||
openSecureChat: 'Open Private Chat',
|
||
reportUser: 'Report User',
|
||
reportSpam: 'Spam',
|
||
reportHarassment: 'Harassment',
|
||
reportThreats: 'Threats',
|
||
reportIllegal: 'Illegal content',
|
||
reportOther: 'Other',
|
||
reportDetails: 'Optional details',
|
||
submitReport: 'Submit Report',
|
||
reported: 'Report submitted',
|
||
microphoneUnavailable: 'The microphone is unavailable.',
|
||
recordingTooLarge: 'The voice message is too large.',
|
||
errors: {
|
||
not_authenticated: 'Sign in to your iFruit account first.',
|
||
invalid_dark_id: 'Enter a valid Dark-ID or invitation code.',
|
||
profile_not_found: 'This private identity was not found.',
|
||
self_chat: 'You cannot message your own identity.',
|
||
conversation_not_found: 'This conversation is unavailable.',
|
||
blocked: 'Messages are blocked in this conversation.',
|
||
invalid_message: 'Enter a valid message.',
|
||
invalid_gif: 'This GIF is invalid.',
|
||
invalid_voice: 'This voice message is invalid.',
|
||
invalid_attachment: 'This photo or video is unavailable.',
|
||
invalid_profile: 'Check your alias and privacy settings.',
|
||
rate_limited: 'Too many requests. Try again shortly.',
|
||
gif_provider_unconfigured: 'GIF search is not configured.',
|
||
gif_provider_unauthorized: 'The GIF provider key is invalid.',
|
||
gif_provider_rate_limited: 'GIF search is busy. Try again shortly.',
|
||
gif_provider_failed: 'GIFs are temporarily unavailable.',
|
||
default: 'DarkChat could not complete the request.',
|
||
},
|
||
},
|
||
messages: {
|
||
name: 'Messages',
|
||
newMessage: 'New message from {sender}',
|
||
compose: 'New Message',
|
||
search: 'Search',
|
||
to: 'To:',
|
||
message: 'Message',
|
||
send: 'Send',
|
||
details: 'Details',
|
||
filterUnread: 'Show Unread Messages',
|
||
smsLabel: 'Text Message · SMS',
|
||
photo: 'Photo',
|
||
gif: 'GIF',
|
||
video: 'Video',
|
||
attachPhoto: 'Attach Photo',
|
||
takePhoto: 'Take Photo',
|
||
attachGif: 'Attach GIF',
|
||
attachVideo: 'Attach Video',
|
||
photos: 'Photos',
|
||
gifs: 'GIFs',
|
||
videos: 'Videos',
|
||
noPhotos: 'Take a photo first to attach it here.',
|
||
noVideos: 'Record a video in Camera first.',
|
||
searchGifs: 'Search GIPHY',
|
||
loadMore: 'Load More',
|
||
retryGifs: 'Try Again',
|
||
moreActions: 'More Actions',
|
||
contactDetails: 'Contact Details',
|
||
contactName: 'Name',
|
||
phoneNumber: 'Phone Number',
|
||
call: 'Call',
|
||
messageAction: 'Message',
|
||
addContact: 'Add Contact',
|
||
deleteContact: 'Delete Contact',
|
||
selectedCount: '{count} Selected',
|
||
deleteSelected: 'Delete',
|
||
contactSaveFailed: 'The contact could not be saved.',
|
||
contactDeleteFailed: 'The contact could not be deleted.',
|
||
callFailed: 'The call could not be started.',
|
||
emoji: 'Emoji',
|
||
voiceMessage: 'Audio Message',
|
||
recordVoice: 'Record Audio',
|
||
playAudio: 'Play Audio',
|
||
pauseAudio: 'Pause Audio',
|
||
recording: 'Recording',
|
||
stopAndSend: 'Stop and Send',
|
||
cancelRecording: 'Cancel Recording',
|
||
sending: 'Sending...',
|
||
delivered: 'Delivered',
|
||
notDelivered: 'Not Delivered',
|
||
microphoneUnavailable: 'The microphone is unavailable.',
|
||
recordingTooLarge: 'The recording is too large.',
|
||
noSim: 'No SIM',
|
||
noSimBody: 'Insert a SIM card in Settings to send and receive messages.',
|
||
noMessages: 'No Messages',
|
||
noMessagesBody: 'Start a conversation with a phone number.',
|
||
noResults: 'No Results',
|
||
today: 'Today',
|
||
yesterday: 'Yesterday',
|
||
errors: {
|
||
invalid_number: 'Enter a valid phone number.',
|
||
invalid_message: 'Enter a message.',
|
||
invalid_voice: 'The audio message is invalid.',
|
||
invalid_attachment: 'The attachment is invalid.',
|
||
media_provider_unconfigured: 'Photo uploads are not configured.',
|
||
capture_provider_unavailable: 'The screenshot resource is unavailable.',
|
||
capture_failed: 'The photo could not be captured.',
|
||
video_provider_unavailable:
|
||
'Video capture requires the screencapture resource.',
|
||
video_capture_failed: 'The video could not be recorded.',
|
||
recording_in_progress: 'A video is already being recorded.',
|
||
recording_not_found: 'No active video recording was found.',
|
||
gif_provider_unconfigured: 'GIF search is not configured.',
|
||
gif_provider_unauthorized: 'The configured GIPHY API key is invalid.',
|
||
gif_provider_rate_limited: 'GIPHY is busy. Try again in a moment.',
|
||
gif_provider_failed: 'GIF search is temporarily unavailable.',
|
||
self_message: 'You cannot message your own number.',
|
||
recipient_not_found: 'That number is unavailable.',
|
||
no_sim: 'This phone has no SIM card.',
|
||
rate_limited: 'Too many messages. Try again in a minute.',
|
||
request_failed: 'Messages are temporarily unavailable.',
|
||
default: 'The message could not be sent.',
|
||
},
|
||
},
|
||
appStore: {
|
||
name: 'App Store',
|
||
get: 'GET',
|
||
open: 'OPEN',
|
||
installing: 'Installing',
|
||
searchPlaceholder: 'Search apps and games',
|
||
appsTitle: 'Built-in Apps',
|
||
gamesTitle: 'Games',
|
||
selected: 'Apps available for your Sky Phone',
|
||
tabs: {
|
||
apps: 'Apps',
|
||
games: 'Games',
|
||
search: 'Search',
|
||
},
|
||
},
|
||
phone: {
|
||
name: 'Phone',
|
||
recents: 'Recents',
|
||
contacts: 'Contacts',
|
||
keypad: 'Keypad',
|
||
noSim: 'No SIM',
|
||
noSimBody: 'Insert a SIM card in Settings to make calls.',
|
||
noRecents: 'No Recent Calls',
|
||
noContacts: 'No Contacts',
|
||
searchContacts: 'Search Contacts',
|
||
addContact: 'New Contact',
|
||
editContact: 'Edit Contact',
|
||
contactName: 'Name',
|
||
phoneNumber: 'Phone Number',
|
||
call: 'Call',
|
||
calling: 'calling...',
|
||
incoming: 'Incoming Call',
|
||
incomingDirection: 'Incoming',
|
||
outgoingDirection: 'Outgoing',
|
||
connected: 'Connected',
|
||
missed: 'Missed',
|
||
declined: 'Declined',
|
||
busy: 'Busy',
|
||
unavailable: 'Unavailable',
|
||
noAnswer: 'No Answer',
|
||
cancelled: 'Cancelled',
|
||
disconnected: 'Disconnected',
|
||
sim_removed: 'SIM Removed',
|
||
completed: 'Call Ended',
|
||
answer: 'Answer',
|
||
decline: 'Decline',
|
||
hangup: 'End',
|
||
addToContacts: 'Add to Contacts',
|
||
deleteContact: 'Delete Contact',
|
||
replaceTitle: 'Replace SIM?',
|
||
replaceBody: 'The current SIM will be returned to your inventory.',
|
||
choosePhone: 'Choose a Phone',
|
||
choosePhoneBody: 'Select the phone that should receive {number}.',
|
||
emptyPhone: 'No SIM inserted',
|
||
errors: {
|
||
invalid_contact: 'Enter a name and valid phone number.',
|
||
invalid_number: 'Enter a valid phone number.',
|
||
no_sim: 'This phone has no SIM card.',
|
||
airplane_mode: 'Turn off Airplane Mode to make calls.',
|
||
self_call: 'You cannot call your own number.',
|
||
busy: 'The line is busy.',
|
||
rate_limited: 'Too many calls. Try again in a minute.',
|
||
voice_unavailable: 'The configured phone voice service is unavailable.',
|
||
inventory_full: 'There is no room for the ejected SIM card.',
|
||
operation_in_progress:
|
||
'Another phone operation is already in progress.',
|
||
sim_request_expired:
|
||
'The SIM selection expired. Use the SIM card again.',
|
||
sim_not_owned: 'That SIM card is no longer in your inventory.',
|
||
phone_not_owned: 'That phone is no longer in your inventory.',
|
||
metadata_unsupported:
|
||
'The configured inventory cannot store unique SIM metadata.',
|
||
request_failed: 'The phone request failed.',
|
||
default: 'The phone request failed.',
|
||
},
|
||
},
|
||
radio: {
|
||
name: 'Radio',
|
||
disconnected: 'Not connected',
|
||
connectedTo: 'Connected - {frequency} MHz',
|
||
noProvider: 'Voice service unavailable',
|
||
channel: 'Channel',
|
||
primaryFrequency: 'Primary frequency',
|
||
secondaryFrequency: 'Secondary frequency',
|
||
frequencyPlaceholder: 'e.g. 120.5',
|
||
optional: 'Optional',
|
||
mhz: 'MHz',
|
||
volume: 'Volume',
|
||
connect: 'Connect',
|
||
disconnect: 'Disconnect',
|
||
members: 'Currently connected ({count})',
|
||
noMembers: 'No participants',
|
||
history: 'Recently connected',
|
||
noHistory: 'No history',
|
||
badge: 'Service number',
|
||
badgePlaceholder: 'e.g. 231',
|
||
profileSaved: 'Radio profile saved',
|
||
displayName: 'Radio display name',
|
||
displayNamePlaceholder: 'Leave empty to use your character name',
|
||
displayNameDescription:
|
||
'This name is shown to other participants and in the built-in radio overlay.',
|
||
displayNameNotAllowed:
|
||
'Your current job or grade is not allowed to change the radio display name.',
|
||
otherSettings: 'Other',
|
||
autoRejoin: 'Automatic rejoin',
|
||
autoRejoinDescription:
|
||
'Reconnect after spawning or restarting the phone resource',
|
||
radioNotifications: 'Radio notifications',
|
||
notificationsDescription: 'Show joins and leaves on your current channel',
|
||
memberJoined: '{name} joined the radio',
|
||
memberLeft: '{name} left the radio',
|
||
unknownMember: 'Unknown',
|
||
tabs: { radio: 'Radio', settings: 'Settings' },
|
||
errors: {
|
||
invalid_frequency: 'Enter a valid frequency.',
|
||
invalid_volume: 'Enter a valid volume.',
|
||
channel_locked: 'You do not have access to this channel.',
|
||
secondary_locked: 'You do not have access to the secondary channel.',
|
||
voice_unavailable: 'The configured radio voice service is unavailable.',
|
||
player_unavailable: 'Your player data is not available.',
|
||
rate_limited: 'Please wait before changing channel again.',
|
||
invalid_setting: 'This setting is invalid.',
|
||
badge_disabled: 'Service numbers are disabled.',
|
||
badge_forbidden: 'This service number is not allowed.',
|
||
display_name_disabled: 'Radio display names are disabled.',
|
||
display_name_forbidden:
|
||
'Your job or grade cannot change the radio display name.',
|
||
invalid_display_name: 'Enter a valid radio display name.',
|
||
request_failed: 'The radio request failed.',
|
||
default: 'The radio request failed.',
|
||
},
|
||
},
|
||
banking: {
|
||
name: 'Banking',
|
||
welcome: 'Welcome back',
|
||
totalBalance: 'Total Balance',
|
||
recentPeriod: 'in recent activity',
|
||
actions: 'Banking actions',
|
||
send: 'Send',
|
||
accounts: 'Accounts',
|
||
bankAccount: 'Bank account',
|
||
cash: 'Cash',
|
||
latestTransactions: 'Latest Transactions',
|
||
allTransactions: 'All Transactions',
|
||
viewAll: 'View all',
|
||
noTransactions: 'Your banking activity will appear here.',
|
||
home: 'Home',
|
||
activity: 'Activity',
|
||
chartDaySummary: '{day}: {incoming} incoming, {outgoing} outgoing',
|
||
navigation: 'Banking navigation',
|
||
incoming: 'Incoming',
|
||
outgoing: 'Outgoing',
|
||
refresh: 'Refresh banking data',
|
||
unavailable: 'Banking unavailable',
|
||
tryAgain: 'Try Again',
|
||
recipientPhone: 'Phone number',
|
||
recipientPhonePlaceholder: "Enter the recipient's number",
|
||
chooseContact: 'Choose from contacts',
|
||
noContacts: 'No contacts saved yet.',
|
||
amount: 'Amount',
|
||
amountPlaceholder: 'Enter an amount',
|
||
transactions: {
|
||
deposit: 'Cash deposit',
|
||
withdrawal: 'Cash withdrawal',
|
||
transfer_in: 'Incoming transfer',
|
||
transfer_out: 'Outgoing transfer',
|
||
},
|
||
forms: {
|
||
transfer: {
|
||
title: 'Send money',
|
||
body: 'Transfer money from your bank account to an online player.',
|
||
submit: 'Send transfer',
|
||
},
|
||
},
|
||
errors: {
|
||
invalid_request: 'Enter a valid phone number and whole amount.',
|
||
insufficient_funds: 'There is not enough money in this account.',
|
||
target_not_found: 'The recipient is not online.',
|
||
self_transfer: 'You cannot send money to yourself.',
|
||
rate_limited: 'Please wait before making another transaction.',
|
||
transfer_failed: 'The transfer could not be completed.',
|
||
banking_unavailable: 'Banking is currently unavailable.',
|
||
request_failed: 'The banking request failed.',
|
||
default: 'The banking request failed.',
|
||
},
|
||
},
|
||
billing: {
|
||
name: 'Billing',
|
||
back: 'Back',
|
||
navigation: 'Billing navigation',
|
||
search: 'Search invoices',
|
||
loadMore: 'Load more',
|
||
tryAgain: 'Try again',
|
||
tabs: { overview: 'Overview', inbox: 'Inbox', history: 'History' },
|
||
direction: { inbox: 'Received', sent: 'Sent' },
|
||
summary: { open: 'Open invoices', due: 'Amount due', overdue: 'Overdue' },
|
||
overview: {
|
||
eyebrow: 'Payment center',
|
||
urgent: 'Needs attention',
|
||
viewAll: 'View all invoices',
|
||
},
|
||
filters: {
|
||
scope: 'Invoice source',
|
||
status: 'Status',
|
||
all: 'All',
|
||
open: 'Open',
|
||
overdue: 'Overdue',
|
||
paid: 'Paid',
|
||
},
|
||
status: {
|
||
open: 'Open',
|
||
overdue: 'Overdue',
|
||
processing: 'Processing',
|
||
paid: 'Paid',
|
||
disputed: 'Disputed',
|
||
cancelled: 'Cancelled',
|
||
refunded: 'Refunded',
|
||
},
|
||
detail: {
|
||
title: 'Invoice details',
|
||
noDueDate: 'No due date',
|
||
total: 'Total',
|
||
reason: 'Reason',
|
||
issued: 'Issued',
|
||
due: 'Due',
|
||
paidAt: 'Paid',
|
||
paidOn: 'Payment on {date}',
|
||
issuer: 'Issuer',
|
||
invoiceNumber: 'Invoice number',
|
||
invoiceInformation: 'Invoice information',
|
||
paymentInformation: 'Payment information',
|
||
note: 'Invoice note',
|
||
paymentReference: 'Payment reference',
|
||
dispute: 'Dispute invoice',
|
||
disputedSuccess: 'The invoice was disputed.',
|
||
},
|
||
payment: {
|
||
payNow: 'Pay this invoice',
|
||
title: 'Confirm payment',
|
||
body: 'Pay this invoice from {issuer} with your bank account.',
|
||
confirm: 'Pay invoice',
|
||
cancel: 'Cancel',
|
||
success: 'Invoice paid successfully.',
|
||
},
|
||
empty: {
|
||
openTitle: "You're all caught up",
|
||
openBody: 'Invoices requiring your attention will appear here.',
|
||
inboxTitle: 'No invoices found',
|
||
inboxBody: 'Received invoices matching your filters will appear here.',
|
||
historyTitle: 'No payment history',
|
||
historyBody: 'Completed and closed invoices will appear here.',
|
||
},
|
||
notifications: { newInvoice: 'New invoice from {issuer}: {amount}' },
|
||
errors: {
|
||
billing_unavailable: 'Billing is currently unavailable.',
|
||
invoice_not_found: 'This invoice could not be found.',
|
||
invoice_not_payable: 'This invoice can no longer be paid.',
|
||
invoice_already_paid: 'This invoice has already been paid.',
|
||
insufficient_funds: 'There is not enough money in your bank account.',
|
||
payment_in_progress: 'This invoice is already being processed.',
|
||
payment_failed: 'The payment could not be completed.',
|
||
dispute_unavailable: 'This invoice cannot be disputed.',
|
||
rate_limited: 'Please wait before trying again.',
|
||
request_failed: 'The billing request failed.',
|
||
default: 'The billing request failed.',
|
||
},
|
||
},
|
||
garage: {
|
||
name: 'Garage',
|
||
subtitle: 'Your vehicle collection',
|
||
myVehicles: 'My Vehicles',
|
||
searchPlaceholder: 'Search name, plate or garage',
|
||
tryAgain: 'Try Again',
|
||
unavailable: 'Garage unavailable',
|
||
noVehicles: 'No Vehicles',
|
||
noVehiclesBody: 'Vehicles owned by this character will appear here.',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try another vehicle, plate or location.',
|
||
unknownVehicle: 'Unknown Vehicle',
|
||
unknownLocation: 'Location unavailable',
|
||
location: 'Current location',
|
||
fuel: 'Fuel',
|
||
engine: 'Engine',
|
||
body: 'Body',
|
||
vin: 'Vehicle Identification Number',
|
||
notAvailable: '—',
|
||
provider: 'Vehicle data · {system}',
|
||
valet: {
|
||
title: 'Sky Valet',
|
||
eyebrow: 'Premium delivery',
|
||
body: 'A professional driver brings this vehicle directly to your current location.',
|
||
deliver: 'Deliver Vehicle',
|
||
liveEyebrow: 'Live delivery',
|
||
tracked: 'Live tracked',
|
||
onDemand: 'On demand',
|
||
cancel: 'Cancel',
|
||
confirmTitle: 'Request Valet?',
|
||
confirmBody: '{vehicle} will be delivered to you for {price}.',
|
||
confirm: 'Request Valet',
|
||
ordering: 'Requesting…',
|
||
account: 'The fee is charged to your {account} account.',
|
||
connecting: 'Locating driver',
|
||
arriving: 'Arriving now',
|
||
eta: 'About {seconds}s away',
|
||
distance: '{distance} m away',
|
||
unavailableStatus: 'Only parked vehicles can be delivered.',
|
||
unsupported: 'Valet delivery is disabled for this vehicle type.',
|
||
activeOrder: 'A valet driver is already delivering another vehicle.',
|
||
status: {
|
||
ordering: 'Confirming order',
|
||
preparing: 'Preparing vehicle',
|
||
en_route: 'Driver en route',
|
||
arriving: 'Arriving now',
|
||
delivered: 'Vehicle delivered',
|
||
cancelled: 'Delivery cancelled',
|
||
failed: 'Delivery failed',
|
||
},
|
||
},
|
||
filters: {
|
||
all: 'All',
|
||
garaged: 'Parked',
|
||
out: 'Outside',
|
||
impounded: 'Impound',
|
||
},
|
||
status: {
|
||
garaged: 'Parked',
|
||
out: 'Outside',
|
||
impounded: 'Impounded',
|
||
},
|
||
kinds: {
|
||
car: 'Vehicle',
|
||
bike: 'Motorcycle',
|
||
boat: 'Boat',
|
||
plane: 'Aircraft',
|
||
helicopter: 'Helicopter',
|
||
},
|
||
errors: {
|
||
garage_unavailable: 'Vehicle data is unavailable for this character.',
|
||
valet_disabled: 'Valet service is currently unavailable.',
|
||
valet_active: 'A valet delivery is already active.',
|
||
valet_cooldown: 'Please wait before requesting another vehicle.',
|
||
invalid_vehicle: 'Select a valid vehicle.',
|
||
vehicle_not_owned: 'This vehicle does not belong to this character.',
|
||
vehicle_not_garaged: 'This vehicle is not parked in a garage.',
|
||
valet_vehicle_type: 'Valet supports cars and motorcycles only.',
|
||
insufficient_funds: 'You do not have enough money for this delivery.',
|
||
valet_status_unsupported:
|
||
'This garage system cannot release the vehicle.',
|
||
valet_not_found: 'The valet order is no longer active.',
|
||
valet_not_cancellable: 'The delivery can no longer be cancelled.',
|
||
invalid_vehicle_model: 'The vehicle model could not be loaded.',
|
||
vehicle_already_out: 'This vehicle is already nearby.',
|
||
valet_spawn_failed: 'The driver could not prepare your vehicle.',
|
||
valet_no_road: 'No suitable road was found near you.',
|
||
valet_interrupted: 'The valet delivery was interrupted.',
|
||
valet_timeout:
|
||
'The driver could not reach you in time. Your fee was refunded.',
|
||
valet_completion_failed: 'The handover could not be confirmed.',
|
||
valet_vehicle_unverified:
|
||
'The delivered vehicle could not be verified.',
|
||
rate_limited: 'Please wait before refreshing again.',
|
||
device_not_open: 'Open the phone again to view your vehicles.',
|
||
request_failed: 'The garage request failed.',
|
||
default: 'The garage request failed.',
|
||
},
|
||
},
|
||
skyride: {
|
||
name: 'SkyRide',
|
||
loading: 'Loading SkyRide',
|
||
unavailable: 'SkyRide unavailable',
|
||
tryAgain: 'Try Again',
|
||
mode: {
|
||
rider: 'Ride',
|
||
driver: 'Drive',
|
||
},
|
||
riderEyebrow: 'Ride anywhere',
|
||
whereTo: 'Where to?',
|
||
pickup: 'Pickup',
|
||
destination: 'Destination',
|
||
currentLocation: 'Current location',
|
||
notSelected: 'Not selected',
|
||
location: 'Choose a location',
|
||
chooseLocation: {
|
||
pickup: 'Set pickup',
|
||
destination: 'Choose destination',
|
||
},
|
||
chooseOnMap: 'Choose on map',
|
||
chooseOnMapBody: 'Tap a precise point on the Los Santos map.',
|
||
pickOnMap: {
|
||
pickup: 'Tap your pickup point',
|
||
destination: 'Tap your destination',
|
||
},
|
||
tapMapHint: 'Drag and zoom the map, then tap to place the pin.',
|
||
mapPoint: 'Map pin',
|
||
map: 'Map',
|
||
useGps: 'Use your live position',
|
||
zoomIn: 'Zoom in',
|
||
zoomOut: 'Zoom out',
|
||
resetMap: 'Reset map view',
|
||
savedPlaces: 'Saved places',
|
||
savedPlace: 'Saved place',
|
||
quickDestinations: 'Quick destinations',
|
||
quickLocations: {
|
||
'legion-square': 'Legion Square',
|
||
'diamond-casino': 'Diamond Casino',
|
||
airport: 'Los Santos Airport',
|
||
vinewood: 'Vinewood',
|
||
},
|
||
viewRides: 'View Ride Options',
|
||
change: 'Change',
|
||
requestRide: 'Request SkyRide',
|
||
serviceMeta: '{eta} min away · {seats} seats',
|
||
distanceMeters: '{distance} m',
|
||
distanceKilometers: '{distance} km',
|
||
distanceMiles: '{distance} mi',
|
||
durationMinutes: '{minutes} min',
|
||
fare: 'Trip fare',
|
||
calculatedFare: 'Calculated',
|
||
customFare: 'Own price',
|
||
calculatedFareBody: 'Distance, time, and selected service included.',
|
||
customFareRange: 'Allowed range: {minimum} to {maximum}',
|
||
applyCustomFare: 'Apply Own Price',
|
||
perKilometer: '{price} / km',
|
||
perMile: '{price} / mi',
|
||
rideStatus: 'Ride status',
|
||
services: {
|
||
taxi: { name: 'Taxi' },
|
||
comfort: { name: 'Comfort' },
|
||
xl: { name: 'XL' },
|
||
premium: { name: 'Premium' },
|
||
},
|
||
ride: 'SkyRide',
|
||
driver: {
|
||
online: 'You are online',
|
||
offline: 'You are offline',
|
||
onlineBody: 'New ride requests can reach you now.',
|
||
offlineBody: 'Go online when you are ready to drive.',
|
||
toggleStatus: 'Toggle driver status',
|
||
today: 'Today',
|
||
requests: 'Nearby requests',
|
||
accept: 'Accept Ride',
|
||
noRequests: 'No nearby requests',
|
||
noRequestsBody: 'New requests will appear here automatically.',
|
||
goOnline: 'Go online to receive rides',
|
||
goOnlineBody: 'Switch your driver status on to see nearby requests.',
|
||
statusBody: {
|
||
searching: 'A passenger is waiting for a driver.',
|
||
accepted: 'Navigate to the passenger pickup.',
|
||
driver_arriving: 'You are heading to the passenger.',
|
||
arrived: 'Confirm when the passenger is in the vehicle.',
|
||
in_progress: 'Follow the route to the destination.',
|
||
completed: 'The ride has been completed.',
|
||
cancelled: 'This ride was cancelled.',
|
||
},
|
||
actions: {
|
||
arrive: 'I Have Arrived',
|
||
start: 'Start Ride',
|
||
complete: 'Complete Ride',
|
||
},
|
||
},
|
||
passenger: 'Passenger',
|
||
vehicle: 'Vehicle',
|
||
navigate: 'Navigate',
|
||
call: 'Call',
|
||
message: 'Message',
|
||
cancelRide: 'Cancel Ride',
|
||
cancelTitle: 'Cancel this ride?',
|
||
cancelBody:
|
||
'Your driver or passenger will be notified. A cancellation fee may apply.',
|
||
safetyNote: 'Trip details are shared only while this ride is active.',
|
||
waypointSet: 'Route added to your GPS.',
|
||
status: {
|
||
searching: 'Finding a driver',
|
||
accepted: 'Driver found',
|
||
driver_arriving: 'Driver en route',
|
||
arrived: 'Driver arrived',
|
||
in_progress: 'On the way',
|
||
completed: 'Completed',
|
||
cancelled: 'Cancelled',
|
||
},
|
||
statusBody: {
|
||
searching: 'We are matching you with a nearby driver.',
|
||
accepted: 'Your driver is preparing to pick you up.',
|
||
driver_arriving: 'Your driver is on the way to your pickup.',
|
||
arrived: 'Your driver is waiting at the pickup point.',
|
||
in_progress: 'You are on the way to your destination.',
|
||
completed: 'You have arrived at your destination.',
|
||
cancelled: 'This ride is no longer active.',
|
||
},
|
||
navigation: 'SkyRide navigation',
|
||
tabs: {
|
||
home: 'Home',
|
||
rides: 'Rides',
|
||
activity: 'Activity',
|
||
messages: 'Messages',
|
||
profile: 'Profile',
|
||
},
|
||
rides: 'Your Rides',
|
||
ridesBody: 'Review completed and cancelled rides.',
|
||
noRides: 'No rides yet',
|
||
noRidesBody: 'Completed and cancelled rides will appear here.',
|
||
activity: 'Activity',
|
||
activityBody: 'Recent trip updates and milestones.',
|
||
noActivity: 'No recent activity',
|
||
noActivityBody: 'Ride updates will appear here.',
|
||
messages: 'Messages',
|
||
messagesBody: 'Contact your active driver or passenger.',
|
||
noMessages: 'No active contact',
|
||
noMessagesBody: 'Book or accept a ride to contact the other person.',
|
||
openMessages: 'Open Messages',
|
||
account: 'Account',
|
||
paymentMethod: 'Payment Method',
|
||
paymentMethods: {
|
||
bank: 'Bank',
|
||
cash: 'Cash',
|
||
default: 'Payment method',
|
||
},
|
||
safety: 'Safety Center',
|
||
safetyBody: 'Help, trip sharing, and emergency information',
|
||
driverMode: 'Driver Mode',
|
||
driverModeBody: 'Accept ride requests and track your earnings',
|
||
completedRides: 'Completed',
|
||
cancelledRides: 'Cancelled',
|
||
acceptance: 'Acceptance',
|
||
rating: 'Rating',
|
||
notAvailable: 'N/A',
|
||
memberSince: 'SkyRide member since {date}',
|
||
rideComplete: 'Ride Complete',
|
||
rateRide: 'Rate your ride',
|
||
rateRideBody: 'How was your trip? Your feedback helps every rider.',
|
||
ratingValue: '{rating} stars',
|
||
tip: 'Add a tip',
|
||
noTip: 'No tip',
|
||
comment: 'Comment',
|
||
commentPlaceholder: 'Share an optional note...',
|
||
submitRating: 'Submit Rating',
|
||
notNow: 'Not Now',
|
||
ratingSaved: 'Thanks for your feedback.',
|
||
errors: {
|
||
skyride_unavailable: 'SkyRide is currently unavailable.',
|
||
invalid_location: 'Choose a valid pickup and destination.',
|
||
pickup_too_far: 'Move closer to the selected pickup point.',
|
||
invalid_route: 'No suitable route was found for this ride.',
|
||
invalid_custom_fare: 'Choose an own price inside the allowed range.',
|
||
quote_not_found: 'These ride options are no longer available.',
|
||
quote_expired: 'These ride options expired. Request a new quote.',
|
||
active_ride_exists: 'Finish or cancel your active ride first.',
|
||
invalid_driver_status: 'That driver status is not available.',
|
||
driver_not_eligible: 'This account is not eligible for Driver Mode.',
|
||
driver_offline: 'Go online before accepting a ride.',
|
||
driver_unavailable: 'That driver is no longer available.',
|
||
driver_vehicle_required:
|
||
"Sit in the driver's seat of a vehicle before accepting a ride.",
|
||
ride_not_found: 'This ride is no longer available.',
|
||
invalid_ride_status: 'This action is not available for the ride.',
|
||
not_ride_participant: 'You are not part of this ride.',
|
||
driver_too_far: 'Move closer to the required ride location.',
|
||
passenger_too_far: 'The passenger is not close enough to start.',
|
||
destination_too_far:
|
||
'Move closer to the destination to complete the ride.',
|
||
payout_failed: 'The driver payout could not be completed.',
|
||
refund_failed: 'The ride refund could not be completed.',
|
||
invalid_cancel_reason: 'Choose a valid cancellation reason.',
|
||
invalid_rating: 'Choose a rating from one to five stars.',
|
||
invalid_tip: 'Choose a valid tip amount.',
|
||
invalid_comment: 'The rating comment is not valid.',
|
||
tip_failed: 'The tip could not be processed.',
|
||
insufficient_funds:
|
||
'Your selected payment method has insufficient funds.',
|
||
rate_limited: 'Please wait a moment before trying again.',
|
||
device_not_open: 'Open the phone again to use SkyRide.',
|
||
device_not_owned: 'This phone does not belong to this character.',
|
||
device_locked: 'Unlock this phone to use SkyRide.',
|
||
request_failed: 'SkyRide could not complete the request.',
|
||
default: 'SkyRide could not complete the request.',
|
||
},
|
||
},
|
||
house: {
|
||
name: 'House',
|
||
subtitle: 'Homes and access',
|
||
myHomes: 'My Homes',
|
||
properties: 'Properties',
|
||
owned: 'Owned',
|
||
shared: 'Shared with you',
|
||
locked: 'Locked',
|
||
unlocked: 'Unlocked',
|
||
owner: 'Owner',
|
||
keyholder: 'Key holder',
|
||
openDetails: 'Open property details',
|
||
tryAgain: 'Try Again',
|
||
refresh: 'Refresh',
|
||
offline: 'House is offline',
|
||
offlineBody:
|
||
'Start a supported housing resource or review Config.Housing.',
|
||
unavailable: 'Homes unavailable',
|
||
empty: 'No Homes',
|
||
emptyBody: 'Owned homes and homes shared with you will appear here.',
|
||
provider: 'Housing data · {system}',
|
||
access: 'Access',
|
||
status: 'Status',
|
||
actions: 'Controls',
|
||
setWaypoint: 'Directions',
|
||
viewCamera: 'View Camera',
|
||
garage: 'Garage',
|
||
garageEnabled: 'Enabled',
|
||
garageDisabled: 'Unavailable',
|
||
storedVehicles: '{count} stored',
|
||
camera: 'Camera',
|
||
cameraAvailable: 'Ready',
|
||
cameraUnavailable: 'Unavailable',
|
||
keys: 'Keys',
|
||
manageKeys: 'Manage Keys',
|
||
addKey: 'Add Key',
|
||
revokeKey: 'Revoke',
|
||
noKeys: 'No shared keys',
|
||
noKeysBody: 'Only you currently have access to this home.',
|
||
chooseResident: 'Give a Key',
|
||
chooseResidentBody: 'Choose a person currently inside this property.',
|
||
noCandidates: 'Nobody is waiting inside',
|
||
noCandidatesBody:
|
||
'A person must be inside the property before you can give them a key.',
|
||
revokeTitle: 'Revoke Key?',
|
||
revokeBody: '{name} will lose access to this property.',
|
||
offlineKey: 'Offline',
|
||
onlineKey: 'Online',
|
||
cancel: 'Cancel',
|
||
confirm: 'Confirm',
|
||
lockSuccess: 'Door status updated.',
|
||
waypointSuccess: 'Route set on your map.',
|
||
keyGranted: 'Key granted.',
|
||
keyRevoked: 'Key revoked.',
|
||
cameraStarting: 'Opening camera…',
|
||
errors: {
|
||
provider_unavailable: 'The housing provider is unavailable.',
|
||
provider_error: 'The housing provider could not be reached.',
|
||
housing_unavailable: 'Housing data is unavailable for this character.',
|
||
property_not_found: 'This property no longer exists.',
|
||
property_access_denied: 'You no longer have access to this property.',
|
||
owner_required: 'Only the property owner can do that.',
|
||
invalid_property: 'Select a valid property.',
|
||
invalid_action: 'This housing action is unavailable.',
|
||
capability_unavailable:
|
||
'This property provider does not support that control.',
|
||
invalid_target: 'Select a valid resident.',
|
||
target_not_in_property: 'That person is no longer inside the property.',
|
||
key_not_found: 'That key no longer exists.',
|
||
key_already_exists: 'That person already has access to this property.',
|
||
keyholder_offline:
|
||
'Offline keys must be revoked through the property menu.',
|
||
cctv_unavailable: 'The camera is unavailable for this property.',
|
||
provider_rejected: 'The housing provider rejected the request.',
|
||
action_failed: 'The property change could not be saved.',
|
||
rate_limited: 'Please wait before using another housing control.',
|
||
device_not_open: 'Open the phone again to manage your homes.',
|
||
device_locked: 'Unlock the phone to manage your homes.',
|
||
request_failed: 'The housing request failed.',
|
||
default: 'The housing request failed.',
|
||
},
|
||
},
|
||
calculator: { name: 'Calculator' },
|
||
snake: {
|
||
name: 'Snake',
|
||
backToMenu: 'Back to game menu',
|
||
board: 'Snake game board',
|
||
controls: 'Direction controls',
|
||
directions: {
|
||
down: 'Move down',
|
||
left: 'Move left',
|
||
right: 'Move right',
|
||
up: 'Move up',
|
||
},
|
||
gameOver: 'Game Over',
|
||
highScore: 'High Score',
|
||
menu: 'Main Menu',
|
||
pause: 'Pause game',
|
||
paused: 'Paused',
|
||
readyBody: 'Collect fruit, grow longer, and stay clear of every wall.',
|
||
readyTitle: 'Ready to play?',
|
||
restart: 'Play Again',
|
||
resume: 'Resume game',
|
||
score: 'Score',
|
||
speed: 'Speed',
|
||
speeds: { fast: 'Fast', normal: 'Normal', relaxed: 'Relaxed' },
|
||
start: 'Start Game',
|
||
swipeHint: 'Swipe, tap the controls, or use arrow keys / WASD',
|
||
},
|
||
memory: {
|
||
name: 'Memory',
|
||
backToMenu: 'Back to board selection',
|
||
board: 'Memory card board',
|
||
chooseBody: 'Find every matching pair with as few moves as possible.',
|
||
chooseTitle: 'Choose a board',
|
||
completeEyebrow: 'Board cleared',
|
||
completeTitle: 'Great memory!',
|
||
difficulties: { large: 'Expert', medium: 'Classic', small: 'Quick' },
|
||
eyebrow: 'Matching game',
|
||
hiddenCard: 'Hidden card',
|
||
menu: 'Board Selection',
|
||
moves: 'Moves',
|
||
mute: 'Mute game sounds',
|
||
noBest: 'No best score yet',
|
||
playAgain: 'Play Again',
|
||
restart: 'Restart',
|
||
revealedCard: 'Revealed card',
|
||
time: 'Time',
|
||
unmute: 'Turn on game sounds',
|
||
},
|
||
numberMerge: {
|
||
name: '2048',
|
||
backToMenu: 'Back to game menu',
|
||
best: 'Best',
|
||
board: '2048 game board',
|
||
cancel: 'Keep Current Game',
|
||
confirmBody:
|
||
'Your current board will be replaced. This cannot be undone.',
|
||
confirmNew: 'Start New Game',
|
||
confirmTitle: 'Replace current game?',
|
||
continueGame: 'Continue Game',
|
||
controls: 'Move controls',
|
||
directions: {
|
||
down: 'Move tiles down',
|
||
left: 'Move tiles left',
|
||
right: 'Move tiles right',
|
||
up: 'Move tiles up',
|
||
},
|
||
eyebrow: 'Number puzzle',
|
||
gameOverBody: 'No empty spaces or matching neighbours remain.',
|
||
gameOverTitle: 'No more moves',
|
||
highest: 'Highest Tile',
|
||
howToBody:
|
||
'Swipe to combine matching tiles. Each pair becomes one larger tile.',
|
||
howToExample: '2 + 2 = 4 · 4 + 4 = 8 · … · 1024 + 1024 = 2048',
|
||
howToGoal:
|
||
'Goal: create the 2048 tile before the board has no moves left.',
|
||
howToTitle: 'How to play',
|
||
keepPlaying: 'Keep Playing',
|
||
mainMenu: 'Main Menu',
|
||
menuBody: 'Slide matching numbers together and work your way up to 2048.',
|
||
menuTitle: 'Build the 2048 tile',
|
||
mute: 'Mute game sounds',
|
||
newGame: 'New Game',
|
||
score: 'Score',
|
||
swipeHint: 'Swipe the board, use the buttons, or press arrow keys / WASD',
|
||
unmute: 'Turn on game sounds',
|
||
wonBody:
|
||
'You reached the legendary tile. Continue climbing or begin again.',
|
||
wonTitle: 'You made 2048!',
|
||
},
|
||
minesweeper: {
|
||
name: 'Minesweeper',
|
||
backToMenu: 'Back to game menu',
|
||
board: 'Minesweeper board',
|
||
chooseBody:
|
||
'Reveal every safe field. Numbers show how many mines touch that field.',
|
||
chooseTitle: 'Choose a minefield',
|
||
difficulties: { classic: 'Classic', expert: 'Expert', quick: 'Quick' },
|
||
emptyCell: 'Revealed empty field',
|
||
eyebrow: 'Mine puzzle',
|
||
flaggedCell: 'Flagged field',
|
||
gameHint: 'Tap to reveal · Hold or right-click to flag',
|
||
hiddenCell: 'Hidden field',
|
||
longPressHint: 'Tap to reveal · Hold to place a flag',
|
||
lostBody:
|
||
'A mine was hidden there. Mark suspicious fields and try again.',
|
||
lostTitle: 'Mine triggered',
|
||
mainMenu: 'Main Menu',
|
||
mineCell: 'Revealed mine',
|
||
mines: 'Mines',
|
||
mute: 'Mute game sounds',
|
||
noBest: 'No best time yet',
|
||
numberCell: 'Revealed field with {count} adjacent mines',
|
||
playAgain: 'Play Again',
|
||
restart: 'Restart',
|
||
resume: 'Continue Game',
|
||
time: 'Time',
|
||
unmute: 'Turn on game sounds',
|
||
wonBody: 'Minefield cleared in {time}.',
|
||
wonTitle: 'Field cleared!',
|
||
},
|
||
towerStack: {
|
||
name: 'Tower Stack',
|
||
backToMenu: 'Back to game menu',
|
||
bestHeight: 'Best Height',
|
||
bestScore: 'Best Score',
|
||
blocks: 'Blocks',
|
||
eyebrow: 'Timing challenge',
|
||
finalScore: 'Final Score',
|
||
gameHint: 'Tap the playfield to place the moving block',
|
||
gameOver: 'Tower collapsed',
|
||
height: 'Height',
|
||
mainMenu: 'Main Menu',
|
||
menuBody:
|
||
'Stop each moving block above the tower. Only the overlapping part remains.',
|
||
menuTitle: 'How high can you build?',
|
||
mute: 'Mute game sounds',
|
||
newGame: 'Start New Game',
|
||
pause: 'Pause or resume game',
|
||
paused: 'Paused',
|
||
perfect: 'PERFECT!',
|
||
placeBlock: 'Place moving block',
|
||
playAgain: 'Play Again',
|
||
ready: 'Ready to stack?',
|
||
resume: 'Continue Game',
|
||
score: 'Score',
|
||
start: 'Start Game',
|
||
tapHint: 'Every tap places one block · perfect hits earn bonus points',
|
||
unmute: 'Turn on game sounds',
|
||
},
|
||
skyFlappy: {
|
||
name: 'Sky Flappy',
|
||
backToMenu: 'Back to game menu',
|
||
best: 'Best',
|
||
design: 'Sky design',
|
||
designs: { dawn: 'Dawn', neon: 'Neon', storm: 'Storm' },
|
||
eyebrow: 'Sky challenge',
|
||
firstTap: 'Tap to launch',
|
||
flap: 'Fly upward',
|
||
gameHint: 'Tap anywhere in the sky to fly upward',
|
||
gameOver: 'Flight ended',
|
||
highScore: 'High Score',
|
||
mainMenu: 'Main Menu',
|
||
menuBody:
|
||
'Guide the sky bird through every tower opening without touching the edges.',
|
||
menuTitle: 'Fly between the towers',
|
||
mute: 'Mute game sounds',
|
||
pause: 'Pause or resume game',
|
||
paused: 'Paused',
|
||
playAgain: 'Fly Again',
|
||
points: 'Points',
|
||
ready: 'Ready for takeoff?',
|
||
resume: 'Continue Flight',
|
||
score: 'Score',
|
||
start: 'Start Flight',
|
||
tapHint: 'Tap to flap · gravity pulls the bird down',
|
||
unmute: 'Turn on game sounds',
|
||
},
|
||
neonDrop: {
|
||
name: 'Neon Drop',
|
||
backToMenu: 'Back to game menu',
|
||
bestLines: 'Best Lines',
|
||
bestScore: 'Best Score',
|
||
board: 'Neon Drop reactor board',
|
||
controls: 'Block controls',
|
||
eyebrow: 'Energy block puzzle',
|
||
gameHint: 'Swipe the reactor or use the five controls',
|
||
gameOver: 'Reactor overloaded',
|
||
hardDrop: 'Drop block immediately',
|
||
howToBody:
|
||
'Fill complete horizontal rows to dissolve them and keep the reactor clear.',
|
||
howToTitle: 'Charge the reactor',
|
||
left: 'Move block left',
|
||
level: 'Level',
|
||
lines: 'Lines',
|
||
mainMenu: 'Main Menu',
|
||
menuBody:
|
||
'Rotate and place falling energy shapes. Every cleared row increases your charge.',
|
||
menuTitle: 'Build complete energy lines',
|
||
mute: 'Mute game sounds',
|
||
newGame: 'Start New Game',
|
||
next: 'Next',
|
||
pause: 'Pause or resume game',
|
||
paused: 'Paused',
|
||
playAgain: 'Play Again',
|
||
points: 'Points',
|
||
ready: 'Reactor ready',
|
||
resume: 'Continue Game',
|
||
right: 'Move block right',
|
||
rotate: 'Rotate block',
|
||
score: 'Score',
|
||
softDrop: 'Move block down',
|
||
start: 'Start Reactor',
|
||
unmute: 'Turn on game sounds',
|
||
},
|
||
citymarkt: {
|
||
name: 'CityMarkt',
|
||
eyebrow: 'Los Santos marketplace',
|
||
tabs: {
|
||
discover: 'Discover',
|
||
search: 'Search',
|
||
sell: 'Sell',
|
||
inbox: 'Inbox',
|
||
profile: 'Me',
|
||
},
|
||
categories: {
|
||
vehicles: 'Vehicles',
|
||
property: 'Property',
|
||
electronics: 'Electronics',
|
||
clothing: 'Clothing',
|
||
tools: 'Tools',
|
||
leisure: 'Leisure',
|
||
services: 'Services',
|
||
jobs: 'Jobs',
|
||
wanted: 'Wanted',
|
||
other: 'Other',
|
||
},
|
||
conditions: {
|
||
new: 'New',
|
||
very_good: 'Very good',
|
||
used: 'Used',
|
||
defective: 'Defective',
|
||
},
|
||
priceTypes: {
|
||
fixed: 'Fixed price',
|
||
negotiable: 'Negotiable',
|
||
free: 'Free',
|
||
},
|
||
offerStatus: {
|
||
pending: 'Open',
|
||
accepted: 'Accepted',
|
||
declined: 'Declined',
|
||
countered: 'Negotiated',
|
||
},
|
||
districts: {
|
||
los_santos: 'Los Santos',
|
||
vinewood: 'Vinewood',
|
||
vespucci: 'Vespucci',
|
||
south_los_santos: 'South Los Santos',
|
||
sandy_shores: 'Sandy Shores',
|
||
paleto_bay: 'Paleto Bay',
|
||
blaine_county: 'Blaine County',
|
||
},
|
||
status: {
|
||
active: 'Active',
|
||
reserved: 'Reserved',
|
||
sold: 'Sold',
|
||
expired: 'Expired',
|
||
removed: 'Removed',
|
||
},
|
||
reportReasons: {
|
||
prohibited: 'Prohibited item',
|
||
fraud: 'Suspected fraud',
|
||
spam: 'Spam',
|
||
offensive: 'Offensive content',
|
||
other: 'Other',
|
||
},
|
||
searchPlaceholder: 'What are you looking for?',
|
||
allCategories: 'All categories',
|
||
allDistricts: 'All districts',
|
||
sortNewest: 'Newest first',
|
||
sortPriceAsc: 'Lowest price',
|
||
sortPriceDesc: 'Highest price',
|
||
freshOffers: 'Fresh offers',
|
||
offers: 'offers',
|
||
compactView: 'Compact view',
|
||
largeView: 'Large view',
|
||
noListings: 'No offers found',
|
||
noListingsBody: 'Try another search or category.',
|
||
noDistrict: 'No district',
|
||
free: 'Free',
|
||
negotiablePrice: '${price} negotiable',
|
||
money: '${price}',
|
||
hoursAgo: '{count}h ago',
|
||
daysAgo: '{count}d ago',
|
||
photos: 'photos',
|
||
activeListings: 'active listings',
|
||
signInTitle: 'Sign in to iFruit',
|
||
signInBody:
|
||
'Use Settings to sign in before selling, saving or messaging.',
|
||
noMessages: 'No conversations',
|
||
noMessagesBody: 'Messages about offers will appear here.',
|
||
myListings: 'My listings',
|
||
favorites: 'Favorites',
|
||
noProfileListings: 'Nothing here yet',
|
||
phone: 'Phone',
|
||
contactSeller: 'Contact seller',
|
||
messagePlaceholder: 'Hi, is this still available?',
|
||
signInToMessage: 'Sign in to your iFruit account to send a message.',
|
||
edit: 'Edit',
|
||
makeActive: 'Make active',
|
||
markSold: 'Mark sold',
|
||
remove: 'Remove',
|
||
createListing: 'Create listing',
|
||
editListing: 'Edit listing',
|
||
save: 'Save',
|
||
step: 'Step {current} of {total}',
|
||
next: 'Next',
|
||
previous: 'Back',
|
||
publish: 'Publish',
|
||
addPhotos: 'Add photos',
|
||
addPhotosBody:
|
||
'Choose up to six photos from your gallery or take new ones. Photos are optional and the first becomes the cover.',
|
||
chooseGallery: 'Choose from gallery',
|
||
chooseGalleryBody: 'Use photos saved on this phone.',
|
||
takePhotos: 'Take photos',
|
||
takePhotosBody: 'Take several new photos for this listing.',
|
||
selectedPhotos: 'Selected photos',
|
||
gallery: 'Gallery',
|
||
camera: 'Camera',
|
||
takePhoto: 'Take photo',
|
||
cameraHint:
|
||
'Take as many photos as you need. Each shot is added to the listing automatically.',
|
||
noPhoto: 'No photo available',
|
||
noPhotoBody: 'The seller did not add a photo to this listing.',
|
||
noPhotoOptional:
|
||
'You can publish without a photo or add one from the gallery or camera.',
|
||
previousPhoto: 'Previous photo',
|
||
nextPhoto: 'Next photo',
|
||
photo: 'Photo',
|
||
removePhoto: 'Remove photo {number}',
|
||
photoLimit: 'You can add up to six photos.',
|
||
describeOffer: 'Describe your offer',
|
||
title: 'Title',
|
||
description: 'Description',
|
||
category: 'Category',
|
||
condition: 'Condition',
|
||
characterCount: '{current} / {maximum} characters',
|
||
minimumCharacters: 'min. {minimum}',
|
||
priceAndPlace: 'Price and location',
|
||
priceType: 'Price type',
|
||
price: 'Price',
|
||
district: 'District',
|
||
showPhone: 'Show my current phone number',
|
||
preview: 'Preview',
|
||
published: 'Your listing is live.',
|
||
writeMessage: 'Write a message',
|
||
reserveForBuyer: 'Reserve for this buyer',
|
||
statusChanged: 'Listing updated.',
|
||
offer: 'Offer',
|
||
counterOffer: 'Counteroffer',
|
||
makeOffer: 'Make an offer',
|
||
offeredByYou: 'You sent this offer.',
|
||
offeredToYou: 'You received this offer.',
|
||
acceptOffer: 'Accept',
|
||
declineOffer: 'Decline',
|
||
negotiateOffer: 'Counter',
|
||
offerFor: 'Your offer for',
|
||
offerAmount: 'Offer amount',
|
||
sendOffer: 'Send offer',
|
||
offerSent: 'Your offer was sent.',
|
||
offerAccepted: 'Offer accepted.',
|
||
offerDeclined: 'Offer declined.',
|
||
reportListing: 'Report listing',
|
||
reportWhy: 'Why are you reporting this?',
|
||
reportDetails: 'Add details (optional)',
|
||
sendReport: 'Send report',
|
||
blockSeller: 'Block seller',
|
||
reported: 'Report sent.',
|
||
blocked: 'Seller blocked.',
|
||
newMessage: 'New CityMarkt message from {sender}',
|
||
newOffer: '{sender} offered ${price}.',
|
||
offerAcceptedNotification: '{sender} accepted your ${price} offer.',
|
||
offerRejectedNotification: '{sender} declined your ${price} offer.',
|
||
errors: {
|
||
invalid_listing: 'Check the listing details.',
|
||
invalid_price: 'Enter a valid price.',
|
||
invalid_images: 'Choose valid photos from this phone.',
|
||
phone_unavailable: 'Insert a SIM or hide your number.',
|
||
listing_limit: 'You have reached the active listing limit.',
|
||
listing_not_found: 'This listing is no longer available.',
|
||
invalid_message: 'Enter a valid message.',
|
||
inquiry_not_found: 'This conversation is no longer available.',
|
||
invalid_offer: 'Enter a valid whole-number offer.',
|
||
invalid_offer_response: 'This offer action is invalid.',
|
||
offer_listing_unavailable:
|
||
'This listing is no longer available for offers.',
|
||
offer_closed: 'This negotiation is already complete.',
|
||
offer_waiting: 'Wait for the other person to respond.',
|
||
offer_not_allowed: 'You cannot make an offer right now.',
|
||
offer_not_actionable: 'This offer can no longer be changed.',
|
||
offer_conflict: 'The offer changed. Please try again.',
|
||
blocked: 'Messages are blocked between these accounts.',
|
||
already_reported: 'You already reported this listing.',
|
||
rate_limited: 'Too many requests. Try again shortly.',
|
||
not_authenticated: 'Sign in to your iFruit account first.',
|
||
conflict: 'The listing changed. Open it again.',
|
||
request_failed: 'CityMarkt is temporarily unavailable.',
|
||
default: 'The CityMarkt request failed.',
|
||
},
|
||
},
|
||
localPages: {
|
||
name: 'Local Pages',
|
||
eyebrow: 'Your city. Your stories.',
|
||
cityPulse: 'Live from Los Santos',
|
||
heroTitle: 'What is happening nearby?',
|
||
heroBody: 'Discover places, people and local tips.',
|
||
searchPlaceholder: 'Search posts and places',
|
||
search: 'Search',
|
||
allCategories: 'All categories',
|
||
allLosSantos: 'Los Santos',
|
||
hoursAgo: '{count}h ago',
|
||
daysAgo: '{count}d ago',
|
||
categories: {
|
||
recommendation: 'Recommended',
|
||
wanted: 'Wanted',
|
||
service: 'Service',
|
||
event: 'Event',
|
||
place: 'Place',
|
||
community: 'Community',
|
||
citymarkt: 'CityMarkt',
|
||
},
|
||
discover: 'Discover',
|
||
create: 'Post',
|
||
profile: 'Profile',
|
||
post: 'Post',
|
||
posts: 'posts',
|
||
noPosts: 'Nothing here yet',
|
||
noPostsBody: 'Be the first to share something with the city.',
|
||
noPhoto: 'No photo attached',
|
||
signInTitle: 'Your Local Pages profile',
|
||
signInBody: 'Sign in to iFruit in Settings to publish and save posts.',
|
||
localCreator: 'Local creator',
|
||
myPosts: 'My posts',
|
||
saved: 'Saved',
|
||
save: 'Save',
|
||
likes: 'likes',
|
||
location: 'Location',
|
||
sharedFrom: 'Shared from CityMarkt',
|
||
openCityMarkt: 'Open CityMarkt listing',
|
||
newPost: 'New local post',
|
||
shareWithCity: 'Share with the city',
|
||
publish: 'Publish',
|
||
published: 'Your post is live.',
|
||
deleted: 'Post deleted.',
|
||
title: 'Title',
|
||
body: 'Your story',
|
||
category: 'Category',
|
||
titlePlaceholder: 'What should people know?',
|
||
bodyPlaceholder: 'Add details, a recommendation or directions...',
|
||
photos: 'Photos',
|
||
optional: 'optional',
|
||
camera: 'Camera',
|
||
gallery: 'Gallery',
|
||
photoLimit: 'You can add up to six photos.',
|
||
cityMarktShare: 'Share to Local Pages',
|
||
cityMarktShared: 'Shared to Local Pages.',
|
||
cityMarktShareHint: 'One CityMarkt share per day',
|
||
errors: {
|
||
invalid_post: 'Add a title and a little more detail.',
|
||
invalid_images: 'Choose valid photos from this phone.',
|
||
invalid_request: 'This action is not valid.',
|
||
post_not_found: 'This post is no longer available.',
|
||
citymarkt_not_found: 'This CityMarkt listing is unavailable.',
|
||
citymarkt_daily_limit: 'You already shared a CityMarkt listing today.',
|
||
citymarkt_already_shared: 'This listing was already shared.',
|
||
not_authenticated: 'Sign in to iFruit first.',
|
||
rate_limited: 'Too many requests. Try again shortly.',
|
||
request_failed: 'The post could not be saved.',
|
||
default: 'Local Pages is temporarily unavailable.',
|
||
},
|
||
},
|
||
map: {
|
||
name: 'Map',
|
||
controls: 'Map controls',
|
||
currentLocation: 'Current Location',
|
||
imageError: 'The map image could not be loaded.',
|
||
switchStyle: 'Switch Map Type',
|
||
addMarker: 'Add Marker',
|
||
placeMarker: 'Place Marker',
|
||
placeMarkerHint:
|
||
'Move the map until the crosshair is over the destination.',
|
||
addHere: 'Add Here',
|
||
newMarker: 'New Marker',
|
||
newMarkerDescription: 'Give this saved place a name and color.',
|
||
markerName: 'Name',
|
||
markerNamePlaceholder: 'e.g. Meeting point',
|
||
markerColor: 'Marker Color',
|
||
saveMarker: 'Save Marker',
|
||
deleteMarker: 'Delete Marker',
|
||
setWaypoint: 'Set Waypoint',
|
||
waypointSet: 'Waypoint set.',
|
||
markerSaved: 'Marker saved.',
|
||
markerDeleted: 'Marker deleted.',
|
||
colors: {
|
||
blue: 'Blue',
|
||
green: 'Green',
|
||
orange: 'Orange',
|
||
purple: 'Purple',
|
||
red: 'Red',
|
||
},
|
||
errors: {
|
||
invalid_marker: 'Enter a valid marker name and position.',
|
||
marker_limit: 'This phone has reached its marker limit.',
|
||
marker_not_found: 'This marker no longer exists.',
|
||
rate_limited: 'Too many changes. Try again shortly.',
|
||
request_failed: 'The marker could not be saved.',
|
||
},
|
||
styles: {
|
||
default: 'Default Map',
|
||
satellite: 'Satellite Map',
|
||
atlas: 'Atlas Map',
|
||
roads: 'Road Map',
|
||
},
|
||
},
|
||
weather: {
|
||
name: 'Weather',
|
||
now: 'Now',
|
||
today: 'Today',
|
||
refresh: 'Refresh weather',
|
||
details: 'Weather details',
|
||
feelsLike: 'Feels Like',
|
||
wind: 'Wind',
|
||
humidity: 'Humidity',
|
||
rain: 'Precipitation',
|
||
hourly: 'Next Hours',
|
||
unavailable: 'Weather is unavailable',
|
||
stale: 'Showing the last available weather update.',
|
||
tryAgain: 'Try Again',
|
||
webcam: {
|
||
title: 'Weather Cameras',
|
||
live: 'LIVE',
|
||
view: 'View Live Camera',
|
||
subtitle: 'A live look at current conditions',
|
||
unavailable: 'No weather cameras are available.',
|
||
locations: {
|
||
legion_square: 'Legion Square',
|
||
sandy_shores: 'Sandy Shores',
|
||
cayo_airstrip: 'Cayo Perico Airstrip',
|
||
},
|
||
errors: {
|
||
camera_disabled: 'Weather cameras are currently disabled.',
|
||
camera_not_found: 'This weather camera is no longer available.',
|
||
camera_requires_safe_position:
|
||
'Stand still and exit your vehicle before opening a camera.',
|
||
camera_active: 'A weather camera is already open.',
|
||
rate_limited: 'Too many camera requests. Try again shortly.',
|
||
request_failed: 'The weather camera could not be opened.',
|
||
not_authenticated: 'Unlock the phone before opening a camera.',
|
||
},
|
||
},
|
||
regions: {
|
||
los_santos: 'Los Santos',
|
||
blaine_county: 'Blaine County',
|
||
cayo_perico: 'Cayo Perico',
|
||
},
|
||
conditions: {
|
||
sunny: 'Sunny',
|
||
clear: 'Clear',
|
||
partly_cloudy: 'Partly Cloudy',
|
||
cloudy: 'Cloudy',
|
||
rain: 'Rain',
|
||
thunder: 'Thunderstorms',
|
||
fog: 'Foggy',
|
||
snow: 'Snow',
|
||
},
|
||
summaries: {
|
||
sunny: 'Bright skies throughout the current period.',
|
||
clear: 'Calm and clear conditions across the region.',
|
||
partly_cloudy: 'Sunshine mixed with passing clouds.',
|
||
cloudy: 'Cloud cover will remain over the region.',
|
||
rain: 'Wet conditions are moving through the area.',
|
||
thunder: 'Stormy conditions with heavy bursts of rain.',
|
||
fog: 'Reduced visibility in low-lying areas.',
|
||
snow: 'Cold conditions with snow across the region.',
|
||
},
|
||
},
|
||
camera: {
|
||
name: 'Camera',
|
||
flash: 'Flash',
|
||
flip: 'Flip camera',
|
||
landscape: 'Switch to landscape',
|
||
portrait: 'Switch to portrait',
|
||
photo: 'Photo',
|
||
video: 'Video',
|
||
microphoneOn: 'Microphone on',
|
||
microphoneOff: 'Microphone muted',
|
||
focusHelp: 'Space for movement',
|
||
returnHelp: 'Space to return',
|
||
uploading: '{count} uploading',
|
||
saving: 'Saving video...',
|
||
openGallery: 'Open Gallery',
|
||
takePhoto: 'Take photo',
|
||
startRecording: 'Start recording',
|
||
stopRecording: 'Stop recording',
|
||
saved: 'Saved to Gallery.',
|
||
zoom: 'Set camera zoom to {zoom}',
|
||
errors: {
|
||
cancelled: 'Capture cancelled.',
|
||
capture_failed: 'Unable to capture the game view.',
|
||
invalid_media_type: 'The uploaded media type is invalid.',
|
||
invalid_upload: 'The upload could not be verified.',
|
||
invalid_upload_token: 'The upload session is no longer valid.',
|
||
missing_config: 'Camera uploads are not configured.',
|
||
microphone_unavailable:
|
||
'Allow microphone access or mute the microphone before recording.',
|
||
not_found: 'The media item no longer exists.',
|
||
operation_in_progress:
|
||
'Another media operation is already in progress.',
|
||
owner_changed: 'The active phone account changed during upload.',
|
||
rate_limited: 'Too many media actions. Try again shortly.',
|
||
request_failed: 'The camera request failed.',
|
||
request_timeout: 'The media service timed out.',
|
||
unsupported: 'Video recording is not supported.',
|
||
upload_failed: 'The media upload failed.',
|
||
upload_timeout: 'The media upload timed out.',
|
||
},
|
||
},
|
||
calendar: {
|
||
name: 'Calendar',
|
||
today: 'Today',
|
||
calendars: 'Calendars',
|
||
calendar: 'Calendar',
|
||
calendarName: 'iFruit',
|
||
searchPlaceholder: 'Search events',
|
||
previousMonth: 'Previous month',
|
||
nextMonth: 'Next month',
|
||
eyebrow: 'Your time. Clearly planned.',
|
||
schedule: 'Schedule',
|
||
event: 'Event',
|
||
appointment: 'Appointment',
|
||
newEvent: 'New event',
|
||
editEvent: 'Edit event',
|
||
deleteEvent: 'Delete event',
|
||
details: 'Event details',
|
||
title: 'Title',
|
||
titlePlaceholder: 'What is planned?',
|
||
date: 'Date',
|
||
starts: 'Starts',
|
||
ends: 'Ends',
|
||
reminder: 'Reminder',
|
||
note: 'Note',
|
||
notePlaceholder: 'Add details, an address or anything to remember...',
|
||
noEvents: 'Nothing planned',
|
||
noEventsBody:
|
||
'This day is still free. Add an event whenever you are ready.',
|
||
signInTitle: 'Your iFruit calendar',
|
||
signInBody:
|
||
'Sign in to iFruit in Settings to sync appointments and receive reminders.',
|
||
reminderNotification: 'Upcoming: {title}',
|
||
views: {
|
||
compact: 'Compact',
|
||
stacked: 'Stacked',
|
||
details: 'Details',
|
||
list: 'List',
|
||
},
|
||
reminders: {
|
||
none: 'No reminder',
|
||
atStart: 'At start time',
|
||
tenMinutes: '10 minutes before',
|
||
thirtyMinutes: '30 minutes before',
|
||
oneHour: '1 hour before',
|
||
oneDay: '1 day before',
|
||
},
|
||
errors: {
|
||
invalid_event:
|
||
'Enter a title and make sure the end is after the start.',
|
||
invalid_range: 'This calendar period is invalid.',
|
||
conflict: 'This event changed on another phone. Open it again.',
|
||
rate_limited: 'Too many changes. Try again shortly.',
|
||
not_authenticated: 'Sign in to your iFruit account first.',
|
||
request_failed: 'Calendar is temporarily unavailable.',
|
||
default: 'The calendar request failed.',
|
||
},
|
||
},
|
||
clock: {
|
||
name: 'Clock',
|
||
lap: 'Lap',
|
||
minutes: 'Minutes',
|
||
add: 'Add alarm',
|
||
location: 'Los Santos',
|
||
tabs: {
|
||
world: 'Clock',
|
||
alarm: 'Alarm',
|
||
stopwatch: 'Stopwatch',
|
||
timer: 'Timer',
|
||
},
|
||
alarm: {
|
||
add: 'Add Alarm',
|
||
edit: 'Edit Alarm',
|
||
ringing: 'Alarm',
|
||
time: 'Time',
|
||
hours: 'Hours',
|
||
repeat: 'Repeat',
|
||
note: 'Note',
|
||
notePlaceholder: 'Alarm',
|
||
sound: 'Sound',
|
||
delete: 'Delete Alarm',
|
||
never: 'Never',
|
||
everyDay: 'Every Day',
|
||
weekdays: 'Weekdays',
|
||
weekends: 'Weekends',
|
||
days: {
|
||
sunday: 'Sunday',
|
||
monday: 'Monday',
|
||
tuesday: 'Tuesday',
|
||
wednesday: 'Wednesday',
|
||
thursday: 'Thursday',
|
||
friday: 'Friday',
|
||
saturday: 'Saturday',
|
||
},
|
||
daysShort: {
|
||
sunday: 'Sun',
|
||
monday: 'Mon',
|
||
tuesday: 'Tue',
|
||
wednesday: 'Wed',
|
||
thursday: 'Thu',
|
||
friday: 'Fri',
|
||
saturday: 'Sat',
|
||
},
|
||
sounds: {
|
||
radar: 'Radar',
|
||
beacon: 'Beacon',
|
||
chimes: 'Chimes',
|
||
apex: 'Apex',
|
||
aurora: 'Aurora',
|
||
circuit: 'Circuit',
|
||
constellation: 'Constellation',
|
||
daybreak: 'Daybreak',
|
||
uplift: 'Uplift',
|
||
},
|
||
},
|
||
timer: {
|
||
time: 'Timer duration',
|
||
hours: 'Hours',
|
||
hoursShort: 'hr',
|
||
minutes: 'Minutes',
|
||
minutesShort: 'min',
|
||
seconds: 'Seconds',
|
||
secondsShort: 'sec',
|
||
note: 'Note',
|
||
notePlaceholder: 'Timer',
|
||
sound: 'Sound',
|
||
ringing: 'Timer',
|
||
},
|
||
},
|
||
mail: {
|
||
name: 'Mail',
|
||
login: 'Sign In',
|
||
register: 'Create Account',
|
||
registerLink: 'Register',
|
||
loginTitle: 'iFruit Mail',
|
||
loginBody: 'Sign in to use your shared iFruit mailbox.',
|
||
registerBody: 'Choose your new @ifruit.com address.',
|
||
localPart: 'Email address',
|
||
email: 'Email',
|
||
password: 'Password',
|
||
confirmPassword: 'Confirm password',
|
||
accountEyebrow: 'Your iFruit account',
|
||
emailPlaceholder: 'name@ifruit.com',
|
||
passwordPlaceholder: 'Enter password',
|
||
passwordWarning:
|
||
'Use an in-character password. Do not reuse a real-world password.',
|
||
mailboxes: 'Mailboxes',
|
||
inbox: 'Inbox',
|
||
sent: 'Sent',
|
||
drafts: 'Drafts',
|
||
trash: 'Trash',
|
||
compose: 'New Message',
|
||
recipients: 'To',
|
||
recipientHint: 'Separate up to 10 addresses with commas.',
|
||
recipientPlaceholder: 'name@ifruit.com',
|
||
subject: 'Subject',
|
||
subjectPlaceholder: "What's this about?",
|
||
body: 'Message',
|
||
messagePlaceholder: 'Write a message...',
|
||
search: 'Search mail',
|
||
messages: 'messages',
|
||
noMessageContent: 'No message content',
|
||
noMail: 'No Mail',
|
||
noMailBody: 'Messages in this mailbox will appear here.',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try a different search.',
|
||
untitled: '(no subject)',
|
||
loadMore: 'Load More',
|
||
logout: 'Sign Out',
|
||
from: 'From',
|
||
to: 'To',
|
||
reply: 'Reply',
|
||
replyAll: 'Reply All',
|
||
forward: 'Forward',
|
||
formatBold: 'Bold',
|
||
formatItalic: 'Italic',
|
||
formatBulletList: 'Bullet list',
|
||
formatNumberedList: 'Numbered list',
|
||
formatQuote: 'Quote',
|
||
undo: 'Undo',
|
||
redo: 'Redo',
|
||
markRead: 'Read',
|
||
markUnread: 'Mark as Unread',
|
||
delete: 'Delete',
|
||
moveToTrash: 'Move to Trash',
|
||
restore: 'Restore',
|
||
deleteForever: 'Delete Forever',
|
||
emptyTrash: 'Empty Trash',
|
||
emptyTrashTitle: 'Empty Trash?',
|
||
emptyTrashBody: 'Every message in Trash will be permanently deleted.',
|
||
deleteDraft: 'Delete Draft',
|
||
sentSuccess: 'Message sent.',
|
||
newMessage: 'New mail from {sender}',
|
||
passwordsMismatch: 'Passwords do not match.',
|
||
errors: {
|
||
invalid_email: 'Choose a valid 3–32 character iFruit address.',
|
||
invalid_password: 'Password must be 6–64 characters.',
|
||
invalid_credentials: 'Email or password is incorrect.',
|
||
email_taken: 'That iFruit address is already registered.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
invalid_message: 'Add a valid recipient and a subject or message.',
|
||
recipient_not_found: 'One or more recipients do not exist.',
|
||
invalid_draft: 'This draft contains invalid content.',
|
||
not_authenticated: 'Your mail session has ended. Sign in again.',
|
||
request_failed: 'Mail is temporarily unavailable.',
|
||
invalid_request: 'The mail request was invalid.',
|
||
default: 'The mail request failed.',
|
||
},
|
||
},
|
||
music: {
|
||
name: 'Music',
|
||
loading: 'Loading Music...',
|
||
navigation: 'Music navigation',
|
||
tabs: { library: 'Library', playlists: 'Playlists', search: 'Search' },
|
||
librarySubtitle: 'Your music, all in one place.',
|
||
playlistsSubtitle: 'Mix server tracks and your YouTube favorites.',
|
||
featured: 'Made for the city',
|
||
recentlyAdded: 'Recently Added',
|
||
songs: 'Songs',
|
||
play: 'Play',
|
||
previous: 'Previous track',
|
||
next: 'Next track',
|
||
nowPlaying: 'Now Playing',
|
||
progress: 'Playback position',
|
||
volume: 'Music volume',
|
||
addMusic: 'Add music',
|
||
addYouTube: 'Add from YouTube',
|
||
youtubeBody:
|
||
'Paste a public YouTube link. Leave the optional fields empty to use the video title and channel automatically.',
|
||
youtubeUrl: 'YouTube URL',
|
||
youtubePlaceholder: 'https://youtube.com/watch?v=...',
|
||
youtubeTitle: 'Song title (optional)',
|
||
youtubeTitlePlaceholder: 'Use the YouTube video title',
|
||
youtubeArtist: 'Artist (optional)',
|
||
youtubeArtistPlaceholder: 'Use the YouTube channel',
|
||
addToLibrary: 'Add to Library',
|
||
songAdded: 'Song added to your library.',
|
||
songRemoved: 'Song removed from your library.',
|
||
newPlaylist: 'New Playlist',
|
||
renamePlaylist: 'Rename Playlist',
|
||
addSongs: 'Add Songs',
|
||
addSongsBody: 'Choose songs from your library for this playlist.',
|
||
allSongsAdded: 'All Songs Added',
|
||
allSongsAddedBody:
|
||
'Every song in your library is already in this playlist.',
|
||
playlistBody: 'Give this playlist a name you will recognize.',
|
||
playlistName: 'Playlist Name',
|
||
playlistPlaceholder: 'My Playlist',
|
||
playlistCreated: 'Playlist created.',
|
||
playlistCreatedWithSong: 'Playlist created and song added.',
|
||
playlistRenamed: 'Playlist renamed.',
|
||
playlistDeleted: 'Playlist deleted.',
|
||
playlistActions: 'Playlist actions',
|
||
deletePlaylist: 'Delete Playlist',
|
||
deletePlaylistTitle: 'Delete Playlist?',
|
||
deletePlaylistBody:
|
||
'The playlist will be deleted. Songs stay in your library.',
|
||
choosePlaylist: 'Choose a Playlist',
|
||
addToPlaylist: 'Add to Playlist',
|
||
addedToPlaylist: 'Added to playlist.',
|
||
alreadyAdded: 'Added',
|
||
removeFromPlaylist: 'Remove from Playlist',
|
||
removedFromPlaylist: 'Removed from playlist.',
|
||
createPlaylistFirst: 'Create a playlist before adding this song.',
|
||
songActions: 'Song actions',
|
||
removeFromLibrary: 'Remove from Library',
|
||
removeSongTitle: 'Remove Song?',
|
||
removeSongBody:
|
||
'This YouTube song will also be removed from your playlists.',
|
||
songCount: '{count} songs',
|
||
emptyLibrary: 'Your Library is Empty',
|
||
emptyLibraryBody:
|
||
'Add a YouTube song or ask the server owner to publish MP3 and OGG tracks.',
|
||
emptyPlaylist: 'No Songs Yet',
|
||
emptyPlaylistBody: 'Add songs from your library to this playlist.',
|
||
noPlaylists: 'No Playlists',
|
||
noPlaylistsBody: 'Create a playlist for your favorite songs.',
|
||
searchPlaceholder: 'Artists, Songs and Playlists',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try another song title or artist.',
|
||
errors: {
|
||
invalid_youtube_url: 'Paste a valid public YouTube video link.',
|
||
invalid_song_metadata: 'Use a shorter song title or artist name.',
|
||
song_exists: 'That YouTube song is already in your library.',
|
||
song_limit: 'Your personal song limit has been reached.',
|
||
song_not_found: 'That song is no longer in your library.',
|
||
invalid_playlist: 'Choose a valid playlist name.',
|
||
playlist_limit: 'Your playlist limit has been reached.',
|
||
playlist_not_found: 'That playlist no longer exists.',
|
||
playlist_song_limit: 'That playlist has reached its song limit.',
|
||
invalid_song: 'That song cannot be added to this playlist.',
|
||
song_already_in_playlist: 'That song is already in this playlist.',
|
||
rate_limited: 'Too many music changes. Try again shortly.',
|
||
playback_failed: 'This song could not be played.',
|
||
request_failed: 'Music is temporarily unavailable.',
|
||
default: 'The Music request failed.',
|
||
},
|
||
},
|
||
notes: {
|
||
name: 'Notes',
|
||
note: 'Note',
|
||
back: 'Back',
|
||
actions: 'Note actions',
|
||
newNote: 'New Note',
|
||
searchPlaceholder: 'Search Notes',
|
||
title: 'Title',
|
||
titlePlaceholder: 'Note title',
|
||
body: 'Note',
|
||
bodyPlaceholder: 'Start writing...',
|
||
untitled: 'Untitled',
|
||
noText: 'No additional text',
|
||
emptyBadge: 'ON DEVICE',
|
||
emptyTitle: 'No Notes',
|
||
emptyBody: 'Create a note to keep important details close at hand.',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try searching for a different word or phrase.',
|
||
pin: 'Pin note',
|
||
unpin: 'Unpin note',
|
||
deleteNote: 'Delete note',
|
||
},
|
||
photos: {
|
||
name: 'Gallery',
|
||
count: '{count} items',
|
||
loading: 'Loading Gallery...',
|
||
emptyTitle: 'No Photos or Videos',
|
||
emptyBody: 'Captures from Camera will appear here.',
|
||
photo: 'Photo',
|
||
video: 'Video',
|
||
photoAlt: 'Gallery photo',
|
||
videoAlt: 'Gallery video',
|
||
delete: 'Delete media',
|
||
deleteTitle: 'Delete Media?',
|
||
deleteBody: 'This photo or video will be permanently deleted.',
|
||
deleted: 'Media deleted.',
|
||
zoomIn: 'Zoom in',
|
||
zoomOut: 'Zoom out',
|
||
resetZoom: 'Reset zoom',
|
||
filters: { all: 'All', photos: 'Photos', videos: 'Videos' },
|
||
errors: {
|
||
cancelled: 'The media action was cancelled.',
|
||
capture_failed: 'Unable to capture the game view.',
|
||
invalid_media_type: 'The media type is invalid.',
|
||
invalid_upload: 'The upload could not be verified.',
|
||
invalid_upload_token: 'The upload session is no longer valid.',
|
||
missing_config: 'Gallery uploads are not configured.',
|
||
not_found: 'The media item no longer exists.',
|
||
operation_in_progress:
|
||
'Another media operation is already in progress.',
|
||
owner_changed: 'The active phone account changed.',
|
||
rate_limited: 'Too many media actions. Try again shortly.',
|
||
request_failed: 'The Gallery request failed.',
|
||
request_timeout: 'The media service timed out.',
|
||
unsupported: 'This media format is not supported.',
|
||
upload_failed: 'The media upload failed.',
|
||
upload_timeout: 'The media upload timed out.',
|
||
},
|
||
},
|
||
settings: {
|
||
name: 'Settings',
|
||
searchPlaceholder: 'Search',
|
||
airplaneMode: 'Airplane Mode',
|
||
streamerMode: 'Streamer Mode',
|
||
wallpaper: 'Wallpaper',
|
||
on: 'On',
|
||
off: 'Off',
|
||
accountName: 'iFruit Account',
|
||
accountDetail: 'Important Data & Cloud',
|
||
accountLocalDetail: 'Not signed in',
|
||
accountCloudDetail: 'Important data syncs through iFruit Cloud',
|
||
accountLoginBody:
|
||
'Sign in to keep your important data safe. Without an iFruit Account, it will be lost with the phone.',
|
||
accountInformation: 'Account Information',
|
||
accountStatus: 'Account Status',
|
||
accountStatusValue: 'Active',
|
||
accountStorage: 'Cloud Storage',
|
||
accountStorageValue: 'On Device',
|
||
accountPurchases: 'Media & Purchases',
|
||
accountPurchasesValue: 'Available',
|
||
notifications: 'Notifications',
|
||
sounds: 'Sounds & Haptics',
|
||
general: 'General Settings',
|
||
security: 'Passcode & Security',
|
||
appearance: 'Appearance',
|
||
connectivity: 'Connectivity',
|
||
connections: 'Connections',
|
||
wifi: 'Wi-Fi',
|
||
bluetooth: 'Bluetooth',
|
||
cellular: 'Cellular',
|
||
connectivityDescription:
|
||
'Wi-Fi, Bluetooth, and cellular settings are saved on this phone.',
|
||
focus: 'Focus',
|
||
focusMode: 'Focus',
|
||
focusDescription:
|
||
'Focus silences non-critical notifications while keeping alarms and important alerts available.',
|
||
allowNotifications: 'Allow Notifications',
|
||
notificationSounds: 'Sounds',
|
||
notificationDuration: 'Notification Duration',
|
||
seconds: '{seconds} seconds',
|
||
ringtoneVolume: 'Ringtone Volume',
|
||
notificationVolume: 'Notification Volume',
|
||
ringtone: 'Ringtone',
|
||
notificationSound: 'Notification Sound',
|
||
graphicsMode: 'Graphics Mode',
|
||
performanceMode: 'Performance Mode',
|
||
performanceModeDescription:
|
||
'Blur-free glass simulation for better CEF performance',
|
||
ultimateMode: 'Ultimate Mode',
|
||
ultimateModeDescription:
|
||
'Full blur and glass effects for compatible FiveM clients',
|
||
appearanceMode: 'Appearance Mode',
|
||
automatic: 'Automatic',
|
||
light: 'Light',
|
||
dark: 'Dark',
|
||
phoneScale: 'Phone Scale',
|
||
phoneFrame: 'Phone Frame',
|
||
screenBrightness: 'Screen Brightness',
|
||
about: 'About',
|
||
deviceName: 'Device Name',
|
||
deviceNameValue: 'Sky Phone',
|
||
softwareVersion: 'Software Version',
|
||
language: 'Language',
|
||
languageValue: 'English',
|
||
localStorage: 'Local Storage',
|
||
localStorageValue: 'On Device',
|
||
deviceInformation: 'Device Information',
|
||
imei: 'IMEI',
|
||
simCard: 'SIM Card',
|
||
simNumber: 'Phone Number',
|
||
simType: 'SIM Type',
|
||
registeredSim: 'Registered',
|
||
anonymousSim: 'Anonymous',
|
||
noSim: 'No SIM',
|
||
ejectSim: 'Eject SIM',
|
||
ejectSimBody: 'Return this SIM card to your inventory?',
|
||
linkedDevices: 'Linked Devices',
|
||
thisDevice: 'This Phone',
|
||
removeDevice: 'Remove Device',
|
||
removeDeviceBody:
|
||
'Enter your iFruit password to remove this device from the account.',
|
||
signOut: 'Sign Out',
|
||
factoryReset: 'Erase All Content and Settings',
|
||
factoryResetBody:
|
||
'This removes the account and all local data from this phone. Cloud data and the IMEI remain.',
|
||
factoryResetProgress: 'Erasing iFruit Phone',
|
||
factoryResetWarning: 'Do not turn off this phone. This takes 60 seconds.',
|
||
passcode: {
|
||
description:
|
||
'A passcode protects the contents of this phone. It stays with the device when the SIM or iFruit account changes.',
|
||
status: 'Passcode',
|
||
codeLength: 'Code Length',
|
||
sixDigit: '6-Digit Code',
|
||
fourDigit: '4-Digit Code',
|
||
turnOn: 'Turn Passcode On',
|
||
turnOff: 'Turn Passcode Off',
|
||
change: 'Change Passcode',
|
||
enterNew: 'Enter New Passcode',
|
||
confirmNew: 'Verify New Passcode',
|
||
enterCurrent: 'Enter Current Passcode',
|
||
screenSubtitle: 'Use 4 or 6 numbers.',
|
||
incorrect: 'Incorrect passcode.',
|
||
mismatch: 'The passcodes did not match.',
|
||
locked: 'Too many incorrect attempts. Try again later.',
|
||
rateLimited: 'Too many attempts. Please wait.',
|
||
failed: 'The passcode could not be updated.',
|
||
saved: 'Passcode saved.',
|
||
disabled: 'Passcode turned off.',
|
||
},
|
||
accountErrors: {
|
||
invalid_email: 'Choose a valid 3–32 character iFruit address.',
|
||
invalid_password: 'Password must be 6–64 characters.',
|
||
invalid_credentials: 'Email or password is incorrect.',
|
||
email_taken: 'That iFruit address is already registered.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
current_device: 'Sign out instead of removing the current phone.',
|
||
device_not_found: 'That device is no longer linked.',
|
||
default: 'The account request failed.',
|
||
},
|
||
back: 'Settings',
|
||
wallpaperPicker: 'Built-in Wallpapers',
|
||
toggle: {
|
||
airplaneMode: 'Toggle Airplane Mode',
|
||
streamerMode: 'Toggle Streamer Mode',
|
||
focusMode: 'Toggle Focus',
|
||
wifiEnabled: 'Toggle Wi-Fi',
|
||
bluetoothEnabled: 'Toggle Bluetooth',
|
||
cellularEnabled: 'Toggle Cellular Data',
|
||
notifications: 'Toggle notifications for {app}',
|
||
notificationSounds: 'Toggle notification sounds for {app}',
|
||
},
|
||
frames: {
|
||
black: 'Black',
|
||
blue: 'Blue',
|
||
green: 'Green',
|
||
lavender: 'Lavender',
|
||
red: 'Red',
|
||
white: 'White',
|
||
orange: 'Orange',
|
||
yellow: 'Yellow',
|
||
lime: 'Lime',
|
||
teal: 'Teal',
|
||
cyan: 'Cyan',
|
||
purple: 'Purple',
|
||
pink: 'Pink',
|
||
gold: 'Gold',
|
||
rgb: 'RGB',
|
||
},
|
||
ringtones: {
|
||
skyline: 'Skyline',
|
||
horizon: 'Horizon',
|
||
pulse: 'Pulse',
|
||
},
|
||
notificationSoundsList: {
|
||
chime: 'Chime',
|
||
signal: 'Signal',
|
||
soft: 'Soft',
|
||
},
|
||
wallpapers: {
|
||
midnight: 'Midnight wallpaper',
|
||
aurora: 'Aurora wallpaper',
|
||
ember: 'Ember wallpaper',
|
||
},
|
||
},
|
||
},
|
||
ControlCenter: {
|
||
airplaneMode: 'Airplane Mode',
|
||
bluetooth: 'Bluetooth',
|
||
brightness: 'Brightness',
|
||
calculator: 'Calculator',
|
||
camera: 'Camera',
|
||
cellular: 'Cellular Data',
|
||
close: 'Close Control Center',
|
||
flashlight: 'Flashlight',
|
||
focus: 'Focus',
|
||
label: 'Control Center',
|
||
media: 'Media',
|
||
muteRingtone: 'Mute ringtone and notifications',
|
||
next: 'Next track',
|
||
notPlaying: 'Not Playing',
|
||
open: 'Open Control Center',
|
||
play: 'Play',
|
||
previous: 'Previous track',
|
||
quickActions: 'Quick actions',
|
||
timer: 'Timer',
|
||
unmuteRingtone: 'Unmute ringtone and notifications',
|
||
volume: 'Volume',
|
||
wifi: 'Wi-Fi',
|
||
},
|
||
Common: {
|
||
add: 'Add',
|
||
cancel: 'Cancel',
|
||
clear: 'Clear',
|
||
close: 'Close',
|
||
back: 'Back',
|
||
delete: 'Delete',
|
||
done: 'Done',
|
||
edit: 'Edit',
|
||
home: 'Home',
|
||
pause: 'Pause',
|
||
phone: 'Phone',
|
||
phoneStatus: 'Phone status',
|
||
reset: 'Reset',
|
||
loading: 'Loading',
|
||
search: 'Search',
|
||
save: 'Save',
|
||
send: 'Send',
|
||
start: 'Start',
|
||
stop: 'Stop',
|
||
use: 'Use',
|
||
},
|
||
Notifications: { now: 'now' },
|
||
LockScreen: {
|
||
label: 'Lock Screen',
|
||
flashlight: 'Flashlight',
|
||
camera: 'Camera',
|
||
swipeUp: 'Swipe up to open',
|
||
passcode: {
|
||
enter: 'Enter Passcode',
|
||
unlockSubtitle: 'Enter the passcode for this phone.',
|
||
cancel: 'Cancel',
|
||
delete: 'Delete digit',
|
||
incorrect: 'Incorrect passcode',
|
||
locked: 'Too many attempts. Try again in {seconds} seconds.',
|
||
tryAgain: 'Try again in {seconds} seconds',
|
||
rateLimited: 'Too many attempts. Please wait.',
|
||
},
|
||
},
|
||
Home: {
|
||
appLibrary: 'App Library',
|
||
appLibrarySearch: 'Search apps',
|
||
allApps: 'All Apps',
|
||
apps: 'Apps',
|
||
dock: 'Dock',
|
||
noApps: 'No apps found',
|
||
removeApp: 'Remove {app} from Home Screen',
|
||
addToHome: 'Add {app} to Home Screen',
|
||
addPage: 'Add Home Screen page',
|
||
deletePage: 'Delete current Home Screen page',
|
||
removedFromHome: 'Removed from Home Screen',
|
||
page: 'Page',
|
||
pages: 'Home screen pages',
|
||
groups: {
|
||
suggestions: 'Suggestions',
|
||
recentlyAdded: 'Recently Added',
|
||
games: 'Games',
|
||
productivity: 'Productivity',
|
||
shopping: 'Shopping',
|
||
social: 'Social Networks',
|
||
utilities: 'Utilities',
|
||
},
|
||
widgets: {
|
||
label: 'Widgets',
|
||
weather: { city: 'Los Santos', condition: 'Partly Cloudy' },
|
||
calendar: { event: 'No more events today' },
|
||
battery: { label: 'Phone' },
|
||
media: {
|
||
title: 'Night Drive',
|
||
artist: 'Sky Radio',
|
||
play: 'Play',
|
||
pause: 'Pause',
|
||
},
|
||
},
|
||
widgetSystem: {
|
||
galleryTitle: 'Widgets',
|
||
search: 'Search Widgets',
|
||
size: 'Size',
|
||
add: 'Add Widget',
|
||
addWidget: 'Add Widget',
|
||
editWidget: 'Edit Widget',
|
||
removeWidget: 'Remove Widget',
|
||
remove: 'Remove widget',
|
||
configure: 'Configure Widget',
|
||
noResults: 'No widgets found',
|
||
sizes: { small: 'Small', medium: 'Medium', large: 'Large' },
|
||
categories: {
|
||
essentials: 'Essentials',
|
||
information: 'Information',
|
||
media: 'Media',
|
||
finance: 'Finance',
|
||
people: 'People',
|
||
},
|
||
clock: {
|
||
name: 'Clock',
|
||
description: 'The current time, with an optional date.',
|
||
showDate: 'Show Date',
|
||
},
|
||
date: {
|
||
name: 'Date',
|
||
description: 'Weekday, month, and date at a glance.',
|
||
},
|
||
weather: {
|
||
name: 'Weather',
|
||
description: 'Current conditions, location, and high and low.',
|
||
},
|
||
music: {
|
||
name: 'Now Playing',
|
||
description: 'Music controls and the current track.',
|
||
},
|
||
wallet: {
|
||
name: 'Wallet',
|
||
description: 'Your current bank or cash balance.',
|
||
balance: 'Displayed Balance',
|
||
bank: 'Bank',
|
||
cash: 'Cash',
|
||
},
|
||
transactions: {
|
||
name: 'Transactions',
|
||
description: 'Your latest incoming and outgoing payments.',
|
||
},
|
||
contacts: {
|
||
name: 'Favorites',
|
||
description: 'Call or message your favorite contacts.',
|
||
choose: 'Favorite Contacts',
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
function getByPath(source: LocaleTree, path: string): unknown {
|
||
return path.split('.').reduce<unknown>((current, segment) => {
|
||
if (
|
||
typeof current !== 'object' ||
|
||
current === null ||
|
||
Array.isArray(current)
|
||
)
|
||
return undefined
|
||
return (current as LocaleTree)[segment]
|
||
}, source)
|
||
}
|
||
|
||
export const usePhoneStore = defineStore('phone', {
|
||
state: () => ({
|
||
cameraLandscape: false,
|
||
currentPage: 1,
|
||
device: null as PhoneDevice | null,
|
||
deviceRevisions: {} as Record<string, number>,
|
||
isOpen: false,
|
||
lang: 'en',
|
||
launchOrigin: null as AppLaunchOrigin | null,
|
||
locales: defaultLocales,
|
||
preferences: cloneJsonData(DEFAULT_PHONE_PREFERENCES),
|
||
security: {
|
||
enabled: false,
|
||
length: null,
|
||
lockedUntil: 0,
|
||
} as DeviceSecurity,
|
||
systemDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||
}),
|
||
getters: {
|
||
isDarkMode(state): boolean {
|
||
if (state.preferences.settings.appearanceMode === 'dark') return true
|
||
if (state.preferences.settings.appearanceMode === 'light') return false
|
||
return state.systemDarkMode
|
||
},
|
||
},
|
||
actions: {
|
||
close(): void {
|
||
this.cameraLandscape = false
|
||
this.isOpen = false
|
||
},
|
||
open(payload: PhoneOpenPayload = {}): void {
|
||
this.lang = payload.lang ?? 'en'
|
||
this.locales = payload.locales ?? defaultLocales
|
||
if (payload.device) this.hydrateDevice(payload.device)
|
||
this.security = payload.security ?? {
|
||
enabled: false,
|
||
length: null,
|
||
lockedUntil: 0,
|
||
}
|
||
this.isOpen = true
|
||
},
|
||
hydrateDevice(device: PhoneDevice): void {
|
||
this.device = device
|
||
this.deviceRevisions = Object.fromEntries(
|
||
Object.entries(device.data).map(([key, value]) => [
|
||
key,
|
||
value?.revision ?? 0,
|
||
]),
|
||
)
|
||
this.preferences = parsePhonePreferences(
|
||
JSON.stringify(device.data.settings?.payload ?? null),
|
||
)
|
||
},
|
||
saveDeviceNamespace(namespace: string, payload: unknown): void {
|
||
const previous = namespaceQueues.get(namespace) ?? Promise.resolve()
|
||
const queued = previous.then(async () => {
|
||
const response = await nuiCall<{ revision: number }>('device:save', {
|
||
namespace,
|
||
payload,
|
||
revision: this.deviceRevisions[namespace] ?? 0,
|
||
})
|
||
if (response.success && response.data) {
|
||
this.deviceRevisions[namespace] = response.data.revision
|
||
}
|
||
})
|
||
const tracked = queued.finally(() => {
|
||
if (namespaceQueues.get(namespace) === tracked)
|
||
namespaceQueues.delete(namespace)
|
||
})
|
||
namespaceQueues.set(namespace, tracked)
|
||
},
|
||
setCurrentPage(page: number, pageCount?: number): void {
|
||
this.currentPage = clampPage(page, pageCount)
|
||
},
|
||
setCameraLandscape(landscape: boolean): void {
|
||
this.cameraLandscape = landscape
|
||
},
|
||
setLaunchOrigin(origin: AppLaunchOrigin | null): void {
|
||
this.launchOrigin = origin
|
||
},
|
||
setAppNotification(
|
||
appId: LaunchablePhoneAppId,
|
||
key: keyof AppNotificationPreferences,
|
||
value: boolean,
|
||
): void {
|
||
this.preferences.settings.notifications[appId][key] = value
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
setPreference<K extends keyof PhonePreferencesV1['settings']>(
|
||
key: K,
|
||
value: PhonePreferencesV1['settings'][K],
|
||
): void {
|
||
this.preferences.settings[key] = value
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
setAlertVolumes(value: number): void {
|
||
const volume = Math.min(100, Math.max(0, Math.round(value)))
|
||
this.preferences.settings.notificationVolume = volume
|
||
this.preferences.settings.ringtoneVolume = volume
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
setSystemDarkMode(value: boolean): void {
|
||
this.systemDarkMode = value
|
||
},
|
||
setWallpaper(wallpaper: WallpaperId): void {
|
||
this.preferences.settings.wallpaper = wallpaper
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
async unlockWithPasscode(
|
||
passcode: string,
|
||
): Promise<NuiResponse<PasscodeResponseData>> {
|
||
const response = await nuiCall<PasscodeResponseData>('security:unlock', {
|
||
passcode,
|
||
})
|
||
if (response.success && response.data?.security) {
|
||
this.security = response.data.security
|
||
}
|
||
return response
|
||
},
|
||
async setPasscode(
|
||
passcode: string,
|
||
): Promise<NuiResponse<PasscodeResponseData>> {
|
||
const response = await nuiCall<PasscodeResponseData>(
|
||
'security:set-passcode',
|
||
{ passcode },
|
||
)
|
||
if (response.success && response.data?.security) {
|
||
this.security = response.data.security
|
||
}
|
||
return response
|
||
},
|
||
async changePasscode(
|
||
currentPasscode: string,
|
||
newPasscode: string,
|
||
): Promise<NuiResponse<PasscodeResponseData>> {
|
||
const response = await nuiCall<PasscodeResponseData>(
|
||
'security:change-passcode',
|
||
{ currentPasscode, newPasscode },
|
||
)
|
||
if (response.success && response.data?.security) {
|
||
this.security = response.data.security
|
||
}
|
||
return response
|
||
},
|
||
async disablePasscode(
|
||
passcode: string,
|
||
): Promise<NuiResponse<PasscodeResponseData>> {
|
||
const response = await nuiCall<PasscodeResponseData>(
|
||
'security:disable-passcode',
|
||
{ passcode },
|
||
)
|
||
if (response.success && response.data?.security) {
|
||
this.security = response.data.security
|
||
}
|
||
return response
|
||
},
|
||
t(path: string, replacements: Record<string, string> = {}): string {
|
||
const translated = getByPath(this.locales, path)
|
||
const fallback = getByPath(defaultLocales, path)
|
||
const value =
|
||
typeof translated === 'string'
|
||
? translated
|
||
: typeof fallback === 'string'
|
||
? fallback
|
||
: path
|
||
return Object.entries(replacements).reduce(
|
||
(result, [key, replacement]) =>
|
||
result.split(`{${key}}`).join(replacement),
|
||
value,
|
||
)
|
||
},
|
||
},
|
||
})
|