about summary refs log tree commit diff
path: root/src/state/persisted
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-09-20 10:50:33 -0500
committerGitHub <noreply@github.com>2024-09-20 10:50:33 -0500
commitfa6f6f9e473a0dd731ea95210fbd66e0b8c0c283 (patch)
tree1c5166f9b31d3b4967fcf8cd8cdb969d2efa92cb /src/state/persisted
parentcd88cbeab83169410fff3245505b53122dfe28aa (diff)
downloadvoidsky-fa6f6f9e473a0dd731ea95210fbd66e0b8c0c283.tar.zst
Language fixes (#5384)
* Add some comments

* Decouple language settings

* Normalize on read/write

* Refactor

* Support device locale on app startup

* Cleanup, port to web

* Clean up comments

* Comment

* Try not to mutate

* Protect util handling, update test

* Dedupe array values
Diffstat (limited to 'src/state/persisted')
-rw-r--r--src/state/persisted/index.ts10
-rw-r--r--src/state/persisted/index.web.ts13
-rw-r--r--src/state/persisted/schema.ts52
-rw-r--r--src/state/persisted/util.ts51
4 files changed, 109 insertions, 17 deletions
diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts
index 6f4beae2c..51d757ad8 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -8,6 +8,7 @@ import {
   tryStringify,
 } from '#/state/persisted/schema'
 import {PersistedApi} from './types'
+import {normalizeData} from './util'
 
 export type {PersistedAccount, Schema} from '#/state/persisted/schema'
 export {defaults} from '#/state/persisted/schema'
@@ -33,10 +34,10 @@ export async function write<K extends keyof Schema>(
   key: K,
   value: Schema[K],
 ): Promise<void> {
-  _state = {
+  _state = normalizeData({
     ..._state,
     [key]: value,
-  }
+  })
   await writeToStorage(_state)
 }
 write satisfies PersistedApi['write']
@@ -81,6 +82,9 @@ async function readFromStorage(): Promise<Schema | undefined> {
     })
   }
   if (rawData) {
-    return tryParse(rawData)
+    const parsed = tryParse(rawData)
+    if (parsed) {
+      return normalizeData(parsed)
+    }
   }
 }
diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts
index 7521776bc..4cfc87cdb 100644
--- a/src/state/persisted/index.web.ts
+++ b/src/state/persisted/index.web.ts
@@ -9,6 +9,7 @@ import {
   tryStringify,
 } from '#/state/persisted/schema'
 import {PersistedApi} from './types'
+import {normalizeData} from './util'
 
 export type {PersistedAccount, Schema} from '#/state/persisted/schema'
 export {defaults} from '#/state/persisted/schema'
@@ -56,10 +57,10 @@ export async function write<K extends keyof Schema>(
   } catch (e) {
     // Ignore and go through the normal path.
   }
-  _state = {
+  _state = normalizeData({
     ..._state,
     [key]: value,
-  }
+  })
   writeToStorage(_state)
   broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
   broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
@@ -140,9 +141,11 @@ function readFromStorage(): Schema | undefined {
       return lastResult
     } else {
       const result = tryParse(rawData)
-      lastRawData = rawData
-      lastResult = result
-      return result
+      if (result) {
+        lastRawData = rawData
+        lastResult = normalizeData(result)
+        return lastResult
+      }
     }
   }
 }
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 331a111a2..804017949 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -1,7 +1,8 @@
 import {z} from 'zod'
 
+import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales'
+import {findSupportedAppLanguage} from '#/locale/helpers'
 import {logger} from '#/logger'
-import {deviceLocales} from '#/platform/detection'
 import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army'
 
 const externalEmbedOptions = ['show', 'hide'] as const
@@ -55,10 +56,39 @@ const schema = z.object({
     lastEmailConfirm: z.string().optional(),
   }),
   languagePrefs: z.object({
-    primaryLanguage: z.string(), // should move to server
-    contentLanguages: z.array(z.string()), // should move to server
-    postLanguage: z.string(), // should move to server
+    /**
+     * The target language for translating posts.
+     *
+     * BCP-47 2-letter language code without region.
+     */
+    primaryLanguage: z.string(),
+    /**
+     * The languages the user can read, passed to feeds.
+     *
+     * BCP-47 2-letter language codes without region.
+     */
+    contentLanguages: z.array(z.string()),
+    /**
+     * The language(s) the user is currently posting in, configured within the
+     * composer. Multiple languages are psearate by commas.
+     *
+     * BCP-47 2-letter language code without region.
+     */
+    postLanguage: z.string(),
+    /**
+     * The user's post language history, used to pre-populate the post language
+     * selector in the composer. Within each value, multiple languages are
+     * separated by values.
+     *
+     * BCP-47 2-letter language codes without region.
+     */
     postLanguageHistory: z.array(z.string()),
+    /**
+     * The language for UI translations in the app.
+     *
+     * BCP-47 2-letter language code with or without region,
+     * to match with {@link AppLanguage}.
+     */
     appLanguage: z.string(),
   }),
   requireAltTextEnabled: z.boolean(), // should move to server
@@ -108,13 +138,17 @@ export const defaults: Schema = {
     lastEmailConfirm: undefined,
   },
   languagePrefs: {
-    primaryLanguage: deviceLocales[0] || 'en',
-    contentLanguages: deviceLocales || [],
-    postLanguage: deviceLocales[0] || 'en',
-    postLanguageHistory: (deviceLocales || [])
+    primaryLanguage: deviceLanguageCodes[0] || 'en',
+    contentLanguages: deviceLanguageCodes || [],
+    postLanguage: deviceLanguageCodes[0] || 'en',
+    postLanguageHistory: (deviceLanguageCodes || [])
       .concat(['en', 'ja', 'pt', 'de'])
       .slice(0, 6),
-    appLanguage: deviceLocales[0] || 'en',
+    // try full language tag first, then fallback to language code
+    appLanguage: findSupportedAppLanguage([
+      deviceLocales.at(0)?.languageTag,
+      deviceLanguageCodes[0],
+    ]),
   },
   requireAltTextEnabled: false,
   largeAltBadgeEnabled: false,
diff --git a/src/state/persisted/util.ts b/src/state/persisted/util.ts
new file mode 100644
index 000000000..64a8bf945
--- /dev/null
+++ b/src/state/persisted/util.ts
@@ -0,0 +1,51 @@
+import {parse} from 'bcp-47'
+
+import {dedupArray} from '#/lib/functions'
+import {logger} from '#/logger'
+import {Schema} from '#/state/persisted/schema'
+
+export function normalizeData(data: Schema) {
+  const next = {...data}
+
+  /**
+   * Normalize language prefs to ensure that these values only contain 2-letter
+   * country codes without region.
+   */
+  try {
+    const langPrefs = {...next.languagePrefs}
+    langPrefs.primaryLanguage = normalizeLanguageTagToTwoLetterCode(
+      langPrefs.primaryLanguage,
+    )
+    langPrefs.contentLanguages = dedupArray(
+      langPrefs.contentLanguages.map(lang =>
+        normalizeLanguageTagToTwoLetterCode(lang),
+      ),
+    )
+    langPrefs.postLanguage = langPrefs.postLanguage
+      .split(',')
+      .map(lang => normalizeLanguageTagToTwoLetterCode(lang))
+      .filter(Boolean)
+      .join(',')
+    langPrefs.postLanguageHistory = dedupArray(
+      langPrefs.postLanguageHistory.map(postLanguage => {
+        return postLanguage
+          .split(',')
+          .map(lang => normalizeLanguageTagToTwoLetterCode(lang))
+          .filter(Boolean)
+          .join(',')
+      }),
+    )
+    next.languagePrefs = langPrefs
+  } catch (e: any) {
+    logger.error(`persisted state: failed to normalize language prefs`, {
+      safeMessage: e.message,
+    })
+  }
+
+  return next
+}
+
+export function normalizeLanguageTagToTwoLetterCode(lang: string) {
+  const result = parse(lang).language
+  return result ?? lang
+}