From 377daaa92b131665bc139ab07ddb9ef8112faa6d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 20 Oct 2023 10:43:36 +0530 Subject: [PATCH] refactor: use config providers for resolving config --- index.ts | 2 - providers/i18n_provider.ts | 24 +++++- src/define_config.ts | 83 ++++++++++++++++--- src/formatters/main.ts | 50 ----------- src/i18n_manager.ts | 6 +- src/loaders/{fs_loader.ts => fs.ts} | 0 src/loaders/main.ts | 54 ------------ .../icu.ts} | 0 src/types/extended.ts | 21 ----- src/types/main.ts | 48 ++++------- tests/configure.spec.ts | 2 +- tests/define_config.spec.ts | 43 +++++----- tests/fs_loader.spec.ts | 2 +- tests/i18n.spec.ts | 4 +- tests/i18n_manager.spec.ts | 7 +- tests/i18n_provider.spec.ts | 21 ++--- tests/icu_message_formatter.spec.ts | 4 +- 17 files changed, 153 insertions(+), 218 deletions(-) delete mode 100644 src/formatters/main.ts rename src/loaders/{fs_loader.ts => fs.ts} (100%) delete mode 100644 src/loaders/main.ts rename src/{formatters/icu_messages_formatter.ts => messages_formatters/icu.ts} (100%) delete mode 100644 src/types/extended.ts diff --git a/index.ts b/index.ts index 9f04d17..2d8e61a 100644 --- a/index.ts +++ b/index.ts @@ -12,5 +12,3 @@ export { configure } from './configure.js' export { stubsRoot } from './stubs/main.js' export { I18nManager } from './src/i18n_manager.js' export { defineConfig } from './src/define_config.js' -export { default as loadersList } from './src/loaders/main.js' -export { default as formattersList } from './src/formatters/main.js' diff --git a/providers/i18n_provider.ts b/providers/i18n_provider.ts index df616e7..5c8c5c8 100644 --- a/providers/i18n_provider.ts +++ b/providers/i18n_provider.ts @@ -8,9 +8,20 @@ */ import type { Edge } from 'edge.js' +import { I18nManager } from '../src/i18n_manager.js' import type { ApplicationService } from '@adonisjs/core/types' +import type { MissingTranslationEventPayload } from '../src/types/main.js' +import { configProvider } from '@adonisjs/core' +import { RuntimeException } from '@poppinss/utils' -import '../src/types/extended.js' +declare module '@adonisjs/core/types' { + export interface EventsList { + 'i18n:missing:translation': MissingTranslationEventPayload + } + export interface ContainerBindings { + i18n: I18nManager + } +} /** * Registers a singleton instance of I18nManager to the container, @@ -36,9 +47,16 @@ export default class I18nProvider { */ register() { this.app.container.singleton('i18n', async (resolver) => { - const { I18nManager } = await import('../src/i18n_manager.js') + const i18nConfigProvider = this.app.config.get('i18n', {}) + const config = await configProvider.resolve(this.app, i18nConfigProvider) + + if (!config) { + throw new RuntimeException( + 'Invalid default export from "config/i18n.ts" file. Make sure to use defineConfig method' + ) + } + const emitter = await resolver.make('emitter') - const config = this.app.config.get('i18n', {}) return new I18nManager(emitter, config) }) } diff --git a/src/define_config.ts b/src/define_config.ts index 316fcee..00d3656 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,26 +7,83 @@ * file that was distributed with this source code. */ +import { configProvider } from '@adonisjs/core' import { RuntimeException } from '@poppinss/utils' +import type { ConfigProvider } from '@adonisjs/core/types' -import loadersList from './loaders/main.js' -import formattersList from './formatters/main.js' -import type { I18nConfig, I18nServiceConfig } from './types/main.js' +import type { + LoaderFactory, + BaseI18nConfig, + FormatterFactory, + I18nManagerConfig, + FsLoaderOptions, +} from './types/main.js' /** - * Define i18n config + * Config helper to define i18n config */ -export function defineConfig(config: Partial): I18nConfig { +export function defineConfig( + config: Partial & { + formatter: FormatterFactory | ConfigProvider + loaders: (ConfigProvider | LoaderFactory)[] + } +): ConfigProvider { if (!config.formatter) { throw new RuntimeException('Cannot configure i18n manager. Missing property "formatter"') } - return { - defaultLocale: 'en', - ...config, - formatter: (i18Config) => formattersList.create(config.formatter!, i18Config), - loaders: (config.loaders || []).map((loaderConfig) => { - return (i18nConfig) => loadersList.create(loaderConfig.driver, loaderConfig, i18nConfig) - }), - } satisfies I18nConfig + const { formatter, loaders, ...rest } = config + + return configProvider.create(async (app) => { + /** + * Resolving formatter + */ + const resolvedFormatter = + typeof formatter === 'function' ? formatter : await formatter.resolver(app) + + /** + * Resolving loaders. Each loader can be a factory or a + * config provider. + */ + const resolvedLoaders = await Promise.all( + loaders.map((loader) => { + return typeof loader === 'function' ? loader : loader.resolver(app) + }) + ) + + return { + defaultLocale: 'en', + formatter: resolvedFormatter, + loaders: resolvedLoaders, + ...rest, + } satisfies I18nManagerConfig + }) +} + +/** + * Config helper to configure a formatter for i18n + */ +export const formatters: { + icu: () => ConfigProvider +} = { + icu() { + return configProvider.create(async () => { + const { IcuFormatter } = await import('../src/messages_formatters/icu.js') + return () => new IcuFormatter() + }) + }, +} + +/** + * Config helper to configure loaders for i18n + */ +export const loaders: { + fs: (config: FsLoaderOptions) => ConfigProvider +} = { + fs(config) { + return configProvider.create(async () => { + const { FsLoader } = await import('../src/loaders/fs.js') + return () => new FsLoader(config) + }) + }, } diff --git a/src/formatters/main.ts b/src/formatters/main.ts deleted file mode 100644 index 61b6387..0000000 --- a/src/formatters/main.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * @adonisjs/i18n - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' - -import { IcuFormatter } from './icu_messages_formatter.js' -import type { I18nConfig, TranslationsFormattersList } from '../types/main.js' - -class FormattersList { - /** - * List of registered formatter - */ - list: Partial = { - icu: () => new IcuFormatter(), - } - - /** - * Extend formatter collection and add a custom - * formatter to it. - */ - extend( - name: Name, - factoryCallback: TranslationsFormattersList[Name] - ): this { - this.list[name] = factoryCallback - return this - } - - /** - * Creates the formatter instance with config - */ - create(name: Name, i18nConfig: I18nConfig) { - const formatterFactory = this.list[name] - if (!formatterFactory) { - throw new RuntimeException( - `Unknown i18n formatter "${String(name)}". Make sure the formatter is registered` - ) - } - - return formatterFactory(i18nConfig) - } -} - -export default new FormattersList() diff --git a/src/i18n_manager.ts b/src/i18n_manager.ts index 73c7710..2074132 100644 --- a/src/i18n_manager.ts +++ b/src/i18n_manager.ts @@ -10,7 +10,7 @@ import Negotiator from 'negotiator' import type { Emitter } from '@adonisjs/core/events' import type { - I18nConfig, + I18nManagerConfig, TranslationsFormatterContract, MissingTranslationEventPayload, } from './types/main.js' @@ -22,7 +22,7 @@ export class I18nManager { /** * i18n config */ - #config: I18nConfig + #config: I18nManagerConfig /** * Reference to the emitter for emitting events @@ -71,7 +71,7 @@ export class I18nManager { constructor( emitter: Emitter<{ 'i18n:missing:translation': MissingTranslationEventPayload } & any>, - config: I18nConfig + config: I18nManagerConfig ) { this.#config = config this.#emitter = emitter diff --git a/src/loaders/fs_loader.ts b/src/loaders/fs.ts similarity index 100% rename from src/loaders/fs_loader.ts rename to src/loaders/fs.ts diff --git a/src/loaders/main.ts b/src/loaders/main.ts deleted file mode 100644 index fe530fc..0000000 --- a/src/loaders/main.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * @adonisjs/i18n - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' - -import { FsLoader } from './fs_loader.js' -import type { I18nConfig, TranslationsLoadersList } from '../types/main.js' - -class LoadersList { - /** - * List of registered loaders - */ - list: Partial = { - fs: (config) => new FsLoader(config), - } - - /** - * Extend loaders collection and add a custom - * loaders to it. - */ - extend( - name: Name, - factoryCallback: TranslationsLoadersList[Name] - ): this { - this.list[name] = factoryCallback - return this - } - - /** - * Creates the loaders instance with config - */ - create( - name: Name, - config: Parameters[0], - i18nConfig: I18nConfig - ) { - const loaderFactory = this.list[name] - if (!loaderFactory) { - throw new RuntimeException( - `Unknown i18n loader "${String(name)}". Make sure the loader is registered` - ) - } - - return loaderFactory(config as any, i18nConfig) - } -} - -export default new LoadersList() diff --git a/src/formatters/icu_messages_formatter.ts b/src/messages_formatters/icu.ts similarity index 100% rename from src/formatters/icu_messages_formatter.ts rename to src/messages_formatters/icu.ts diff --git a/src/types/extended.ts b/src/types/extended.ts deleted file mode 100644 index ccaa203..0000000 --- a/src/types/extended.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * @adonisjs/i18n - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import '@adonisjs/core/types' -import type { I18nManager } from '../i18n_manager.js' -import type { MissingTranslationEventPayload } from './main.js' - -declare module '@adonisjs/core/types' { - export interface EventsList { - 'i18n:missing:translation': MissingTranslationEventPayload - } - export interface ContainerBindings { - i18n: I18nManager - } -} diff --git a/src/types/main.ts b/src/types/main.ts index 179f8b4..2909e2e 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -7,9 +7,6 @@ * file that was distributed with this source code. */ -import type { FsLoader } from '../loaders/fs_loader.js' -import type { IcuFormatter } from '../formatters/icu_messages_formatter.js' - /** * Options for formatting a numeric value. We override loose * types from "Intl.NumberFormatOptions". @@ -84,25 +81,11 @@ export type FsLoaderOptions = { location: string | URL } -/** - * Collection of loaders - */ -export interface TranslationsLoadersList { - fs: (config: FsLoaderOptions, i18nConfig: I18nConfig) => FsLoader -} - -/** - * Collection of formatters - */ -export interface TranslationsFormattersList { - icu: (i18nConfig: I18nConfig) => IcuFormatter -} - /** * Base config shared between i18n config and i18n service * config */ -type BaseI18nConfig = { +export type BaseI18nConfig = { /** * Default locale for the application. This locale is * used when request locale is not supported by the @@ -129,30 +112,31 @@ type BaseI18nConfig = { fallback?: (identifier: string, locale: string) => string } +/** + * Formatter factory is responsible for returning a + * formatter + */ +export type FormatterFactory = (i18nConfig: I18nManagerConfig) => TranslationsFormatterContract + +/** + * Loader factory is responsible for returning a + * loader + */ +export type LoaderFactory = (i18nConfig: I18nManagerConfig) => TranslationsLoaderContract + /** * Config for the package */ -export interface I18nConfig extends BaseI18nConfig { +export interface I18nManagerConfig extends BaseI18nConfig { /** * Translations format to use */ - formatter: (i18nConfig: I18nConfig) => TranslationsFormatterContract + formatter: FormatterFactory /** * Configured loaders for loading translations */ - loaders: ((i18nConfig: I18nConfig) => TranslationsLoaderContract)[] -} - -/** - * The service config auto resolves the formatter and loaders - * lazily using their unique names - */ -export interface I18nServiceConfig extends BaseI18nConfig { - formatter: keyof TranslationsFormattersList - loaders: { - [K in keyof TranslationsLoadersList]: Parameters[0] & { driver: K } - }[keyof TranslationsLoadersList][] + loaders: LoaderFactory[] } /** diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 0fb94ee..6a5c93d 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -60,5 +60,5 @@ test.group('Configure', (group) => { 'app/middleware/detect_user_locale_middleware.ts', `export default class DetectUserLocaleMiddleware` ) - }).timeout(10000) + }).timeout(60 * 1000) }) diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index 639ec11..9d4a426 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -8,37 +8,42 @@ */ import { test } from '@japa/runner' -import { defineConfig } from '../index.js' -import { FsLoader } from '../src/loaders/fs_loader.js' -import { IcuFormatter } from '../src/formatters/icu_messages_formatter.js' +import { ApplicationService } from '@adonisjs/core/types' +import { AppFactory } from '@adonisjs/core/factories/app' + +import { FsLoader } from '../src/loaders/fs.js' +import { IcuFormatter } from '../src/messages_formatters/icu.js' +import { defineConfig, formatters, loaders } from '../src/define_config.js' + +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService test.group('Define config', () => { test('throw error when missing formatter', ({ assert }) => { - assert.throws( - () => defineConfig({}), + assert.rejects( + () => defineConfig({} as any), 'Cannot configure i18n manager. Missing property "formatter"' ) }) - test('resolve formatter using formattersList', ({ assert }) => { - const config = defineConfig({ - formatter: 'icu', - }) + test('transform config with a formatter', async ({ assert }) => { + const config = await defineConfig({ + loaders: [], + formatter: formatters.icu(), + }).resolver(app) assert.isFunction(config.formatter) assert.instanceOf(config.formatter(config), IcuFormatter) }) - test('resolve loader using loadersList', ({ fs, assert }) => { - const config = defineConfig({ - formatter: 'icu', - loaders: [ - { - driver: 'fs', - location: fs.basePath, - }, - ], - }) + test('transform config with loaders', async ({ assert }) => { + const config = await defineConfig({ + loaders: [loaders.fs({ location: BASE_URL })], + formatter: formatters.icu(), + }).resolver(app) + + assert.isFunction(config.formatter) + assert.instanceOf(config.formatter(config), IcuFormatter) assert.isFunction(config.loaders[0]) assert.instanceOf(config.loaders[0](config), FsLoader) diff --git a/tests/fs_loader.spec.ts b/tests/fs_loader.spec.ts index a453c4e..a142be7 100644 --- a/tests/fs_loader.spec.ts +++ b/tests/fs_loader.spec.ts @@ -9,7 +9,7 @@ import { join } from 'node:path' import { test } from '@japa/runner' -import { FsLoader } from '../src/loaders/fs_loader.js' +import { FsLoader } from '../src/loaders/fs.js' test.group('Fs loader | JSON', () => { test('load all .json files from the config location', async ({ fs, assert }) => { diff --git a/tests/i18n.spec.ts b/tests/i18n.spec.ts index a5de461..301d868 100644 --- a/tests/i18n.spec.ts +++ b/tests/i18n.spec.ts @@ -14,9 +14,9 @@ import { Emitter } from '@adonisjs/core/events' import { AppFactory } from '@adonisjs/core/factories/app' import { I18n } from '../src/i18n.js' +import { FsLoader } from '../src/loaders/fs.js' import { I18nManager } from '../src/i18n_manager.js' -import { FsLoader } from '../src/loaders/fs_loader.js' -import { IcuFormatter } from '../src/formatters/icu_messages_formatter.js' +import { IcuFormatter } from '../src/messages_formatters/icu.js' import type { MissingTranslationEventPayload } from '../src/types/main.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) diff --git a/tests/i18n_manager.spec.ts b/tests/i18n_manager.spec.ts index e34ea5c..0f8aead 100644 --- a/tests/i18n_manager.spec.ts +++ b/tests/i18n_manager.spec.ts @@ -13,12 +13,13 @@ import { Emitter } from '@adonisjs/core/events' import { AppFactory } from '@adonisjs/core/factories/app' import { I18n } from '../src/i18n.js' +import { FsLoader } from '../src/loaders/fs.js' import { I18nManager } from '../src/i18n_manager.js' -import { FsLoader } from '../src/loaders/fs_loader.js' +import { IcuFormatter } from '../src/messages_formatters/icu.js' import type { MissingTranslationEventPayload } from '../src/types/main.js' -import { IcuFormatter } from '../src/formatters/icu_messages_formatter.js' -const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) const emitter = new Emitter<{ 'i18n:missing:translation': MissingTranslationEventPayload }>(app) test.group('I18nManager', () => { diff --git a/tests/i18n_provider.spec.ts b/tests/i18n_provider.spec.ts index 62a6200..70bf0d0 100644 --- a/tests/i18n_provider.spec.ts +++ b/tests/i18n_provider.spec.ts @@ -8,12 +8,11 @@ */ import edge from 'edge.js' -import { join } from 'node:path' import { test } from '@japa/runner' import { IgnitorFactory } from '@adonisjs/core/factories' -import { defineConfig } from '../src/define_config.js' import { I18nManager } from '../src/i18n_manager.js' +import { defineConfig, formatters, loaders } from '../src/define_config.js' const BASE_URL = new URL('./tmp/', import.meta.url) const IMPORTER = (filePath: string) => { @@ -31,12 +30,11 @@ test.group('I18n Provider', () => { .merge({ config: { i18n: defineConfig({ - formatter: 'icu', + formatter: formatters.icu(), loaders: [ - { - driver: 'fs', - location: join(fs.basePath, 'resources/lang'), - }, + loaders.fs({ + location: fs.baseUrl, + }), ], }), }, @@ -61,12 +59,11 @@ test.group('I18n Provider', () => { .merge({ config: { i18n: defineConfig({ - formatter: 'icu', + formatter: formatters.icu(), loaders: [ - { - driver: 'fs', - location: join(fs.basePath, 'resources/lang'), - }, + loaders.fs({ + location: fs.baseUrl, + }), ], }), }, diff --git a/tests/icu_message_formatter.spec.ts b/tests/icu_message_formatter.spec.ts index d957ca9..634f16b 100644 --- a/tests/icu_message_formatter.spec.ts +++ b/tests/icu_message_formatter.spec.ts @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -import { test } from '@japa/runner' -import { IcuFormatter } from '../src/formatters/icu_messages_formatter.js' import { DateTime } from 'luxon' +import { test } from '@japa/runner' +import { IcuFormatter } from '../src/messages_formatters/icu.js' test.group('ICU message formatter', () => { test('format a string value', ({ assert }) => {