-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: improve language selection resolution #1011
base: develop
Are you sure you want to change the base?
Changes from 4 commits
b924d31
3a244a6
0a97fb5
fee2099
240421a
4f2cd22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { | ||
getLocales as _getLocales, | ||
useLocales as _useLocales, | ||
type Locale, | ||
} from 'expo-localization'; | ||
|
||
import {extractLanguageCode} from '../lib/intl'; | ||
|
||
export const getLocales: typeof _getLocales = () => { | ||
return [createBaseLocale('en-US')]; | ||
}; | ||
|
||
// eslint-disable-next-line @eslint-react/hooks-extra/no-useless-custom-hooks | ||
export const useLocales: typeof _useLocales = () => { | ||
return [createBaseLocale('en-US')]; | ||
}; | ||
|
||
function createBaseLocale(languageTag: string): Locale { | ||
return { | ||
languageTag, | ||
languageCode: extractLanguageCode(languageTag), | ||
langageCurrencyCode: null, | ||
langageCurrencySymbol: null, | ||
languageRegionCode: null, | ||
regionCode: null, | ||
currencyCode: null, | ||
currencySymbol: null, | ||
decimalSeparator: null, | ||
digitGroupingSeparator: null, | ||
textDirection: null, | ||
measurementSystem: null, | ||
temperatureUnit: null, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import {createContext, useContext} from 'react'; | ||
import {createStore, useStore, type StoreApi} from 'zustand'; | ||
import { | ||
createJSONStorage, | ||
persist as createPersistedState, | ||
} from 'zustand/middleware'; | ||
|
||
import {MMKVZustandStorage} from '../hooks/persistedState/createPersistedState'; | ||
|
||
export const STORAGE_KEY = 'MapeoLocale'; | ||
|
||
export type SelectedLocaleState = { | ||
/** | ||
* Value consisting of a language tag (see https://en.wikipedia.org/wiki/IETF_language_tag) | ||
* Represents the language that is explicitly chosen via a user action within the app. If null, it means that either: | ||
* | ||
* 1. The user has never chosen the language explicitly. | ||
* 2. The user has unset the language (e.g. to defer to system preferences) | ||
*/ | ||
languageTag: string | null; | ||
}; | ||
|
||
function createInitialState() { | ||
return { | ||
languageTag: null, | ||
}; | ||
} | ||
|
||
export function createSelectedLocaleStore({persist} = {persist: false}) { | ||
let store: StoreApi<SelectedLocaleState>; | ||
|
||
if (persist) { | ||
store = createStore( | ||
createPersistedState(createInitialState, { | ||
name: STORAGE_KEY, | ||
storage: createJSONStorage(() => MMKVZustandStorage), | ||
version: 1, | ||
migrate: (persistedState, version) => { | ||
/** | ||
* Version 0 stores the state as `{ locale: string, setLocale: (locale: string) => void }`. | ||
* We only need to handle the `locale` field, which is more specifically a language tag. | ||
*/ | ||
if (version === 0) { | ||
// Ensure that the persisted state for version has expected shape before attempting to migrate | ||
if ( | ||
typeof persistedState === 'object' && | ||
persistedState !== null && | ||
'locale' in persistedState && | ||
typeof persistedState.locale === 'string' | ||
) { | ||
// TODO: log to Sentry to help understand how often this is happening? | ||
return {languageTag: persistedState.locale}; | ||
} | ||
} | ||
|
||
return {languageTag: null}; | ||
}, | ||
Comment on lines
+38
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Highlighting this migration implementation since it's important to confirm that it's doing the right thing (since it's kind of hard to test...). also would help to get some input on whether to log to sentry (see todo comments here) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have an idea of how to go about writing tests for this, but would require a decent chunk of additional work I think. might wait for input before trying. |
||
}), | ||
); | ||
} else { | ||
store = createStore(createInitialState); | ||
} | ||
|
||
const actions = { | ||
setLanguageTag: (languageTag: string | null) => { | ||
store.setState({languageTag}); | ||
}, | ||
}; | ||
|
||
return {instance: store, actions}; | ||
} | ||
|
||
export type SelectedLocaleStore = ReturnType<typeof createSelectedLocaleStore>; | ||
|
||
const SelectedLocaleContext = createContext<SelectedLocaleStore | null>(null); | ||
|
||
export const SelectedLocaleStoreProvider = SelectedLocaleContext.Provider; | ||
|
||
function useSelectedLocaleContext() { | ||
const value = useContext(SelectedLocaleContext); | ||
|
||
if (!value) { | ||
throw new Error('Must set up SelectedLocaleStoreProvider first'); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
export function useSelectedLocaleState(): SelectedLocaleState; | ||
export function useSelectedLocaleState<T>( | ||
selector: (state: SelectedLocaleState) => T, | ||
): T; | ||
export function useSelectedLocaleState<T>( | ||
selector?: (state: SelectedLocaleState) => T, | ||
) { | ||
const {instance} = useSelectedLocaleContext(); | ||
return useStore(instance, selector!); | ||
} | ||
|
||
export function useSelectedLocaleActions() { | ||
const {actions} = useSelectedLocaleContext(); | ||
return actions; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ type PersistedStoreKey = | |
| 'ActiveProjectId' | ||
| 'Settings' | ||
| 'MetricDiagnosticsPermission'; | ||
const MMKVZustandStorage: StateStorage = { | ||
export const MMKVZustandStorage: StateStorage = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably should either:
kind of taking the lazy route for now |
||
setItem: (name, value) => { | ||
return storage.set(name, value); | ||
}, | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just noting that this is the same storage key that is currently used for persisting the locale in the app.