mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
197dce8767
# Conflicts: # sky_phone/source/client/focus.lua # sky_phone/source/server/sim.lua
6022 lines
224 KiB
TypeScript
6022 lines
224 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,
|
||
PHONE_SETUP_LAST_STEP,
|
||
clampPhoneScale,
|
||
ensureAppNotificationPreferences,
|
||
parsePhonePreferences,
|
||
type AppNotificationPreferences,
|
||
type PhonePreferencesV1,
|
||
type WallpaperId,
|
||
type WallpaperTarget,
|
||
} 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
|
||
fallbackLocales?: LocaleTree
|
||
lang?: string
|
||
locales?: LocaleTree
|
||
memos?: DeviceBootstrap['memos']
|
||
notes?: DeviceBootstrap['notes']
|
||
phoneNumberFormat?: DeviceBootstrap['phoneNumberFormat']
|
||
player?: DeviceBootstrap['player']
|
||
security?: DeviceSecurity
|
||
token?: string
|
||
}
|
||
|
||
const namespaceQueues = new Map<string, Promise<boolean>>()
|
||
let nextPersistenceSession = 0
|
||
|
||
const companiesFallbackLocales = {
|
||
name: 'Companies',
|
||
navigation: 'Companies navigation',
|
||
actions: 'Request actions',
|
||
request: 'Service Request',
|
||
verified: 'Verified company',
|
||
back: 'Back',
|
||
close: 'Close',
|
||
tryAgain: 'Try Again',
|
||
loadMore: 'Load More',
|
||
routeSet: 'GPS route set.',
|
||
tabs: {
|
||
directory: 'Discover',
|
||
requests: 'Requests',
|
||
work: 'Work',
|
||
},
|
||
availability: {
|
||
available: 'Available',
|
||
busy: 'Busy',
|
||
closed: 'Closed',
|
||
},
|
||
requestStatuses: {
|
||
new: 'New',
|
||
assigned: 'Assigned',
|
||
in_progress: 'In Progress',
|
||
waiting_customer: 'Waiting',
|
||
completed: 'Completed',
|
||
cancelled: 'Cancelled',
|
||
},
|
||
roles: {
|
||
employee: 'Employee',
|
||
manager: 'Manager',
|
||
},
|
||
categories: {
|
||
gastronomy: 'Food & Drink',
|
||
vehicles: 'Vehicles',
|
||
transport: 'Transport',
|
||
crafts: 'Trades',
|
||
retail: 'Retail',
|
||
real_estate: 'Real Estate',
|
||
media: 'Media',
|
||
nightlife: 'Nightlife',
|
||
public_services: 'Public Services',
|
||
emergency: 'Emergency Services',
|
||
medical: 'Medical',
|
||
mechanics: 'Mechanics',
|
||
government: 'Government',
|
||
},
|
||
days: {
|
||
monday: 'Monday',
|
||
tuesday: 'Tuesday',
|
||
wednesday: 'Wednesday',
|
||
thursday: 'Thursday',
|
||
friday: 'Friday',
|
||
saturday: 'Saturday',
|
||
sunday: 'Sunday',
|
||
},
|
||
directory: {
|
||
searchPlaceholder: 'Search companies or services',
|
||
categories: 'Company categories',
|
||
allCategories: 'All',
|
||
allCompanies: 'All Companies',
|
||
availableNow: 'Available now',
|
||
availableNowHint: 'Only show companies taking calls right now.',
|
||
availableSection: 'Available Now',
|
||
moreFilters: 'More filters',
|
||
hasLocation: 'Location',
|
||
acceptsRequests: 'Requests',
|
||
resetFilters: 'Reset Filters',
|
||
},
|
||
loading: {
|
||
directory: 'Loading companies...',
|
||
profile: 'Loading company profile...',
|
||
request: 'Loading request...',
|
||
requests: 'Loading your requests...',
|
||
work: 'Loading workspace...',
|
||
},
|
||
states: {
|
||
directoryError: 'Companies are unavailable',
|
||
requestsError: 'Requests are unavailable',
|
||
workError: 'Workspace unavailable',
|
||
profileError: 'Company unavailable',
|
||
requestError: 'Request unavailable',
|
||
noCompanies: 'No companies listed',
|
||
noCompaniesBody: 'Public companies will appear here when configured.',
|
||
noResults: 'No matching companies',
|
||
noResultsBody: 'Try another search or reset your filters.',
|
||
noOpenRequests: 'No open requests',
|
||
noClosedRequests: 'No completed requests',
|
||
noRequestsBody: 'Requests you create with a company will appear here.',
|
||
},
|
||
actionUnavailable: {
|
||
call: 'This company cannot be called right now.',
|
||
message: 'This service line does not accept text messages.',
|
||
route: 'This company has no public location.',
|
||
request: 'This company is not accepting service requests.',
|
||
},
|
||
profile: {
|
||
updated: 'Status updated {time}',
|
||
announcement: 'Latest Update',
|
||
location: 'Location',
|
||
noLocation: 'No public location',
|
||
hours: 'Opening Hours',
|
||
closed: 'Closed',
|
||
byAvailability: 'Open by availability',
|
||
services: 'Services',
|
||
noServices: 'No public services listed',
|
||
call: 'Call',
|
||
message: 'Message',
|
||
route: 'Route',
|
||
request: 'Request',
|
||
},
|
||
composer: {
|
||
title: 'New Service Request',
|
||
chooseService: 'Choose a Service',
|
||
subject: 'Subject',
|
||
subjectPlaceholder: 'What do you need?',
|
||
description: 'Details',
|
||
descriptionPlaceholder:
|
||
'Describe what happened and how the company can help.',
|
||
contact: 'Contact number',
|
||
registeredSim: 'Replies go to your registered SIM {number}.',
|
||
registeredSimRequired: 'A registered SIM is required to send a request.',
|
||
addPhotos: 'Add Photos ({count}/3)',
|
||
selectedPhoto: 'Selected request photo',
|
||
removePhoto: 'Remove photo',
|
||
review: 'Review Request',
|
||
confirmTitle: 'Send this request?',
|
||
confirmBody: 'Send your {service} request to {company}.',
|
||
edit: 'Keep Editing',
|
||
send: 'Send Request',
|
||
},
|
||
requests: {
|
||
open: 'Open',
|
||
closed: 'Completed',
|
||
findCompany: 'Find a Company',
|
||
generalService: 'General service',
|
||
attachments: 'Request photos',
|
||
attachedPhoto: 'Attached request photo',
|
||
timeline: 'Timeline',
|
||
conversation: 'Conversation',
|
||
noMessages: 'No replies yet',
|
||
replyPlaceholder: 'Write a reply',
|
||
sendReply: 'Send reply',
|
||
call: 'Call Company',
|
||
cancel: 'Cancel Request',
|
||
cancelTitle: 'Cancel this request?',
|
||
cancelBody: 'The company will be notified and can no longer complete it.',
|
||
keep: 'Keep Request',
|
||
cancelConfirm: 'Cancel Request',
|
||
},
|
||
timeline: {
|
||
created: 'Request created',
|
||
assigned: 'Request assigned',
|
||
cancelled: 'Request cancelled',
|
||
completed: 'Request completed',
|
||
statusChanged: 'Status changed to {status}',
|
||
},
|
||
time: {
|
||
justNow: 'just now',
|
||
minutesAgo: '{count} min ago',
|
||
hoursAgo: '{count} hr ago',
|
||
daysAgo: '{count} days ago',
|
||
},
|
||
work: {
|
||
notAuthorized: 'No company workspace',
|
||
notAuthorizedBody:
|
||
'Your current job is not connected to a configured company.',
|
||
workspace: 'Company Workspace',
|
||
publicAvailability: 'Public Availability',
|
||
takeCalls: 'Take company calls',
|
||
takeCallsBody: 'Route new service-line calls to this active SIM.',
|
||
dialServiceLine: 'Call from service line',
|
||
dialServiceLineBody: 'Make an outgoing call that displays {number}.',
|
||
dialServiceLineHint:
|
||
'The recipient will see {number} as the incoming caller.',
|
||
targetNumber: 'Phone number',
|
||
targetNumberHint: 'Enter a phone number',
|
||
callNow: 'Call Now',
|
||
overview: 'Today at a Glance',
|
||
metrics: {
|
||
new: 'New',
|
||
assigned: 'Mine',
|
||
waiting: 'Waiting',
|
||
completedToday: 'Done Today',
|
||
},
|
||
queue: 'Request Queue',
|
||
filters: {
|
||
new: 'New',
|
||
assigned: 'Mine',
|
||
in_progress: 'In Progress',
|
||
waiting_customer: 'Waiting',
|
||
completed: 'Done',
|
||
},
|
||
unassigned: 'Unassigned',
|
||
emptyQueue: 'Queue is clear',
|
||
emptyQueueBody: 'No requests match this filter.',
|
||
},
|
||
workActions: {
|
||
claim: 'Claim Request',
|
||
assign: 'Assign Employee',
|
||
callCustomer: 'Call Customer',
|
||
setStatus: 'Set status: {status}',
|
||
},
|
||
assignment: {
|
||
title: 'Assign Employee',
|
||
online: 'Online',
|
||
offline: 'Offline',
|
||
unknownMember: 'Unknown employee',
|
||
confirm: 'Assign Request',
|
||
},
|
||
assignmentLabels: {
|
||
you: 'Assigned to you',
|
||
assigned: 'Assigned to a colleague',
|
||
},
|
||
messageAuthors: {
|
||
you: 'You',
|
||
customer: 'Customer',
|
||
company: 'Company',
|
||
},
|
||
manager: {
|
||
title: 'Company Profile',
|
||
subtitle: 'Manage public information, services, and announcements.',
|
||
revision: 'Server revision {revision}',
|
||
availability: 'Availability',
|
||
profile: 'Public Profile',
|
||
description: 'Description',
|
||
descriptionPlaceholder: 'Describe the company and what it offers.',
|
||
phoneNumber: 'Service number',
|
||
phoneNumberManaged: 'Configured by the server and cannot be edited here.',
|
||
noPhoneNumber: 'No service number configured',
|
||
chooseCover: 'Choose cover photo',
|
||
chooseLogo: 'Choose logo',
|
||
coverPhoto: 'Company cover photo',
|
||
logoPhoto: 'Company logo',
|
||
locationLabel: 'Location name',
|
||
locationReady: 'A map position is attached to this profile.',
|
||
useCurrentLocation: 'Use Current Location',
|
||
address: 'Address',
|
||
district: 'District',
|
||
acceptRequests: 'Accept service requests',
|
||
acceptRequestsBody: 'Allow registered SIMs to open structured requests.',
|
||
saveProfile: 'Save Profile',
|
||
hours: 'Opening Hours',
|
||
dayOpen: 'Company is open on this day',
|
||
opensAt: 'Opens',
|
||
closesAt: 'Closes',
|
||
saveHours: 'Save Hours',
|
||
services: 'Services',
|
||
serviceTitle: 'Service name',
|
||
serviceDescription: 'Description',
|
||
priceText: 'Price text',
|
||
serviceActive: 'Publicly visible',
|
||
serviceRequests: 'Accept requests for this service',
|
||
removeService: 'Remove Service',
|
||
addService: 'Add Service',
|
||
saveServices: 'Save Services',
|
||
announcement: 'Current Announcement',
|
||
announcementText: 'Announcement',
|
||
announcementPlaceholder: 'Share a short, timely update.',
|
||
expiresAt: 'Expires at',
|
||
publish: 'Publish Announcement',
|
||
},
|
||
feedback: {
|
||
requestCreated: 'Request sent.',
|
||
requestCancelled: 'Request cancelled.',
|
||
requestClaimed: 'Request claimed.',
|
||
requestAssigned: 'Request assigned.',
|
||
statusUpdated: 'Request status updated.',
|
||
availabilityUpdated: 'Public availability updated.',
|
||
locationUpdated: 'Current location selected.',
|
||
callsEnabled: 'Company calls enabled.',
|
||
callsDisabled: 'Company calls disabled.',
|
||
profileSaved: 'Company profile saved.',
|
||
hoursSaved: 'Opening hours saved.',
|
||
servicesSaved: 'Services saved.',
|
||
announcementPublished: 'Announcement published.',
|
||
},
|
||
conflict: {
|
||
title: 'Newer changes are available',
|
||
body: 'Another manager updated this information. Reload before saving again.',
|
||
reload: 'Reload',
|
||
},
|
||
notifications: {
|
||
newRequest: 'New company request',
|
||
requestUpdated: 'A company request was updated.',
|
||
newMessage: 'New reply to your company request.',
|
||
assigned: 'A company request was assigned to you.',
|
||
},
|
||
errors: {
|
||
anonymous_sim: 'A registered SIM is required for service requests.',
|
||
call_unavailable: 'This service line is currently unavailable.',
|
||
company_not_found: 'This company is no longer available.',
|
||
invalid_profile: 'Check the company profile fields.',
|
||
invalid_request: 'Check the subject and request details.',
|
||
invalid_media: 'One or more attached photos are unavailable.',
|
||
invalid_expiration: 'Choose a valid announcement expiration.',
|
||
invalid_service: 'This service is no longer available.',
|
||
invalid_status: 'That status change is not allowed.',
|
||
messaging_unavailable: 'This company does not accept text messages.',
|
||
no_sim: 'Insert a SIM card to continue.',
|
||
not_authorized: 'You are not authorized for this company action.',
|
||
rate_limited: 'Please wait before trying again.',
|
||
request_not_found: 'This request is no longer available.',
|
||
revision_conflict:
|
||
'The data changed on another device. Reload and try again.',
|
||
service_unavailable: 'The Companies service is temporarily unavailable.',
|
||
too_many_open_requests: 'You already have too many open requests.',
|
||
request_failed: 'Companies could not complete the request.',
|
||
},
|
||
}
|
||
|
||
const healthFallbackLocales = {
|
||
name: 'Health',
|
||
navigation: 'Health navigation',
|
||
tabs: { today: 'Today', trends: 'Trends', medicalId: 'Medical ID' },
|
||
loading: 'Loading health data...',
|
||
errorTitle: 'Health is unavailable',
|
||
errorBody: 'Your activity data could not be loaded.',
|
||
tryAgain: 'Try Again',
|
||
goal: 'of {count}',
|
||
steps: 'Steps',
|
||
distance: 'Distance',
|
||
active: 'Active',
|
||
energy: 'Energy',
|
||
kilometers: '{count} km',
|
||
minutes: '{count} min',
|
||
kilocalories: '{count} kcal',
|
||
thisWeek: 'This week',
|
||
snapshot: 'Health snapshot',
|
||
trends: {
|
||
title: 'Trends',
|
||
week: 'Week',
|
||
month: 'Month',
|
||
total: '{count} steps',
|
||
more: '{count}% more than last week',
|
||
less: '{count}% less than last week',
|
||
same: 'Same as last week',
|
||
dailyAverage: 'Daily average',
|
||
activeTime: 'Active time',
|
||
dailyActivity: 'Daily activity',
|
||
goal: 'Goal',
|
||
},
|
||
medicalId: {
|
||
title: 'Medical ID',
|
||
edit: 'Edit',
|
||
done: 'Done',
|
||
resident: 'Los Santos resident',
|
||
emergencyInformation: 'Emergency information',
|
||
bloodType: 'Blood type',
|
||
allergies: 'Allergies',
|
||
conditions: 'Conditions',
|
||
medication: 'Medication',
|
||
noneRecorded: 'None recorded',
|
||
emergencyContact: 'Emergency contact',
|
||
contactName: 'Contact name',
|
||
relation: 'Relation',
|
||
phoneNumber: 'Phone number',
|
||
emergencyCall: 'Emergency call',
|
||
callContact: 'Call emergency contact',
|
||
saveFailed: 'Medical ID could not be saved.',
|
||
},
|
||
errors: {
|
||
invalid_phone_number: 'Enter a valid in-game phone number.',
|
||
invalid_request: 'Check the Medical ID fields.',
|
||
rate_limited: 'Please wait before trying again.',
|
||
request_failed: 'Health could not complete the request.',
|
||
},
|
||
}
|
||
|
||
const cryptoFallbackLocales = {
|
||
name: 'VaultX',
|
||
subtitle: 'Fictional asset exchange',
|
||
loading: 'Opening secure exchange…',
|
||
logout: 'Sign out',
|
||
logoutConfirm: {
|
||
title: 'Sign out of VaultX?',
|
||
body: 'You will need your VaultX password to unlock this profile again.',
|
||
cancel: 'Cancel',
|
||
confirm: 'Sign out',
|
||
signingOut: 'Signing out…',
|
||
},
|
||
refresh: 'Refresh portfolio',
|
||
navigation: 'VaultX navigation',
|
||
auth: {
|
||
eyebrow: 'Character-bound exchange',
|
||
loginTitle: 'Welcome back',
|
||
registerTitle: 'Create your VaultX profile',
|
||
body: 'Trade and transfer fictional digital assets through a protected, server-controlled account.',
|
||
network: 'VAULT NETWORK ONLINE',
|
||
serverSecured: 'Server secured',
|
||
personalKey: 'Personal crypto key',
|
||
characterBound: 'Character bound',
|
||
login: 'Log in',
|
||
register: 'Register',
|
||
accountEmail: 'Sky Cloud email',
|
||
accountMissing: 'No Sky Cloud account',
|
||
loginPanelTitle: 'Secure account access',
|
||
loginPanelBody: 'Your character identity is detected automatically.',
|
||
registerPanelTitle: 'Set up your private vault',
|
||
registerPanelBody: 'Choose a profile identity and secure access phrase.',
|
||
handle: 'VaultX handle',
|
||
handlePlaceholder: '3–20 letters or numbers',
|
||
password: 'Password',
|
||
passwordPlaceholder: '8–72 characters',
|
||
confirmPassword: 'Confirm password',
|
||
confirmPasswordPlaceholder: 'Enter the same password again',
|
||
showPassword: 'Show password',
|
||
hidePassword: 'Hide password',
|
||
security: 'Use an in-character password. Never reuse a real password.',
|
||
ruleLength: '8+ characters',
|
||
ruleMixed: 'Upper & lowercase',
|
||
ruleNumber: 'Contains a number',
|
||
ruleSpecial: 'Special character',
|
||
strength0: 'Start entering a secure password',
|
||
strength1: 'Weak protection',
|
||
strength2: 'Basic protection',
|
||
strength3: 'Strong protection',
|
||
strength4: 'Excellent protection',
|
||
acceptTitle: 'Create a fictional wallet',
|
||
acceptBody: 'I understand that VaultX assets have no real-world value.',
|
||
footer: 'Encrypted session · Rate-limited access · Memory-hard password',
|
||
loginAction: 'Unlock VaultX',
|
||
registerAction: 'Create secure profile',
|
||
},
|
||
tabs: {
|
||
portfolio: 'Portfolio',
|
||
markets: 'Markets',
|
||
activity: 'Activity',
|
||
profile: 'Profile',
|
||
},
|
||
portfolio: {
|
||
total: 'Total portfolio',
|
||
performance: 'Profit / loss',
|
||
return: 'Total return',
|
||
current: 'Current position',
|
||
history: 'Performance',
|
||
forecast: 'Forecast',
|
||
chartLabel: 'Profit and loss performance with dashed forecast',
|
||
cash: 'Available cash',
|
||
invested: 'Invested assets',
|
||
allocation: 'Portfolio allocation',
|
||
inMarket: 'in markets',
|
||
assets: 'assets',
|
||
holdings: 'Your assets',
|
||
empty: 'No assets yet',
|
||
emptyBody: 'Open Markets to request a protected quote.',
|
||
avg: 'Avg.',
|
||
insightTitle: 'Portfolio insight',
|
||
insightBody:
|
||
'Your holdings are valued against the latest synthetic server prices.',
|
||
},
|
||
quick: { trade: 'Trade', send: 'Send', more: 'More' },
|
||
actions: { deposit: 'Deposit', withdraw: 'Withdraw' },
|
||
markets: {
|
||
title: 'Fictional markets',
|
||
live: 'Server priced',
|
||
noticeTitle: 'In-game exchange',
|
||
notice:
|
||
'Prices are synthetic and have no real-world value. Every trade is checked against finite exchange liquidity.',
|
||
eyebrow: 'Market pulse',
|
||
movers: 'Top movers',
|
||
today: 'Today',
|
||
},
|
||
marketDetail: {
|
||
back: 'Back to markets',
|
||
today: 'Today',
|
||
description:
|
||
'The reference price is generated by the server. Executable prices use a protected short-lived quote.',
|
||
investment: 'Your investment',
|
||
totalValue: 'Current value',
|
||
statistics: 'Market statistics',
|
||
high24h: '24h high',
|
||
low24h: '24h low',
|
||
supply: 'Issued supply',
|
||
liquidity: 'Exchange liquidity',
|
||
},
|
||
trade: {
|
||
actions: 'Trade actions',
|
||
buy: 'Buy',
|
||
buyBody: 'Add',
|
||
sell: 'Sell',
|
||
sellBody: 'Reduce',
|
||
quantity: 'Quantity',
|
||
available: 'Available:',
|
||
estimated: 'Estimated value',
|
||
marketPrice: 'Market price',
|
||
protected: 'Protected server quote',
|
||
protectedBody:
|
||
'VaultX validates price, liquidity, balance and limits before execution.',
|
||
review: 'Order review',
|
||
price: 'Quote price',
|
||
gross: 'Gross',
|
||
fee: 'Exchange fee',
|
||
total: 'Total',
|
||
quoteExpiry:
|
||
'This server quote expires after 8 seconds and can only be used once.',
|
||
getQuote: 'Get protected quote',
|
||
confirm: 'Confirm trade',
|
||
},
|
||
settlement: {
|
||
depositBody: 'Move bank funds into your settled VaultX cash balance.',
|
||
withdrawBody:
|
||
"Return settled VaultX cash to your character's bank account.",
|
||
amount: 'Whole bank amount',
|
||
confirm: 'Confirm securely',
|
||
},
|
||
transfer: {
|
||
title: 'Send crypto',
|
||
subtitle: 'VaultX transfer',
|
||
keyTitle: 'Public receiving key',
|
||
keyBody:
|
||
'Verify the recipient before selecting an asset. Cash can never be sent here.',
|
||
walletKey: 'Recipient crypto key',
|
||
verify: 'Verify',
|
||
verifiedRecipient: 'Verified VaultX recipient',
|
||
asset: 'Choose asset',
|
||
quantity: 'Crypto amount',
|
||
available: 'Available balance',
|
||
password: 'Confirm with password',
|
||
protected: 'Atomic wallet transfer',
|
||
protectedBody:
|
||
'VaultX rechecks recipient, ownership, quantity and balance on the server, then books both wallets together.',
|
||
confirm: 'Send crypto securely',
|
||
},
|
||
activity: {
|
||
title: 'Financial activity',
|
||
empty: 'No activity',
|
||
emptyBody:
|
||
'Deposits, withdrawals, trades and crypto transfers will appear here.',
|
||
volume: 'Lifetime trading volume',
|
||
completedTrades: 'completed trades',
|
||
cash: 'Cash',
|
||
filters: { all: 'All', trades: 'Trades', wallet: 'Wallet' },
|
||
},
|
||
profile: {
|
||
verified: 'Character ownership verified',
|
||
walletKey: 'Public crypto key',
|
||
walletKeyBody:
|
||
'Share this key to receive crypto. It cannot access your account.',
|
||
copyKey: 'Copy',
|
||
copied: 'Copied',
|
||
shareKey: 'Share',
|
||
shareTitle: 'My VaultX crypto key',
|
||
shareText: 'Send crypto to my public VaultX key:',
|
||
trades: 'Trades',
|
||
volume: 'Volume',
|
||
memberSince: 'Member since',
|
||
preferences: 'Preferences',
|
||
priceAlerts: 'Price alerts',
|
||
priceAlertsBody: 'Notify me about notable market moves.',
|
||
confirmations: 'Trade confirmations',
|
||
confirmationsBody: 'Keep an extra confirmation before execution.',
|
||
hideBalances: 'Privacy mode',
|
||
hideBalancesBody: 'Hide balances across VaultX.',
|
||
identity: 'Profile identity',
|
||
edit: 'Edit',
|
||
account: 'Account & security',
|
||
editTitle: 'Edit profile',
|
||
editBody:
|
||
'Update your handle or set a new password. Your current password confirms identity changes.',
|
||
currentPassword: 'Current password',
|
||
currentPasswordPlaceholder: 'Required for handle or password changes',
|
||
newPassword: 'New password',
|
||
newPasswordPlaceholder: 'Leave blank to keep your current password',
|
||
passwordSecurity:
|
||
'Passwords use memory-hard hashing and are never shown again.',
|
||
saved: 'Profile saved securely.',
|
||
save: 'Save profile',
|
||
saveChanges: 'Save changes',
|
||
securityTitle: 'Protected profile',
|
||
securityBody:
|
||
'Your profile stays bound to this framework character and financial session.',
|
||
},
|
||
activityTypes: {
|
||
buy: 'Asset purchase',
|
||
sell: 'Asset sale',
|
||
deposit: 'Bank deposit',
|
||
withdrawal: 'Bank withdrawal',
|
||
transfer_in: 'Crypto received',
|
||
transfer_out: 'Crypto sent',
|
||
},
|
||
statuses: {
|
||
completed: 'Completed',
|
||
pending: 'Pending review',
|
||
failed: 'Failed',
|
||
manual_review: 'Manual review',
|
||
},
|
||
errors: {
|
||
invalid_handle: 'Use 3–20 letters, numbers, dots or underscores.',
|
||
handle_taken: 'That VaultX handle is already taken.',
|
||
profile_exists: 'This character already owns a VaultX profile.',
|
||
invalid_password:
|
||
'Use 8–72 characters with uppercase, lowercase, a number, and a special character.',
|
||
password_mismatch: 'The passwords do not match.',
|
||
accept_terms: 'Confirm that this is a fictional in-game wallet.',
|
||
invalid_credentials: 'The password is incorrect.',
|
||
locked: 'Too many attempts. Try again later.',
|
||
not_authenticated: 'Sign in to VaultX first.',
|
||
invalid_amount: 'Enter a positive whole amount.',
|
||
invalid_quantity: 'Enter a quantity with up to six decimals.',
|
||
invalid_wallet_key: 'Enter a complete VX receiving key.',
|
||
recipient_not_found: 'No active VaultX account uses this key.',
|
||
self_transfer: 'You cannot send crypto to your own key.',
|
||
invalid_transfer: 'Verify a recipient, asset and valid crypto amount.',
|
||
recipient_limit_exceeded: 'The recipient wallet cannot hold that amount.',
|
||
insufficient_funds: 'Your available balance is too low.',
|
||
insufficient_liquidity: 'The exchange cannot fill that amount right now.',
|
||
quote_expired: 'This quote expired. Request a new one.',
|
||
quote_unavailable: 'The quote is no longer available.',
|
||
market_unavailable: 'This market is temporarily paused.',
|
||
limit_exceeded: 'This request exceeds an exchange limit.',
|
||
duplicate_request: 'This request was already processed.',
|
||
rate_limited: 'Too many requests. Try again shortly.',
|
||
settlement_pending: 'A money transfer is already pending review.',
|
||
service_unavailable: 'VaultX is temporarily unavailable.',
|
||
request_failed: 'The secure exchange request failed.',
|
||
invalid_profile: 'Check your profile settings and handle.',
|
||
default: 'VaultX could not complete the request.',
|
||
},
|
||
}
|
||
|
||
const citywarnFallbackLocales = {
|
||
name: 'CityWarn',
|
||
navigation: 'CityWarn navigation',
|
||
loading: 'Loading warnings...',
|
||
tabs: {
|
||
active: 'Current',
|
||
map: 'Map',
|
||
archive: 'History',
|
||
settings: 'Settings',
|
||
},
|
||
hero: {
|
||
safe: 'No immediate danger',
|
||
safeBody: 'There are no active warnings for your location.',
|
||
active: '{count} active warnings',
|
||
activeBody: 'Follow the guidance from the responsible authorities.',
|
||
},
|
||
issuedBy: 'Issued by',
|
||
updated: 'Updated',
|
||
expires: 'Valid until',
|
||
affectedArea: 'Affected area',
|
||
currentLocation: 'Current location',
|
||
details: 'Warning details',
|
||
instructions: 'What you should do now',
|
||
timeline: 'Updates',
|
||
publishedBy: 'Published by {name}',
|
||
radius: '{count} m radius',
|
||
status: {
|
||
active: 'Active',
|
||
resolved: 'Resolved',
|
||
expired: 'Expired',
|
||
},
|
||
severity: {
|
||
information: 'Information',
|
||
warning: 'Warning',
|
||
danger: 'Danger',
|
||
extreme: 'Extreme danger',
|
||
},
|
||
categories: {
|
||
public_safety: 'Public safety',
|
||
police: 'Police incident',
|
||
fire: 'Fire & rescue',
|
||
medical: 'Health',
|
||
infrastructure: 'Infrastructure',
|
||
evacuation: 'Evacuation',
|
||
},
|
||
emptyArchive: 'No history yet',
|
||
emptyArchiveBody: 'Resolved and expired warnings will appear here.',
|
||
emptyFiltered: 'No matching warnings',
|
||
emptyFilteredBody: 'Your personal filters hide the current alerts.',
|
||
mapTitle: 'Warning areas',
|
||
mapBody: 'Highlighted areas show the approximate scope of active alerts.',
|
||
publisher: {
|
||
title: 'Authority tools',
|
||
body: 'You are on duty as {job} ({grade}) and may publish warnings.',
|
||
offDuty: 'You must be on duty to publish warnings.',
|
||
unavailable: 'Authority tools are not available for your current job.',
|
||
},
|
||
compose: {
|
||
new: 'New warning',
|
||
step: 'Step {step} of 4',
|
||
categoryTitle: 'Type of danger',
|
||
categoryBody: 'Choose the category that best matches the incident.',
|
||
severityTitle: 'Danger level',
|
||
areaTitle: 'Affected area',
|
||
areaBody: 'Define where this alert applies.',
|
||
areaTypes: {
|
||
radius: 'Radius',
|
||
district: 'District',
|
||
city: 'Whole city',
|
||
},
|
||
areaLabel: 'Area name',
|
||
areaPlaceholder: 'e.g. Mission Row',
|
||
radius: 'Radius in metres',
|
||
useLocation: 'Use current location',
|
||
locationSet: 'Location captured',
|
||
contentTitle: 'Warning content',
|
||
title: 'Short title',
|
||
titlePlaceholder: 'What happened?',
|
||
body: 'Description',
|
||
bodyPlaceholder: 'Describe the situation clearly and factually...',
|
||
instructions: 'Recommended action',
|
||
instructionsPlaceholder: 'What should residents do now?',
|
||
duration: 'Validity',
|
||
durationMinutes: '{count} minutes',
|
||
previewTitle: 'Review & send',
|
||
recipients: 'About {count} people are currently reachable.',
|
||
legal: 'Your name, job and every change are recorded permanently.',
|
||
back: 'Back',
|
||
next: 'Continue',
|
||
publish: 'Publish warning',
|
||
publishing: 'Publishing...',
|
||
success: 'The warning was published and distributed.',
|
||
},
|
||
manage: {
|
||
update: 'Send update',
|
||
resolve: 'Resolve warning',
|
||
updateTitle: 'Update situation',
|
||
resolveTitle: 'Resolve warning',
|
||
updatePlaceholder: 'What changed?',
|
||
resolvePlaceholder: 'Why has the warning ended?',
|
||
send: 'Publish update',
|
||
confirm: 'Resolve warning',
|
||
success: 'The warning was updated.',
|
||
resolved: 'The warning was resolved.',
|
||
},
|
||
settings: {
|
||
locationTitle: 'Location-based warnings',
|
||
locationBody:
|
||
'Shows alerts for radiuses and districts. City-wide alerts always remain visible.',
|
||
levelTitle: 'Minimum level',
|
||
levelBody: 'Alerts below this level are hidden from the current view.',
|
||
categoryTitle: 'Warning types',
|
||
categoryBody: 'Choose which types of alerts you want to see.',
|
||
notificationHint:
|
||
'Critical alerts may bypass focus modes. Manage sounds in the phone settings.',
|
||
},
|
||
notifications: {
|
||
published: 'New population warning',
|
||
update: 'A warning was updated',
|
||
resolved: 'A warning was resolved',
|
||
},
|
||
errors: {
|
||
feature_disabled: 'CityWarn is disabled on this server.',
|
||
not_authorized: 'You are not allowed to perform this action.',
|
||
invalid_warning: 'Check all warning details.',
|
||
invalid_update: 'Enter a valid update.',
|
||
active_limit: 'Too many warnings are already active.',
|
||
revision_conflict: 'The warning changed in the meantime. Reload it.',
|
||
rate_limited: 'Please wait before trying again.',
|
||
request_failed: 'CityWarn could not complete the request.',
|
||
not_found: 'This warning is no longer available.',
|
||
device_not_open: 'Open and unlock this phone before trying again.',
|
||
device_not_owned: 'This phone is no longer available.',
|
||
device_locked: 'Unlock this phone before trying again.',
|
||
default: 'CityWarn is temporarily unavailable.',
|
||
},
|
||
}
|
||
|
||
const adminPanelFallbackLocales = {
|
||
name: 'Phone Admin',
|
||
subtitle: 'Administration',
|
||
navigation: 'Admin navigation',
|
||
refresh: 'Refresh admin data',
|
||
loading: 'Loading protected data...',
|
||
tabs: {
|
||
overview: 'Overview',
|
||
players: 'Players',
|
||
devices: 'Devices',
|
||
apps: 'Apps',
|
||
accounts: 'Accounts',
|
||
messages: 'Messages',
|
||
calls: 'Calls',
|
||
moderation: 'Moderation',
|
||
audit: 'Audit',
|
||
configurator: 'Phone configurator',
|
||
},
|
||
overview: {
|
||
eyebrow: 'Server',
|
||
title: 'Dashboard',
|
||
body: 'Players, devices, apps, and phone data.',
|
||
stats: 'Server phone statistics',
|
||
online: 'Online',
|
||
devices: 'Devices',
|
||
accounts: 'Accounts',
|
||
audit: 'Audit entries',
|
||
control: 'Navigation',
|
||
features: 'Modules',
|
||
featuresBody: 'Open an administration module.',
|
||
recent: 'Recent activity',
|
||
playerFeature: 'Identity, finances, job, and duty',
|
||
deviceFeature: 'IMEI, SIM, number, and activity',
|
||
appFeature: 'Install or remove phone apps',
|
||
accountFeature: 'Account access and protected credentials',
|
||
messageFeature: 'Review recent SMS activity',
|
||
callFeature: 'Review recent call activity',
|
||
moderationFeature: 'Reset access, number, or device data',
|
||
auditFeature: 'Review sensitive admin actions',
|
||
configuratorFeature: 'Manage config.lua and media.lua through SQL',
|
||
},
|
||
appearance: {
|
||
eyebrow: 'Appearance',
|
||
title: 'Interface',
|
||
body: 'Personalize colors and typography across the admin workspace.',
|
||
colors: {
|
||
emerald: 'Emerald',
|
||
blue: 'Blue',
|
||
violet: 'Violet',
|
||
orange: 'Orange',
|
||
red: 'Red',
|
||
},
|
||
controls: {
|
||
customColor: 'Custom color',
|
||
customColorBody: 'Choose any RGB accent or enter its channel values.',
|
||
red: 'R',
|
||
green: 'G',
|
||
blue: 'B',
|
||
hex: 'HEX color',
|
||
value: 'value',
|
||
fontFamily: 'Font family',
|
||
fontFamilyBody: 'Choose the typeface used by the complete admin panel.',
|
||
fontSize: 'Font size',
|
||
fontSizeBody: 'Scale text and controls for comfortable readability.',
|
||
fonts: {
|
||
inter: 'Inter',
|
||
system: 'System',
|
||
classic: 'Classic',
|
||
verdana: 'Verdana',
|
||
tahoma: 'Tahoma',
|
||
trebuchet: 'Trebuchet',
|
||
georgia: 'Georgia',
|
||
mono: 'Monospace',
|
||
},
|
||
},
|
||
},
|
||
configurator: {
|
||
context: 'Runtime configuration',
|
||
eyebrow: 'System tool',
|
||
sections: 'Configuration',
|
||
search: 'Search settings or paths',
|
||
configScope: 'config.lua',
|
||
mediaScope: 'media.lua',
|
||
noResults: 'No matching settings',
|
||
loading: 'Loading SQL configuration...',
|
||
title: 'Phone configurator',
|
||
body: 'Manage phone and media settings from the protected admin workspace.',
|
||
disabledTitle: 'SQL configuration is not active',
|
||
disabledBody:
|
||
'Enable the configurator at the beginning of config.lua and restart sky_phone. Until then, file values remain active and editing is locked.',
|
||
manualSave: 'Manual save',
|
||
refreshNotice:
|
||
'Nothing is written automatically. The green check verifies config.lua and media.lua in SQL and refreshes the active server, client, media and UI configuration immediately.',
|
||
fieldCount: '{count} fields',
|
||
secretConfigured: 'Secret configured · enter a replacement',
|
||
invalidValue: 'Check the highlighted table or number value.',
|
||
saved: 'SQL configuration saved and applied.',
|
||
descriptions: {
|
||
featureToggle: 'Turns {name} on or off.',
|
||
boolean: 'Controls whether {name} is allowed.',
|
||
number: 'Sets the numeric value for {name}.',
|
||
text: 'Sets the text value used for {name}.',
|
||
optionalText:
|
||
'Sets the optional value for {name}; switch it off to disable it.',
|
||
list: 'Manages all entries used for {name}.',
|
||
table: 'Groups the related settings for {name}.',
|
||
credential: 'Stores the protected credential used by {name}.',
|
||
url: 'Sets the URL or endpoint used by {name}.',
|
||
hosts: 'Defines which domains are allowed for {name}.',
|
||
milliseconds: 'Sets the timing for {name} in milliseconds.',
|
||
seconds: 'Sets the timing for {name} in seconds.',
|
||
rateLimit: 'Limits how many {name} actions are allowed per minute.',
|
||
byteLimit: 'Sets the maximum data size allowed for {name}.',
|
||
textLimit: 'Sets the maximum text length allowed for {name}.',
|
||
distance: 'Sets the world distance used for {name}.',
|
||
coordinates: 'Sets the world coordinates or orientation for {name}.',
|
||
gameAsset: 'Sets the GTA model or prop used for {name}.',
|
||
animation: 'Sets the animation asset used for {name}.',
|
||
access: 'Defines the jobs, groups or permission level for {name}.',
|
||
integration: 'Selects the connected framework or provider for {name}.',
|
||
path: 'Sets the storage or resource path used for {name}.',
|
||
color: 'Sets the interface color used for {name}.',
|
||
displayText: 'Sets the text shown to players for {name}.',
|
||
phoneNumber: 'Sets the phone or service number used for {name}.',
|
||
routing: 'Controls how incoming requests are routed for {name}.',
|
||
command: 'Sets the chat command used to open or run {name}.',
|
||
locale: 'Selects the language used for {name}.',
|
||
debug: 'Controls detailed diagnostic output for {name}.',
|
||
mediaQuality: 'Sets the media quality or volume used for {name}.',
|
||
amount: 'Sets the maximum or displayed amount for {name}.',
|
||
},
|
||
table: {
|
||
list: 'List',
|
||
table: 'Key table',
|
||
vector: 'Vector',
|
||
entry: 'Entry',
|
||
general: 'General',
|
||
subtabs: {
|
||
AdminGroups: 'Admin Groups',
|
||
Dictionaries: 'Dictionaries',
|
||
Clips: 'Clips',
|
||
Transforms: 'Transforms',
|
||
RateLimits: 'Rate Limits',
|
||
Publishers: 'Publishers',
|
||
ExternalPingResources: 'External Ping Resources',
|
||
Markets: 'Markets',
|
||
TrustedAdapters: 'Trusted Adapters',
|
||
AllowedDisappearTimers: 'Allowed Disappear Timers',
|
||
MusicTracks: 'Music Tracks',
|
||
VehicleImages: 'Vehicle Images',
|
||
Custom: 'Custom',
|
||
Valet: 'Valet',
|
||
AutoPriority: 'Automatic Priority',
|
||
Camera: 'Camera',
|
||
Categories: 'Categories',
|
||
Districts: 'Districts',
|
||
PhotoGradients: 'Photo Gradients',
|
||
LbPhone: 'LB Phone',
|
||
Tracks: 'Tracks',
|
||
Props: 'Props',
|
||
CustomLocations: 'Custom Locations',
|
||
Animation: 'Animation',
|
||
ReportReasons: 'Report Reasons',
|
||
DisplayName: 'Display Name',
|
||
Hud: 'HUD',
|
||
Badge: 'Badge',
|
||
LockedChannels: 'Locked Channels',
|
||
NumberGroups: 'Number Groups',
|
||
CustomFare: 'Custom Fare',
|
||
DriverJobs: 'Driver Jobs',
|
||
Services: 'Services',
|
||
QuickLocations: 'Quick Locations',
|
||
AllowedJobs: 'Allowed Jobs',
|
||
Websites: 'Websites',
|
||
},
|
||
addRow: 'Add row',
|
||
addField: 'Add field',
|
||
remove: 'Remove',
|
||
emptyList: 'No rows yet. Add the first row with plus.',
|
||
emptyTable: 'No fields yet. Add the first key below.',
|
||
keyPlaceholder: 'New key',
|
||
convertToList: 'Use list',
|
||
convertToMap: 'Use typed key table',
|
||
convertToTable: 'Use key table',
|
||
types: {
|
||
string: 'Text',
|
||
number: 'Number',
|
||
boolean: 'Switch',
|
||
list: 'List',
|
||
table: 'Table',
|
||
},
|
||
},
|
||
},
|
||
players: {
|
||
eyebrow: 'Active sessions',
|
||
title: 'Online players',
|
||
online: 'Online now',
|
||
empty: 'No players found',
|
||
emptyBody: 'Adjust the search or refresh the live player list.',
|
||
},
|
||
search: {
|
||
players: 'Search name, ID, job, or number',
|
||
apps: 'Search apps',
|
||
clear: 'Clear search',
|
||
},
|
||
detail: {
|
||
character: 'Character profile',
|
||
data: 'Player data overview',
|
||
cash: 'Cash',
|
||
bank: 'Bank',
|
||
job: 'Job',
|
||
duty: 'Duty',
|
||
onDuty: 'On duty',
|
||
offDuty: 'Off duty',
|
||
identity: 'Identity',
|
||
playerData: 'Player data',
|
||
identifier: 'Character identifier',
|
||
birthdate: 'Birthdate',
|
||
grade: 'Job grade',
|
||
unknown: 'Unknown',
|
||
},
|
||
devices: {
|
||
eyebrow: 'Device control',
|
||
title: 'Phones',
|
||
body: 'Inspect every phone assigned to the selected player.',
|
||
choose: 'Choose phone',
|
||
empty: 'No phone found',
|
||
emptyBody: 'This player currently has no phone device that can be managed.',
|
||
noNumber: 'No phone number',
|
||
noSim: 'No SIM',
|
||
imei: 'IMEI',
|
||
updated: 'Last activity',
|
||
apps: 'Claimed apps',
|
||
account: 'Linked account',
|
||
},
|
||
credentials: {
|
||
eyebrow: 'Protected data',
|
||
title: 'Credentials',
|
||
email: 'iFruit email',
|
||
password: 'iFruit password',
|
||
reveal: 'Reveal',
|
||
copy: 'Copy password',
|
||
copied: 'Password copied.',
|
||
noAccount: 'No iFruit account is linked to this phone.',
|
||
passcode: 'Device passcode',
|
||
passcodeHashed: '{length}-digit PIN · securely hashed and not recoverable',
|
||
passcodeDisabled: 'No passcode configured',
|
||
revealTitle: 'Reveal protected password?',
|
||
revealBody:
|
||
'This action is server-authorized, rate-limited, and written to the admin audit log.',
|
||
cancel: 'Cancel',
|
||
confirmReveal: 'Reveal password',
|
||
},
|
||
apps: {
|
||
eyebrow: 'Remote management',
|
||
title: 'App access',
|
||
description:
|
||
'Stage app access for this device. Nothing changes until you save.',
|
||
installed: 'Installed',
|
||
available: 'Available',
|
||
protected: 'System app',
|
||
changes: '{count} pending changes',
|
||
},
|
||
activity: {
|
||
protected: 'Protected activity',
|
||
messagesTitle: 'Messages',
|
||
messagesBody: 'Recent SMS activity for the selected SIM.',
|
||
callsTitle: 'Calls',
|
||
callsBody: 'Recent call activity for the selected SIM.',
|
||
loading: 'Loading activity...',
|
||
incoming: 'Incoming',
|
||
outgoing: 'Outgoing',
|
||
mediaMessage: '{type} message',
|
||
noMessages: 'No message activity found.',
|
||
noCalls: 'No call activity found.',
|
||
status: {
|
||
completed: 'Completed',
|
||
missed: 'Missed',
|
||
rejected: 'Rejected',
|
||
busy: 'Busy',
|
||
unanswered: 'Unanswered',
|
||
cancelled: 'Cancelled',
|
||
failed: 'Failed',
|
||
ringing: 'Ringing',
|
||
},
|
||
},
|
||
moderation: {
|
||
eyebrow: 'Device administration',
|
||
title: 'Moderation actions',
|
||
body: 'Every action is server-authorized, rate-limited, and audited.',
|
||
resetPasscode: 'Reset passcode',
|
||
resetPasscodeBody: 'Remove the device PIN and clear failed attempts.',
|
||
changeNumber: 'Change number',
|
||
changeNumberBody: 'Assign a new unique number to the current SIM.',
|
||
factoryReset: 'Factory reset',
|
||
factoryResetBody: 'Clear local device data and disconnect the account.',
|
||
saveFirst: 'Save or discard pending app changes first.',
|
||
phoneNumber: 'New phone number',
|
||
phoneNumberPlaceholder: 'Enter the full configured number',
|
||
typeToConfirm: 'Type {word} to confirm the factory reset.',
|
||
confirmWord: 'RESET',
|
||
cancel: 'Cancel',
|
||
'reset-passcodeSuccess': 'Passcode reset.',
|
||
'change-numberSuccess': 'Phone number changed.',
|
||
'factory-resetSuccess': 'Phone factory reset completed.',
|
||
dialogs: {
|
||
'reset-passcodeTitle': 'Reset device passcode?',
|
||
'reset-passcodeBody':
|
||
'The player can unlock this phone without the previous PIN afterward.',
|
||
'change-numberTitle': 'Change phone number?',
|
||
'change-numberBody':
|
||
'The new number must match the configured server number format and be unique.',
|
||
'factory-resetTitle': 'Factory reset this phone?',
|
||
'factory-resetBody':
|
||
'This clears local device data, app settings, security, and the linked account. This cannot be undone.',
|
||
},
|
||
confirm: {
|
||
'reset-passcode': 'Reset passcode',
|
||
'change-number': 'Change number',
|
||
'factory-reset': 'Factory reset',
|
||
},
|
||
},
|
||
editor: {
|
||
brand: 'SKY PHONE',
|
||
workspace: 'ADMIN',
|
||
players: 'Player directory',
|
||
audit: 'Audit log',
|
||
selectPlayer:
|
||
'Select a player to inspect identity, devices, credentials, and app access.',
|
||
save: 'Save changes',
|
||
saveHint: 'Apply pending changes',
|
||
saved: 'Changes saved.',
|
||
unsaved: 'Unsaved changes',
|
||
close: 'Close admin panel',
|
||
refresh: 'Refresh live data',
|
||
online: 'LIVE',
|
||
profile: 'PROFILE',
|
||
financial: 'FINANCIAL',
|
||
device: 'DEVICE',
|
||
security: 'SECURITY',
|
||
noAutoSave: 'Manual save',
|
||
noAutoSaveBody: 'Changes stay local until the green check is pressed.',
|
||
discardTitle: 'Discard unsaved changes?',
|
||
discardBody:
|
||
'Your staged app or configuration changes have not been saved.',
|
||
keepEditing: 'Keep editing',
|
||
discard: 'Discard changes',
|
||
saveFailed: 'Some changes could not be saved.',
|
||
noSelection: 'No player selected',
|
||
},
|
||
audit: {
|
||
eyebrow: 'Accountability',
|
||
title: 'Audit trail',
|
||
body: 'Sensitive reveals and remote app changes are recorded here.',
|
||
empty: 'No admin actions yet',
|
||
emptyBody: 'Protected actions will appear here after they are performed.',
|
||
by: '{actor} · target ID {target}',
|
||
actions: {
|
||
grant_app: 'App installed',
|
||
revoke_app: 'App removed',
|
||
reveal_account_password: 'Password revealed',
|
||
view_messages: 'Messages viewed',
|
||
view_calls: 'Calls viewed',
|
||
reset_passcode: 'Passcode reset',
|
||
change_number: 'Phone number changed',
|
||
factory_reset: 'Phone factory reset',
|
||
save_configuration: 'Configuration saved',
|
||
},
|
||
},
|
||
errors: {
|
||
not_authorized: 'You do not have access to the admin panel.',
|
||
rate_limited: 'Too many admin requests. Please wait.',
|
||
player_unavailable: 'That player is no longer online.',
|
||
device_not_owned: 'That phone no longer belongs to the selected player.',
|
||
invalid_app: 'That app is not registered on the server.',
|
||
app_protected: 'This system app cannot be removed.',
|
||
revision_conflict:
|
||
'The data changed in the meantime. Reopen the section and try again.',
|
||
configurator_disabled: 'Enable the phone configurator in config.lua first.',
|
||
invalid_field: 'That configuration field is no longer available.',
|
||
invalid_value: 'A configuration value is invalid.',
|
||
account_not_found: 'No iFruit account is linked to this phone.',
|
||
invalid_phone_number:
|
||
'Enter a phone number in the configured server format.',
|
||
phone_number_unchanged: 'This SIM already uses that phone number.',
|
||
phone_number_taken: 'That phone number is already assigned.',
|
||
no_sim: 'This phone has no SIM that can be changed.',
|
||
passcode_not_set: 'This phone has no passcode configured.',
|
||
device_not_found: 'This phone no longer exists.',
|
||
metadata_unsupported: 'The phone inventory metadata could not be updated.',
|
||
invalid_request: 'The admin request was invalid.',
|
||
request_failed: 'The admin request failed.',
|
||
default: 'The admin panel is temporarily unavailable.',
|
||
},
|
||
}
|
||
|
||
const defaultLocales: LocaleTree = {
|
||
AdminPanel: adminPanelFallbackLocales,
|
||
Apps: {
|
||
citywarn: citywarnFallbackLocales,
|
||
crypto: cryptoFallbackLocales,
|
||
easyShare: {
|
||
name: 'EasyShare',
|
||
incoming: 'Incoming Share',
|
||
recentChats: 'Contacts and Chats',
|
||
destinations: 'Share destinations',
|
||
newMessage: 'New Message',
|
||
shareProfile: 'Share Profile',
|
||
share: 'Share',
|
||
kinds: {
|
||
contact: 'Contact',
|
||
document: 'Document',
|
||
link: 'Link',
|
||
location: 'Location',
|
||
media: 'Media',
|
||
note: 'Note',
|
||
photo: 'Photo',
|
||
playlist: 'Playlist',
|
||
post: 'Post',
|
||
profile: 'Profile',
|
||
text: 'Message',
|
||
track: 'Track',
|
||
video: 'Video',
|
||
},
|
||
chooseConversation: 'Choose a conversation',
|
||
sentToChat: 'Sent to chat.',
|
||
savedToNotes: 'Saved to Notes.',
|
||
copy: 'Copy',
|
||
copyLink: 'Copy Link',
|
||
nearby: 'People Nearby',
|
||
history: 'Transfer History',
|
||
noHistory: 'No transfers yet.',
|
||
noNearby: 'No visible players are nearby.',
|
||
visibility: 'Visibility',
|
||
requestSent: 'Waiting for acceptance...',
|
||
incomingFrom: '{name} wants to share',
|
||
distance: '{distance} m away',
|
||
cancel: 'Cancel',
|
||
visibilityOptions: {
|
||
everyone: 'Everyone',
|
||
contacts: 'Contacts Only',
|
||
hidden: 'Receiving Off',
|
||
},
|
||
status: {
|
||
pending: 'Waiting for acceptance',
|
||
transferring: 'Transferring',
|
||
completed: 'Completed',
|
||
declined: 'Declined',
|
||
cancelled: 'Cancelled',
|
||
expired: 'Expired',
|
||
failed: 'Failed',
|
||
accepted: 'Accepted',
|
||
},
|
||
errors: {
|
||
disabled: 'EasyShare is disabled.',
|
||
invalid_payload: 'This item cannot be shared.',
|
||
invalid_target: 'Choose a valid recipient.',
|
||
target_unavailable: 'That player is no longer available.',
|
||
transfer_not_found: 'That transfer is no longer available.',
|
||
too_far: 'The recipient is too far away.',
|
||
not_owned: 'This item no longer belongs to this phone.',
|
||
unsupported_payload: 'This item is not available for EasyShare.',
|
||
payload_too_large: 'This item is too large to share.',
|
||
location_unavailable: 'Your location is unavailable.',
|
||
rate_limited: 'Too many share requests. Try again shortly.',
|
||
request_failed: 'EasyShare is temporarily unavailable.',
|
||
},
|
||
},
|
||
weazelNews: {
|
||
name: 'Weazel News',
|
||
brand: 'WEAZEL NEWS',
|
||
navigation: 'Weazel News navigation',
|
||
tabs: {
|
||
home: 'Home',
|
||
categories: 'Categories',
|
||
search: 'Search',
|
||
editorial: 'Editorial',
|
||
},
|
||
back: 'Back',
|
||
retry: 'Try Again',
|
||
loadMore: 'Load More',
|
||
home: {
|
||
eyebrow: 'Los Santos, live',
|
||
latest: 'Latest News',
|
||
},
|
||
categories: {
|
||
title: 'Categories',
|
||
subtitle: 'Browse every story from the Weazel News desk.',
|
||
all: 'All Stories',
|
||
official: 'Official Notices',
|
||
events: 'Events & Leisure',
|
||
jobs: 'Jobs',
|
||
news: 'News & Events',
|
||
business: 'Business',
|
||
},
|
||
search: {
|
||
placeholder: 'Search articles...',
|
||
title: 'Search Weazel News',
|
||
emptyTitle: 'No Articles Found',
|
||
emptyBody: 'Try another headline, topic, or category.',
|
||
},
|
||
states: {
|
||
loading: 'Loading Weazel News...',
|
||
emptyTitle: 'No News Yet',
|
||
emptyBody:
|
||
'The Weazel News desk has not published any articles here yet.',
|
||
errorTitle: 'Weazel News Is Unavailable',
|
||
readOnlyTitle: 'Read-only Access',
|
||
readOnlyBody:
|
||
'Your current job can read Weazel News, but cannot manage articles.',
|
||
noManagedTitle: 'No Editorial Articles',
|
||
noManagedBody:
|
||
'Create an article or change the current editorial filter.',
|
||
},
|
||
article: {
|
||
readMore: 'Read Article',
|
||
byline: 'By {author}',
|
||
updated: 'Updated {date}',
|
||
published: 'Published {date}',
|
||
coverAlt: 'Cover image for {title}',
|
||
},
|
||
editorial: {
|
||
title: 'Editorial Desk',
|
||
subtitle: 'Create, publish, and maintain Weazel News articles.',
|
||
newArticle: 'New Article',
|
||
published: 'Published',
|
||
drafts: 'Drafts',
|
||
all: 'All',
|
||
jobAccess: 'Signed in as {job}',
|
||
edit: 'Edit Article',
|
||
delete: 'Delete Article',
|
||
},
|
||
composer: {
|
||
newTitle: 'New Article',
|
||
editTitle: 'Edit Article',
|
||
title: 'Headline',
|
||
titlePlaceholder: 'Write a clear headline',
|
||
body: 'Article',
|
||
bodyPlaceholder: 'Write the full story...',
|
||
category: 'Category',
|
||
status: 'Publication Status',
|
||
cover: 'Cover Image',
|
||
photos: 'Article Photos',
|
||
photoCount: '{count} of {maximum} photos',
|
||
addPhotos: 'Add Photos',
|
||
choosePhotos: 'Choose from Photos',
|
||
takePhoto: 'Take Photo',
|
||
photoSourceHint:
|
||
'Choose several photos from your library or take a new one.',
|
||
primaryPhoto: 'Cover',
|
||
makePrimary: 'Use as Cover',
|
||
chooseCover: 'Choose from Photos',
|
||
changeCover: 'Change Cover',
|
||
removeCover: 'Remove Cover',
|
||
coverAlt: 'Selected article cover',
|
||
publish: 'Publish Article',
|
||
saveDraft: 'Save Draft',
|
||
saveChanges: 'Save Changes',
|
||
statusPublished: 'Published',
|
||
statusDraft: 'Draft',
|
||
},
|
||
delete: {
|
||
title: 'Delete Article?',
|
||
body: 'This article will be removed from Weazel News.',
|
||
cancel: 'Cancel',
|
||
confirm: 'Delete',
|
||
},
|
||
feedback: {
|
||
created: 'Article created.',
|
||
updated: 'Article updated.',
|
||
deleted: 'Article deleted.',
|
||
},
|
||
accessibility: {
|
||
articleList: 'Weazel News articles',
|
||
categoryList: 'News categories',
|
||
editorialList: 'Editorial articles',
|
||
openArticle: 'Open {title}',
|
||
openCategory: 'Open {category}',
|
||
openEditorial: 'Open the editorial desk',
|
||
newArticle: 'Create a new article',
|
||
editArticle: 'Edit {title}',
|
||
deleteArticle: 'Delete {title}',
|
||
search: 'Search Weazel News',
|
||
clearSearch: 'Clear article search',
|
||
articleImages: 'Article photos',
|
||
previousPhoto: 'Show the previous article photo',
|
||
nextPhoto: 'Show the next article photo',
|
||
showPhoto: 'Show article photo {current} of {total}',
|
||
coverPreview: 'Article cover preview',
|
||
removeCover: 'Remove the selected cover image',
|
||
removePhoto: 'Remove this article photo',
|
||
status: 'Article status: {status}',
|
||
},
|
||
errors: {
|
||
feature_disabled: 'Weazel News is currently disabled.',
|
||
invalid_article: 'Enter a valid headline, article, and category.',
|
||
invalid_draft:
|
||
'Enter a headline and article text before saving the draft.',
|
||
invalid_publish: 'Enter a headline and article text before publishing.',
|
||
invalid_request: 'Check the article details and try again.',
|
||
invalid_image: 'Choose a valid photo from this phone.',
|
||
invalid_attachment: 'Choose a valid photo from this phone.',
|
||
not_authorized: 'Your current job cannot manage Weazel News articles.',
|
||
article_not_found: 'This article is no longer available.',
|
||
not_found: 'This article is no longer available.',
|
||
revision_conflict:
|
||
'This article changed on another device. Reload it and try again.',
|
||
rate_limited: 'Too many requests. Try again shortly.',
|
||
request_failed: 'Weazel News could not complete the request.',
|
||
default: 'Weazel News is temporarily unavailable.',
|
||
},
|
||
},
|
||
companies: companiesFallbackLocales,
|
||
health: healthFallbackLocales,
|
||
crewlink: {
|
||
name: 'CrewLink',
|
||
connecting: 'Connecting your crew...',
|
||
privateNetwork: 'Private location network',
|
||
signInTitle: 'Connect your Sky Cloud account',
|
||
signInBody:
|
||
'CrewLink uses your private Sky Cloud identity to keep groups and roles available across your phones.',
|
||
openSettings: 'Open Sky Cloud settings',
|
||
authEyebrow: 'Private crew network',
|
||
authTitle: 'Welcome to CrewLink',
|
||
authBody:
|
||
'Your iFruit email is linked automatically. Enter your password to continue.',
|
||
login: 'Log in',
|
||
register: 'Register',
|
||
ifruitEmail: 'iFruit address',
|
||
password: 'Password',
|
||
passwordPlaceholder: '8–72 characters',
|
||
gallery: 'Photos',
|
||
camera: 'Camera',
|
||
backToLogin: 'Back to CrewLink login',
|
||
authErrors: {
|
||
no_ifruit_account:
|
||
'Sign in to your Sky Cloud account in Settings first.',
|
||
invalid_username: 'Use 3–20 letters, numbers, dots, or underscores.',
|
||
invalid_password: 'Password must be 8–72 characters.',
|
||
invalid_credentials: 'The password for this iFruit email is incorrect.',
|
||
profile_not_found: 'No CrewLink profile exists for this iFruit email.',
|
||
profile_exists: 'This iFruit email already has a CrewLink profile.',
|
||
username_taken: 'That CrewLink username is already taken.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
rate_limited: 'Too many attempts. Try again shortly.',
|
||
request_failed: 'CrewLink could not complete the request.',
|
||
},
|
||
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',
|
||
shareInvite: 'Invite by message',
|
||
shareInviteTitle: 'Join {group} on CrewLink',
|
||
shareInviteBody: 'Use invitation code {code} to join {group}.',
|
||
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: 'Choose a point on the map, then add a label and ping type.',
|
||
pingLabel: 'Ping label',
|
||
pingLabelPlaceholder: 'e.g. Meet at the garage',
|
||
placeOnMap: 'Place ping on map',
|
||
placeOnMapBody: 'Move the map until the crosshair marks the exact spot.',
|
||
positionSelected: 'Position selected · tap to change',
|
||
addHere: 'Place Here',
|
||
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.',
|
||
editProfile: 'Edit CrewLink Profile',
|
||
editProfileBody: 'Change your username and profile photo.',
|
||
removeProfilePhoto: 'Remove profile photo',
|
||
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 Sky Cloud 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.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
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 Sky Cloud 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:
|
||
'Add one to six photos from Photos or Camera. Your first photo is shown first.',
|
||
profilePhotoRequired: 'Add at least one profile photo to continue.',
|
||
addPhotos: 'Add photos',
|
||
choosePhotos: 'Choose from Photos',
|
||
primaryPhoto: 'Main',
|
||
removePhoto: 'Remove photo {number}',
|
||
profilePhotoFallback: 'Flare profile avatar',
|
||
profilePhotoNumber: 'Profile photo {number} of {count}',
|
||
viewProfilePhoto: 'Show profile photo {number} of {count}',
|
||
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',
|
||
editRelationshipGoal: 'Edit relationship goal',
|
||
profileGoalBody: 'Shown on your discovery card.',
|
||
accountActions: 'Account',
|
||
signOut: 'Sign Out',
|
||
signOutTitle: 'Sign out of Sky Cloud?',
|
||
signOutBody:
|
||
'This signs the whole phone out of Sky Cloud. Your Flare profile, matches and chats stay stored.',
|
||
signingOut: 'Signing Out...',
|
||
signOutHint:
|
||
'Signing out affects every app that uses Sky Cloud on this phone.',
|
||
deleteAccount: 'Delete Flare Account',
|
||
deleteAccountTitle: 'Delete your Flare account?',
|
||
deleteAccountBody:
|
||
'Your Flare profile, swipes, matches and conversations will be permanently deleted. Your Sky Cloud account and Photos library stay available.',
|
||
deletingAccount: 'Deleting...',
|
||
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.',
|
||
profile_not_found: 'This Flare account is no longer available.',
|
||
invalid_profile_photos:
|
||
'Choose or take at least one and up to six photos saved in your own Photos library.',
|
||
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',
|
||
allPosts: 'All posts',
|
||
videos: 'Videos',
|
||
taggedPosts: 'Tagged posts',
|
||
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:
|
||
'A Sky Cloud 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',
|
||
photo: 'Photo',
|
||
video: 'Video',
|
||
gallery: 'Gallery',
|
||
camera: 'Camera',
|
||
choosePhotosHint: 'Select up to five photos or one video',
|
||
chooseStoryHint: 'Select one photo or video',
|
||
selectedPhotos: '{count} selected',
|
||
changePhotos: 'Change selection',
|
||
previousPhoto: 'Previous photo',
|
||
nextPhoto: 'Next photo',
|
||
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',
|
||
replyPlaceholder: 'Write a reply...',
|
||
replyingTo: 'Replying to {handle}',
|
||
removeComment: 'Remove comment',
|
||
likes: '{count} likes',
|
||
viewComments: 'View all {count} comments',
|
||
showReplies: 'Show {count} replies',
|
||
hideReplies: 'Hide replies',
|
||
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',
|
||
noConnections: 'No profiles here yet',
|
||
shareProfile: 'Share profile',
|
||
share: 'Share',
|
||
followRequests: 'Follow requests',
|
||
requestAccepted: 'Follow request accepted.',
|
||
requestDeclined: 'Follow request declined.',
|
||
noActivity: 'No activity yet',
|
||
markRead: 'Mark as read',
|
||
publicProfile: 'Public profile',
|
||
privateProfileSetting: 'Private profile',
|
||
profileVisibility: 'Profile visibility',
|
||
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',
|
||
comment_like: 'liked your comment',
|
||
reply: 'replied to your comment',
|
||
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.',
|
||
comment_like: '{actor} liked your comment.',
|
||
reply: '{actor} replied to your comment.',
|
||
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 Sky Cloud 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 Sky Cloud 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:
|
||
'Your iFruit email is linked automatically. Use your Feather username to continue.',
|
||
login: 'Log in',
|
||
register: 'Register',
|
||
loginTitle: 'Good to see you again',
|
||
loginBody:
|
||
'Use your Feather username. Your iFruit email is linked automatically.',
|
||
registerTitle: 'Create your Feather profile',
|
||
registerBody:
|
||
'Choose a username and optional profile photo. No password is needed.',
|
||
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 profile',
|
||
noAccount: 'New to Feather?',
|
||
haveAccount: 'Already registered?',
|
||
registerNow: 'Create a profile',
|
||
loginNow: 'Log in',
|
||
authTrust:
|
||
'Your Feather profile is linked to your Sky Cloud account through your iFruit email.',
|
||
profileStep: 'Step 2 of 2',
|
||
accountConnected: 'Sky Cloud account connected',
|
||
authErrors: {
|
||
no_ifruit_account:
|
||
'Sign in to your Sky Cloud account in Settings first.',
|
||
invalid_handle: 'Use 3–30 letters, numbers or underscores.',
|
||
invalid_username: 'That username does not match your Feather profile.',
|
||
profile_not_found: 'No Feather profile exists for this iFruit email.',
|
||
already_registered: 'This iFruit email already has a Feather profile.',
|
||
handle_taken: 'That Feather username is already taken.',
|
||
invalid_media: 'Choose a valid photo from this phone.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
default: 'Feather could not complete the request. Please try again.',
|
||
},
|
||
welcome: 'Find your voice',
|
||
welcomeBody:
|
||
'Join the live conversation in Los Santos. Your Feather profile stays linked to your Sky Cloud 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: 'Photos',
|
||
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',
|
||
moreActions: 'More actions',
|
||
now: 'now',
|
||
minutesShort: '{count}m',
|
||
hoursShort: '{count}h',
|
||
daysShort: '{count}d',
|
||
comments: 'Comments',
|
||
noComments: 'No comments yet',
|
||
addComment: 'Add a comment...',
|
||
postComment: 'Post comment',
|
||
likeComment: 'Like comment by {name}',
|
||
likesCount: '{count} likes',
|
||
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 Sky Cloud 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',
|
||
navigation: 'FlipTok navigation',
|
||
emptyFeed: 'No videos yet',
|
||
emptyFeedBody: 'Follow creators or post the first FlipTok.',
|
||
searchPlaceholder: 'Search creators and videos',
|
||
clearSearch: 'Clear search',
|
||
trendLosSantos: '# Los Santos',
|
||
trendRoleplay: '# Roleplay',
|
||
trendTrending: '# Trending',
|
||
noActivity: 'No activity yet',
|
||
followers: 'Followers',
|
||
videos: 'Videos',
|
||
emptyBio: 'No bio yet.',
|
||
editProfile: 'Edit profile',
|
||
more: 'More',
|
||
newVideo: 'New FlipTok',
|
||
createTitle: 'Create your next FlipTok',
|
||
createBody: 'Record a clip or turn photos into a swipeable FlipTok.',
|
||
chooseMedia: 'Choose media',
|
||
chooseMediaHint:
|
||
'Record a video, choose a clip, or select up to 10 photos.',
|
||
recordVideo: 'Record video',
|
||
camera: 'Camera',
|
||
chooseVideo: 'Choose video',
|
||
photoSlideshow: 'Photos as FlipTok',
|
||
changeMedia: 'Change',
|
||
previousPhoto: 'Previous photo',
|
||
nextPhoto: 'Next photo',
|
||
caption: 'Caption',
|
||
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.',
|
||
videoDeleted: 'Video removed.',
|
||
deleteVideoTitle: 'Remove this video?',
|
||
deleteVideoBody:
|
||
'The video disappears from FlipTok and cannot be restored.',
|
||
deletingVideo: 'Removing...',
|
||
linkCopied: 'Video link copied.',
|
||
reported: 'Report submitted.',
|
||
blocked: 'Creator blocked.',
|
||
comments: 'Comments',
|
||
noComments: 'No comments yet',
|
||
addComment: 'Add comment...',
|
||
reply: 'Reply',
|
||
showReplies: 'Show {count} replies',
|
||
hideReplies: 'Hide {count} replies',
|
||
replyPlaceholder: 'Write a reply...',
|
||
replyingTo: 'Replying to {handle}',
|
||
cancelReply: 'Cancel reply',
|
||
report: 'Report video',
|
||
reportReason: 'Reason',
|
||
reportDetails: 'Additional details (optional)',
|
||
submitReport: 'Submit report',
|
||
reportDiscordNote:
|
||
'This report is sent directly to the moderation team through Discord.',
|
||
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.',
|
||
customSound: 'Custom sound',
|
||
customSoundLink: 'YouTube or audio link',
|
||
customSoundHint:
|
||
'Paste a YouTube link or a direct HTTPS audio-file link.',
|
||
customSoundPlaceholder:
|
||
'https://youtu.be/... or https://example.com/song.mp3',
|
||
customSoundFormats:
|
||
'YouTube, YouTube Music, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV, or WEBM',
|
||
useCustomSound: 'Use sound',
|
||
loadingSound: 'Loading sound',
|
||
invalidCustomSoundLink:
|
||
'Use a YouTube link or a direct public HTTPS audio-file link.',
|
||
customSoundLoadFailed: 'This sound could not be loaded.',
|
||
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',
|
||
savingProfile: 'Saving...',
|
||
displayName: 'Name',
|
||
username: 'Username',
|
||
bio: 'Bio',
|
||
accountType: 'Account type',
|
||
profilePhoto: 'Profile photo',
|
||
changePhoto: 'Change photo',
|
||
chooseFromGallery: 'Photos',
|
||
takePhoto: 'Camera',
|
||
removePhoto: 'Remove photo',
|
||
noConnections: 'No profiles to show',
|
||
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:
|
||
'A Sky Cloud 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_music_url:
|
||
'Use a valid YouTube or direct HTTPS audio-file link.',
|
||
invalid_media: 'Choose valid media from this phone.',
|
||
invalid_comment: 'Enter a valid comment.',
|
||
comments_disabled: 'Comments are disabled.',
|
||
invalid_profile: 'Check your profile details.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
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 Sky Cloud account already owns a registered FlipTok profile.',
|
||
handle_taken: 'This username is already taken.',
|
||
video_not_found: 'This video is unavailable.',
|
||
profile_not_found: 'This profile is unavailable.',
|
||
rate_limited: 'Too many actions. Try again shortly.',
|
||
not_authenticated: 'Sign in to Sky Cloud first.',
|
||
blocked: 'This account is blocked.',
|
||
not_authorized: 'You do not have moderation access.',
|
||
report_not_found: 'This report is no longer open.',
|
||
report_unavailable: 'Discord reporting is not configured yet.',
|
||
request_failed:
|
||
'Your FlipTok request could not be completed. Try again.',
|
||
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 Sky Cloud account.',
|
||
signInHint: 'Sign in through Settings to continue.',
|
||
createIdentity: 'Create Dark Identity',
|
||
createIdentityBody:
|
||
'Create a private identity before starting a DarkChat conversation.',
|
||
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',
|
||
to: 'To:',
|
||
connectPrivately: 'Connect privately',
|
||
newChatBody:
|
||
"Enter another person's Dark-ID or invitation code. You can share your own ID from your profile.",
|
||
darkIdOrInvite: 'Dark-ID or invitation code',
|
||
continue: 'Continue',
|
||
contacts: 'DarkChat Contacts',
|
||
contactsHint:
|
||
'Saved private identities appear only on your DarkChat account.',
|
||
noContacts: 'No saved DarkChat contacts',
|
||
private: 'Private',
|
||
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',
|
||
messageActions: 'Message actions',
|
||
contactSecurity: 'Contact & Security',
|
||
chatSettings: 'Chat Settings',
|
||
contactActions: 'Contact Actions',
|
||
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',
|
||
returnToCall: 'Return to Call',
|
||
block: 'Block User',
|
||
unblock: 'Unblock User',
|
||
clearChat: 'Clear Chat',
|
||
chatCleared: 'Chat cleared',
|
||
myIdentity: 'My Dark Identity',
|
||
alias: 'Alias',
|
||
privacySettings: 'Privacy Settings',
|
||
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',
|
||
shareInvite: 'Share Invite',
|
||
privacyDisclaimer:
|
||
'DarkChat stores messages on the server and does not claim end-to-end encryption.',
|
||
profileActions: 'Account Actions',
|
||
signOut: 'Sign Out',
|
||
signOutTitle: 'Sign out of Sky Cloud?',
|
||
signOutBody:
|
||
'This signs the whole phone out of Sky Cloud. Your DarkChat profile and messages remain stored.',
|
||
signingOut: 'Signing Out...',
|
||
signOutHint:
|
||
'Signing out affects every app that uses Sky Cloud on this phone.',
|
||
deleteProfile: 'Delete DarkChat Profile',
|
||
deleteProfileTitle: 'Delete DarkChat profile?',
|
||
deleteProfileBody:
|
||
'Your identity, contacts and DarkChat conversations will be permanently deleted.',
|
||
deletingProfile: 'Deleting...',
|
||
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 Sky Cloud 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.',
|
||
sign_out_failed: 'Sky Cloud could not sign out.',
|
||
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',
|
||
officialContact: 'Official company contact',
|
||
messagingUnavailable: 'This company contact does not accept messages.',
|
||
filterUnread: 'Show Unread Messages',
|
||
filterLabel: 'Conversation Filter',
|
||
allMessages: 'All',
|
||
unreadMessages: 'Unread',
|
||
smsLabel: 'Text Message · SMS',
|
||
photo: 'Photo',
|
||
gif: 'GIF',
|
||
video: 'Video',
|
||
contact: 'Contact',
|
||
attachPhoto: 'Attach Photo',
|
||
takePhoto: 'Take Photo',
|
||
attachGif: 'Attach GIF',
|
||
attachVideo: 'Attach Video',
|
||
photos: 'Photos',
|
||
gifs: 'GIFs',
|
||
videos: 'Videos',
|
||
contacts: 'Contacts',
|
||
shareContact: 'Share Contact',
|
||
noContactsToShare: 'Save a contact first to share it here.',
|
||
contactSaved: 'Contact Saved',
|
||
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',
|
||
inboxActions: 'Conversation Actions',
|
||
sortLabel: 'Conversation Sort',
|
||
sortNewest: 'Newest First',
|
||
sortOldest: 'Oldest First',
|
||
unreadConversation: '{name}, {count} unread messages',
|
||
attachmentPreview: 'Selected attachments',
|
||
attachmentLimit: 'You can attach up to {count} photos or videos.',
|
||
removeAttachment: 'Remove attachment {number}',
|
||
contactDetails: 'Contact Details',
|
||
contactName: 'Name',
|
||
phoneNumber: 'Phone Number',
|
||
company: 'Company',
|
||
contactActions: 'Contact Actions',
|
||
call: 'Call',
|
||
messageAction: 'Message',
|
||
addContact: 'Add Contact',
|
||
showInContacts: 'Show in Contacts',
|
||
blockContact: 'Block Contact',
|
||
blockContactTitle: 'Block this contact?',
|
||
blockContactBody:
|
||
'{name} will no longer be able to call or message this SIM.',
|
||
blockContactFailed: 'The contact could not be blocked.',
|
||
contactBlocked: 'Contact blocked.',
|
||
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',
|
||
emojiPicker: 'Emoji picker',
|
||
emojiCategories: 'Emoji categories',
|
||
skinTone: 'Skin tone {number}',
|
||
gifLabels: {
|
||
celebrate: 'Celebrate!',
|
||
hearts: 'Love it',
|
||
party: 'Party time',
|
||
thumbsUp: 'Perfect',
|
||
wow: 'WOW',
|
||
},
|
||
voiceMessage: 'Audio Message',
|
||
recordVoice: 'Record Audio',
|
||
playAudio: 'Play Audio',
|
||
pauseAudio: 'Pause Audio',
|
||
seekAudio: 'Seek 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.',
|
||
invalid_contact: 'The contact is invalid.',
|
||
contact_not_found: 'This contact is no longer available.',
|
||
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.',
|
||
blocked: 'This contact has blocked calls and messages from your SIM.',
|
||
messaging_unavailable: 'This company contact does not accept messages.',
|
||
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',
|
||
player: 'Player',
|
||
profileLabel: '{name} profile',
|
||
searchPlaceholder: 'Games, apps, stories and more',
|
||
appsTitle: 'Built-in Apps',
|
||
gamesTitle: 'Games',
|
||
selected: 'Apps available for your Sky Phone',
|
||
tabs: {
|
||
today: 'Today',
|
||
apps: 'Apps',
|
||
games: 'Games',
|
||
search: 'Search',
|
||
},
|
||
today: {
|
||
featured: 'Featured today',
|
||
gameHighlight: 'Game highlight',
|
||
appHighlight: 'App highlight',
|
||
curatedForYou: 'Curated for you',
|
||
editorsChoice: "Editor's Choice",
|
||
discoverTitle: 'Discover {app}',
|
||
playTitle: 'Play something new with {app}',
|
||
description: '{app} is in the spotlight today on Sky Phone.',
|
||
categoryDescription: '{category} · Made for Sky Phone',
|
||
moreHighlights: "Today's Highlights",
|
||
topToday: 'Top Today',
|
||
topTodayDescription: 'Popular picks from today’s edition',
|
||
oneMoreThing: 'One more thing',
|
||
},
|
||
filters: {
|
||
all: 'All',
|
||
social: 'Social',
|
||
utilities: 'Utilities',
|
||
shopping: 'Shopping',
|
||
productivity: 'Productivity',
|
||
arcade: 'Arcade',
|
||
puzzle: 'Puzzle',
|
||
classic: 'Classics',
|
||
},
|
||
browse: {
|
||
categories: 'Store categories',
|
||
availableNow: 'Available now',
|
||
featureTitle: 'Discover {app}',
|
||
featuredPages: 'Featured apps',
|
||
handPicked: 'Hand-picked for you',
|
||
essentialApps: 'Essential Apps',
|
||
essentialGames: 'Essential Games',
|
||
},
|
||
taglines: {
|
||
citywarn: 'Live city alerts and public safety updates',
|
||
crypto: 'Synthetic markets, portfolio and wallet transfers',
|
||
health: 'Health, activity and medical information',
|
||
'weazel-news': 'Local news and city stories',
|
||
companies: 'Businesses, jobs and services',
|
||
music: 'Songs, playlists and audio',
|
||
picstagram: 'Photo sharing and social feed',
|
||
feather: 'Short posts and city conversations',
|
||
fliptok: 'Short videos and trends',
|
||
flare: 'Social posts and live moments',
|
||
calendar: 'Events, schedules and reminders',
|
||
radio: 'Live radio and team communication',
|
||
'local-pages': 'Local businesses and community pages',
|
||
crewlink: 'Crews, members and coordination',
|
||
phone: 'Calls, contacts and voicemail',
|
||
messages: 'Private texts and media',
|
||
darkchat: 'Private messaging and secure groups',
|
||
garage: 'Vehicles, parking and valet',
|
||
house: 'Homes, access and management',
|
||
map: 'Navigation and nearby places',
|
||
skyride: 'City rides and driver booking',
|
||
banking: 'Accounts, transfers and payments',
|
||
billing: 'Invoices and payment requests',
|
||
mail: 'Email and attachments',
|
||
notes: 'Notes, lists and ideas',
|
||
memos: 'Voice recordings and memos',
|
||
calculator: 'Everyday and scientific calculations',
|
||
camera: 'Photos and video capture',
|
||
clock: 'Time, alarms and timers',
|
||
weather: 'Forecasts and current conditions',
|
||
photos: 'Photo and video library',
|
||
settings: 'Phone preferences and security',
|
||
snake: 'Classic arcade snake',
|
||
memory: 'Card matching challenge',
|
||
'number-merge': 'Number puzzle and strategy',
|
||
minesweeper: 'Classic logic puzzle',
|
||
'tower-stack': 'Precision tower-building game',
|
||
'sky-flappy': 'Fast arcade flying challenge',
|
||
citymarkt: 'Local marketplace and listings',
|
||
'neon-drop': 'Neon block-dropping puzzle',
|
||
},
|
||
previews: {
|
||
citywarn: {
|
||
first: 'Live alerts',
|
||
second: 'Safety zones',
|
||
third: 'Incident updates',
|
||
},
|
||
crypto: {
|
||
first: 'Synthetic markets',
|
||
second: 'Portfolio',
|
||
third: 'Wallet transfers',
|
||
},
|
||
health: {
|
||
first: 'Activity rings',
|
||
second: 'Medical ID',
|
||
third: 'Health records',
|
||
},
|
||
'weazel-news': {
|
||
first: 'Top stories',
|
||
second: 'Local reports',
|
||
third: 'Breaking news',
|
||
},
|
||
companies: {
|
||
first: 'Business directory',
|
||
second: 'Job requests',
|
||
third: 'Services',
|
||
},
|
||
music: {
|
||
first: 'Now playing',
|
||
second: 'Playlists',
|
||
third: 'Music library',
|
||
},
|
||
picstagram: {
|
||
first: 'Photo feed',
|
||
second: 'Stories',
|
||
third: 'Profiles',
|
||
},
|
||
feather: {
|
||
first: 'Short posts',
|
||
second: 'Following feed',
|
||
third: 'Conversations',
|
||
},
|
||
fliptok: {
|
||
first: 'Video feed',
|
||
second: 'Creator tools',
|
||
third: 'Trends',
|
||
},
|
||
flare: {
|
||
first: 'Discover people',
|
||
second: 'Matches',
|
||
third: 'Live moments',
|
||
},
|
||
calendar: {
|
||
first: 'Upcoming events',
|
||
second: 'Day planner',
|
||
third: 'Reminders',
|
||
},
|
||
radio: {
|
||
first: 'Live channels',
|
||
second: 'Team radio',
|
||
third: 'Favorites',
|
||
},
|
||
'local-pages': {
|
||
first: 'Local pages',
|
||
second: 'Reviews',
|
||
third: 'City discovery',
|
||
},
|
||
crewlink: {
|
||
first: 'Crew roster',
|
||
second: 'Shared locations',
|
||
third: 'Coordination',
|
||
},
|
||
phone: {
|
||
first: 'Recent calls',
|
||
second: 'Contacts',
|
||
third: 'Voicemail',
|
||
},
|
||
messages: {
|
||
first: 'Conversations',
|
||
second: 'Media sharing',
|
||
third: 'Quick replies',
|
||
},
|
||
darkchat: {
|
||
first: 'Private chats',
|
||
second: 'Secure groups',
|
||
third: 'Invitations',
|
||
},
|
||
garage: {
|
||
first: 'Vehicle list',
|
||
second: 'Parking locations',
|
||
third: 'Valet',
|
||
},
|
||
house: {
|
||
first: 'Property access',
|
||
second: 'Residents',
|
||
third: 'Management',
|
||
},
|
||
map: {
|
||
first: 'Live navigation',
|
||
second: 'Nearby places',
|
||
third: 'Route guidance',
|
||
},
|
||
skyride: {
|
||
first: 'Ride booking',
|
||
second: 'Driver tracking',
|
||
third: 'Trip history',
|
||
},
|
||
banking: {
|
||
first: 'Account balance',
|
||
second: 'Transfers',
|
||
third: 'Transactions',
|
||
},
|
||
billing: {
|
||
first: 'Open invoices',
|
||
second: 'Payment requests',
|
||
third: 'Payment history',
|
||
},
|
||
mail: { first: 'Inbox', second: 'Attachments', third: 'Mailboxes' },
|
||
notes: { first: 'Notes', second: 'Checklists', third: 'Pinned ideas' },
|
||
memos: {
|
||
first: 'Voice recordings',
|
||
second: 'Playback',
|
||
third: 'Favorites',
|
||
},
|
||
calculator: {
|
||
first: 'Basic calculation',
|
||
second: 'Scientific tools',
|
||
third: 'History',
|
||
},
|
||
camera: {
|
||
first: 'Photo mode',
|
||
second: 'Video capture',
|
||
third: 'Zoom controls',
|
||
},
|
||
clock: { first: 'World clock', second: 'Alarms', third: 'Timers' },
|
||
weather: {
|
||
first: 'Current weather',
|
||
second: 'Hourly forecast',
|
||
third: 'Seven-day outlook',
|
||
},
|
||
photos: {
|
||
first: 'Media library',
|
||
second: 'Albums',
|
||
third: 'Shared media',
|
||
},
|
||
settings: {
|
||
first: 'Device controls',
|
||
second: 'Privacy',
|
||
third: 'Personalization',
|
||
},
|
||
snake: { first: 'High score', second: 'Speed', third: 'Classic grid' },
|
||
memory: {
|
||
first: 'Matched pairs',
|
||
second: 'Best time',
|
||
third: 'Card themes',
|
||
},
|
||
'number-merge': {
|
||
first: 'Highest tile',
|
||
second: 'Score',
|
||
third: 'Strategy grid',
|
||
},
|
||
minesweeper: {
|
||
first: 'Mine counter',
|
||
second: 'Best time',
|
||
third: 'Difficulty',
|
||
},
|
||
'tower-stack': {
|
||
first: 'Tower height',
|
||
second: 'Perfect drops',
|
||
third: 'High score',
|
||
},
|
||
'sky-flappy': {
|
||
first: 'Flight score',
|
||
second: 'Best run',
|
||
third: 'Obstacles',
|
||
},
|
||
citymarkt: {
|
||
first: 'Listings',
|
||
second: 'Categories',
|
||
third: 'Saved offers',
|
||
},
|
||
'neon-drop': {
|
||
first: 'Lines cleared',
|
||
second: 'Level',
|
||
third: 'Neon pieces',
|
||
},
|
||
},
|
||
search: {
|
||
recommended: 'Recommended',
|
||
discover: 'Discover',
|
||
results: 'Search Results',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try searching for a different app or game.',
|
||
ad: 'Ad',
|
||
topApps: 'Top Apps',
|
||
topGames: 'Top Games',
|
||
bestApps: 'Bestselling Apps',
|
||
bestGames: 'Bestselling Games',
|
||
productivity: 'Productivity',
|
||
photoVideo: 'Photo & Video',
|
||
},
|
||
account: {
|
||
account: 'Account',
|
||
title: 'App Management',
|
||
skyAccount: 'Sky Phone Account',
|
||
apps: 'Installed Apps',
|
||
games: 'Games',
|
||
library: 'Your Library',
|
||
myApps: 'My Apps',
|
||
downloadedOn: 'Downloaded {date}',
|
||
uninstall: 'Uninstall',
|
||
uninstallApp: 'Uninstall {app}',
|
||
uninstallTitle: 'Uninstall this app?',
|
||
uninstallBody:
|
||
'{app} will be removed from this phone. You can download it again from the App Store.',
|
||
},
|
||
details: {
|
||
skyStudios: 'Sky Studios',
|
||
share: 'Share app',
|
||
openDetails: 'View {app}',
|
||
shareReady: 'App information is ready to share.',
|
||
shareCopy: 'Take a look at {app} in the App Store.',
|
||
ratings: '{count} ratings',
|
||
age: 'Age Rating',
|
||
years: 'Years',
|
||
chart: 'Chart',
|
||
whatsNew: "What's New",
|
||
version: 'Version {version}',
|
||
updatedToday: 'Today',
|
||
releaseNotes:
|
||
'{app} now includes fresh content, faster loading and several interface improvements.',
|
||
preview: 'Preview',
|
||
testData: 'Preview data',
|
||
previewLive: 'Live Preview',
|
||
previousPreview: 'Previous preview',
|
||
nextPreview: 'Next preview',
|
||
previewOverview: 'Everything important at a glance',
|
||
previewCommunity: 'Community',
|
||
previewInsights: 'Insights',
|
||
today: 'Today',
|
||
progress: 'Progress',
|
||
communityTitle: 'Together in {app}',
|
||
communityBody: 'Discover activity from players around Los Santos.',
|
||
featuredMoment: 'Featured moment from today',
|
||
weeklyActivity: 'Weekly Activity',
|
||
fromLastWeek: 'from last week',
|
||
about: 'About this App',
|
||
description:
|
||
'{app} is a detailed {category} experience made for Sky Phone. Explore its features, connect with the city and keep everything close at hand.',
|
||
crypto: {
|
||
releaseNotes:
|
||
'VaultX now offers clearer portfolio performance, live synthetic market updates and improved wallet transfers with server-validated quotes.',
|
||
previewOverview: 'Portfolio, balance and performance at a glance',
|
||
previewCommunity: 'Markets',
|
||
markets: 'Markets',
|
||
serverPrices: 'Server prices',
|
||
communityTitle: 'Trade synthetic markets',
|
||
communityBody:
|
||
'Follow fictional assets, review short-lived quotes and trade against available exchange liquidity.',
|
||
featuredMoment: 'Live market movers',
|
||
previewInsights: 'Portfolio',
|
||
weeklyActivity: 'Simulated performance',
|
||
fromLastWeek: 'fictional weekly portfolio change',
|
||
description:
|
||
"VaultX is Sky Phone's fictional crypto exchange and wallet. Track synthetic markets, manage your character-bound portfolio, deposit or withdraw funds, trade with short-lived server quotes and transfer assets using public VaultX keys. All prices are fictional and have no real-world value.",
|
||
},
|
||
},
|
||
},
|
||
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',
|
||
allCalls: 'All',
|
||
missedCalls: 'Missed',
|
||
searchRecents: 'Search Calls',
|
||
contactDetails: 'Contact Details',
|
||
unknownCaller: 'Unknown Caller',
|
||
callHistory: 'Call History',
|
||
noCallHistory: 'No calls with this number yet.',
|
||
noContacts: 'No Contacts',
|
||
favorites: 'Favorites',
|
||
searchContacts: 'Search Contacts',
|
||
myCard: 'My Card',
|
||
myNumber: 'My Number',
|
||
device: 'Device',
|
||
contactIndex: 'Contact Index',
|
||
addContact: 'New Contact',
|
||
newContact: 'New',
|
||
editContact: 'Edit Contact',
|
||
contactName: 'Name',
|
||
firstName: 'First Name',
|
||
lastName: 'Last Name',
|
||
companyOrGroup: 'Company or Group',
|
||
phoneNumber: 'Phone Number',
|
||
officialContact: 'Official company contact',
|
||
choosePhoto: 'Choose Contact Photo',
|
||
chooseGallery: 'Photos',
|
||
takePhoto: 'Camera',
|
||
removePhoto: 'Remove Photo',
|
||
call: 'Call',
|
||
callFailed: 'The call could not be started.',
|
||
message: 'Message',
|
||
video: 'Video',
|
||
mail: 'Mail',
|
||
defaultLabel: 'Default',
|
||
privateLabel: 'Private',
|
||
contactCard: 'Contact Card',
|
||
removeContact: 'Remove Contact',
|
||
mobile: 'Mobile',
|
||
messagesProfile: 'Messages',
|
||
notes: 'Notes',
|
||
sendMessage: 'Send Message',
|
||
shareContact: 'Share Contact',
|
||
addFavorite: 'Add to Favorites',
|
||
removeFavorite: 'Remove from Favorites',
|
||
addEmergency: 'Add to Emergency Contacts',
|
||
blockContact: 'Block Contact',
|
||
block: 'Block',
|
||
blockCaller: 'Block Caller',
|
||
blockCallerTitle: 'Block this caller?',
|
||
blockCallerBody: '{number} will no longer be able to call this SIM.',
|
||
faceTime: 'FaceTime',
|
||
hideKeypad: 'Hide Keypad',
|
||
more: 'More',
|
||
mute: 'Mute',
|
||
speaker: 'Speaker',
|
||
viewContact: 'View Contact',
|
||
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.',
|
||
invalid_sim: 'The active SIM card is unavailable.',
|
||
message_unavailable: 'The conversation could not be opened.',
|
||
contact_remove_failed: 'The contact could not be removed.',
|
||
contact_favorite_failed: 'The favorite could not be updated.',
|
||
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.',
|
||
blocked: 'This number is blocked.',
|
||
recipient_not_found: 'This number is not known.',
|
||
busy: 'The line is busy.',
|
||
company_unavailable: 'This company service line is unavailable.',
|
||
readonly_contact: 'Official company contacts cannot be changed.',
|
||
rate_limited: 'Too many calls. Try again in a minute.',
|
||
voice_unavailable: 'The configured phone voice service is unavailable.',
|
||
call_not_connected:
|
||
'Connect the call before changing its audio controls.',
|
||
speaker_unavailable:
|
||
'Speaker mode is not available for the configured phone voice service.',
|
||
speaker_unsupported:
|
||
'The configured phone voice service does not support speaker mode.',
|
||
mute_unavailable:
|
||
'Mute is not available for the configured phone voice service.',
|
||
mute_unsupported:
|
||
'The configured phone voice service does not support mute.',
|
||
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',
|
||
speaker: 'Speaker',
|
||
speakerDescription: 'Play radio traffic through speaker mode',
|
||
providerFeatureUnavailable: 'Not supported by this voice service',
|
||
notSupported: 'Not supported',
|
||
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: 'Character name',
|
||
displayNameDescription:
|
||
'Leave empty to use your character name. 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.',
|
||
radio_not_connected:
|
||
'Connect to a radio channel before enabling speaker mode.',
|
||
speaker_unavailable:
|
||
'Speaker mode is not available for the configured radio voice service.',
|
||
speaker_unsupported:
|
||
'The configured radio voice service does not support speaker mode.',
|
||
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',
|
||
transactionDetails: 'Transaction details',
|
||
transactionDate: 'Date',
|
||
transactionDirection: 'Direction',
|
||
transactionReference: 'Reference',
|
||
notifications: {
|
||
receivedTitle: 'Money received',
|
||
received: 'You received {amount} from {sender}.',
|
||
},
|
||
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.',
|
||
reload_cooldown:
|
||
'Too many refreshes. Please wait a moment before trying again.',
|
||
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: '—',
|
||
filtersLabel: 'Filter vehicles',
|
||
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',
|
||
playerDriverNotice:
|
||
'SkyRide matches you with real player drivers. A driver must be online and accept your request.',
|
||
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:
|
||
'Your request is waiting for an available player driver to accept it.',
|
||
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: 'Chat',
|
||
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',
|
||
profile: 'Your profile',
|
||
editProfile: 'Edit Profile',
|
||
editProfileBody: 'Change your public name and profile photo',
|
||
profilePhoto: 'Profile photo',
|
||
gallery: 'Photos',
|
||
camera: 'Camera',
|
||
profileDetails: 'Profile details',
|
||
profileName: 'Name',
|
||
profileNamePlaceholder: 'Your SkyRide name',
|
||
removeProfilePhoto: 'Remove profile photo',
|
||
saveProfile: 'Save Profile',
|
||
profileSaved: 'Your SkyRide profile was updated.',
|
||
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.',
|
||
invalid_profile: 'Choose a valid profile name and photo.',
|
||
invalid_profile_name:
|
||
'Your profile name must contain 2 to 50 characters.',
|
||
invalid_profile_image: 'Choose a profile photo from your own gallery.',
|
||
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: 'Manage homes and access',
|
||
myHomes: 'Your Homes',
|
||
properties: 'Properties',
|
||
owned: 'Owned',
|
||
shared: 'Shared with you',
|
||
locked: 'Locked',
|
||
unlocked: 'Unlocked',
|
||
lockDoor: 'Lock Door',
|
||
unlockDoor: 'Unlock Door',
|
||
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 yet',
|
||
emptyBody: 'Homes you own or can access will appear here automatically.',
|
||
provider: 'Housing data · {system}',
|
||
access: 'Access',
|
||
status: 'Status',
|
||
actions: 'Quick Actions',
|
||
setWaypoint: 'Get 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.',
|
||
reload_cooldown:
|
||
'Too many refreshes. Please wait a moment before trying again.',
|
||
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',
|
||
history: 'Calculation history',
|
||
changeMode: 'Change calculator mode',
|
||
scientificFunctions: 'Scientific functions',
|
||
edit: 'Edit',
|
||
done: 'Done',
|
||
closeHistory: 'Close history',
|
||
today: 'Today',
|
||
yesterday: 'Yesterday',
|
||
earlier: 'Earlier',
|
||
noHistory: 'No calculations yet',
|
||
clearHistory: 'Clear History',
|
||
},
|
||
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 Sky Cloud',
|
||
signInBody: 'Log in to your CityMarkt profile to sell, save or message.',
|
||
authEyebrow: 'CityMarkt account',
|
||
authLoginTitle: 'Welcome back',
|
||
authRegisterTitle: 'Create your profile',
|
||
authLoginBody: 'Sign in with the iFruit account linked to this phone.',
|
||
authRegisterBody:
|
||
'Confirm your linked iFruit account to create a CityMarkt profile.',
|
||
login: 'Sign in',
|
||
register: 'Create profile',
|
||
authPassword: 'iFruit password',
|
||
authPasswordPlaceholder: 'Enter your iFruit password',
|
||
authPasswordHelp: 'Use 6–64 characters.',
|
||
authConfirmPassword: 'Repeat password',
|
||
authConfirmPlaceholder: 'Repeat your iFruit password',
|
||
passwordsMismatch: 'The passwords do not match.',
|
||
authErrors: {
|
||
no_ifruit_account: 'Connect a Sky Cloud account in Settings first.',
|
||
invalid_password: 'Password must be 6–64 characters.',
|
||
invalid_credentials: 'The iFruit email or password is incorrect.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
profile_not_found: 'No CityMarkt profile exists for this iFruit email.',
|
||
profile_exists:
|
||
'A CityMarkt profile already exists. Use Sign in instead.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
default: 'CityMarkt authentication failed.',
|
||
},
|
||
noMessages: 'No conversations',
|
||
noMessagesBody: 'Messages about offers will appear here.',
|
||
myListings: 'My listings',
|
||
favorites: 'Favorites',
|
||
addFavorite: 'Add to favorites',
|
||
removeFavorite: 'Remove from favorites',
|
||
noProfileListings: 'Nothing here yet',
|
||
createProfile: 'Create your CityMarkt profile',
|
||
editProfile: 'Edit profile',
|
||
profileIntro: 'Your iFruit email stays linked to this profile.',
|
||
profileEmail: 'iFruit email',
|
||
displayName: 'Display name',
|
||
profileBio: 'About you',
|
||
saveProfile: 'Save profile',
|
||
cancel: 'Cancel',
|
||
profileSaved: 'Your profile was saved.',
|
||
removeProfilePhoto: 'Remove photo',
|
||
phone: 'Phone',
|
||
contactSeller: 'Contact seller',
|
||
messagePlaceholder: 'Hi, is this still available?',
|
||
signInToMessage: 'Sign in to your Sky Cloud 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 Photos or take new ones. Photos are optional and the first becomes the cover.',
|
||
chooseGallery: 'Choose from Photos',
|
||
chooseGalleryBody: 'Use photos saved on this phone.',
|
||
takePhotos: 'Take photos',
|
||
takePhotosBody: 'Take several new photos for this listing.',
|
||
selectedPhotos: 'Selected photos',
|
||
gallery: 'Photos',
|
||
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 Sky Cloud 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 Sky Cloud in Settings to publish and save posts.',
|
||
authEyebrow: 'Local Pages account',
|
||
authWelcome: 'Welcome to Local Pages',
|
||
authBody:
|
||
'Your iFruit email is linked automatically. Use your Local Pages username to continue.',
|
||
login: 'Sign in',
|
||
register: 'Register',
|
||
loginTitle: 'Continue with Sky Cloud',
|
||
loginBody:
|
||
'Your posts, saved items and profile stay linked to your account.',
|
||
registerTitle: 'Create your Local Pages profile',
|
||
registerBody:
|
||
'Choose a username and optional profile photo. Your iFruit email stays linked.',
|
||
authEmail: 'iFruit address',
|
||
authEmailPlaceholder: 'your.name',
|
||
authPassword: 'Password',
|
||
authPasswordPlaceholder: 'At least 6 characters',
|
||
authConfirmPassword: 'Confirm password',
|
||
authConfirmPlaceholder: 'Enter the password again',
|
||
showPassword: 'Show password',
|
||
hidePassword: 'Hide password',
|
||
localCreator: 'Local creator',
|
||
createProfile: 'Create your profile',
|
||
editProfile: 'Edit profile',
|
||
profileSetupBody: 'Your iFruit email is linked automatically.',
|
||
profileEmail: 'iFruit email',
|
||
profileHandle: 'Username',
|
||
profileHandlePlaceholder: 'your.name',
|
||
profileHandleHint:
|
||
'Use 3–24 lowercase letters, numbers, dots or underscores.',
|
||
profileBio: 'Bio',
|
||
profileBioPlaceholder: 'Tell the city a little about yourself...',
|
||
profilePhoto: 'Profile photo',
|
||
profilePhotoHint: 'Choose a photo from Photos or take a new one.',
|
||
removeProfilePhoto: 'Remove photo',
|
||
profileSave: 'Save profile',
|
||
profileCancel: 'Cancel',
|
||
profileSaved: 'Your profile was saved.',
|
||
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: 'Photos',
|
||
photoLimit: 'You can add up to six photos.',
|
||
cityMarktShare: 'Share to Local Pages',
|
||
cityMarktShared: 'Shared to Local Pages.',
|
||
cityMarktShareHint: 'One CityMarkt share per day',
|
||
cityMarktAlreadyShared: 'Already published',
|
||
cityMarktOpenSharedHint: 'Open the Local Pages post',
|
||
cityMarktAppMissing: 'Local Pages is not installed',
|
||
cityMarktInstallHint: 'Install Local Pages to share this listing',
|
||
cityMarktAccountMissing: 'Local Pages profile required',
|
||
cityMarktAccountHint: 'Create a Local Pages profile before sharing',
|
||
cityMarktComposeTitle: 'Share CityMarkt listing',
|
||
cityMarktComposeNavTitle: 'Share listing',
|
||
cityMarktComposeHint:
|
||
'Review the listing and publish it when you are ready.',
|
||
cityMarktPhotosHint: 'The CityMarkt listing photos will be included.',
|
||
authErrors: {
|
||
no_ifruit_account:
|
||
'Sign in to your Sky Cloud account in Settings first.',
|
||
invalid_username:
|
||
'Use 3–24 lowercase letters, numbers, dots or underscores.',
|
||
profile_not_found:
|
||
'No Local Pages profile exists for this iFruit email.',
|
||
profile_exists: 'This iFruit email already has a Local Pages profile.',
|
||
invalid_profile: 'Check your Local Pages username.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
profile_handle_taken: 'This Local Pages username is already taken.',
|
||
rate_limited: 'Too many attempts. Try again shortly.',
|
||
default: 'Local Pages could not complete the request.',
|
||
},
|
||
errors: {
|
||
profile_required: 'Create your Local Pages profile first.',
|
||
invalid_profile: 'Check your username and bio.',
|
||
invalid_profile_image: 'Choose a valid photo from this phone.',
|
||
profile_handle_taken: 'This username is already taken.',
|
||
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 Sky Cloud 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.',
|
||
errors: {
|
||
reload_cooldown:
|
||
'Too many refreshes. Please wait a moment before trying again.',
|
||
},
|
||
tryAgain: 'Try Again',
|
||
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 Photos',
|
||
takePhoto: 'Take photo',
|
||
startRecording: 'Start recording',
|
||
stopRecording: 'Stop recording',
|
||
saved: 'Saved to Photos.',
|
||
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.',
|
||
media_provider_failed: 'The camera upload service is unavailable.',
|
||
media_provider_rate_limited:
|
||
'The camera upload service is busy. Try again shortly.',
|
||
media_provider_unauthorized:
|
||
'The configured FiveManage API key was rejected.',
|
||
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: 'Sky Cloud',
|
||
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',
|
||
allDay: 'All-day',
|
||
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 Sky Cloud calendar',
|
||
signInBody:
|
||
'Sign in to Sky Cloud 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 Sky Cloud 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',
|
||
description: 'Description',
|
||
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 Sky Cloud account',
|
||
emailPlaceholder: 'name@ifruit.com',
|
||
passwordPlaceholder: 'Enter password',
|
||
passwordWarning:
|
||
'Use an in-character password. Do not reuse a real-world password.',
|
||
mailboxes: 'Mailboxes',
|
||
editMailboxes: 'Edit',
|
||
newMailbox: 'New Mailbox',
|
||
mailboxName: 'Name',
|
||
mailboxNamePlaceholder: 'Mailbox name',
|
||
mailboxLocation: 'Mailbox Location',
|
||
deleteMailbox: 'Delete {mailbox}',
|
||
deleteMailboxTitle: 'Delete Mailbox?',
|
||
deleteMailboxBody:
|
||
'Messages in {mailbox} will return to their original mailbox.',
|
||
moveToMailbox: 'Move to Mailbox',
|
||
movedToMailbox: 'Moved to {mailbox}.',
|
||
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',
|
||
toolbar: 'Mail tools',
|
||
filterTitle: 'Filter',
|
||
filteredBy: 'Filtered by',
|
||
filterActiveLabel: 'Filtered by {filter}',
|
||
filterStatus: 'Status',
|
||
filterAddressed: 'Addressed',
|
||
filterDirection: 'Direction',
|
||
filterOther: 'Other',
|
||
filterUnreadLabel: 'Unread',
|
||
filterReadLabel: 'Read',
|
||
filterReceived: 'Received',
|
||
filterSent: 'Sent',
|
||
filterToMe: 'To Me',
|
||
filterFromMe: 'From Me',
|
||
filterToday: 'Today',
|
||
filterMultiple: '{count} Filters',
|
||
select: 'Select',
|
||
selectedCount: '{count} Selected',
|
||
deleteSelected: 'Delete Selected',
|
||
selectionLimit: 'You can select up to {count} messages at once.',
|
||
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.',
|
||
invalid_mailbox: 'Enter a mailbox name with up to 50 characters.',
|
||
mailbox_exists: 'A mailbox with that name already exists.',
|
||
mailbox_limit: 'You have reached the mailbox limit.',
|
||
mailbox_not_found: 'That mailbox is no longer available.',
|
||
message_not_found: 'That message is no longer available.',
|
||
invalid_filter: 'That mail filter is not available.',
|
||
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',
|
||
deleteTitle: 'Delete Note?',
|
||
deleteBody: 'This note will be permanently deleted.',
|
||
tools: {
|
||
bold: 'Bold',
|
||
bulletList: 'Bullet list',
|
||
decreaseText: 'Smaller text',
|
||
increaseText: 'Larger text',
|
||
italic: 'Italic',
|
||
numberedList: 'Numbered list',
|
||
quote: 'Quote',
|
||
redo: 'Redo',
|
||
strike: 'Strikethrough',
|
||
toolbar: 'Formatting tools',
|
||
underline: 'Underline',
|
||
undo: 'Undo',
|
||
},
|
||
},
|
||
memos: {
|
||
name: 'Memos',
|
||
memo: 'Voice Memo',
|
||
back: 'Back',
|
||
actions: 'Memo actions',
|
||
newMemo: 'New Recording',
|
||
newMemoWithDate: 'New Recording · {date}',
|
||
searchPlaceholder: 'Search Recordings',
|
||
emptyTitle: 'No Recordings',
|
||
emptyBody: 'Tap the record button to capture your first voice memo.',
|
||
noResults: 'No Results',
|
||
noResultsBody: 'Try searching for a different title or note.',
|
||
recording: 'Recording',
|
||
paused: 'Paused',
|
||
preparing: 'Preparing microphone…',
|
||
saving: 'Saving Recording…',
|
||
stop: 'Stop',
|
||
pause: 'Pause',
|
||
resume: 'Resume',
|
||
cancel: 'Cancel',
|
||
done: 'Done',
|
||
play: 'Play recording',
|
||
pausePlayback: 'Pause recording',
|
||
skipBack: 'Back 15 seconds',
|
||
skipForward: 'Forward 15 seconds',
|
||
playbackSpeed: 'Playback Speed',
|
||
title: 'Title',
|
||
titlePlaceholder: 'Recording title',
|
||
note: 'Note',
|
||
notePlaceholder: 'Add details…',
|
||
delete: 'Delete memo',
|
||
deleteTitle: 'Delete Voice Memo?',
|
||
deleteBody: 'This recording will be permanently deleted.',
|
||
discardTitle: 'Discard Recording?',
|
||
discardBody: 'The current recording will not be saved.',
|
||
discard: 'Discard',
|
||
keepRecording: 'Keep Recording',
|
||
deleted: 'Memo deleted.',
|
||
microphoneUnavailable: 'The microphone is unavailable.',
|
||
recordingTooLarge: 'The recording is too large to save.',
|
||
maximumDuration: 'The maximum recording duration was reached.',
|
||
errors: {
|
||
conflict: 'This memo changed on another device. It has been reloaded.',
|
||
invalid_memo: 'The voice memo contains invalid data.',
|
||
invalid_request: 'The memo request is invalid.',
|
||
invalid_upload: 'The uploaded recording could not be verified.',
|
||
invalid_upload_token: 'The recording upload could not be verified.',
|
||
memo_limit: 'The limit of 100 voice memos has been reached.',
|
||
memo_not_found: 'This voice memo no longer exists.',
|
||
media_provider_failed: 'The voice memo upload service is unavailable.',
|
||
media_provider_rate_limited:
|
||
'The voice memo upload service is busy. Try again shortly.',
|
||
media_provider_unauthorized:
|
||
'The configured FiveManage API key was rejected.',
|
||
missing_config: 'Voice memo uploads are not configured.',
|
||
operation_in_progress: 'Finish the current recording first.',
|
||
owner_changed: 'The active phone changed while saving the memo.',
|
||
rate_limited: 'Too many memo changes. Try again shortly.',
|
||
recording_failed: 'The recording could not be captured.',
|
||
request_timeout: 'The memo request timed out.',
|
||
upload_failed: 'The recording could not be uploaded.',
|
||
upload_timeout: 'The recording upload timed out.',
|
||
request_failed: 'Memos are temporarily unavailable.',
|
||
},
|
||
},
|
||
photos: {
|
||
name: 'Photos',
|
||
library: 'Library',
|
||
counts: {
|
||
all: '{count} Objects',
|
||
allOne: '{count} Object',
|
||
favorite: '{count} Favorite',
|
||
favorites: '{count} Favorites',
|
||
photo: '{count} Photo',
|
||
photos: '{count} Photos',
|
||
video: '{count} Video',
|
||
videos: '{count} Videos',
|
||
},
|
||
sorting: {
|
||
action: 'Sort Photos',
|
||
title: 'Sort Order',
|
||
show: 'Show',
|
||
allItems: 'All Items',
|
||
favorites: 'Favorites',
|
||
newestFirst: 'Newest First',
|
||
oldestFirst: 'Oldest First',
|
||
},
|
||
selection: {
|
||
action: 'Select',
|
||
selected: '{count} Selected',
|
||
share: 'Share Selected',
|
||
shareTitle: '{count} Media Items',
|
||
shareCopy: '{count} photos and videos shared from Photos.',
|
||
delete: 'Delete Selected',
|
||
deleteTitle: 'Delete Selected Media?',
|
||
deleteBody:
|
||
'The selected photos and videos will be permanently deleted.',
|
||
deleted: '{count} media items deleted.',
|
||
limit: 'You can select up to 50 media items.',
|
||
},
|
||
loading: 'Loading Photos...',
|
||
emptyTitle: 'No Photos or Videos',
|
||
emptyBody: 'Captures from Camera will appear here.',
|
||
photo: 'Photo',
|
||
video: 'Video',
|
||
photoAlt: 'Photo library image',
|
||
videoAlt: 'Photo library video',
|
||
today: 'Today',
|
||
yesterday: 'Yesterday',
|
||
addFavorite: 'Add to Favorites',
|
||
removeFavorite: 'Remove from Favorites',
|
||
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' },
|
||
import: {
|
||
action: 'Import',
|
||
title: 'Import',
|
||
chooseSource: 'Choose a website',
|
||
linkTitle: 'Import from Link',
|
||
linkBody: 'Paste a direct link to an image or video from this website.',
|
||
linkLabel: 'Image or video link',
|
||
linkPlaceholder: 'https://...',
|
||
linkCompleted: 'Media imported.',
|
||
loading: 'Loading media...',
|
||
emptyTitle: 'No Media',
|
||
emptyBody: 'This website has no media of this type.',
|
||
loadMore: 'Load More',
|
||
alreadyImported: 'Imported',
|
||
failed: 'Failed',
|
||
completed: 'Imported {imported} media.',
|
||
partial: 'Imported {imported} of {total} media.',
|
||
},
|
||
errors: {
|
||
cancelled: 'The media action was cancelled.',
|
||
capture_failed: 'Unable to capture the game view.',
|
||
invalid_import_request: 'The import request is invalid.',
|
||
invalid_import_media: 'This media item cannot be imported.',
|
||
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: 'Photos uploads are not configured.',
|
||
import_media_not_allowed: 'This media item is not allowed.',
|
||
import_media_too_large: 'This media item is too large.',
|
||
import_media_unavailable: 'This media item is no longer available.',
|
||
import_provider_failed: 'The website could not load its media.',
|
||
import_provider_unauthorized: 'The website credentials are invalid.',
|
||
import_source_not_found: 'This website is not registered.',
|
||
import_source_unavailable: 'This website is temporarily unavailable.',
|
||
invalid_import_url: 'Enter a valid HTTPS media link.',
|
||
import_url_not_allowed: 'This link is not from the selected website.',
|
||
import_url_unavailable: 'The linked media could not be reached.',
|
||
import_size_unavailable: 'The website did not provide the media size.',
|
||
not_found: 'The media item no longer exists.',
|
||
profile_photo_required:
|
||
'This is the last photo on your Flare profile. Add another profile photo before deleting it.',
|
||
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 Photos 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: 'Sky Cloud',
|
||
accountDetail: 'Important Data & Cloud',
|
||
accountLocalDetail: 'Not signed in',
|
||
accountCloudDetail: 'Important data syncs through Sky Cloud',
|
||
accountLoginBody:
|
||
'Sign in to keep your important data safe. Without a Sky Cloud 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',
|
||
development: 'Development',
|
||
skyUiKitchenSink: 'Sky UI Kitchen Sink',
|
||
skyUiKitchenSinkDescription:
|
||
'Interactive catalog of first-party Sky UI components',
|
||
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 Sky Cloud password to remove this device from the account.',
|
||
signOut: 'Sign Out',
|
||
reset: 'Transfer or Reset Phone',
|
||
transferOrReset: 'Transfer or Reset Phone',
|
||
transferOrResetDescription: 'Prepare this phone for a fresh setup',
|
||
resetHeroTitle: 'A clean start, without losing what follows you',
|
||
resetHeroBody:
|
||
'Erase local content and settings from this phone. Information attached to your SIM or stored in supported accounts remains available.',
|
||
erasedFromPhone: 'Erased From This Phone',
|
||
eraseDeviceSettings: 'Passcode, preferences and Home Screen layout',
|
||
eraseLocalContent: 'Photos, notes and content stored only locally',
|
||
eraseLocalApps: 'Downloaded apps and local app sessions',
|
||
keptSafe: 'Kept Safe',
|
||
keepSimData: 'SIM Card & Phone Number',
|
||
keepSimDataBody:
|
||
'Your number and SIM-backed communication remain on the SIM.',
|
||
keepCloudData: 'Account & Cloud Data',
|
||
keepCloudDataBody:
|
||
'Supported information remains in its app or Sky Cloud account.',
|
||
resetSetupAssistant:
|
||
'When erasing is complete, Setup Assistant starts automatically.',
|
||
factoryReset: 'Erase All Content and Settings',
|
||
factoryResetBody:
|
||
'This cannot be undone. Local settings, apps and unsynced content are erased. Your SIM number and account-backed data remain available.',
|
||
factoryResetProgress: 'Erasing Sky Phone',
|
||
factoryResetWarning:
|
||
'Keep this phone open. Your SIM and cloud data are safe.',
|
||
factoryResetSystemProcess: 'Secure system process',
|
||
factoryResetPreparing: 'Preparing secure reset',
|
||
factoryResetPreparingDetail: 'Checking device data',
|
||
factoryResetRemoving: 'Removing personal data',
|
||
factoryResetRemovingDetail: 'Clearing apps and local content',
|
||
factoryResetSecuring: 'Protecting linked services',
|
||
factoryResetSecuringDetail: 'Preserving SIM and cloud data',
|
||
factoryResetFinishing: 'Finishing setup',
|
||
factoryResetFinishingDetail: 'Preparing the welcome screen',
|
||
factoryResetSimSafe: 'SIM protected',
|
||
factoryResetCloudSafe: 'Cloud protected',
|
||
passcode: {
|
||
description:
|
||
'A passcode protects the contents of this phone. It stays with the device when the SIM or Sky Cloud 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: 'Sky Wallpapers',
|
||
wallpaperFromPhotos: 'Choose from Photos',
|
||
wallpaperFromPhotosDescription: 'Use one of your photos',
|
||
wallpaperFromCamera: 'Take Photo',
|
||
wallpaperFromCameraDescription: 'Create a new wallpaper',
|
||
wallpaperCustomUpload: 'Upload Custom Image',
|
||
wallpaperCustomUploadDescription: 'Use a verified HTTPS image link',
|
||
wallpaperHistory: 'Recent',
|
||
wallpaperCustom: 'Photo wallpaper',
|
||
wallpaperTarget: 'Wallpaper destination',
|
||
wallpaperHomeScreen: 'Home Screen',
|
||
wallpaperLockScreen: 'Lock Screen',
|
||
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',
|
||
aurora: 'Aurora',
|
||
ember: 'Ember',
|
||
ocean: 'Ocean',
|
||
sunrise: 'Sunrise',
|
||
violet: 'Violet',
|
||
forest: 'Forest',
|
||
cobalt: 'Cobalt',
|
||
rose: 'Rose',
|
||
sand: 'Sand',
|
||
graphite: 'Graphite',
|
||
prism: 'Prism',
|
||
},
|
||
},
|
||
},
|
||
ControlCenter: {
|
||
airplaneMode: 'Airplane Mode',
|
||
bluetooth: 'Bluetooth',
|
||
brightness: 'Brightness',
|
||
calculator: 'Calculator',
|
||
camera: 'Camera',
|
||
cellular: 'Cellular Data',
|
||
close: 'Close Control Center',
|
||
easyShareContact: 'Share my contact with nearby players',
|
||
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',
|
||
},
|
||
Setup: {
|
||
title: 'Sky Phone Setup Assistant',
|
||
ownerFallback: 'Sky Phone Owner',
|
||
getStarted: 'Get Started',
|
||
setUpLater: 'Set Up Later',
|
||
development: { skip: 'Skip setup' },
|
||
welcome: {
|
||
title: 'Welcome to Sky Phone',
|
||
eyebrow: 'Designed around you',
|
||
hello: 'hello',
|
||
hallo: 'hallo',
|
||
bonjour: 'bonjour',
|
||
body: 'Hello, {name}. Let’s make this phone unmistakably yours.',
|
||
private: 'Private by design',
|
||
personal: 'Made for you',
|
||
},
|
||
connection: {
|
||
eyebrow: 'Mobile Connection',
|
||
title: 'Your connection is ready',
|
||
body: 'Sky Phone automatically detects the SIM installed in this device.',
|
||
noSim: 'No SIM installed',
|
||
ready: 'Connected to the Sky network',
|
||
offline: 'Phone works offline; calls and messages require a SIM',
|
||
preserved:
|
||
'Your SIM number and SIM-backed conversations remain with the SIM, even if this phone is erased.',
|
||
},
|
||
cloud: {
|
||
eyebrow: 'Sky Cloud',
|
||
title: 'Keep important data with you',
|
||
body: 'Sign in to sync supported app data and recover it on another Sky Phone.',
|
||
signIn: 'Sign In',
|
||
create: 'Create Account',
|
||
signInTitle: 'Sign in to Sky Cloud',
|
||
createTitle: 'Create your Sky Cloud account',
|
||
signInBody:
|
||
'Access your protected data, purchases and settings on this phone.',
|
||
createBody:
|
||
'Choose your personal iFruit address and keep important data available across devices.',
|
||
accountPreview: 'Your Sky Cloud ID',
|
||
accountName: 'Account name',
|
||
addressPlaceholder: 'alex.morgan',
|
||
email: 'Sky Cloud Email',
|
||
password: 'Password',
|
||
confirmPassword: 'Confirm Password',
|
||
passwordMismatch: 'The passwords do not match.',
|
||
strength0: 'At least 6 characters',
|
||
strength1: 'Basic password',
|
||
strength2: 'Good password',
|
||
strength3: 'Strong password',
|
||
strength4: 'Excellent password',
|
||
signInAction: 'Sign In Securely',
|
||
createAction: 'Create Sky Cloud Account',
|
||
securityNote: 'Encrypted connection · Your password stays private',
|
||
connected: 'Sky Cloud connected',
|
||
invalid: 'Enter a valid email and a password with at least 6 characters.',
|
||
errors: {
|
||
invalid_email: 'Enter a valid Sky Cloud email.',
|
||
invalid_password: 'Your password must contain at least 6 characters.',
|
||
invalid_credentials: 'Email or password is incorrect.',
|
||
email_taken: 'This Sky Cloud email is already registered.',
|
||
rate_limited: 'Too many attempts. Try again shortly.',
|
||
request_failed: 'Sky Cloud is temporarily unavailable.',
|
||
},
|
||
},
|
||
security: {
|
||
eyebrow: 'Privacy & Security',
|
||
title: 'Protect this phone',
|
||
body: 'Choose a four- or six-digit passcode to keep your apps and local information private.',
|
||
local:
|
||
'The passcode belongs to this physical phone and is never transferred with the SIM.',
|
||
create: 'Create Passcode',
|
||
createSelected: 'Create {count}-Digit Passcode',
|
||
lengthTitle: 'Choose passcode length',
|
||
fourDigit: '4-Digit Code',
|
||
sixDigit: '6-Digit Code',
|
||
enter: 'Create a Passcode',
|
||
confirm: 'Verify Your Passcode',
|
||
codeHint: 'Enter a memorable six-digit code.',
|
||
fourDigitHint: 'Enter a memorable four-digit code.',
|
||
sixDigitHint: 'Enter a memorable six-digit code.',
|
||
mismatch: 'The passcodes did not match. Try again.',
|
||
failed: 'The passcode could not be saved.',
|
||
},
|
||
appearance: {
|
||
eyebrow: 'Display',
|
||
title: 'Choose your appearance',
|
||
body: 'Automatic follows your system. You can change this at any time in Settings.',
|
||
},
|
||
performance: {
|
||
eyebrow: 'Performance',
|
||
title: 'Choose how your phone feels',
|
||
body: 'Balance responsiveness and visual effects for your FiveM setup.',
|
||
performance: 'Fastest response with optimized, blur-free glass.',
|
||
ultimate: 'Rich depth, live blur and the complete glass experience.',
|
||
changeLater: 'You can switch modes later under Settings › Appearance.',
|
||
},
|
||
wallpaper: {
|
||
eyebrow: 'Personalize',
|
||
title: 'Make it yours',
|
||
body: 'Choose one of twelve crafted Sky Phone backgrounds.',
|
||
},
|
||
notifications: {
|
||
eyebrow: 'Stay Informed',
|
||
title: 'Notifications your way',
|
||
body: 'Choose how apps may reach you. Critical alarms and calls stay available.',
|
||
allow: 'Allow App Notifications',
|
||
allowBody: 'Show banners, lock-screen updates and badges.',
|
||
sounds: 'Notification Sounds',
|
||
soundsBody: 'Play a subtle sound for incoming updates.',
|
||
},
|
||
apps: {
|
||
eyebrow: 'Suggested for You',
|
||
title: 'Start with your favorites',
|
||
body: 'Choose optional apps. Every essential phone app is already included.',
|
||
install: 'Add {count} Apps',
|
||
descriptions: {
|
||
banking: 'Secure city banking',
|
||
garage: 'Your vehicles at a glance',
|
||
skyride: 'Request a ride',
|
||
citymarkt: 'Shop local listings',
|
||
picstagram: 'Share photos',
|
||
snake: 'A timeless game',
|
||
},
|
||
},
|
||
ready: {
|
||
eyebrow: 'Setup Complete',
|
||
title: 'Welcome, {name}',
|
||
body: 'Your Sky Phone is configured and ready. Your choices can always be refined in Settings.',
|
||
localOnly: 'Stored on this phone',
|
||
enter: 'Enter Sky Phone',
|
||
review: 'Review Setup',
|
||
saveFailed: 'Setup could not be saved. Try again.',
|
||
saving: 'Saving setup',
|
||
},
|
||
},
|
||
Common: {
|
||
add: 'Add',
|
||
cancel: 'Cancel',
|
||
clear: 'Clear',
|
||
close: 'Close',
|
||
continue: 'Continue',
|
||
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',
|
||
signOut: 'Sign Out',
|
||
signingOut: 'Signing Out...',
|
||
signOutTitle: 'Sign out of {app}?',
|
||
signOutBody:
|
||
'You will only be signed out of {app}. Your other iFruit apps stay signed in.',
|
||
signOutFailed: 'Could not sign out. Please try again.',
|
||
appAuth: {
|
||
eyebrow: 'iFruit account',
|
||
title: 'Continue to {app}',
|
||
body: 'Use your iFruit email and password. This login applies only to {app}.',
|
||
login: 'Login',
|
||
register: 'Register',
|
||
email: 'iFruit email',
|
||
password: 'Password',
|
||
confirm: 'Confirm password',
|
||
loginAction: 'Log in',
|
||
registerAction: 'Create account',
|
||
errors: {
|
||
invalid_email: 'Enter a valid iFruit email.',
|
||
invalid_password: 'Password must be 6–64 characters.',
|
||
invalid_credentials: 'Email or password is incorrect.',
|
||
email_taken: 'That iFruit email is already registered.',
|
||
rate_limited: 'Too many attempts. Try again in a minute.',
|
||
default: 'The account request failed.',
|
||
},
|
||
},
|
||
use: 'Use',
|
||
},
|
||
Notifications: {
|
||
clearAll: 'Clear All',
|
||
clear: 'Clear',
|
||
now: 'now',
|
||
open: 'Open notification',
|
||
},
|
||
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.',
|
||
},
|
||
},
|
||
HardwareButtons: {
|
||
lock: 'Lock phone',
|
||
mute: 'Mute ringtone and notifications',
|
||
unlock: 'Unlock phone',
|
||
unmute: 'Unmute ringtone and notifications',
|
||
volumeDown: 'Volume down',
|
||
volumeLevel: 'Volume {value} percent',
|
||
volumeUp: 'Volume up',
|
||
},
|
||
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',
|
||
folders: {
|
||
defaultName: 'Folder',
|
||
close: 'Close folder',
|
||
rename: 'Rename Folder',
|
||
name: 'Folder name',
|
||
pages: 'Folder 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.',
|
||
range: 'H: {high}° L: {low}°',
|
||
},
|
||
music: {
|
||
name: 'Now Playing',
|
||
description: 'Music controls and the current track.',
|
||
empty: 'No recently played music',
|
||
},
|
||
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>,
|
||
deviceSessionToken: null as string | null,
|
||
isOpen: false,
|
||
lang: 'en',
|
||
launchOrigin: null as AppLaunchOrigin | null,
|
||
fallbackLocales: defaultLocales,
|
||
locales: defaultLocales,
|
||
preferences: cloneJsonData(DEFAULT_PHONE_PREFERENCES),
|
||
persistenceGeneration: 0,
|
||
persistenceSession: ++nextPersistenceSession,
|
||
player: {
|
||
firstName: '',
|
||
lastName: '',
|
||
} as DeviceBootstrap['player'],
|
||
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
|
||
},
|
||
setLocale(
|
||
lang: string,
|
||
locales: LocaleTree,
|
||
fallbackLocales: LocaleTree,
|
||
): void {
|
||
this.lang = lang
|
||
this.locales = locales
|
||
this.fallbackLocales = fallbackLocales
|
||
},
|
||
open(payload: PhoneOpenPayload = {}): void {
|
||
const nextImei = payload.device?.imei ?? this.device?.imei ?? null
|
||
const nextToken = payload.token ?? this.deviceSessionToken
|
||
if (
|
||
nextImei !== (this.device?.imei ?? null) ||
|
||
nextToken !== this.deviceSessionToken
|
||
) {
|
||
this.persistenceGeneration += 1
|
||
}
|
||
this.deviceSessionToken = nextToken
|
||
this.lang = payload.lang ?? 'en'
|
||
this.fallbackLocales = payload.fallbackLocales ?? defaultLocales
|
||
this.locales = payload.locales ?? this.fallbackLocales
|
||
if (payload.device) this.hydrateDevice(payload.device)
|
||
if (payload.player) this.player = payload.player
|
||
this.security = payload.security ?? {
|
||
enabled: false,
|
||
length: null,
|
||
lockedUntil: 0,
|
||
}
|
||
this.isOpen = true
|
||
},
|
||
endDeviceSession(): void {
|
||
this.close()
|
||
this.player = { firstName: '', lastName: '' }
|
||
if (this.deviceSessionToken !== null) {
|
||
this.deviceSessionToken = null
|
||
this.persistenceGeneration += 1
|
||
}
|
||
},
|
||
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): Promise<boolean> {
|
||
const imei = this.device?.imei
|
||
if (!imei) {
|
||
console.error(
|
||
`[Phone persistence] Could not save ${namespace} without an active device.`,
|
||
)
|
||
return Promise.resolve(false)
|
||
}
|
||
const generation = this.persistenceGeneration
|
||
const session = this.persistenceSession
|
||
const token = this.deviceSessionToken
|
||
const queuedPayload = cloneJsonData(payload)
|
||
const queueKey = `${session}:${generation}:${imei}:${namespace}`
|
||
const isCurrentScope = (): boolean =>
|
||
this.persistenceSession === session &&
|
||
this.persistenceGeneration === generation &&
|
||
this.device?.imei === imei &&
|
||
this.deviceSessionToken === token
|
||
const previous = namespaceQueues.get(queueKey) ?? Promise.resolve()
|
||
const queued = previous.then(async () => {
|
||
if (!isCurrentScope()) return false
|
||
const response = await nuiCall<{ revision: number }>('device:save', {
|
||
imei,
|
||
namespace,
|
||
payload: queuedPayload,
|
||
revision: this.deviceRevisions[namespace] ?? 0,
|
||
sessionToken: token,
|
||
})
|
||
if (
|
||
isCurrentScope() &&
|
||
response.success &&
|
||
Number.isInteger(response.data?.revision) &&
|
||
Number(response.data?.revision) >= 0
|
||
) {
|
||
this.deviceRevisions[namespace] = Number(response.data?.revision)
|
||
return true
|
||
}
|
||
if (isCurrentScope()) {
|
||
console.error(
|
||
`[Phone persistence] Could not save ${namespace}: ${response.error ?? 'request_failed'}`,
|
||
)
|
||
}
|
||
return false
|
||
})
|
||
const tracked = queued.finally(() => {
|
||
if (namespaceQueues.get(queueKey) === tracked)
|
||
namespaceQueues.delete(queueKey)
|
||
})
|
||
namespaceQueues.set(queueKey, tracked)
|
||
return tracked
|
||
},
|
||
async flushDevicePersistence(): Promise<void> {
|
||
const imei = this.device?.imei
|
||
if (!imei) return
|
||
const queuePrefix = `${this.persistenceSession}:${this.persistenceGeneration}:${imei}:`
|
||
while (true) {
|
||
const activeQueues = [...namespaceQueues.entries()]
|
||
.filter(([key]) => key.startsWith(queuePrefix))
|
||
.map(([, queue]) => queue)
|
||
if (!activeQueues.length) return
|
||
await Promise.all(activeQueues)
|
||
}
|
||
},
|
||
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
|
||
},
|
||
ensureAppNotificationPreferences(appIds: LaunchablePhoneAppId[]): void {
|
||
ensureAppNotificationPreferences(this.preferences, appIds)
|
||
},
|
||
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] = (
|
||
key === 'phoneScale' ? clampPhoneScale(Number(value)) : value
|
||
) as PhonePreferencesV1['settings'][K]
|
||
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)
|
||
},
|
||
setAllAppNotifications(enabled: boolean, sounds: boolean): void {
|
||
for (const preferences of Object.values(
|
||
this.preferences.settings.notifications,
|
||
)) {
|
||
preferences.enabled = enabled
|
||
preferences.sounds = enabled && sounds
|
||
}
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
setSetupStep(step: number): void {
|
||
this.preferences.settings.setupStep = Math.min(
|
||
PHONE_SETUP_LAST_STEP,
|
||
Math.max(0, Math.floor(step)),
|
||
)
|
||
this.saveDeviceNamespace('settings', this.preferences)
|
||
},
|
||
async completeSetup(): Promise<boolean> {
|
||
const completedPreferences = cloneJsonData(this.preferences)
|
||
completedPreferences.settings.setupCompleted = true
|
||
completedPreferences.settings.setupStep = PHONE_SETUP_LAST_STEP
|
||
const saved = await this.saveDeviceNamespace(
|
||
'settings',
|
||
completedPreferences,
|
||
)
|
||
if (saved) this.preferences = completedPreferences
|
||
return saved
|
||
},
|
||
resetAfterFactoryReset(): void {
|
||
this.persistenceGeneration += 1
|
||
this.preferences = cloneJsonData(DEFAULT_PHONE_PREFERENCES)
|
||
this.deviceRevisions = {}
|
||
if (this.device) this.device.data = {}
|
||
this.security = { enabled: false, length: null, lockedUntil: 0 }
|
||
},
|
||
setSystemDarkMode(value: boolean): void {
|
||
this.systemDarkMode = value
|
||
},
|
||
setWallpaper(
|
||
wallpaper: WallpaperId,
|
||
imageUrl: string | null = null,
|
||
target: WallpaperTarget | 'both' = 'home',
|
||
): void {
|
||
const normalizedImageUrl =
|
||
wallpaper === 'custom' ? imageUrl?.trim() : null
|
||
if (wallpaper === 'custom' && !normalizedImageUrl) {
|
||
throw new Error('Custom wallpapers require a photo URL.')
|
||
}
|
||
|
||
if (target === 'home' || target === 'both') {
|
||
this.preferences.settings.wallpaper = wallpaper
|
||
this.preferences.settings.wallpaperImageUrl = normalizedImageUrl ?? null
|
||
}
|
||
if (target === 'lock' || target === 'both') {
|
||
this.preferences.settings.lockWallpaper = wallpaper
|
||
this.preferences.settings.lockWallpaperImageUrl =
|
||
normalizedImageUrl ?? null
|
||
}
|
||
this.preferences.settings.wallpaperHistory = [
|
||
{ imageUrl: normalizedImageUrl ?? null, wallpaper },
|
||
...this.preferences.settings.wallpaperHistory.filter((entry) =>
|
||
wallpaper === 'custom'
|
||
? entry.wallpaper !== 'custom' ||
|
||
entry.imageUrl !== normalizedImageUrl
|
||
: entry.wallpaper !== wallpaper,
|
||
),
|
||
].slice(0, 4)
|
||
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 english = getByPath(this.fallbackLocales, path)
|
||
const emergencyFallback = getByPath(defaultLocales, path)
|
||
const value =
|
||
typeof translated === 'string'
|
||
? translated
|
||
: typeof english === 'string'
|
||
? english
|
||
: typeof emergencyFallback === 'string'
|
||
? emergencyFallback
|
||
: path
|
||
return Object.entries(replacements).reduce(
|
||
(result, [key, replacement]) =>
|
||
result.split(`{${key}}`).join(replacement),
|
||
value,
|
||
)
|
||
},
|
||
},
|
||
})
|