@shubhamdeol/locale-ota-react-native
v1.1.0
Published
expo-file-system storage, expo-font loader, expo-crypto digest, and useLocaleFont.
Readme
@shubhamdeol/locale-ota-react-native
The Expo pieces of locale-ota: a disk cache on expo-file-system, a font loader
on expo-font, a SHA-256 on expo-crypto, and two hooks.
Install
bun add @shubhamdeol/locale-ota-react-native
npx expo install expo-file-system expo-font expo-cryptoThis package re-exports createClient and every client type, so an Expo app
names one package. @shubhamdeol/locale-ota-client comes in under it; you never
install it yourself.
react, react-native, expo-file-system and expo-font are peer
dependencies, and expo-crypto is an optional one. The storage adapter uses the
current File and Directory API, the one that became the default in SDK 54;
it never touches expo-file-system/legacy.
Setup
import {
createClient,
expoCryptoDigest,
expoFileSystemStorage,
expoFontLoader,
useLocaleFont,
useLocaleOtaStatus,
} from "@shubhamdeol/locale-ota-react-native";
import en from "./locales/en.json";
export const localeOta = createClient({
baseUrl: "https://translations.acme.example",
source: { translation: en }, // the file this build bundles; its hash names the release
digest: expoCryptoDigest(), // Hermes has no crypto.subtle; expo-crypto supplies the SHA-256
storage: expoFileSystemStorage(),
fontLoader: expoFontLoader(),
});function Greeting({ language }: { language: string }) {
const fontFamily = useLocaleFont(localeOta, language);
const status = useLocaleOtaStatus(localeOta);
return <Text style={{ fontFamily }}>{status === "offline" ? "…" : "नमस्ते"}</Text>;
}baseUrl is the bucket, or the CDN in front of it. The app carries no key: the
client reads a manifest and a language file, both public GETs. On any failure it
answers with what it cached, and with nothing on a cold start, so the screen
keeps the bundled English.
For i18next, hand the same client to @shubhamdeol/locale-ota-i18next and let
it do the loading.
Point baseUrl at http://127.0.0.1:<port>/preview to read the local
dashboard's preview of the working tree instead of a published release. On an
iOS simulator 127.0.0.1 is the machine running the packager; on a device, use
that machine's LAN address.
API
| Export | Answers with | Notes |
| --- | --- | --- |
| expoFileSystemStorage(options?) | StorageAdapter | JSON under <document>/locale-ota/json, font files under <document>/locale-ota/files. |
| expoFontLoader() | FontLoader | Calls Font.loadAsync with one entry per file. |
| expoCryptoDigest() | Digest | expo-crypto's SHA-256 as lowercase hex, on a native thread. Hermes has no crypto.subtle, and the client needs one to hash source and name the release it reads. Optional peer: install expo-crypto, or pass any (text) => Promise<hex> of your own. |
| fontSourceMap(family, files) | Record<string, { uri }> | The map expoFontLoader builds. Exported so you can inspect or extend it. |
| fontFamilyName(family, file) | string | Noto-700, Noto-400-italic. |
| useLocaleFont(client, language) | string \| undefined | The family to put in a style. undefined while it loads and for a language with no font. |
| useLocaleOtaStatus(client) | Status | Re-renders on every status change. |
expoFileSystemStorage options
| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| directoryName | string | "locale-ota" | The folder under the document directory. The document directory is the one the system does not clear when storage runs low, which is what a translation cache wants. |
How the files are named
JSON entries are one file each, named encodeURIComponent(key) + ".json", so a
key survives the round trip and cannot escape its folder. keys(prefix) lists
the folder and decodes the names back.
Font files keep the extension their content type implies. font/ttf becomes
.ttf, because the native font loaders care about it. getFile only has the
hash, so it tries the handful of names a putFile could have written.
A font is stored under the SHA-256 of its bytes, which is also its name in the
bucket, so the same file shared by two languages is downloaded once and kept
once. A relaunch with no network still shows the family it showed before,
because getFont falls back to the last manifest the client stored.
How the font families are named
React Native has no weight axis on a loaded family, so every weight and style
has to be its own family name. A manifest with one file registers the plain
family. A manifest with several registers Noto-400, Noto-700,
Noto-400-italic, and the plain family for the 400 normal file, or for the
first file when the family ships no 400.
