Stores
A store is anything that can return a translation leaf for a given (locale, group, key). The IStore port has three methods:
import type { Leaf } from 'ilingo';
export type StoreGetContext = { locale: string, group: string, key: string };
export type StoreSetContext = StoreGetContext & { value: Leaf };
export interface IStore {
get(context: StoreGetContext): Promise<Leaf | undefined>;
set(context: StoreSetContext): Promise<void>;
getLocales(): Promise<string[]>;
}All methods are async — keep that contract even when the implementation is synchronous, because Ilingo.lookup awaits every store call.
MemoryStore
The default. Holds translations in a plain nested object:
import { Ilingo, MemoryStore } from 'ilingo';
const store = new MemoryStore({
data: {
en: { app: { hi: 'Hello, {{name}}!' } },
de: { app: { hi: 'Hallo, {{name}}!' } },
},
});
const ilingo = new Ilingo({ store });You can also set() at runtime — useful when translations come from an API:
await store.set({
locale: 'es',
group: 'app',
key: 'hi',
value: '¡Hola, {{name}}!',
});FSStore
Lazy-loads files from disk. See Integrations → File System for the full story; the gist:
import { Ilingo } from 'ilingo';
import { FSStore } from '@ilingo/fs';
const ilingo = new Ilingo({
store: new FSStore({ directory: './locales' }),
});
// reads ./locales/en/app.json (or .ts / .mjs / .cjs / .conf) on first access
await ilingo.get({ group: 'app', key: 'hi' });Multiple stores
An Ilingo instance exposes a public readonly stores: Set<IStore> — add as many as you want. They are queried in parallel within each locale, with the first declared hit winning:
const ilingo = new Ilingo({
store: new MemoryStore({ data: { /* core strings */ } }),
});
// add more after construction
ilingo.stores.add(new FSStore({ directory: './locales/overrides' }));The constructor's store option seeds the first entry; everything else goes through ilingo.stores.add(...). Set semantics make repeated adds of the same reference a no-op.
For network-backed or side-effecting stores, remember that every store in a locale is queried even if an earlier one hits — the parallel walk picks the winner after Promise.all resolves. Cheap for in-memory and disk; document the cost on custom adapters.
Merging instances
merge(other) adds the foreign instance's stores that are not already present. Identity-based — the same MemoryStore reference is never added twice:
const base = new Ilingo({ store: storeA });
const themed = new Ilingo({ store: storeB });
base.merge(themed); // base now queries storeA then storeBWriting a custom store
Implement the interface as a class — not an object literal — so signature drift is caught at compile time:
import type { IStore, StoreGetContext, StoreSetContext, Leaf } from 'ilingo';
export class HttpStore implements IStore {
async get(ctx: StoreGetContext): Promise<Leaf | undefined> {
const res = await fetch(`/i18n/${ctx.locale}/${ctx.group}.json`);
if (!res.ok) return undefined;
const data = await res.json();
return data[ctx.key];
}
async set(_ctx: StoreSetContext): Promise<void> {
throw new Error('read-only');
}
async getLocales(): Promise<string[]> {
return ['en', 'de'];
}
}Rules of thumb:
- Return
undefinedon miss. Never throw. Throwing breaks the fallback walk. - Returning a
PluralLeafis allowed but optional. String-only stores are valid. - If you need a load cache, extending
MemoryStoreand using the parent map is idiomatic (FSStoredoes this).