@oxyhq/core 9.2.3 → 10.0.0

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.
Files changed (64) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/index.js +22 -2
  3. package/dist/cjs/i18n/locales/en-US.json +4 -1
  4. package/dist/cjs/i18n/locales/es-ES.json +4 -1
  5. package/dist/cjs/i18n/locales/locales/en-US.json +4 -1
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +4 -1
  7. package/dist/cjs/index.js +10 -5
  8. package/dist/cjs/mixins/OxyServices.assets.js +10 -6
  9. package/dist/cjs/mixins/OxyServices.language.js +11 -10
  10. package/dist/cjs/mixins/OxyServices.privacy.js +6 -0
  11. package/dist/cjs/mixins/OxyServices.user.js +40 -0
  12. package/dist/cjs/server/safeFetch.js +3 -3
  13. package/dist/cjs/session/SessionClient.js +1 -1
  14. package/dist/cjs/shared/utils/colorUtils.js +9 -9
  15. package/dist/cjs/utils/languageUtils.js +195 -158
  16. package/dist/cjs/utils/platform.js +9 -2
  17. package/dist/esm/.tsbuildinfo +1 -1
  18. package/dist/esm/i18n/index.js +22 -2
  19. package/dist/esm/i18n/locales/en-US.json +4 -1
  20. package/dist/esm/i18n/locales/es-ES.json +4 -1
  21. package/dist/esm/i18n/locales/locales/en-US.json +4 -1
  22. package/dist/esm/i18n/locales/locales/es-ES.json +4 -1
  23. package/dist/esm/index.js +1 -1
  24. package/dist/esm/mixins/OxyServices.assets.js +10 -6
  25. package/dist/esm/mixins/OxyServices.language.js +12 -11
  26. package/dist/esm/mixins/OxyServices.privacy.js +6 -0
  27. package/dist/esm/mixins/OxyServices.user.js +40 -0
  28. package/dist/esm/server/safeFetch.js +3 -3
  29. package/dist/esm/session/SessionClient.js +1 -1
  30. package/dist/esm/shared/utils/colorUtils.js +9 -9
  31. package/dist/esm/utils/languageUtils.js +189 -156
  32. package/dist/esm/utils/platform.js +9 -2
  33. package/dist/types/.tsbuildinfo +1 -1
  34. package/dist/types/index.d.ts +3 -3
  35. package/dist/types/mixins/OxyServices.language.d.ts +4 -4
  36. package/dist/types/mixins/OxyServices.user.d.ts +30 -0
  37. package/dist/types/models/interfaces.d.ts +8 -0
  38. package/dist/types/session/accountDialogController.d.ts +1 -1
  39. package/dist/types/utils/languageUtils.d.ts +86 -24
  40. package/package.json +3 -3
  41. package/src/HttpService.ts +2 -2
  42. package/src/crypto/keyManager.ts +3 -3
  43. package/src/crypto/recoveryPhrase.ts +1 -1
  44. package/src/i18n/index.ts +21 -2
  45. package/src/i18n/locales/en-US.json +4 -1
  46. package/src/i18n/locales/es-ES.json +4 -1
  47. package/src/index.ts +8 -2
  48. package/src/mixins/OxyServices.assets.ts +15 -11
  49. package/src/mixins/OxyServices.language.ts +31 -17
  50. package/src/mixins/OxyServices.privacy.ts +6 -0
  51. package/src/mixins/OxyServices.security.ts +1 -1
  52. package/src/mixins/OxyServices.user.ts +57 -0
  53. package/src/models/interfaces.ts +8 -0
  54. package/src/server/safeFetch.ts +3 -3
  55. package/src/session/SessionClient.ts +1 -1
  56. package/src/session/accountDialogController.ts +1 -1
  57. package/src/shared/utils/colorUtils.ts +11 -11
  58. package/src/shared/utils/errorUtils.ts +1 -1
  59. package/src/utils/__tests__/languageUtils.test.ts +219 -0
  60. package/src/utils/avatarUtils.ts +1 -1
  61. package/src/utils/languageUtils.ts +223 -162
  62. package/src/utils/platform.ts +21 -3
  63. package/src/utils/requestUtils.ts +3 -3
  64. package/src/utils/sessionUtils.ts +2 -2
@@ -26,7 +26,7 @@ export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
26
26
  export type { CommonsSignInHandle, CommonsSignInStatus, CommonsApprovalInfo, CommonsSignInActionResult, } from './mixins/OxyServices.auth';
27
27
  export type { ServiceApp, ServiceActingAsVerification } from './mixins/OxyServices.utility';
28
28
  export type { ContactDiscoveryMatch, ContactDiscoveryResponse, } from './mixins/OxyServices.contacts';
29
- export type { BulkFollowEntry, BulkFollowResult, BulkUnfollowEntry, BulkUnfollowResult, } from './mixins/OxyServices.user';
29
+ export type { BulkFollowEntry, BulkFollowResult, BulkUnfollowEntry, BulkUnfollowResult, ViewerGraph, } from './mixins/OxyServices.user';
30
30
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
31
31
  export { getNormalizedUserId, normalizeUserIdentity, normalizeUserIdentityOrNull, } from './utils/userIdentity';
32
32
  export { getCanonicalUserHandle, getNormalizedUserHandle, } from './utils/userHandle';
@@ -58,8 +58,8 @@ export type { OxyConfig, PrivacySettings, NotificationPreferences, UserPreferenc
58
58
  export { SECURITY_EVENT_SEVERITY_MAP } from './models/interfaces';
59
59
  export { TopicType, TopicSource } from './models/Topic';
60
60
  export type { TopicData, TopicTranslation } from './models/Topic';
61
- export { SUPPORTED_LANGUAGES, getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeLanguageCode, isRTLLocale, } from './utils/languageUtils';
62
- export type { LanguageMetadata } from './utils/languageUtils';
61
+ export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, } from './utils/languageUtils';
62
+ export type { SupportedLanguage } from './utils/languageUtils';
63
63
  export { getPlatformOS, setPlatformOS, isWeb, isNative, isIOS, isAndroid, isWebBrowser, } from './utils/platform';
64
64
  export type { PlatformOS } from './utils/platform';
65
65
  export { darkenColor, lightenColor, hexToRgb, rgbToHex, withOpacity, isLightColor, getContrastTextColor, } from './shared/utils/colorUtils';
@@ -1,4 +1,4 @@
1
- import type { LanguageMetadata } from '../utils/languageUtils';
1
+ import type { SupportedLanguage } from '../utils/languageUtils';
2
2
  import type { OxyServicesBase } from '../OxyServices.base';
3
3
  export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T): {
4
4
  new (...args: any[]): {
@@ -11,9 +11,9 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
11
11
  removeItem: (key: string) => Promise<void>;
12
12
  }>;
13
13
  /**
14
- * Get the current language from storage or user profile
14
+ * Get the current locale from the user profile or local storage.
15
15
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
16
- * @returns The current language code (e.g., 'en-US') or null if not set
16
+ * @returns The current BCP-47 locale (e.g., 'en-US') or null if not set
17
17
  */
18
18
  getCurrentLanguage(storageKeyPrefix?: string): Promise<string | null>;
19
19
  /**
@@ -21,7 +21,7 @@ export declare function OxyServicesLanguageMixin<T extends typeof OxyServicesBas
21
21
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
22
22
  * @returns Language metadata object or null if not set
23
23
  */
24
- getCurrentLanguageMetadata(storageKeyPrefix?: string): Promise<LanguageMetadata | null>;
24
+ getCurrentLanguageMetadata(storageKeyPrefix?: string): Promise<SupportedLanguage | null>;
25
25
  /**
26
26
  * Get the current language name (e.g., 'English')
27
27
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
@@ -21,6 +21,22 @@ export interface BulkFollowResult {
21
21
  /** Number of users newly followed by this request. */
22
22
  followedCount: number;
23
23
  }
24
+ /**
25
+ * The authenticated viewer's OWN social graph, ids-only — the response of
26
+ * `GET /users/me/graph`. Consolidates the accounts the viewer follows, the
27
+ * subset who follow back (mutuals), and the accounts the viewer has blocked
28
+ * into one payload so a consumer can fetch its whole viewer graph in a single
29
+ * round trip instead of three. Each list is server-bounded; bare ids only (no
30
+ * hydrated DTOs) because the consumer hydrates/ranks itself.
31
+ */
32
+ export interface ViewerGraph {
33
+ /** Accounts the viewer follows (most-recent first, bounded). */
34
+ followingIds: string[];
35
+ /** Accounts the viewer follows that ALSO follow the viewer back (bounded). */
36
+ mutualIds: string[];
37
+ /** Accounts the viewer has blocked (bounded). */
38
+ blockedIds: string[];
39
+ }
24
40
  /** Per-user outcome returned by `POST /users/unfollow/bulk`. */
25
41
  export interface BulkUnfollowEntry {
26
42
  /** The user ID that was processed. */
@@ -383,6 +399,20 @@ export declare function OxyServicesUserMixin<T extends typeof OxyServicesBase>(B
383
399
  getFollowsOfFollowsIds(params?: {
384
400
  limit?: number;
385
401
  }): Promise<string[]>;
402
+ /**
403
+ * Get the authenticated VIEWER's OWN social graph — the accounts they follow,
404
+ * the subset who follow back (mutuals), and the accounts they have blocked —
405
+ * as ONE ids-only payload. The viewer is derived server-side from the SDK's
406
+ * auth token (never a param).
407
+ *
408
+ * Consolidates what were three separate round trips (`getUserFollowing` /
409
+ * `getMutualUserIds` / `getBlockedUsers`) into a single request so a consumer
410
+ * can prime its whole viewer graph at once. Mirrors {@link getMutualUserIds}'s
411
+ * caching posture (2-minute identity-scoped cache); the follow/unfollow/block/
412
+ * unblock write methods bust this entry so a local mutation is reflected
413
+ * immediately. An anonymous caller resolves to empty lists.
414
+ */
415
+ getViewerGraph(): Promise<ViewerGraph>;
386
416
  /**
387
417
  * Get notifications
388
418
  */
@@ -131,6 +131,14 @@ export interface User {
131
131
  managedBy?: string;
132
132
  /** Real-estate taxonomy when this user is a `kind: 'organization'` account. */
133
133
  organizationCategory?: OrganizationCategory;
134
+ /**
135
+ * The account's languages as full BCP-47 locales (`language-REGION`, e.g.
136
+ * `en-US`, `es-MX`, `pt-BR`), ordered with the PRIMARY (UI) locale first.
137
+ * `languages[0]` is the primary locale — there is no singular `language`
138
+ * field. Resolve via `getUserLanguages` / `getPrimaryLanguage`, which
139
+ * normalize, validate against the supported catalog, and de-duplicate.
140
+ */
141
+ languages?: string[];
134
142
  notificationPreferences?: NotificationPreferences;
135
143
  userPreferences?: UserPreferences;
136
144
  [key: string]: unknown;
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import type { OxyServices } from '../OxyServices';
29
29
  import type { SessionLoginResponse, MinimalUserData } from '../models/session';
30
- import { SessionClient } from './SessionClient';
30
+ import type { SessionClient } from './SessionClient';
31
31
  import { type SwitchableAccount } from './accountProjection';
32
32
  /** The dialog's top-level view. */
33
33
  export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add';
@@ -1,38 +1,100 @@
1
1
  /**
2
- * Language utilities for OxyServices
3
- * Provides access to supported languages and language metadata
2
+ * Locale utilities for OxyServices.
3
+ *
4
+ * The Oxy platform models account languages as full BCP-47 locales
5
+ * (`language-REGION`, e.g. `es-ES`, `es-MX`, `pt-BR`). Region is significant:
6
+ * "Spanish (Spain)" is a different locale than "Spanish (Mexico)". A user's
7
+ * account locales live on `User.languages` as an ordered list with the PRIMARY
8
+ * (UI) locale first — there is no singular `language` field.
9
+ *
10
+ * This module is the single source of truth for the supported-locale catalog
11
+ * and every locale operation the SDK exposes: parsing base subtags, canonical
12
+ * normalization, validation, metadata lookup, and resolving a user's locales.
13
+ * It is pure and side-effect free.
4
14
  */
5
- export interface LanguageMetadata {
6
- id: string;
15
+ import type { User } from '../models/interfaces';
16
+ /**
17
+ * A supported BCP-47 locale in the Oxy catalog.
18
+ */
19
+ export interface SupportedLanguage {
20
+ /** Canonical BCP-47 locale tag, `language-REGION` (e.g. `'es-ES'`). */
21
+ code: string;
22
+ /** ISO 639-1 base language subtag, lowercased (e.g. `'es'`). */
23
+ language: string;
24
+ /** ISO 3166-1 alpha-2 region subtag, uppercased (e.g. `'ES'`). */
25
+ region: string;
26
+ /** English display name, `'Language (Region)'` (e.g. `'Spanish (Spain)'`). */
7
27
  name: string;
28
+ /** Endonym — the name as written in the locale itself (e.g. `'Español (España)'`). */
8
29
  nativeName: string;
9
- flag: string;
10
- icon: string;
11
- color: string;
30
+ /** `true` when the locale is written right-to-left. Omitted for LTR locales. */
31
+ rtl?: boolean;
12
32
  }
13
- export declare const SUPPORTED_LANGUAGES: LanguageMetadata[];
14
33
  /**
15
- * Get language metadata by language code
16
- * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
17
- * @returns Language metadata or null if not found
34
+ * The supported-locale catalog. Ordered by prominence for display; the order
35
+ * is not otherwise significant. Every entry is a full `language-REGION` tag,
36
+ * with the base subtag lowercased and the region uppercased.
37
+ */
38
+ export declare const SUPPORTED_LANGUAGES: readonly SupportedLanguage[];
39
+ /** Canonical fallback locale used when a display value cannot be resolved. */
40
+ export declare const FALLBACK_LOCALE = "en-US";
41
+ /**
42
+ * Extract the base language subtag from a locale, lowercased.
43
+ *
44
+ * Tolerant of bare base subtags and of extra subtags (script/variant):
45
+ * `'es-ES'` → `'es'`, `'es'` → `'es'`, `'zh-Hant-TW'` → `'zh'`. Returns an
46
+ * empty string for empty input.
47
+ */
48
+ export declare function getBaseLanguage(locale: string): string;
49
+ /**
50
+ * Normalize a locale string to its canonical `language-REGION` form when it is
51
+ * a supported locale, otherwise `undefined`.
52
+ *
53
+ * Canonicalization lowercases the base subtag and uppercases the region
54
+ * subtag (`'es-es'` → `'es-ES'`, `'EN-us'` → `'en-US'`) and validates the
55
+ * result against {@link SUPPORTED_LANGUAGES}. A bare base subtag (`'es'`) has
56
+ * no region and is therefore not a locale — it returns `undefined`.
57
+ */
58
+ export declare function normalizeLocale(input: string): string | undefined;
59
+ /**
60
+ * Whether `input` resolves to a supported BCP-47 locale.
61
+ */
62
+ export declare function isSupportedLocale(input: string): boolean;
63
+ /**
64
+ * Resolve catalog metadata for a locale.
65
+ *
66
+ * @param code - A locale tag (any case; e.g. `'es-ES'`, `'es-es'`).
67
+ * @returns The {@link SupportedLanguage} entry, or `null` when the tag is empty
68
+ * or not a supported locale.
18
69
  */
19
- export declare function getLanguageMetadata(languageCode: string | null | undefined): LanguageMetadata | null;
70
+ export declare function getLanguageMetadata(code: string | null | undefined): SupportedLanguage | null;
20
71
  /**
21
- * Get language name by language code
22
- * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
23
- * @returns Language name (e.g., 'English') or the code if not found
72
+ * English display name for a locale (e.g. `'Spanish (Spain)'`).
73
+ * Falls back to the input tag, then {@link FALLBACK_LOCALE}.
24
74
  */
25
- export declare function getLanguageName(languageCode: string | null | undefined): string;
75
+ export declare function getLanguageName(code: string | null | undefined): string;
26
76
  /**
27
- * Get native language name by language code
28
- * @param languageCode - BCP-47 language code (e.g., 'en-US', 'es-ES')
29
- * @returns Native language name (e.g., 'Español') or the code if not found
77
+ * Native display name (endonym) for a locale (e.g. `'Español (España)'`).
78
+ * Falls back to the input tag, then {@link FALLBACK_LOCALE}.
30
79
  */
31
- export declare function getNativeLanguageName(languageCode: string | null | undefined): string;
80
+ export declare function getNativeLanguageName(code: string | null | undefined): string;
32
81
  /**
33
- * Normalize language code to BCP-47 format
34
- * @param lang - Language code (may be short like 'en' or full like 'en-US')
35
- * @returns Normalized BCP-47 language code
82
+ * Whether a locale (or bare base subtag) is written right-to-left. Unknown
83
+ * tags are treated as left-to-right.
36
84
  */
37
- export declare function normalizeLanguageCode(lang?: string | null): string;
38
85
  export declare function isRTLLocale(locale?: string | null): boolean;
86
+ /**
87
+ * Resolve the ordered list of account locales for a user, primary first.
88
+ *
89
+ * Reads `user.languages` (the only source — there is no singular `language`
90
+ * field), then normalizes each entry to its canonical `language-REGION` form,
91
+ * drops non-string and unsupported entries, and de-duplicates while preserving
92
+ * first-seen order. Pure and side-effect free — never throws on bad input.
93
+ */
94
+ export declare function getUserLanguages(user: Pick<User, 'languages'> | null | undefined): string[];
95
+ /**
96
+ * Resolve the single PRIMARY locale for a user (the first entry from
97
+ * {@link getUserLanguages}), or `undefined` when the user has no supported
98
+ * locale.
99
+ */
100
+ export declare function getPrimaryLanguage(user: Pick<User, 'languages'> | null | undefined): string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "9.2.3",
3
+ "version": "10.0.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -94,8 +94,8 @@
94
94
  }
95
95
  },
96
96
  "dependencies": {
97
- "@oxyhq/contracts": "^0.13.2",
98
- "@oxyhq/protocol": "^0.1.3",
97
+ "@oxyhq/contracts": "^0.14.0",
98
+ "@oxyhq/protocol": "^0.1.5",
99
99
  "bip39": "^3.1.0",
100
100
  "buffer": "^6.0.3",
101
101
  "elliptic": "^6.6.1",
@@ -232,7 +232,7 @@ export class HttpService {
232
232
  private logger: SimpleLogger;
233
233
  private config: OxyConfig;
234
234
  private tokenRefreshPromise: Promise<string | null> | null = null;
235
- private tokenRefreshCooldownUntil: number = 0;
235
+ private tokenRefreshCooldownUntil = 0;
236
236
  private authRefreshHandler: AuthRefreshHandler | null = null;
237
237
  private accessTokenProvider: AccessTokenProvider | null = null;
238
238
  private deviceSecretMintInFlight: Promise<DeviceSecretMintOutcome> | null = null;
@@ -242,7 +242,7 @@ export class HttpService {
242
242
  * re-emitted. Throttles the {@link CACHE_SOFT_MAX_ENTRIES} warning to at most
243
243
  * one per {@link CACHE_SIZE_WARNING_THROTTLE_MS} window.
244
244
  */
245
- private cacheSizeWarningSilentUntil: number = 0;
245
+ private cacheSizeWarningSilentUntil = 0;
246
246
 
247
247
  /**
248
248
  * Fan-out listeners notified on EVERY access-token change on this instance:
@@ -1178,9 +1178,9 @@ export class KeyManager {
1178
1178
  * @param userConfirmed - If true, user has explicitly confirmed deletion (default: false)
1179
1179
  */
1180
1180
  static async deleteIdentity(
1181
- skipBackup: boolean = false,
1182
- force: boolean = false,
1183
- userConfirmed: boolean = false
1181
+ skipBackup = false,
1182
+ force = false,
1183
+ userConfirmed = false
1184
1184
  ): Promise<void> {
1185
1185
  if (isWebPlatform()) {
1186
1186
  return; // Identity storage is only available on native platforms, nothing to delete
@@ -160,7 +160,7 @@ export class RecoveryPhraseService {
160
160
  /**
161
161
  * Get suggestions for a partial word
162
162
  */
163
- static getSuggestions(partial: string, limit: number = 5): string[] {
163
+ static getSuggestions(partial: string, limit = 5): string[] {
164
164
  const lowerPartial = partial.toLowerCase();
165
165
  return bip39.wordlists.english
166
166
  .filter((word: string) => word.startsWith(lowerPartial))
package/src/i18n/index.ts CHANGED
@@ -9,6 +9,7 @@ import jaJP from './locales/ja-JP.json';
9
9
  import koKR from './locales/ko-KR.json';
10
10
  import zhCN from './locales/zh-CN.json';
11
11
  import arSA from './locales/ar-SA.json';
12
+ import { getBaseLanguage } from '../utils/languageUtils';
12
13
 
13
14
  export type LocaleDict = Record<string, any>;
14
15
 
@@ -39,12 +40,30 @@ const DICTS: Record<string, LocaleDict> = {
39
40
 
40
41
  const FALLBACK = 'en-US';
41
42
 
43
+ /**
44
+ * Resolve a locale tag to the key of the dictionary that should serve it.
45
+ *
46
+ * Account locales are full BCP-47 tags (e.g. `es-MX`, `pt-BR`, `fr-CA`), but a
47
+ * translation dictionary is shipped per base language. Resolution order:
48
+ * 1. Exact dictionary for the tag (e.g. `es-ES`).
49
+ * 2. Dictionary for the base subtag (e.g. `es-MX` → `es`).
50
+ * 3. The English fallback.
51
+ */
52
+ function resolveLang(locale: string | undefined): string {
53
+ if (locale) {
54
+ if (DICTS[locale]) return locale;
55
+ const base = getBaseLanguage(locale);
56
+ if (DICTS[base]) return base;
57
+ }
58
+ return FALLBACK;
59
+ }
60
+
42
61
  function getNested(obj: any, path: string): any {
43
62
  return path.split('.').reduce((acc, key) => (acc && acc[key] != null ? acc[key] : undefined), obj);
44
63
  }
45
64
 
46
65
  export function translate(locale: string | undefined, key: string, vars?: Record<string, string | number>): string {
47
- const lang = locale && DICTS[locale] ? locale : FALLBACK;
66
+ const lang = resolveLang(locale);
48
67
  const dict = DICTS[lang] || DICTS[FALLBACK];
49
68
  let val = getNested(dict, key);
50
69
  // Per-key fallback to the English dictionary when a key is missing from the
@@ -64,6 +83,6 @@ export function translate(locale: string | undefined, key: string, vars?: Record
64
83
  }
65
84
 
66
85
  export function hasKey(locale: string | undefined, key: string): boolean {
67
- const lang = locale && DICTS[locale] ? locale : FALLBACK;
86
+ const lang = resolveLang(locale);
68
87
  return getNested(DICTS[lang], key) != null || getNested(DICTS[FALLBACK], key) != null;
69
88
  }
@@ -169,9 +169,12 @@
169
169
  },
170
170
  "language": {
171
171
  "title": "Language",
172
- "subtitle": "Choose your preferred language",
172
+ "subtitle": "Choose the languages you use. The first is your primary interface language.",
173
173
  "current": "Current Language",
174
+ "selected": "Your languages",
174
175
  "available": "Available Languages",
176
+ "primary": "Primary",
177
+ "remove": "Remove",
175
178
  "changed": "Language changed to {{lang}}",
176
179
  "saveFailed": "Failed to save language preference",
177
180
  "search": "Search languages"
@@ -123,9 +123,12 @@
123
123
  },
124
124
  "language": {
125
125
  "title": "Idioma",
126
- "subtitle": "Elige tu idioma preferido",
126
+ "subtitle": "Elige los idiomas que usas. El primero es tu idioma de interfaz principal.",
127
127
  "current": "Idioma actual",
128
+ "selected": "Tus idiomas",
128
129
  "available": "Idiomas disponibles",
130
+ "primary": "Principal",
131
+ "remove": "Quitar",
129
132
  "changed": "Idioma cambiado a {{lang}}",
130
133
  "saveFailed": "Error al guardar la preferencia de idioma",
131
134
  "search": "Buscar idiomas"
package/src/index.ts CHANGED
@@ -53,6 +53,7 @@ export type {
53
53
  BulkFollowResult,
54
54
  BulkUnfollowEntry,
55
55
  BulkUnfollowResult,
56
+ ViewerGraph,
56
57
  } from './mixins/OxyServices.user';
57
58
  export { OxyAppDataIdentifierError } from './mixins/OxyServices.appData';
58
59
 
@@ -323,13 +324,18 @@ export type { TopicData, TopicTranslation } from './models/Topic';
323
324
  // ---------------------------------------------------------------------------
324
325
  export {
325
326
  SUPPORTED_LANGUAGES,
327
+ FALLBACK_LOCALE,
328
+ getBaseLanguage,
329
+ normalizeLocale,
330
+ isSupportedLocale,
326
331
  getLanguageMetadata,
327
332
  getLanguageName,
328
333
  getNativeLanguageName,
329
- normalizeLanguageCode,
330
334
  isRTLLocale,
335
+ getUserLanguages,
336
+ getPrimaryLanguage,
331
337
  } from './utils/languageUtils';
332
- export type { LanguageMetadata } from './utils/languageUtils';
338
+ export type { SupportedLanguage } from './utils/languageUtils';
333
339
 
334
340
  // ---------------------------------------------------------------------------
335
341
  // Platform detection
@@ -435,12 +435,16 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
435
435
  if (error instanceof Error) {
436
436
  errorMessage = error.message || errorMessage;
437
437
  } else if (error && typeof error === 'object') {
438
- if ('message' in error) {
439
- errorMessage = String((error as any).message) || errorMessage;
440
- } else if ('error' in error && typeof (error as any).error === 'string') {
441
- errorMessage = (error as any).error;
442
- } else if ('data' in error && (error as any).data?.message) {
443
- errorMessage = String((error as any).data.message);
438
+ const errObj = error as Record<string, unknown>;
439
+ if ('message' in errObj) {
440
+ errorMessage = String(errObj.message) || errorMessage;
441
+ } else if (typeof errObj.error === 'string') {
442
+ errorMessage = errObj.error;
443
+ } else if (errObj.data && typeof errObj.data === 'object') {
444
+ const dataObj = errObj.data as Record<string, unknown>;
445
+ if (dataObj.message) {
446
+ errorMessage = String(dataObj.message);
447
+ }
444
448
  }
445
449
  } else if (error) {
446
450
  errorMessage = String(error) || errorMessage;
@@ -462,8 +466,8 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
462
466
  throw handledError;
463
467
  }
464
468
 
465
- const newError = new Error(errorMessage);
466
- (newError as any).fileContext = contextError.fileContext;
469
+ const newError: Error & { fileContext?: Record<string, unknown> } = new Error(errorMessage);
470
+ newError.fileContext = contextError.fileContext;
467
471
  throw this.handleError(newError);
468
472
  }
469
473
  }
@@ -546,7 +550,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
546
550
  /**
547
551
  * Delete asset with optional force
548
552
  */
549
- async assetDelete(fileId: string, force: boolean = false): Promise<any> {
553
+ async assetDelete(fileId: string, force = false): Promise<any> {
550
554
  try {
551
555
  const params: any = force ? { force: 'true' } : undefined;
552
556
  const result = await this.makeRequest('DELETE', `/assets/${fileId}`, params, { cache: false });
@@ -590,7 +594,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
590
594
  }
591
595
  }
592
596
 
593
- async uploadAvatar(file: AssetUploadInput, userId: string, app: string = 'profiles'): Promise<any> {
597
+ async uploadAvatar(file: AssetUploadInput, userId: string, app = 'profiles'): Promise<any> {
594
598
  try {
595
599
  const asset = await this.assetUpload(file, 'public');
596
600
  await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
@@ -600,7 +604,7 @@ export function OxyServicesAssetsMixin<T extends typeof OxyServicesBase>(Base: T
600
604
  }
601
605
  }
602
606
 
603
- async uploadProfileBanner(file: AssetUploadInput, userId: string, app: string = 'profiles'): Promise<any> {
607
+ async uploadProfileBanner(file: AssetUploadInput, userId: string, app = 'profiles'): Promise<any> {
604
608
  try {
605
609
  const asset = await this.assetUpload(file, 'public');
606
610
  await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
@@ -1,12 +1,25 @@
1
1
  /**
2
2
  * Language Methods Mixin
3
3
  */
4
- import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
- import type { LanguageMetadata } from '../utils/languageUtils';
4
+ import { normalizeLocale, getPrimaryLanguage, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
5
+ import type { SupportedLanguage } from '../utils/languageUtils';
6
6
  import type { OxyServicesBase } from '../OxyServices.base';
7
7
  import { loadAsyncStorage } from '@oxyhq/protocol';
8
8
  import { isDev } from '../shared/utils/debugUtils';
9
9
 
10
+ /**
11
+ * Cross-mixin surface consumed by the language methods. `getCurrentUser` is
12
+ * provided by the user mixin at runtime — both live on the composed
13
+ * `OxyServices` instance — but it is not visible on `OxyServicesBase` at the
14
+ * type level. We narrow `this` through this interface (mirroring the
15
+ * `OxyAuthInstance` pattern in `OxyServices.utility.ts`) instead of casting to
16
+ * `any`. Only the fields this mixin reads are declared, keeping it decoupled
17
+ * from the full `User` shape.
18
+ */
19
+ interface LanguageMixinCrossAccess {
20
+ getCurrentUser(): Promise<{ languages?: string[] }>;
21
+ }
22
+
10
23
  export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base: T) {
11
24
  return class extends Base {
12
25
  constructor(...args: any[]) {
@@ -62,29 +75,30 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
62
75
  }
63
76
 
64
77
  /**
65
- * Get the current language from storage or user profile
78
+ * Get the current locale from the user profile or local storage.
66
79
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
67
- * @returns The current language code (e.g., 'en-US') or null if not set
80
+ * @returns The current BCP-47 locale (e.g., 'en-US') or null if not set
68
81
  */
69
- async getCurrentLanguage(storageKeyPrefix: string = 'oxy_session'): Promise<string | null> {
82
+ async getCurrentLanguage(storageKeyPrefix = 'oxy_session'): Promise<string | null> {
70
83
  try {
71
- // First try to get from user profile if authenticated
84
+ // First try the authenticated user's primary account locale.
72
85
  try {
73
- const user = await (this as any).getCurrentUser();
74
- const userLanguage = (user as Record<string, unknown>)?.language as string | undefined;
75
- if (userLanguage) {
76
- return normalizeLanguageCode(userLanguage) || userLanguage;
86
+ const user = await (this as unknown as LanguageMixinCrossAccess).getCurrentUser();
87
+ const primary = getPrimaryLanguage(user);
88
+ if (primary) {
89
+ return primary;
77
90
  }
78
- } catch (e) {
79
- // User not authenticated or error, continue to storage
91
+ } catch {
92
+ // Not authenticated or the profile fetch failed — fall through to the
93
+ // locally stored preference below.
80
94
  }
81
95
 
82
- // Fall back to storage
96
+ // Fall back to the locally stored locale preference.
83
97
  const storage = await this.getStorage();
84
98
  const storageKey = `${storageKeyPrefix}_language`;
85
99
  const storedLanguage = await storage.getItem(storageKey);
86
100
  if (storedLanguage) {
87
- return normalizeLanguageCode(storedLanguage) || storedLanguage;
101
+ return normalizeLocale(storedLanguage) ?? storedLanguage;
88
102
  }
89
103
 
90
104
  return null;
@@ -101,7 +115,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
101
115
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
102
116
  * @returns Language metadata object or null if not set
103
117
  */
104
- async getCurrentLanguageMetadata(storageKeyPrefix: string = 'oxy_session'): Promise<LanguageMetadata | null> {
118
+ async getCurrentLanguageMetadata(storageKeyPrefix = 'oxy_session'): Promise<SupportedLanguage | null> {
105
119
  const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
106
120
  return getLanguageMetadata(languageCode);
107
121
  }
@@ -111,7 +125,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
111
125
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
112
126
  * @returns Language name or null if not set
113
127
  */
114
- async getCurrentLanguageName(storageKeyPrefix: string = 'oxy_session'): Promise<string | null> {
128
+ async getCurrentLanguageName(storageKeyPrefix = 'oxy_session'): Promise<string | null> {
115
129
  const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
116
130
  if (!languageCode) return null;
117
131
  return getLanguageName(languageCode);
@@ -122,7 +136,7 @@ export function OxyServicesLanguageMixin<T extends typeof OxyServicesBase>(Base:
122
136
  * @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
123
137
  * @returns Native language name or null if not set
124
138
  */
125
- async getCurrentNativeLanguageName(storageKeyPrefix: string = 'oxy_session'): Promise<string | null> {
139
+ async getCurrentNativeLanguageName(storageKeyPrefix = 'oxy_session'): Promise<string | null> {
126
140
  const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
127
141
  if (!languageCode) return null;
128
142
  return getNativeLanguageName(languageCode);
@@ -82,6 +82,9 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
82
82
  cache: false,
83
83
  });
84
84
  this.clearCacheEntry('GET:/privacy/blocked');
85
+ // The block changed the viewer's graph (`blockedIds`) — bust the cached
86
+ // consolidated `GET /users/me/graph` so the next read reflects it.
87
+ this.clearCacheEntry('GET:/users/me/graph');
85
88
  return result;
86
89
  } catch (error) {
87
90
  throw this.handleError(error);
@@ -105,6 +108,9 @@ export function OxyServicesPrivacyMixin<T extends typeof OxyServicesBase>(Base:
105
108
  cache: false,
106
109
  });
107
110
  this.clearCacheEntry('GET:/privacy/blocked');
111
+ // Symmetric to blockUser: the unblock changed the viewer's `blockedIds`,
112
+ // so bust the consolidated `GET /users/me/graph` cache.
113
+ this.clearCacheEntry('GET:/users/me/graph');
108
114
  return result;
109
115
  } catch (error) {
110
116
  throw this.handleError(error);
@@ -57,7 +57,7 @@ export function OxyServicesSecurityMixin<T extends typeof OxyServicesBase>(Base:
57
57
  * @param limit - Number of recent events to fetch (default: 10)
58
58
  * @returns Array of recent security activities
59
59
  */
60
- async getRecentSecurityActivity(limit: number = 10): Promise<SecurityActivity[]> {
60
+ async getRecentSecurityActivity(limit = 10): Promise<SecurityActivity[]> {
61
61
  try {
62
62
  const response = await this.getSecurityActivity(limit, 0);
63
63
  return response.data || [];