import { FluentBundle, FluentResource } from '@fluent/bundle'; import type { LocaleCode, NamespaceCode, ResourceKey, } from '../locale-manager/base-locale-manager'; import type { BundleSource, Resource } from './base'; type FetchMethod = (key: ResourceKey, init?: RequestInit) => Promise; export interface HTTPBundleSourceParams { fetch?: FetchMethod; // could control things like... idk caching or whatever here possibly. } // TODO - need to define this a little better if we want a default. currently // this is just a fantasy URL const DEFAULT_FETCH = (key: ResourceKey, init?: RequestInit) => fetch(`/locales/${key}`, init); export class HTTPBundleSource implements BundleSource { private readonly fetch: FetchMethod; private readonly resources: Map = new Map(); constructor(params: HTTPBundleSourceParams) { this.fetch = params.fetch ?? DEFAULT_FETCH; } addSource(resource: Resource): void { throw new Error('Method not implemented.'); } getKey(locale: LocaleCode, namespace: NamespaceCode): ResourceKey { return `${namespace}/${locale}`; } getBundles( locales: LocaleCode[], namespaces: NamespaceCode[], ): FluentBundle[] { const bundles = []; const promises: [ FluentBundle, ResourceKey, Promise, ][] = []; for (const locale of locales) { // is it bad to generate new bundle a lot? IDK. Might need to like, cache // these a little better or something. Some day we can actually think // about it. These Loaders are meant to be global (multi-micro-frontend) // so there won't just be a single bundle for every locale. const bundle = new FluentBundle(locale); bundles.push(bundle); for (const namespace of namespaces) { const key = this.getKey(locale, namespace); const resource = this.resources.get(key); // add the resource if we already have it... if (resource) bundle.addResource(resource); // or start trying to fetch it if we don't. else promises.push([bundle, key, this.fetchResource(key)]); } } // TODO - This could be a problem, but we were using this without waiting for this promise anyway in practice. void this.awaitResources(promises); return bundles; } private async awaitResources( promises: [FluentBundle, ResourceKey, Promise][], ): Promise { for (const [bundle, key, promise] of promises) { let resource: FluentResource | null = null; try { resource = await promise; } catch (error) { console.warn(`exception while fetching resource for "${key}"`); console.error(error); } if (resource === null) { // what would we actually want to do with a failed fetch? we could just // not add the resource, or report this failure back to the caller. for // now I'll just trudge ahead. console.warn(`couldn't load resource for "${key}"`); } else { bundle.addResource(resource); } } } private async fetchResource( key: ResourceKey, ): Promise { const response = await this.fetch(key); if (response.ok) { const resource = new FluentResource(await response.text()); this.resources.set(key, resource); return resource; } else { console.warn(`could not load i18n for ${key}`); // could do like this if we don't want to try re-fetching? idk. // this.resources.get(locale)!.set(namspace, null); return null; } } }