@oxyhq/core 12.9.0 → 12.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +22 -12
- package/dist/cjs/crypto/keyManager.js +89 -0
- package/dist/cjs/i18n/locales/ar-SA.json +4 -0
- package/dist/cjs/i18n/locales/ca-ES.json +4 -0
- package/dist/cjs/i18n/locales/de-DE.json +4 -0
- package/dist/cjs/i18n/locales/en-US.json +69 -3
- package/dist/cjs/i18n/locales/es-ES.json +68 -2
- package/dist/cjs/i18n/locales/fr-FR.json +4 -0
- package/dist/cjs/i18n/locales/it-IT.json +4 -0
- package/dist/cjs/i18n/locales/ja-JP.json +4 -0
- package/dist/cjs/i18n/locales/ko-KR.json +4 -0
- package/dist/cjs/i18n/locales/locales/ar-SA.json +4 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +4 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +4 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +69 -3
- package/dist/cjs/i18n/locales/locales/es-ES.json +68 -2
- package/dist/cjs/i18n/locales/locales/fr-FR.json +4 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +4 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +4 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +4 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +4 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +4 -0
- package/dist/cjs/i18n/locales/pt-PT.json +4 -0
- package/dist/cjs/i18n/locales/zh-CN.json +4 -0
- package/dist/cjs/index.js +7 -4
- package/dist/cjs/mixins/OxyServices.auth.js +51 -20
- package/dist/cjs/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/cjs/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/cjs/mixins/OxyServices.user.js +6 -0
- package/dist/cjs/session/accountDialogController.js +7 -5
- package/dist/cjs/utils/commonsApproval.js +31 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +22 -12
- package/dist/esm/crypto/keyManager.js +89 -0
- package/dist/esm/i18n/locales/ar-SA.json +4 -0
- package/dist/esm/i18n/locales/ca-ES.json +4 -0
- package/dist/esm/i18n/locales/de-DE.json +4 -0
- package/dist/esm/i18n/locales/en-US.json +69 -3
- package/dist/esm/i18n/locales/es-ES.json +68 -2
- package/dist/esm/i18n/locales/fr-FR.json +4 -0
- package/dist/esm/i18n/locales/it-IT.json +4 -0
- package/dist/esm/i18n/locales/ja-JP.json +4 -0
- package/dist/esm/i18n/locales/ko-KR.json +4 -0
- package/dist/esm/i18n/locales/locales/ar-SA.json +4 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +4 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +4 -0
- package/dist/esm/i18n/locales/locales/en-US.json +69 -3
- package/dist/esm/i18n/locales/locales/es-ES.json +68 -2
- package/dist/esm/i18n/locales/locales/fr-FR.json +4 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +4 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +4 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +4 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +4 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +4 -0
- package/dist/esm/i18n/locales/pt-PT.json +4 -0
- package/dist/esm/i18n/locales/zh-CN.json +4 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/mixins/OxyServices.auth.js +48 -19
- package/dist/esm/mixins/OxyServices.connectedApps.js +6 -1
- package/dist/esm/mixins/OxyServices.deviceBoot.js +9 -2
- package/dist/esm/mixins/OxyServices.identityBackup.js +11 -0
- package/dist/esm/mixins/OxyServices.user.js +6 -0
- package/dist/esm/session/accountDialogController.js +7 -5
- package/dist/esm/utils/commonsApproval.js +27 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +14 -0
- package/dist/types/crypto/keyManager.d.ts +33 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/mixins/OxyServices.auth.d.ts +4 -4
- package/dist/types/session/accountDialogController.d.ts +7 -2
- package/dist/types/utils/commonsApproval.d.ts +13 -0
- package/package.json +3 -3
- package/src/HttpService.ts +37 -14
- package/src/__tests__/httpServiceAuthSelfAwait.test.ts +108 -0
- package/src/__tests__/httpServiceQueueDeadlock.test.ts +128 -0
- package/src/crypto/__tests__/keyManager.recoveryMnemonic.test.ts +138 -0
- package/src/crypto/keyManager.ts +99 -0
- package/src/i18n/locales/ar-SA.json +4 -0
- package/src/i18n/locales/ca-ES.json +4 -0
- package/src/i18n/locales/de-DE.json +4 -0
- package/src/i18n/locales/en-US.json +69 -3
- package/src/i18n/locales/es-ES.json +68 -2
- package/src/i18n/locales/fr-FR.json +4 -0
- package/src/i18n/locales/it-IT.json +4 -0
- package/src/i18n/locales/ja-JP.json +4 -0
- package/src/i18n/locales/ko-KR.json +4 -0
- package/src/i18n/locales/pt-PT.json +4 -0
- package/src/i18n/locales/zh-CN.json +4 -0
- package/src/index.ts +7 -1
- package/src/mixins/OxyServices.auth.ts +48 -24
- package/src/mixins/OxyServices.connectedApps.ts +6 -1
- package/src/mixins/OxyServices.deviceBoot.ts +7 -2
- package/src/mixins/OxyServices.identityBackup.ts +13 -0
- package/src/mixins/OxyServices.user.ts +6 -0
- package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +4 -2
- package/src/mixins/__tests__/commonsSignIn.test.ts +4 -4
- package/src/mixins/__tests__/identityBackup.test.ts +57 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +152 -0
- package/src/mixins/__tests__/privacyCacheInvalidation.test.ts +6 -0
- package/src/mixins/__tests__/webauthnAuth.test.ts +6 -6
- package/src/session/__tests__/accountDialogController.test.ts +3 -3
- package/src/session/accountDialogController.ts +7 -6
- package/src/utils/__tests__/commonsApproval.test.ts +60 -0
- package/src/utils/commonsApproval.ts +39 -0
package/dist/cjs/HttpService.js
CHANGED
|
@@ -271,22 +271,28 @@ class HttpService {
|
|
|
271
271
|
}
|
|
272
272
|
this.requestMetrics.cacheMisses++;
|
|
273
273
|
}
|
|
274
|
+
// Resolve the auth preflight OUTSIDE the queue slot. A slot represents
|
|
275
|
+
// NETWORK OCCUPANCY only. `getAuthHeader` may await the single-flight token
|
|
276
|
+
// refresh/mint — itself a request — so if a slot-holding request awaited it
|
|
277
|
+
// here and every slot were held by requests all waiting on that same mint,
|
|
278
|
+
// the mint could never acquire a slot to run (systemic deadlock). Resolving
|
|
279
|
+
// it before enqueue means auth-blocked requests hold NO slot while the shared
|
|
280
|
+
// mint runs. `skipAuth` requests (the body-authenticated mint) send NO bearer
|
|
281
|
+
// and skip the near-expiry preflight — see RequestOptions.skipAuth. The
|
|
282
|
+
// 401/CSRF retry re-enters request() with a fresh config, so it re-resolves
|
|
283
|
+
// these here with the refreshed token / cleared CSRF.
|
|
284
|
+
const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
285
|
+
const authHeader = config.skipAuth ? null : await this.getAuthHeader();
|
|
286
|
+
// CSRF protects cookie-authenticated browser writes. Bearer-authenticated SDK
|
|
287
|
+
// clients are not vulnerable to ambient-cookie CSRF, and linked app APIs
|
|
288
|
+
// should not need to implement a duplicate `/csrf-token` route.
|
|
289
|
+
const csrfToken = isStateChangingMethod && !authHeader ? await this.fetchCsrfToken() : null;
|
|
274
290
|
// Request function
|
|
275
291
|
const requestFn = async () => {
|
|
276
292
|
const startTime = Date.now();
|
|
277
293
|
try {
|
|
278
294
|
// Build URL with params
|
|
279
295
|
const fullUrl = this.buildURL(url, params);
|
|
280
|
-
// Get auth token (with auto-refresh). `skipAuth` requests (the
|
|
281
|
-
// body-authenticated refresh endpoint) send NO bearer and skip the
|
|
282
|
-
// near-expiry preflight — see RequestOptions.skipAuth for the deadlock
|
|
283
|
-
// this avoids.
|
|
284
|
-
const authHeader = config.skipAuth ? null : await this.getAuthHeader();
|
|
285
|
-
// CSRF protects cookie-authenticated browser writes. Bearer-authenticated
|
|
286
|
-
// SDK clients are not vulnerable to ambient-cookie CSRF, and linked app
|
|
287
|
-
// APIs should not need to implement a duplicate `/csrf-token` route.
|
|
288
|
-
const isStateChangingMethod = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
289
|
-
const csrfToken = isStateChangingMethod && !authHeader ? await this.fetchCsrfToken() : null;
|
|
290
296
|
// Determine if data is FormData using robust detection
|
|
291
297
|
const isFormData = this.isFormData(data);
|
|
292
298
|
// Make fetch request
|
|
@@ -493,8 +499,12 @@ class HttpService {
|
|
|
493
499
|
const finalRequest = dedupeKey
|
|
494
500
|
? () => this.deduplicator.deduplicate(dedupeKey, requestWithRetry)
|
|
495
501
|
: requestWithRetry;
|
|
496
|
-
// Execute request
|
|
497
|
-
|
|
502
|
+
// Execute the request. Control-plane calls the auth lane depends on
|
|
503
|
+
// (`bypassQueue`, e.g. the device-secret mint) run DIRECTLY — a queued mint
|
|
504
|
+
// could never acquire a slot when every slot is parked awaiting it.
|
|
505
|
+
const result = config.bypassQueue
|
|
506
|
+
? await finalRequest()
|
|
507
|
+
: await this.requestQueue.enqueue(finalRequest);
|
|
498
508
|
// Cache the result if caching is enabled
|
|
499
509
|
if (cache && cacheKey && result) {
|
|
500
510
|
this.cache.set(cacheKey, result, cacheTTL);
|
|
@@ -122,6 +122,24 @@ const V2_STORAGE_KEYS = {
|
|
|
122
122
|
BACKUP_PUBLIC_KEY: 'oxy_identity_backup_public_key_v2',
|
|
123
123
|
BACKUP_TIMESTAMP: 'oxy_identity_backup_timestamp_v2',
|
|
124
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Dedicated keychain slot for the recovery mnemonic (the 12-word phrase).
|
|
127
|
+
*
|
|
128
|
+
* Stored under its OWN keychain service — distinct from the v2 primary, backup,
|
|
129
|
+
* and shared slots — so it shares an AndroidKeyStore key with none of them
|
|
130
|
+
* (blast-radius isolation, same rationale as the v2 primary/backup split).
|
|
131
|
+
* Written `WHEN_UNLOCKED_THIS_DEVICE_ONLY` and NEVER exported off-device: it
|
|
132
|
+
* exists solely so the user can RE-READ their phrase from Settings on the SAME
|
|
133
|
+
* device that generated/imported it.
|
|
134
|
+
*
|
|
135
|
+
* This is convenience persistence, NOT a recovery mechanism — a keystore death
|
|
136
|
+
* wipes it alongside the keys, exactly like the private key itself. The user's
|
|
137
|
+
* written-down phrase remains the sole out-of-band recovery path. The mnemonic
|
|
138
|
+
* lives ONLY in this slot: it is never mirrored into the identity marker,
|
|
139
|
+
* {@link KeyManager.getIdentityStatus}, logs, or any exported bundle.
|
|
140
|
+
*/
|
|
141
|
+
const RECOVERY_MNEMONIC_KEYCHAIN_SERVICE = 'oxy_identity_mnemonic';
|
|
142
|
+
const RECOVERY_MNEMONIC_STORAGE_KEY = 'oxy_identity_mnemonic_v1';
|
|
125
143
|
/**
|
|
126
144
|
* Advisory AsyncStorage fast-path flag: set once the v2 slots own the identity.
|
|
127
145
|
* Re-derivable (its loss just re-runs the cheap slot check), so it lives in
|
|
@@ -1476,6 +1494,73 @@ class KeyManager {
|
|
|
1476
1494
|
throw new IdentityUnavailableError('Failed to read identity from secure storage.', error);
|
|
1477
1495
|
}
|
|
1478
1496
|
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Persist the recovery mnemonic (the 12-word phrase) into its dedicated,
|
|
1499
|
+
* device-only keychain slot so the user can re-reveal it from Settings after
|
|
1500
|
+
* onboarding.
|
|
1501
|
+
*
|
|
1502
|
+
* Called best-effort at identity creation/import, where the phrase is already
|
|
1503
|
+
* in memory: a failure to persist it must NEVER fail the identity itself, so
|
|
1504
|
+
* callers deliberately swallow the thrown error (logging it). Storage errors
|
|
1505
|
+
* throw {@link IdentityUnavailableError} — same "cannot determine" semantics as
|
|
1506
|
+
* the other getters — so a caller MAY observe/log the failure.
|
|
1507
|
+
*
|
|
1508
|
+
* The mnemonic is stored ONLY here — never in the marker, `getIdentityStatus`,
|
|
1509
|
+
* logs, or any exported bundle.
|
|
1510
|
+
*/
|
|
1511
|
+
static async storeRecoveryMnemonic(mnemonic) {
|
|
1512
|
+
if (isWebPlatform()) {
|
|
1513
|
+
return; // Identity storage is only available on native platforms
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
const store = await initSecureStore();
|
|
1517
|
+
await store.setItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, mnemonic, KeyManager._privateWriteOpts(store, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
if ((0, logger_1.isDev)()) {
|
|
1521
|
+
logger_1.logger.warn('Failed to persist recovery mnemonic', { component: 'KeyManager' }, error);
|
|
1522
|
+
}
|
|
1523
|
+
throw new IdentityUnavailableError('Failed to persist recovery mnemonic.', error);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Read the stored recovery mnemonic for re-reveal in Settings.
|
|
1528
|
+
*
|
|
1529
|
+
* Returns the phrase, or `null` when a read SUCCEEDS and finds none — the
|
|
1530
|
+
* expected result for any identity created/imported before this feature
|
|
1531
|
+
* existed, since the phrase was never captured for those. THROWS
|
|
1532
|
+
* {@link IdentityUnavailableError} when storage is unreadable (keychain locked
|
|
1533
|
+
* / module load failure), matching {@link getPublicKey}'s contract — a thrown
|
|
1534
|
+
* read is never flattened to `null`, so a caller distinguishes "phrase was
|
|
1535
|
+
* never stored" from "keychain temporarily locked, retry".
|
|
1536
|
+
*/
|
|
1537
|
+
static async getRecoveryMnemonic() {
|
|
1538
|
+
if (isWebPlatform()) {
|
|
1539
|
+
return null; // Identity storage is only available on native platforms
|
|
1540
|
+
}
|
|
1541
|
+
try {
|
|
1542
|
+
const store = await initSecureStore();
|
|
1543
|
+
return await store.getItemAsync(RECOVERY_MNEMONIC_STORAGE_KEY, KeyManager._slotOpts(RECOVERY_MNEMONIC_KEYCHAIN_SERVICE));
|
|
1544
|
+
}
|
|
1545
|
+
catch (error) {
|
|
1546
|
+
if ((0, logger_1.isDev)()) {
|
|
1547
|
+
logger_1.logger.warn('Failed to read recovery mnemonic', { component: 'KeyManager' }, error);
|
|
1548
|
+
}
|
|
1549
|
+
throw new IdentityUnavailableError('Failed to read recovery mnemonic from secure storage.', error);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Delete the stored recovery mnemonic. Best-effort: a delete failure is logged
|
|
1554
|
+
* and swallowed, never thrown — it runs inside the identity-deletion path where
|
|
1555
|
+
* an unreadable keychain must not abort the wider teardown.
|
|
1556
|
+
*/
|
|
1557
|
+
static async deleteRecoveryMnemonic() {
|
|
1558
|
+
if (isWebPlatform()) {
|
|
1559
|
+
return; // Identity storage is only available on native platforms
|
|
1560
|
+
}
|
|
1561
|
+
const store = await initSecureStore();
|
|
1562
|
+
await KeyManager._bestEffortDelete(store, RECOVERY_MNEMONIC_STORAGE_KEY, RECOVERY_MNEMONIC_KEYCHAIN_SERVICE);
|
|
1563
|
+
}
|
|
1479
1564
|
/**
|
|
1480
1565
|
* Check if a complete, parseable identity exists on this device.
|
|
1481
1566
|
*
|
|
@@ -1691,6 +1776,10 @@ class KeyManager {
|
|
|
1691
1776
|
await KeyManager._bestEffortDeleteV2Primary(store);
|
|
1692
1777
|
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PRIVATE_KEY);
|
|
1693
1778
|
await KeyManager._bestEffortDelete(store, STORAGE_KEYS.PUBLIC_KEY);
|
|
1779
|
+
// Always drop the stored recovery mnemonic — it is scoped to the identity
|
|
1780
|
+
// being deleted, so a leftover would let Settings reveal a stale phrase for
|
|
1781
|
+
// an identity that no longer exists (or a DIFFERENT one after re-onboarding).
|
|
1782
|
+
await KeyManager.deleteRecoveryMnemonic();
|
|
1694
1783
|
// Also clear backups + the shared slot on force deletion, so a deleted
|
|
1695
1784
|
// identity cannot be resurrected from any recovery source.
|
|
1696
1785
|
if (force) {
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "حساباتك"
|
|
117
|
+
},
|
|
118
|
+
"loading": "جارٍ تحميل الحسابات...",
|
|
115
119
|
"qrHeadline": "سجّل الدخول بهوية Oxy الخاصة بك",
|
|
116
120
|
"signInWithOxy": "تسجيل الدخول باستخدام Oxy",
|
|
117
121
|
"useIdentityOnDevice": "لا تملك Commons؟ استخدم مفتاح مرور على هذا الجهاز بدلاً من ذلك",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "Els teus comptes"
|
|
117
|
+
},
|
|
118
|
+
"loading": "S'estan carregant els comptes...",
|
|
115
119
|
"qrHeadline": "Inicia sessió amb la teva identitat d'Oxy",
|
|
116
120
|
"signInWithOxy": "Inicia sessió amb Oxy",
|
|
117
121
|
"useIdentityOnDevice": "No tens Commons? Fes servir una clau d'accés en aquest dispositiu",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "Ihre Konten"
|
|
117
|
+
},
|
|
118
|
+
"loading": "Konten werden geladen...",
|
|
115
119
|
"qrHeadline": "Melden Sie sich mit Ihrer Oxy-Identität an",
|
|
116
120
|
"signInWithOxy": "Mit Oxy anmelden",
|
|
117
121
|
"useIdentityOnDevice": "Kein Commons? Verwenden Sie stattdessen einen Passkey auf diesem Gerät",
|
|
@@ -421,7 +421,8 @@
|
|
|
421
421
|
"getCommons": "Get Commons",
|
|
422
422
|
"commonsNotInstalled": "Don't have Commons? Get the app to sign in with your Oxy ID.",
|
|
423
423
|
"showQrAnyway": "I have Commons on another device",
|
|
424
|
-
"switchWhileSignedInAs": "Switch account, signed in as {{name}}"
|
|
424
|
+
"switchWhileSignedInAs": "Switch account, signed in as {{name}}",
|
|
425
|
+
"manageOnDevice": "Manage accounts on this device"
|
|
425
426
|
},
|
|
426
427
|
"reputation": {
|
|
427
428
|
"faq": {
|
|
@@ -754,6 +755,8 @@
|
|
|
754
755
|
"saving": "Saving…",
|
|
755
756
|
"helper": "The cropped circle is what will appear on your profile. Pinch to zoom, drag to position.",
|
|
756
757
|
"zoom": "{{value}}×",
|
|
758
|
+
"zoomIn": "Zoom in",
|
|
759
|
+
"zoomOut": "Zoom out",
|
|
757
760
|
"a11yImage": "Crop preview. Pinch to zoom and drag to reposition the image.",
|
|
758
761
|
"a11yReset": "Reset crop to default position",
|
|
759
762
|
"a11yResetAnnouncement": "Crop reset"
|
|
@@ -774,6 +777,47 @@
|
|
|
774
777
|
},
|
|
775
778
|
"changeAvatar": "Change avatar"
|
|
776
779
|
},
|
|
780
|
+
"changeAvatar": {
|
|
781
|
+
"title": "Change profile picture",
|
|
782
|
+
"sources": {
|
|
783
|
+
"upload": {
|
|
784
|
+
"title": "Upload from device",
|
|
785
|
+
"description": "Choose a photo from your gallery"
|
|
786
|
+
},
|
|
787
|
+
"camera": {
|
|
788
|
+
"title": "Take a picture",
|
|
789
|
+
"description": "Use your camera"
|
|
790
|
+
},
|
|
791
|
+
"files": {
|
|
792
|
+
"title": "My Oxy files",
|
|
793
|
+
"description": "Pick an image you already uploaded"
|
|
794
|
+
},
|
|
795
|
+
"remove": {
|
|
796
|
+
"title": "Remove current photo",
|
|
797
|
+
"description": "Go back to your initials"
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
"remove": {
|
|
801
|
+
"confirmTitle": "Remove profile picture?",
|
|
802
|
+
"confirmMessage": "Your profile will show your initials instead. You can add a new photo at any time.",
|
|
803
|
+
"confirmLabel": "Remove"
|
|
804
|
+
},
|
|
805
|
+
"permission": {
|
|
806
|
+
"libraryDenied": "Oxy needs access to your photos to upload a picture.",
|
|
807
|
+
"cameraDenied": "Oxy needs access to your camera to take a picture.",
|
|
808
|
+
"openSettings": "Open settings"
|
|
809
|
+
},
|
|
810
|
+
"errors": {
|
|
811
|
+
"pickerUnavailable": "Photo picking is not available on this device.",
|
|
812
|
+
"cameraUnavailable": "The camera is not available on this device.",
|
|
813
|
+
"pickFailed": "Could not open the photo picker.",
|
|
814
|
+
"cameraFailed": "Could not open the camera.",
|
|
815
|
+
"loadImageFailed": "Could not load the selected image."
|
|
816
|
+
},
|
|
817
|
+
"a11y": {
|
|
818
|
+
"currentPhoto": "Your current profile picture"
|
|
819
|
+
}
|
|
820
|
+
},
|
|
777
821
|
"accountOverview": {
|
|
778
822
|
"title": "Account",
|
|
779
823
|
"sections": {
|
|
@@ -1442,6 +1486,11 @@
|
|
|
1442
1486
|
"uploadPhotos": "Upload Photos",
|
|
1443
1487
|
"uploadFiles": "Upload Files",
|
|
1444
1488
|
"clearSearch": "Clear Search",
|
|
1489
|
+
"retry": "Try again",
|
|
1490
|
+
"loadError": {
|
|
1491
|
+
"title": "Couldn't load your files",
|
|
1492
|
+
"description": "Something went wrong. Check your connection and try again."
|
|
1493
|
+
},
|
|
1445
1494
|
"choosePhoto": "Choose Photo",
|
|
1446
1495
|
"done": "Done",
|
|
1447
1496
|
"doneWithCount": "Done ({{count}})",
|
|
@@ -1614,7 +1663,9 @@
|
|
|
1614
1663
|
},
|
|
1615
1664
|
"accountMenu": {
|
|
1616
1665
|
"label": "Account menu",
|
|
1617
|
-
"manage": "Manage your Oxy
|
|
1666
|
+
"manage": "Manage your Oxy account",
|
|
1667
|
+
"greeting": "Hi, {{name}}!",
|
|
1668
|
+
"switchAccount": "Switch account",
|
|
1618
1669
|
"viewProfile": "View profile",
|
|
1619
1670
|
"addAnother": "Add another account",
|
|
1620
1671
|
"signOutAll": "Sign out of all accounts",
|
|
@@ -1622,7 +1673,22 @@
|
|
|
1622
1673
|
"openHint": "Opens the account menu",
|
|
1623
1674
|
"openWithUser": "Account menu for {{name}}",
|
|
1624
1675
|
"switching": "Switching account…",
|
|
1625
|
-
"signOutAccount": "Sign out {{name}}"
|
|
1676
|
+
"signOutAccount": "Sign out {{name}}",
|
|
1677
|
+
"storage": {
|
|
1678
|
+
"title": "Oxy storage",
|
|
1679
|
+
"usage": "{{used}} of {{total}} used",
|
|
1680
|
+
"used": "Used",
|
|
1681
|
+
"free": "Free",
|
|
1682
|
+
"unavailable": "Usage details unavailable",
|
|
1683
|
+
"upgrade": "Upgrade plan",
|
|
1684
|
+
"manage": "Manage storage"
|
|
1685
|
+
},
|
|
1686
|
+
"data": "Your data in Oxy",
|
|
1687
|
+
"settings": "Oxy settings",
|
|
1688
|
+
"help": "Help & feedback",
|
|
1689
|
+
"signOut": "Sign out",
|
|
1690
|
+
"privacy": "Privacy Policy",
|
|
1691
|
+
"terms": "Terms of Service"
|
|
1626
1692
|
},
|
|
1627
1693
|
"accounts": {
|
|
1628
1694
|
"activeAccount": {
|
|
@@ -290,6 +290,8 @@
|
|
|
290
290
|
"saving": "Guardando…",
|
|
291
291
|
"helper": "El círculo recortado es lo que aparecerá en tu perfil. Pellizca para acercar, arrastra para colocar.",
|
|
292
292
|
"zoom": "{{value}}×",
|
|
293
|
+
"zoomIn": "Acercar",
|
|
294
|
+
"zoomOut": "Alejar",
|
|
293
295
|
"a11yImage": "Vista previa del recorte. Pellizca para acercar y arrastra para reposicionar la imagen.",
|
|
294
296
|
"a11yReset": "Restablecer el recorte a la posición predeterminada",
|
|
295
297
|
"a11yResetAnnouncement": "Recorte restablecido"
|
|
@@ -310,6 +312,47 @@
|
|
|
310
312
|
},
|
|
311
313
|
"changeAvatar": "Cambiar avatar"
|
|
312
314
|
},
|
|
315
|
+
"changeAvatar": {
|
|
316
|
+
"title": "Cambiar foto de perfil",
|
|
317
|
+
"sources": {
|
|
318
|
+
"upload": {
|
|
319
|
+
"title": "Subir desde el dispositivo",
|
|
320
|
+
"description": "Elige una foto de tu galería"
|
|
321
|
+
},
|
|
322
|
+
"camera": {
|
|
323
|
+
"title": "Hacer una foto",
|
|
324
|
+
"description": "Usa tu cámara"
|
|
325
|
+
},
|
|
326
|
+
"files": {
|
|
327
|
+
"title": "Mis archivos de Oxy",
|
|
328
|
+
"description": "Elige una imagen que ya has subido"
|
|
329
|
+
},
|
|
330
|
+
"remove": {
|
|
331
|
+
"title": "Eliminar la foto actual",
|
|
332
|
+
"description": "Vuelve a tus iniciales"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"remove": {
|
|
336
|
+
"confirmTitle": "¿Eliminar la foto de perfil?",
|
|
337
|
+
"confirmMessage": "Tu perfil mostrará tus iniciales. Puedes añadir una foto nueva cuando quieras.",
|
|
338
|
+
"confirmLabel": "Eliminar"
|
|
339
|
+
},
|
|
340
|
+
"permission": {
|
|
341
|
+
"libraryDenied": "Oxy necesita acceso a tus fotos para subir una imagen.",
|
|
342
|
+
"cameraDenied": "Oxy necesita acceso a tu cámara para hacer una foto.",
|
|
343
|
+
"openSettings": "Abrir ajustes"
|
|
344
|
+
},
|
|
345
|
+
"errors": {
|
|
346
|
+
"pickerUnavailable": "La selección de fotos no está disponible en este dispositivo.",
|
|
347
|
+
"cameraUnavailable": "La cámara no está disponible en este dispositivo.",
|
|
348
|
+
"pickFailed": "No se pudo abrir el selector de fotos.",
|
|
349
|
+
"cameraFailed": "No se pudo abrir la cámara.",
|
|
350
|
+
"loadImageFailed": "No se pudo cargar la imagen seleccionada."
|
|
351
|
+
},
|
|
352
|
+
"a11y": {
|
|
353
|
+
"currentPhoto": "Tu foto de perfil actual"
|
|
354
|
+
}
|
|
355
|
+
},
|
|
313
356
|
"common": {
|
|
314
357
|
"actions": {
|
|
315
358
|
"back": "Atrás",
|
|
@@ -619,7 +662,8 @@
|
|
|
619
662
|
"getCommons": "Descargar Commons",
|
|
620
663
|
"commonsNotInstalled": "¿No tienes Commons? Descarga la app para iniciar sesión con tu Oxy ID.",
|
|
621
664
|
"showQrAnyway": "Tengo Commons en otro dispositivo",
|
|
622
|
-
"switchWhileSignedInAs": "Cambiar de cuenta, con la sesión iniciada como {{name}}"
|
|
665
|
+
"switchWhileSignedInAs": "Cambiar de cuenta, con la sesión iniciada como {{name}}",
|
|
666
|
+
"manageOnDevice": "Gestionar cuentas en este dispositivo"
|
|
623
667
|
},
|
|
624
668
|
"feedback": {
|
|
625
669
|
"type": {
|
|
@@ -1442,6 +1486,11 @@
|
|
|
1442
1486
|
"uploadPhotos": "Subir fotos",
|
|
1443
1487
|
"uploadFiles": "Subir archivos",
|
|
1444
1488
|
"clearSearch": "Limpiar búsqueda",
|
|
1489
|
+
"retry": "Reintentar",
|
|
1490
|
+
"loadError": {
|
|
1491
|
+
"title": "No se pudieron cargar tus archivos",
|
|
1492
|
+
"description": "Algo salió mal. Comprueba tu conexión e inténtalo de nuevo."
|
|
1493
|
+
},
|
|
1445
1494
|
"choosePhoto": "Elegir foto",
|
|
1446
1495
|
"done": "Listo",
|
|
1447
1496
|
"doneWithCount": "Listo ({{count}})",
|
|
@@ -1615,6 +1664,8 @@
|
|
|
1615
1664
|
"accountMenu": {
|
|
1616
1665
|
"label": "Menú de cuenta",
|
|
1617
1666
|
"manage": "Gestiona tu cuenta de Oxy",
|
|
1667
|
+
"greeting": "¡Hola, {{name}}!",
|
|
1668
|
+
"switchAccount": "Cambiar de cuenta",
|
|
1618
1669
|
"viewProfile": "Ver perfil",
|
|
1619
1670
|
"addAnother": "Añadir otra cuenta",
|
|
1620
1671
|
"signOutAll": "Cerrar sesión en todas las cuentas",
|
|
@@ -1622,7 +1673,22 @@
|
|
|
1622
1673
|
"openHint": "Abre el menú de cuenta",
|
|
1623
1674
|
"openWithUser": "Menú de cuenta de {{name}}",
|
|
1624
1675
|
"switching": "Cambiando de cuenta…",
|
|
1625
|
-
"signOutAccount": "Cerrar sesión de {{name}}"
|
|
1676
|
+
"signOutAccount": "Cerrar sesión de {{name}}",
|
|
1677
|
+
"storage": {
|
|
1678
|
+
"title": "Almacenamiento de Oxy",
|
|
1679
|
+
"usage": "{{used}} de {{total}} en uso",
|
|
1680
|
+
"used": "Usado",
|
|
1681
|
+
"free": "Libre",
|
|
1682
|
+
"unavailable": "Detalles de uso no disponibles",
|
|
1683
|
+
"upgrade": "Mejorar plan",
|
|
1684
|
+
"manage": "Gestionar almacenamiento"
|
|
1685
|
+
},
|
|
1686
|
+
"data": "Tus datos en Oxy",
|
|
1687
|
+
"settings": "Ajustes de Oxy",
|
|
1688
|
+
"help": "Ayuda y comentarios",
|
|
1689
|
+
"signOut": "Cerrar sesión",
|
|
1690
|
+
"privacy": "Política de privacidad",
|
|
1691
|
+
"terms": "Términos del servicio"
|
|
1626
1692
|
},
|
|
1627
1693
|
"accounts": {
|
|
1628
1694
|
"activeAccount": {
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "Vos comptes"
|
|
117
|
+
},
|
|
118
|
+
"loading": "Chargement des comptes...",
|
|
115
119
|
"qrHeadline": "Connectez-vous avec votre identité Oxy",
|
|
116
120
|
"signInWithOxy": "Se connecter avec Oxy",
|
|
117
121
|
"useIdentityOnDevice": "Pas de Commons ? Utilisez une clé d'accès sur cet appareil",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "I tuoi account"
|
|
117
|
+
},
|
|
118
|
+
"loading": "Caricamento account...",
|
|
115
119
|
"qrHeadline": "Accedi con la tua identità Oxy",
|
|
116
120
|
"signInWithOxy": "Accedi con Oxy",
|
|
117
121
|
"useIdentityOnDevice": "Non hai Commons? Usa una passkey su questo dispositivo",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "あなたのアカウント"
|
|
117
|
+
},
|
|
118
|
+
"loading": "アカウントを読み込み中...",
|
|
115
119
|
"qrHeadline": "Oxy IDでサインイン",
|
|
116
120
|
"signInWithOxy": "Oxyでサインイン",
|
|
117
121
|
"useIdentityOnDevice": "Commonsがありませんか?このデバイスのパスキーを使用",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "내 계정"
|
|
117
|
+
},
|
|
118
|
+
"loading": "계정을 불러오는 중...",
|
|
115
119
|
"qrHeadline": "Oxy ID로 로그인",
|
|
116
120
|
"signInWithOxy": "Oxy로 로그인",
|
|
117
121
|
"useIdentityOnDevice": "Commons가 없으신가요? 이 기기의 패스키를 사용하세요",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "حساباتك"
|
|
117
|
+
},
|
|
118
|
+
"loading": "جارٍ تحميل الحسابات...",
|
|
115
119
|
"qrHeadline": "سجّل الدخول بهوية Oxy الخاصة بك",
|
|
116
120
|
"signInWithOxy": "تسجيل الدخول باستخدام Oxy",
|
|
117
121
|
"useIdentityOnDevice": "لا تملك Commons؟ استخدم مفتاح مرور على هذا الجهاز بدلاً من ذلك",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "Els teus comptes"
|
|
117
|
+
},
|
|
118
|
+
"loading": "S'estan carregant els comptes...",
|
|
115
119
|
"qrHeadline": "Inicia sessió amb la teva identitat d'Oxy",
|
|
116
120
|
"signInWithOxy": "Inicia sessió amb Oxy",
|
|
117
121
|
"useIdentityOnDevice": "No tens Commons? Fes servir una clau d'accés en aquest dispositiu",
|
|
@@ -112,6 +112,10 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
"accountSwitcher": {
|
|
115
|
+
"sections": {
|
|
116
|
+
"yourAccounts": "Ihre Konten"
|
|
117
|
+
},
|
|
118
|
+
"loading": "Konten werden geladen...",
|
|
115
119
|
"qrHeadline": "Melden Sie sich mit Ihrer Oxy-Identität an",
|
|
116
120
|
"signInWithOxy": "Mit Oxy anmelden",
|
|
117
121
|
"useIdentityOnDevice": "Kein Commons? Verwenden Sie stattdessen einen Passkey auf diesem Gerät",
|
|
@@ -421,7 +421,8 @@
|
|
|
421
421
|
"getCommons": "Get Commons",
|
|
422
422
|
"commonsNotInstalled": "Don't have Commons? Get the app to sign in with your Oxy ID.",
|
|
423
423
|
"showQrAnyway": "I have Commons on another device",
|
|
424
|
-
"switchWhileSignedInAs": "Switch account, signed in as {{name}}"
|
|
424
|
+
"switchWhileSignedInAs": "Switch account, signed in as {{name}}",
|
|
425
|
+
"manageOnDevice": "Manage accounts on this device"
|
|
425
426
|
},
|
|
426
427
|
"reputation": {
|
|
427
428
|
"faq": {
|
|
@@ -754,6 +755,8 @@
|
|
|
754
755
|
"saving": "Saving…",
|
|
755
756
|
"helper": "The cropped circle is what will appear on your profile. Pinch to zoom, drag to position.",
|
|
756
757
|
"zoom": "{{value}}×",
|
|
758
|
+
"zoomIn": "Zoom in",
|
|
759
|
+
"zoomOut": "Zoom out",
|
|
757
760
|
"a11yImage": "Crop preview. Pinch to zoom and drag to reposition the image.",
|
|
758
761
|
"a11yReset": "Reset crop to default position",
|
|
759
762
|
"a11yResetAnnouncement": "Crop reset"
|
|
@@ -774,6 +777,47 @@
|
|
|
774
777
|
},
|
|
775
778
|
"changeAvatar": "Change avatar"
|
|
776
779
|
},
|
|
780
|
+
"changeAvatar": {
|
|
781
|
+
"title": "Change profile picture",
|
|
782
|
+
"sources": {
|
|
783
|
+
"upload": {
|
|
784
|
+
"title": "Upload from device",
|
|
785
|
+
"description": "Choose a photo from your gallery"
|
|
786
|
+
},
|
|
787
|
+
"camera": {
|
|
788
|
+
"title": "Take a picture",
|
|
789
|
+
"description": "Use your camera"
|
|
790
|
+
},
|
|
791
|
+
"files": {
|
|
792
|
+
"title": "My Oxy files",
|
|
793
|
+
"description": "Pick an image you already uploaded"
|
|
794
|
+
},
|
|
795
|
+
"remove": {
|
|
796
|
+
"title": "Remove current photo",
|
|
797
|
+
"description": "Go back to your initials"
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
"remove": {
|
|
801
|
+
"confirmTitle": "Remove profile picture?",
|
|
802
|
+
"confirmMessage": "Your profile will show your initials instead. You can add a new photo at any time.",
|
|
803
|
+
"confirmLabel": "Remove"
|
|
804
|
+
},
|
|
805
|
+
"permission": {
|
|
806
|
+
"libraryDenied": "Oxy needs access to your photos to upload a picture.",
|
|
807
|
+
"cameraDenied": "Oxy needs access to your camera to take a picture.",
|
|
808
|
+
"openSettings": "Open settings"
|
|
809
|
+
},
|
|
810
|
+
"errors": {
|
|
811
|
+
"pickerUnavailable": "Photo picking is not available on this device.",
|
|
812
|
+
"cameraUnavailable": "The camera is not available on this device.",
|
|
813
|
+
"pickFailed": "Could not open the photo picker.",
|
|
814
|
+
"cameraFailed": "Could not open the camera.",
|
|
815
|
+
"loadImageFailed": "Could not load the selected image."
|
|
816
|
+
},
|
|
817
|
+
"a11y": {
|
|
818
|
+
"currentPhoto": "Your current profile picture"
|
|
819
|
+
}
|
|
820
|
+
},
|
|
777
821
|
"accountOverview": {
|
|
778
822
|
"title": "Account",
|
|
779
823
|
"sections": {
|
|
@@ -1442,6 +1486,11 @@
|
|
|
1442
1486
|
"uploadPhotos": "Upload Photos",
|
|
1443
1487
|
"uploadFiles": "Upload Files",
|
|
1444
1488
|
"clearSearch": "Clear Search",
|
|
1489
|
+
"retry": "Try again",
|
|
1490
|
+
"loadError": {
|
|
1491
|
+
"title": "Couldn't load your files",
|
|
1492
|
+
"description": "Something went wrong. Check your connection and try again."
|
|
1493
|
+
},
|
|
1445
1494
|
"choosePhoto": "Choose Photo",
|
|
1446
1495
|
"done": "Done",
|
|
1447
1496
|
"doneWithCount": "Done ({{count}})",
|
|
@@ -1614,7 +1663,9 @@
|
|
|
1614
1663
|
},
|
|
1615
1664
|
"accountMenu": {
|
|
1616
1665
|
"label": "Account menu",
|
|
1617
|
-
"manage": "Manage your Oxy
|
|
1666
|
+
"manage": "Manage your Oxy account",
|
|
1667
|
+
"greeting": "Hi, {{name}}!",
|
|
1668
|
+
"switchAccount": "Switch account",
|
|
1618
1669
|
"viewProfile": "View profile",
|
|
1619
1670
|
"addAnother": "Add another account",
|
|
1620
1671
|
"signOutAll": "Sign out of all accounts",
|
|
@@ -1622,7 +1673,22 @@
|
|
|
1622
1673
|
"openHint": "Opens the account menu",
|
|
1623
1674
|
"openWithUser": "Account menu for {{name}}",
|
|
1624
1675
|
"switching": "Switching account…",
|
|
1625
|
-
"signOutAccount": "Sign out {{name}}"
|
|
1676
|
+
"signOutAccount": "Sign out {{name}}",
|
|
1677
|
+
"storage": {
|
|
1678
|
+
"title": "Oxy storage",
|
|
1679
|
+
"usage": "{{used}} of {{total}} used",
|
|
1680
|
+
"used": "Used",
|
|
1681
|
+
"free": "Free",
|
|
1682
|
+
"unavailable": "Usage details unavailable",
|
|
1683
|
+
"upgrade": "Upgrade plan",
|
|
1684
|
+
"manage": "Manage storage"
|
|
1685
|
+
},
|
|
1686
|
+
"data": "Your data in Oxy",
|
|
1687
|
+
"settings": "Oxy settings",
|
|
1688
|
+
"help": "Help & feedback",
|
|
1689
|
+
"signOut": "Sign out",
|
|
1690
|
+
"privacy": "Privacy Policy",
|
|
1691
|
+
"terms": "Terms of Service"
|
|
1626
1692
|
},
|
|
1627
1693
|
"accounts": {
|
|
1628
1694
|
"activeAccount": {
|