@i18n-micro/core
Advanced tools
| import { BaseI18n, BaseI18nOptions } from './base'; | ||
| import { FormatService } from './format-service'; | ||
| import { defaultPlural, interpolate, isNoPrefixStrategy, isPrefixAndDefaultStrategy, isPrefixExceptDefaultStrategy, isPrefixStrategy, withPrefixStrategy } from './helpers'; | ||
| import { TranslationStorage, useTranslationHelper } from './translation'; | ||
| export { useTranslationHelper, interpolate, withPrefixStrategy, isNoPrefixStrategy, isPrefixStrategy, isPrefixExceptDefaultStrategy, isPrefixAndDefaultStrategy, defaultPlural, FormatService, BaseI18n, type TranslationStorage, type BaseI18nOptions, }; |
+45
-9
| { | ||
| "name": "@i18n-micro/core", | ||
| "version": "1.3.0", | ||
| "description": "", | ||
| "repository": "s00d/nuxt-i18n-micro", | ||
| "version": "1.3.1", | ||
| "description": "Core utilities for translations, formatting, and locale routing in Nuxt I18n Micro.", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/s00d/nuxt-i18n-micro.git", | ||
| "directory": "packages/core" | ||
| }, | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "sideEffects": false, | ||
| "author": { | ||
@@ -13,24 +18,55 @@ "name": "s00d", | ||
| }, | ||
| "homepage": "https://github.com/s00d/nuxt-i18n-micro", | ||
| "homepage": "https://github.com/s00d/nuxt-i18n-micro/tree/main/packages/core#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/s00d/nuxt-i18n-micro/issues" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18" | ||
| }, | ||
| "main": "dist/index.cjs", | ||
| "module": "dist/index.mjs", | ||
| "types": "dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/index.d.cts", | ||
| "default": "./dist/index.cjs" | ||
| }, | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "README.md", | ||
| "LICENSE" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "keywords": [], | ||
| "keywords": [ | ||
| "nuxt", | ||
| "i18n", | ||
| "translations", | ||
| "formatting", | ||
| "locale" | ||
| ], | ||
| "dependencies": { | ||
| "@i18n-micro/types": "1.2.0" | ||
| "@i18n-micro/types": "1.2.2" | ||
| }, | ||
| "devDependencies": { | ||
| "publint": "^0.3.17", | ||
| "vite": "^7.3.1", | ||
| "vite-plugin-dts": "^4.5.4" | ||
| "vite-plugin-dts": "^4.5.4", | ||
| "vitest": "^3.2.4" | ||
| }, | ||
| "scripts": { | ||
| "build": "vite build", | ||
| "test": "jest" | ||
| "check:package": "publint", | ||
| "test": "jest", | ||
| "test:dist": "vitest run --config vitest.dist.config.ts" | ||
| } | ||
| } |
-1
| import '@types/jest' |
| module.exports = { | ||
| roots: ['<rootDir>/tests'], | ||
| testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], | ||
| transform: { | ||
| '^.+\\.ts$': 'ts-jest', | ||
| }, | ||
| } |
-194
| import type { CleanTranslation, Getter, MissingHandler, Params, PluralFunc, TranslationKey, Translations } from '@i18n-micro/types' | ||
| import { FormatService } from './format-service' | ||
| import { defaultPlural, interpolate } from './helpers' | ||
| import { type TranslationStorage, useTranslationHelper } from './translation' | ||
| export interface BaseI18nOptions { | ||
| storage?: TranslationStorage | ||
| plural?: PluralFunc | ||
| missingWarn?: boolean | ||
| missingHandler?: (locale: string, key: string, routeName: string) => void | ||
| getCustomMissingHandler?: () => MissingHandler | null | ||
| } | ||
| /** | ||
| * Abstract base class for i18n adapters | ||
| * | ||
| * Contains all common translation logic (t, ts, tc, tn, td, tdr, has). | ||
| * Adapters must implement abstract methods to provide current state (locale, fallbackLocale, route). | ||
| */ | ||
| export abstract class BaseI18n { | ||
| // Public fields (made public to allow type export in Nuxt plugins) | ||
| public helper: ReturnType<typeof useTranslationHelper> | ||
| public formatter = new FormatService() | ||
| public pluralFunc: PluralFunc | ||
| public missingWarn: boolean | ||
| public missingHandler?: (locale: string, key: string, routeName: string) => void | ||
| public getCustomMissingHandler?: () => MissingHandler | null | ||
| constructor(options: BaseI18nOptions = {}) { | ||
| this.helper = useTranslationHelper(options.storage) | ||
| this.formatter = new FormatService() | ||
| this.pluralFunc = options.plural || defaultPlural | ||
| this.missingWarn = options.missingWarn ?? true | ||
| this.missingHandler = options.missingHandler | ||
| this.getCustomMissingHandler = options.getCustomMissingHandler | ||
| } | ||
| // --- Abstract methods (must be implemented by subclasses) --- | ||
| /** | ||
| * Get current locale | ||
| */ | ||
| public abstract getLocale(): string | ||
| /** | ||
| * Get fallback locale | ||
| */ | ||
| public abstract getFallbackLocale(): string | ||
| /** | ||
| * Get current route name | ||
| */ | ||
| public abstract getRoute(): string | ||
| // --- Public methods (implemented in base class) --- | ||
| /** | ||
| * Get translation for a key | ||
| * Based on logic from src/runtime/plugins/01.plugin.ts | ||
| */ | ||
| public t(key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation { | ||
| if (!key) return '' | ||
| // Use abstract getters to get current state | ||
| const locale = this.getLocale() | ||
| const route = routeName || this.getRoute() | ||
| // 1. Try to find translation in current locale | ||
| let value = this.helper.getTranslation<string>(locale, route, key) | ||
| // 2. Fallback to fallbackLocale if not found and different | ||
| if (!value) { | ||
| const fallbackLocale = this.getFallbackLocale() | ||
| if (locale !== fallbackLocale) { | ||
| value = this.helper.getTranslation<string>(fallbackLocale, route, key) | ||
| } | ||
| } | ||
| // 3. Handle missing | ||
| if (!value) { | ||
| // Call custom handler if set (Nuxt runtime), otherwise use instance handler | ||
| const customHandler = this.getCustomMissingHandler?.() | ||
| if (customHandler) { | ||
| customHandler(locale, key, route) | ||
| } else if (this.missingHandler) { | ||
| this.missingHandler(locale, key as string, route) | ||
| } else if (this.missingWarn) { | ||
| const isDev = process.env.NODE_ENV !== 'production' | ||
| const isClient = typeof window !== 'undefined' | ||
| if (isDev && isClient) { | ||
| console.warn(`Not found '${key}' key in '${locale}' locale messages for route '${route}'.`) | ||
| } | ||
| } | ||
| value = defaultValue === undefined ? key : defaultValue || key | ||
| } | ||
| // 4. Interpolate | ||
| return typeof value === 'string' && params ? interpolate(value, params) : (value as CleanTranslation) | ||
| } | ||
| /** | ||
| * Get translation as string | ||
| */ | ||
| public ts(key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string { | ||
| const value = this.t(key, params, defaultValue, routeName) | ||
| return value?.toString() ?? defaultValue ?? key | ||
| } | ||
| /** | ||
| * Plural translation | ||
| */ | ||
| public tc(key: TranslationKey, count: number | Params, defaultValue?: string): string { | ||
| const { count: countValue, ...params } = typeof count === 'number' ? { count } : count | ||
| if (countValue === undefined) { | ||
| return defaultValue ?? key | ||
| } | ||
| // Getter passed to plural function | ||
| const getter: Getter = (k: TranslationKey, p?: Params, dv?: string) => { | ||
| return this.t(k, p, dv) | ||
| } | ||
| const result = this.pluralFunc(key, Number.parseInt(countValue.toString(), 10), params, this.getLocale(), getter) | ||
| return result ?? defaultValue ?? key | ||
| } | ||
| /** | ||
| * Format number | ||
| */ | ||
| public tn(value: number, options?: Intl.NumberFormatOptions): string { | ||
| return this.formatter.formatNumber(value, this.getLocale(), options) | ||
| } | ||
| /** | ||
| * Format date | ||
| */ | ||
| public td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string { | ||
| return this.formatter.formatDate(value, this.getLocale(), options) | ||
| } | ||
| /** | ||
| * Format relative time | ||
| */ | ||
| public tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string { | ||
| return this.formatter.formatRelativeTime(value, this.getLocale(), options) | ||
| } | ||
| /** | ||
| * Check if translation exists | ||
| * Based on logic from src/runtime/plugins/01.plugin.ts | ||
| */ | ||
| public has(key: TranslationKey, routeName?: string): boolean { | ||
| const route = routeName || this.getRoute() | ||
| const locale = this.getLocale() | ||
| // Check only through getTranslation (as in plugin) | ||
| return !!this.helper.getTranslation(locale, route, key) | ||
| } | ||
| /** | ||
| * Clear cache | ||
| */ | ||
| public clearCache(): void { | ||
| this.helper.clearCache() | ||
| } | ||
| // --- Public methods (for subclasses to use) --- | ||
| /** | ||
| * Core translation loading logic (without reactivity) | ||
| * Subclasses can override addTranslations/addRouteTranslations to add reactivity | ||
| */ | ||
| public loadTranslationsCore(locale: string, translations: Translations, merge: boolean, routeName = 'index'): void { | ||
| if (merge) { | ||
| this.helper.mergeTranslation(locale, routeName, translations, true) | ||
| } else { | ||
| this.helper.setTranslations(locale, translations, routeName) | ||
| } | ||
| } | ||
| /** | ||
| * Core route translation loading logic (without reactivity) | ||
| * Subclasses can override addRouteTranslations to add reactivity | ||
| */ | ||
| public loadRouteTranslationsCore(locale: string, routeName: string, translations: Translations, merge: boolean): void { | ||
| if (merge) { | ||
| this.helper.mergeTranslation(locale, routeName, translations, true) | ||
| } else { | ||
| this.helper.loadPageTranslations(locale, routeName, translations) | ||
| } | ||
| } | ||
| } |
| export class FormatService { | ||
| formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string { | ||
| return new Intl.NumberFormat(locale, options).format(value) | ||
| } | ||
| formatDate(value: Date | number | string, locale: string, options?: Intl.DateTimeFormatOptions): string { | ||
| const date = new Date(value) | ||
| if (Number.isNaN(date.getTime())) { | ||
| return 'Invalid Date' | ||
| } | ||
| return new Intl.DateTimeFormat(locale, options).format(date) | ||
| } | ||
| formatRelativeTime(value: Date | number | string, locale: string, options?: Intl.RelativeTimeFormatOptions): string { | ||
| const date = new Date(value) | ||
| if (Number.isNaN(date.getTime())) { | ||
| // Return "0 seconds ago" for invalid dates | ||
| return new Intl.RelativeTimeFormat(locale, options).format(0, 'second') | ||
| } | ||
| const now = new Date() | ||
| const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000) | ||
| const units: { unit: Intl.RelativeTimeFormatUnit; seconds: number }[] = [ | ||
| { unit: 'year', seconds: 31536000 }, | ||
| { unit: 'month', seconds: 2592000 }, | ||
| { unit: 'day', seconds: 86400 }, | ||
| { unit: 'hour', seconds: 3600 }, | ||
| { unit: 'minute', seconds: 60 }, | ||
| { unit: 'second', seconds: 1 }, | ||
| ] | ||
| for (const { unit, seconds } of units) { | ||
| const diff = Math.floor(diffInSeconds / seconds) | ||
| if (diff >= 1) { | ||
| return new Intl.RelativeTimeFormat(locale, options).format(-diff, unit) | ||
| } | ||
| } | ||
| return new Intl.RelativeTimeFormat(locale, options).format(0, 'second') | ||
| } | ||
| } |
| import type { Getter, Params, PluralFunc, Strategies, TranslationKey } from '@i18n-micro/types' | ||
| const RE_TOKEN = /\{(\w+)\}/g | ||
| export function interpolate(template: string, params: Params): string { | ||
| if (!params) return template | ||
| return template.replace(RE_TOKEN, (_, key) => { | ||
| const value = params[key] | ||
| return value !== undefined ? String(value) : `{${key}}` | ||
| }) | ||
| } | ||
| export function withPrefixStrategy(strategy: Strategies) { | ||
| return strategy === 'prefix' || strategy === 'prefix_and_default' | ||
| } | ||
| export function isNoPrefixStrategy(strategy: Strategies) { | ||
| return strategy === 'no_prefix' | ||
| } | ||
| export function isPrefixStrategy(strategy: Strategies) { | ||
| return strategy === 'prefix' | ||
| } | ||
| export function isPrefixExceptDefaultStrategy(strategy: Strategies) { | ||
| return strategy === 'prefix_except_default' | ||
| } | ||
| export function isPrefixAndDefaultStrategy(strategy: Strategies) { | ||
| return strategy === 'prefix_and_default' | ||
| } | ||
| /** | ||
| * Default pluralization function | ||
| * Splits translation by '|' and selects form based on count | ||
| * @param key - Translation key | ||
| * @param count - Count for pluralization | ||
| * @param params - Parameters for translation | ||
| * @param _locale - Current locale (unused in default implementation) | ||
| * @param getTranslation - Function to get translation value | ||
| * @returns Selected plural form or null if not found | ||
| */ | ||
| export const defaultPlural: PluralFunc = (key: TranslationKey, count: number, params: Params, _locale: string, getTranslation: Getter) => { | ||
| const translation = getTranslation(key, params) | ||
| if (!translation) { | ||
| return null | ||
| } | ||
| const forms = translation.toString().split('|') | ||
| if (forms.length === 0) return null | ||
| const selectedForm = count < forms.length ? forms[count] : forms[forms.length - 1] | ||
| if (!selectedForm) return null | ||
| return selectedForm.trim().replace('{count}', count.toString()) | ||
| } |
-27
| import { BaseI18n, type BaseI18nOptions } from './base' | ||
| import { FormatService } from './format-service' | ||
| import { | ||
| defaultPlural, | ||
| interpolate, | ||
| isNoPrefixStrategy, | ||
| isPrefixAndDefaultStrategy, | ||
| isPrefixExceptDefaultStrategy, | ||
| isPrefixStrategy, | ||
| withPrefixStrategy, | ||
| } from './helpers' | ||
| import { type TranslationStorage, useTranslationHelper } from './translation' | ||
| export { | ||
| useTranslationHelper, | ||
| interpolate, | ||
| withPrefixStrategy, | ||
| isNoPrefixStrategy, | ||
| isPrefixStrategy, | ||
| isPrefixExceptDefaultStrategy, | ||
| isPrefixAndDefaultStrategy, | ||
| defaultPlural, | ||
| FormatService, | ||
| BaseI18n, | ||
| type TranslationStorage, | ||
| type BaseI18nOptions, | ||
| } |
| import type { Translations } from '@i18n-micro/types' | ||
| /** | ||
| * Bare Metal: Simple translation storage without Ref, useState, devalue. | ||
| * Map key: `${locale}:${routeName}` (page-specific). | ||
| */ | ||
| export interface TranslationStorage { | ||
| translations: Map<string, Translations> | ||
| } | ||
| function findValue<T = unknown>(data: Translations | null | undefined, key: string): T | null { | ||
| if (!data || typeof key !== 'string') return null | ||
| if (key in data) { | ||
| const value = data[key] | ||
| if (typeof value === 'object' && value !== null) { | ||
| return value as T | ||
| } | ||
| return value as T | ||
| } | ||
| const parts = key.split('.') | ||
| let value: unknown = data | ||
| for (const part of parts) { | ||
| if (value && typeof value === 'object' && part in (value as object)) { | ||
| value = (value as Translations)[part] | ||
| } else { | ||
| return null | ||
| } | ||
| } | ||
| return (value as T) ?? null | ||
| } | ||
| export function useTranslationHelper(storage?: TranslationStorage) { | ||
| const translations = storage?.translations ?? new Map<string, Translations>() | ||
| return { | ||
| hasCache(locale: string, page: string) { | ||
| const p = page || 'index' | ||
| return translations.has(`${locale}:${p}`) | ||
| }, | ||
| getCache(locale: string, routeName: string) { | ||
| const rn = routeName || 'index' | ||
| return translations.get(`${locale}:${rn}`) | ||
| }, | ||
| setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>) { | ||
| // No-op for bare metal | ||
| }, | ||
| hasTranslation(locale: string, key: string): boolean { | ||
| for (const [k, v] of translations) { | ||
| if (k.startsWith(`${locale}:`) && findValue(v, key) !== null) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| }, | ||
| hasPageTranslation(locale: string, routeName: string): boolean { | ||
| const rn = routeName || 'index' | ||
| return translations.has(`${locale}:${rn}`) | ||
| }, | ||
| getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null { | ||
| const rn = routeName || 'index' | ||
| return findValue<T>(translations.get(`${locale}:${rn}`), key) | ||
| }, | ||
| loadTranslations(locale: string, data: Translations, routeName = 'index'): void { | ||
| const rn = routeName || 'index' | ||
| const key = `${locale}:${rn}` | ||
| const existing = translations.get(key) ?? {} | ||
| translations.set(key, { ...existing, ...data }) | ||
| }, | ||
| setTranslations(locale: string, data: Translations, routeName = 'index'): void { | ||
| const rn = routeName || 'index' | ||
| translations.set(`${locale}:${rn}`, data) | ||
| }, | ||
| loadPageTranslations(locale: string, routeName: string, data: Translations): void { | ||
| const rn = routeName || 'index' | ||
| const key = `${locale}:${rn}` | ||
| const existing = translations.get(key) | ||
| if (!existing || Object.keys(existing).length === 0) { | ||
| translations.set(key, data) | ||
| } else { | ||
| translations.set(key, { ...existing, ...data }) | ||
| } | ||
| }, | ||
| mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force = false): void { | ||
| const rn = routeName || 'index' | ||
| const key = `${locale}:${rn}` | ||
| const existing = translations.get(key) ?? {} | ||
| translations.set(key, { ...existing, ...newTranslations }) | ||
| }, | ||
| clearCache(): void { | ||
| translations.clear() | ||
| }, | ||
| } | ||
| } |
| import type { PluralFunc, Translations } from '@i18n-micro/types' | ||
| import { BaseI18n, type BaseI18nOptions } from '../src/base' | ||
| // Test implementation of BaseI18n | ||
| class TestI18n extends BaseI18n { | ||
| private _locale: string | ||
| private _fallbackLocale: string | ||
| private _route: string | ||
| constructor(locale: string, fallbackLocale: string, route: string, options?: BaseI18nOptions) { | ||
| super(options) | ||
| this._locale = locale | ||
| this._fallbackLocale = fallbackLocale | ||
| this._route = route | ||
| } | ||
| public getLocale(): string { | ||
| return this._locale | ||
| } | ||
| public getFallbackLocale(): string { | ||
| return this._fallbackLocale | ||
| } | ||
| public getRoute(): string { | ||
| return this._route | ||
| } | ||
| public setLocale(locale: string): void { | ||
| this._locale = locale | ||
| } | ||
| public setRoute(route: string): void { | ||
| this._route = route | ||
| } | ||
| } | ||
| describe('BaseI18n', () => { | ||
| describe('Constructor', () => { | ||
| test('should initialize with default options', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.getLocale()).toBe('en') | ||
| expect(i18n.getFallbackLocale()).toBe('en') | ||
| expect(i18n.getRoute()).toBe('index') | ||
| }) | ||
| test('should initialize with custom storage', () => { | ||
| const storage = { translations: new Map<string, Translations>() } | ||
| const i18n = new TestI18n('en', 'en', 'index', { storage }) | ||
| expect(i18n).toBeDefined() | ||
| }) | ||
| test('should initialize with custom plural function', () => { | ||
| const customPlural: PluralFunc = () => 'custom' | ||
| const i18n = new TestI18n('en', 'en', 'index', { plural: customPlural }) | ||
| expect(i18n).toBeDefined() | ||
| }) | ||
| test('should initialize with missingWarn option', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index', { missingWarn: false }) | ||
| expect(i18n).toBeDefined() | ||
| }) | ||
| test('should initialize with missingHandler', () => { | ||
| const handler = jest.fn() | ||
| const i18n = new TestI18n('en', 'en', 'index', { missingHandler: handler }) | ||
| expect(i18n).toBeDefined() | ||
| }) | ||
| }) | ||
| describe('t() method', () => { | ||
| test('should return empty string for empty key', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.t('')).toBe('') | ||
| }) | ||
| test('should return translation for existing key', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { greeting: 'Hello' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.t('greeting')).toBe('Hello') | ||
| }) | ||
| test('should interpolate params in translation', async () => { | ||
| const storage = { translations: new Map<string, Translations>() } | ||
| const i18n = new TestI18n('en', 'en', 'index', { storage }) | ||
| const translations: Translations = { greeting: 'Hello, {name}!' } | ||
| await i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.t('greeting', { name: 'John' })).toBe('Hello, John!') | ||
| }) | ||
| test('should use defaultValue when translation is missing', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.t('missing.key', undefined, 'Default value')).toBe('Default value') | ||
| }) | ||
| test('should return key when translation is missing and no defaultValue', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.t('missing.key')).toBe('missing.key') | ||
| }) | ||
| test('should fallback to fallbackLocale when translation is missing', async () => { | ||
| const storage = { translations: new Map<string, Translations>() } | ||
| const i18n = new TestI18n('en', 'fr', 'index', { storage }) | ||
| const translations: Translations = { greeting: 'Bonjour' } | ||
| await i18n['helper'].loadTranslations('fr', translations) | ||
| expect(i18n.t('greeting')).toBe('Bonjour') | ||
| }) | ||
| test('should use route-specific translation when routeName is provided', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const routeTranslations: Translations = { title: 'Route Title' } | ||
| await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations) | ||
| expect(i18n.t('title', undefined, undefined, 'about')).toBe('Route Title') | ||
| }) | ||
| test('should call missingHandler when translation is missing', () => { | ||
| const handler = jest.fn() | ||
| const i18n = new TestI18n('en', 'en', 'index', { missingHandler: handler }) | ||
| i18n.t('missing.key') | ||
| expect(handler).toHaveBeenCalledWith('en', 'missing.key', 'index') | ||
| }) | ||
| test('should call customMissingHandler when set (Nuxt runtime)', () => { | ||
| const customHandler = jest.fn() | ||
| const i18n = new TestI18n('en', 'en', 'index', { | ||
| getCustomMissingHandler: () => customHandler, | ||
| }) | ||
| i18n.t('missing.key') | ||
| expect(customHandler).toHaveBeenCalledWith('en', 'missing.key', 'index') | ||
| }) | ||
| test('should not warn when missingWarn is false', () => { | ||
| const consoleSpy = jest.spyOn(console, 'warn').mockImplementation() | ||
| const i18n = new TestI18n('en', 'en', 'index', { missingWarn: false }) | ||
| i18n.t('missing.key') | ||
| expect(consoleSpy).not.toHaveBeenCalled() | ||
| consoleSpy.mockRestore() | ||
| }) | ||
| }) | ||
| describe('ts() method', () => { | ||
| test('should return translation as string', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { greeting: 'Hello' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.ts('greeting')).toBe('Hello') | ||
| }) | ||
| test('should return defaultValue when translation is missing', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.ts('missing.key', undefined, 'Default')).toBe('Default') | ||
| }) | ||
| test('should return key when translation is missing and no defaultValue', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.ts('missing.key')).toBe('missing.key') | ||
| }) | ||
| test('should convert non-string values to string', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { count: 42 } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.ts('count')).toBe('42') | ||
| }) | ||
| }) | ||
| describe('tc() method', () => { | ||
| test('should return defaultValue when count is undefined', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.tc('apples', { other: 'params' }, 'No count')).toBe('No count') | ||
| }) | ||
| test('should use plural function with count', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { apples: 'apple|apples' } | ||
| await i18n['helper'].loadTranslations('en', translations) | ||
| // defaultPlural selects form by index: forms[count] or last form if count >= forms.length | ||
| // For 'apple|apples': forms[0]='apple', forms[1]='apples' | ||
| expect(i18n.tc('apples', 0)).toBe('apple') | ||
| expect(i18n.tc('apples', 1)).toBe('apples') // forms[1] | ||
| expect(i18n.tc('apples', 5)).toBe('apples') // last form | ||
| }) | ||
| test('should handle count as number', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { apples: 'apple|apples' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.tc('apples', 2)).toBe('apples') | ||
| }) | ||
| test('should handle count as Params object', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { apples: 'apple|apples' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.tc('apples', { count: 2, name: 'John' })).toBe('apples') | ||
| }) | ||
| test('should return defaultValue when plural function returns null', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| // When translation is missing, t() returns key, which is passed to pluralFunc | ||
| // pluralFunc tries to process 'missing.key' as translation, but since it doesn't contain '|', | ||
| // it returns the key itself. So tc returns the key, not defaultValue. | ||
| // To test defaultValue, we need a case where pluralFunc actually returns null. | ||
| // This happens when translation exists but is empty or invalid. | ||
| expect(i18n.tc('missing.key', 1, 'Default')).toBe('missing.key') | ||
| }) | ||
| }) | ||
| describe('tn() method', () => { | ||
| test('should format number with default locale', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const result = i18n.tn(1234.56) | ||
| expect(result).toMatch(/1[,.]234[.,]56/) | ||
| }) | ||
| test('should format number with custom options', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const result = i18n.tn(1234.56, { style: 'currency', currency: 'USD' }) | ||
| expect(result).toContain('1,234.56') | ||
| }) | ||
| test('should use current locale for formatting', () => { | ||
| const i18n = new TestI18n('ru', 'en', 'index') | ||
| const result = i18n.tn(1234.56) | ||
| // Russian locale uses different number formatting | ||
| expect(result).toBeDefined() | ||
| }) | ||
| }) | ||
| describe('td() method', () => { | ||
| test('should format date with default locale', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const date = new Date('2024-01-15') | ||
| const result = i18n.td(date) | ||
| expect(result).toBeDefined() | ||
| expect(result).not.toBe('Invalid Date') | ||
| }) | ||
| test('should format date with custom options', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const date = new Date('2024-01-15') | ||
| const result = i18n.td(date, { year: 'numeric', month: 'long', day: 'numeric' }) | ||
| expect(result).toContain('2024') | ||
| expect(result).toContain('January') | ||
| }) | ||
| test('should handle date as number (timestamp)', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const timestamp = new Date('2024-01-15').getTime() | ||
| const result = i18n.td(timestamp) | ||
| expect(result).toBeDefined() | ||
| expect(result).not.toBe('Invalid Date') | ||
| }) | ||
| test('should handle date as string', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const result = i18n.td('2024-01-15') | ||
| expect(result).toBeDefined() | ||
| expect(result).not.toBe('Invalid Date') | ||
| }) | ||
| }) | ||
| describe('tdr() method', () => { | ||
| test('should format relative time', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const yesterday = new Date(Date.now() - 86400000) | ||
| const result = i18n.tdr(yesterday) | ||
| expect(result).toBeDefined() | ||
| expect(result).toMatch(/day|ago/i) | ||
| }) | ||
| test('should format relative time with custom options', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const yesterday = new Date(Date.now() - 86400000) | ||
| const result = i18n.tdr(yesterday, { numeric: 'always' }) | ||
| expect(result).toBeDefined() | ||
| }) | ||
| test('should handle invalid date gracefully', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const invalidDate = new Date('invalid') | ||
| const result = i18n.tdr(invalidDate) | ||
| expect(result).toBeDefined() | ||
| }) | ||
| }) | ||
| describe('has() method', () => { | ||
| test('should return true when translation exists', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { greeting: 'Hello' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.has('greeting')).toBe(true) | ||
| }) | ||
| test('should return false when translation does not exist', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.has('missing.key')).toBe(false) | ||
| }) | ||
| test('should check route-specific translation when routeName is provided', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const routeTranslations: Translations = { title: 'Route Title' } | ||
| await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations) | ||
| expect(i18n.has('title', 'about')).toBe(true) | ||
| expect(i18n.has('title', 'index')).toBe(false) | ||
| }) | ||
| test('should use current route when routeName is not provided', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'about') | ||
| const routeTranslations: Translations = { title: 'Route Title' } | ||
| await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations) | ||
| expect(i18n.has('title')).toBe(true) | ||
| }) | ||
| }) | ||
| describe('clearCache() method', () => { | ||
| test('should clear all translations from cache', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { greeting: 'Hello' } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.has('greeting')).toBe(true) | ||
| i18n.clearCache() | ||
| expect(i18n.has('greeting')).toBe(false) | ||
| }) | ||
| }) | ||
| describe('loadTranslationsCore() method', () => { | ||
| test('should load translations when merge is false', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { greeting: 'Hello' } | ||
| i18n['loadTranslationsCore']('en', translations, false) | ||
| // Wait for async operation to complete | ||
| await new Promise((resolve) => setTimeout(resolve, 0)) | ||
| expect(i18n.has('greeting')).toBe(true) | ||
| }) | ||
| test('should merge translations when merge is true', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const initial: Translations = { greeting: 'Hello' } | ||
| const additional: Translations = { farewell: 'Goodbye' } | ||
| await i18n['helper'].loadTranslations('en', initial) | ||
| i18n['loadTranslationsCore']('en', additional, true) | ||
| // Wait for async operation to complete | ||
| await new Promise((resolve) => setTimeout(resolve, 0)) | ||
| expect(i18n.has('greeting')).toBe(true) | ||
| expect(i18n.has('farewell')).toBe(true) | ||
| }) | ||
| }) | ||
| describe('loadRouteTranslationsCore() method', () => { | ||
| test('should load route translations when merge is false', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { title: 'Page Title' } | ||
| i18n['loadRouteTranslationsCore']('en', 'about', translations, false) | ||
| // Wait for async operation to complete | ||
| await new Promise((resolve) => setTimeout(resolve, 0)) | ||
| expect(i18n.has('title', 'about')).toBe(true) | ||
| }) | ||
| test('should merge route translations when merge is true', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const initial: Translations = { title: 'Page Title' } | ||
| const additional: Translations = { description: 'Page Description' } | ||
| await i18n['helper'].loadPageTranslations('en', 'about', initial) | ||
| i18n['loadRouteTranslationsCore']('en', 'about', additional, true) | ||
| // Wait for async operation to complete | ||
| await new Promise((resolve) => setTimeout(resolve, 0)) | ||
| expect(i18n.has('title', 'about')).toBe(true) | ||
| expect(i18n.has('description', 'about')).toBe(true) | ||
| }) | ||
| }) | ||
| describe('Edge cases', () => { | ||
| test('should handle nested translation keys', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { | ||
| nested: { | ||
| deep: { | ||
| key: 'Nested value', | ||
| }, | ||
| }, | ||
| } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.t('nested.deep.key')).toBe('Nested value') | ||
| }) | ||
| test('should handle multiple params in interpolation', async () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| const translations: Translations = { | ||
| message: 'Hello, {name}! You are {age} years old.', | ||
| } | ||
| i18n['helper'].loadTranslations('en', translations) | ||
| expect(i18n.t('message', { name: 'John', age: 30 })).toBe('Hello, John! You are 30 years old.') | ||
| }) | ||
| test('should handle null defaultValue', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| expect(i18n.t('missing.key', undefined, null)).toBe('missing.key') | ||
| }) | ||
| test('should handle empty string defaultValue', () => { | ||
| const i18n = new TestI18n('en', 'en', 'index') | ||
| // Empty string is falsy, so it will fall back to key (as per defaultValue || key logic) | ||
| expect(i18n.t('missing.key', undefined, '')).toBe('missing.key') | ||
| }) | ||
| test('should handle route change', async () => { | ||
| const storage = { translations: new Map<string, Translations>() } | ||
| const i18n = new TestI18n('en', 'en', 'index', { storage }) | ||
| const rootTranslations: Translations = { greeting: 'Hello' } | ||
| // Packages (vue, node, etc.) merge root into pages automatically. | ||
| // Core does not — so we simulate pre-merged data here. | ||
| const routeTranslations: Translations = { greeting: 'Hello', title: 'About Page' } | ||
| await i18n['helper'].loadTranslations('en', rootTranslations) | ||
| await i18n['helper'].loadPageTranslations('en', 'about', routeTranslations) | ||
| // Verify translations are loaded | ||
| expect(i18n.has('greeting')).toBe(true) | ||
| expect(i18n.has('title', 'about')).toBe(true) | ||
| // Check route-specific translation with explicit routeName | ||
| const titleValue = i18n.t('title', undefined, undefined, 'about') | ||
| expect(titleValue).toBe('About Page') | ||
| // Change route and check | ||
| i18n.setRoute('about') | ||
| expect(i18n.t('title')).toBe('About Page') | ||
| expect(i18n.t('greeting')).toBe('Hello') | ||
| }) | ||
| }) | ||
| }) |
| import { interpolate, useTranslationHelper } from '../src' | ||
| describe('Translation Helper', () => { | ||
| const translations = { | ||
| en: { | ||
| greeting: 'Hello, {name}!', | ||
| nested: { | ||
| message: 'This is a nested message.', | ||
| }, | ||
| }, | ||
| fr: { | ||
| greeting: 'Bonjour, {name}!', | ||
| }, | ||
| } | ||
| test('interpolate function replaces placeholders correctly', () => { | ||
| const result = interpolate('Hello, {name}!', { name: 'John' }) | ||
| expect(result).toBe('Hello, John!') | ||
| }) | ||
| test('interpolate function handles missing placeholders gracefully', () => { | ||
| const result = interpolate('Hello, {name}!', { age: 30 }) | ||
| expect(result).toBe('Hello, {name}!') | ||
| }) | ||
| test('getTranslation fetches correct translation', () => { | ||
| const helper = useTranslationHelper() | ||
| helper.loadTranslations('en', translations.en) | ||
| const translation = helper.getTranslation('en', 'index', 'greeting') | ||
| expect(translation).toBe('Hello, {name}!') | ||
| }) | ||
| test('getTranslation supports nested keys', () => { | ||
| const helper = useTranslationHelper() | ||
| helper.loadTranslations('en', translations.en) | ||
| const translation = helper.getTranslation('en', 'index', 'nested.message') | ||
| expect(translation).toBe('This is a nested message.') | ||
| }) | ||
| test('getTranslation falls back when translation is missing', () => { | ||
| const helper = useTranslationHelper() | ||
| helper.loadTranslations('en', translations.en) | ||
| const translation = helper.getTranslation('en', 'index', 'nonexistent.key') | ||
| expect(translation).toBeNull() | ||
| }) | ||
| test('loadPageTranslations correctly caches translations', async () => { | ||
| const helper = useTranslationHelper() | ||
| await helper.loadPageTranslations('fr', 'home', translations.fr) | ||
| expect(helper.hasPageTranslation('fr', 'home')).toBe(true) | ||
| expect(helper.getTranslation('fr', 'home', 'greeting')).toBe('Bonjour, {name}!') | ||
| }) | ||
| test('mergeTranslation updates route translations', () => { | ||
| const helper = useTranslationHelper() | ||
| helper.loadPageTranslations('en', 'home', translations.en) | ||
| helper.mergeTranslation('en', 'home', { newKey: 'New value' }) | ||
| expect(helper.getTranslation('en', 'home', 'newKey')).toBe('New value') | ||
| }) | ||
| test('mergeTranslation with index routeName updates index translations', () => { | ||
| const helper = useTranslationHelper() | ||
| helper.loadTranslations('en', translations.en) | ||
| helper.mergeTranslation('en', 'index', { newKey: 'New value' }) | ||
| expect(helper.getTranslation('en', 'index', 'newKey')).toBe('New value') | ||
| }) | ||
| test('deepClone creates a deep copy of objects', () => { | ||
| const original = { nested: { key: 'value' } } | ||
| const cloned = JSON.parse(JSON.stringify(original)) | ||
| expect(cloned).toEqual(original) | ||
| expect(cloned).not.toBe(original) | ||
| }) | ||
| }) |
| import { FormatService } from '../src' | ||
| describe('FormatService', () => { | ||
| let formatService: FormatService | ||
| beforeEach(() => { | ||
| formatService = new FormatService() | ||
| }) | ||
| describe('formatNumber', () => { | ||
| test('should format number with default options', () => { | ||
| const result = formatService.formatNumber(123456.789, 'en-US') | ||
| expect(result).toBe('123,456.789') | ||
| }) | ||
| test('should format number with custom options', () => { | ||
| const result = formatService.formatNumber(123456.789, 'de-DE', { | ||
| style: 'currency', | ||
| currency: 'EUR', | ||
| }) | ||
| expect(result).toBe('123.456,79 €') | ||
| }) | ||
| test('should handle invalid locale by falling back to default formatting', () => { | ||
| const result = formatService.formatNumber(123456.789, 'invalid-locale') | ||
| expect(result).toBe('123,456.789') // Fallback to default formatting | ||
| }) | ||
| }) | ||
| describe('formatDate', () => { | ||
| test('should format date with default options', () => { | ||
| const date = new Date('2023-10-05T12:34:56Z') | ||
| const result = formatService.formatDate(date, 'en-US') | ||
| expect(result).toBe('10/5/2023') | ||
| }) | ||
| test('should format date with custom options', () => { | ||
| const date = new Date('2023-10-05T12:34:56Z') | ||
| const result = formatService.formatDate(date, 'de-DE', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric', | ||
| }) | ||
| expect(result).toBe('5. Oktober 2023') | ||
| }) | ||
| test('should handle invalid date by returning "Invalid Date"', () => { | ||
| const result = formatService.formatDate('invalid-date', 'en-US') | ||
| expect(result).toBe('Invalid Date') | ||
| }) | ||
| }) | ||
| describe('formatRelativeTime', () => { | ||
| test('should format relative time for seconds', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 30 * 1000) // 30 seconds ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('30 seconds ago') | ||
| }) | ||
| test('should format relative time for minutes', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 5 * 60 * 1000) // 5 minutes ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('5 minutes ago') | ||
| }) | ||
| test('should format relative time for hours', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 2 * 60 * 60 * 1000) // 2 hours ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('2 hours ago') | ||
| }) | ||
| test('should format relative time for days', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) // 3 days ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('3 days ago') | ||
| }) | ||
| test('should format relative time for months', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000) // ~2 months ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('2 months ago') | ||
| }) | ||
| test('should format relative time for years', () => { | ||
| const now = new Date() | ||
| const past = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000) // 1 year ago | ||
| const result = formatService.formatRelativeTime(past, 'en-US') | ||
| expect(result).toBe('1 year ago') | ||
| }) | ||
| test('should handle invalid date by returning "0 seconds ago"', () => { | ||
| const result = formatService.formatRelativeTime('invalid-date', 'en-US') | ||
| expect(result).toBe('in 0 seconds') | ||
| }) | ||
| }) | ||
| }) |
| import { | ||
| interpolate, | ||
| isNoPrefixStrategy, | ||
| isPrefixAndDefaultStrategy, | ||
| isPrefixExceptDefaultStrategy, | ||
| isPrefixStrategy, | ||
| withPrefixStrategy, | ||
| } from '../src/helpers' | ||
| describe('Helpers', () => { | ||
| describe('interpolate', () => { | ||
| test('should replace placeholders with params', () => { | ||
| const template = 'Hello, {name}! Your age is {age}.' | ||
| const params = { name: 'John', age: 30 } | ||
| const result = interpolate(template, params) | ||
| expect(result).toBe('Hello, John! Your age is 30.') | ||
| }) | ||
| test('should handle missing params by leaving placeholders', () => { | ||
| const template = 'Hello, {name}! Your age is {age}.' | ||
| const params = { name: 'John' } // age is missing | ||
| const result = interpolate(template, params) | ||
| expect(result).toBe('Hello, John! Your age is {age}.') | ||
| }) | ||
| test('should handle empty params', () => { | ||
| const template = 'Hello, {name}!' | ||
| const params = {} | ||
| const result = interpolate(template, params) | ||
| expect(result).toBe('Hello, {name}!') | ||
| }) | ||
| test('should handle empty template', () => { | ||
| const template = '' | ||
| const params = { name: 'John' } | ||
| const result = interpolate(template, params) | ||
| expect(result).toBe('') | ||
| }) | ||
| }) | ||
| describe('withPrefixStrategy', () => { | ||
| test('should return true for "prefix" strategy', () => { | ||
| expect(withPrefixStrategy('prefix')).toBe(true) | ||
| }) | ||
| test('should return true for "prefix_and_default" strategy', () => { | ||
| expect(withPrefixStrategy('prefix_and_default')).toBe(true) | ||
| }) | ||
| test('should return false for other strategies', () => { | ||
| expect(withPrefixStrategy('no_prefix')).toBe(false) | ||
| expect(withPrefixStrategy('prefix_except_default')).toBe(false) | ||
| }) | ||
| }) | ||
| describe('isNoPrefixStrategy', () => { | ||
| test('should return true for "no_prefix" strategy', () => { | ||
| expect(isNoPrefixStrategy('no_prefix')).toBe(true) | ||
| }) | ||
| test('should return false for other strategies', () => { | ||
| expect(isNoPrefixStrategy('prefix')).toBe(false) | ||
| expect(isNoPrefixStrategy('prefix_and_default')).toBe(false) | ||
| expect(isNoPrefixStrategy('prefix_except_default')).toBe(false) | ||
| }) | ||
| }) | ||
| describe('isPrefixStrategy', () => { | ||
| test('should return true for "prefix" strategy', () => { | ||
| expect(isPrefixStrategy('prefix')).toBe(true) | ||
| }) | ||
| test('should return false for other strategies', () => { | ||
| expect(isPrefixStrategy('no_prefix')).toBe(false) | ||
| expect(isPrefixStrategy('prefix_and_default')).toBe(false) | ||
| expect(isPrefixStrategy('prefix_except_default')).toBe(false) | ||
| }) | ||
| }) | ||
| describe('isPrefixExceptDefaultStrategy', () => { | ||
| test('should return true for "prefix_except_default" strategy', () => { | ||
| expect(isPrefixExceptDefaultStrategy('prefix_except_default')).toBe(true) | ||
| }) | ||
| test('should return false for other strategies', () => { | ||
| expect(isPrefixExceptDefaultStrategy('no_prefix')).toBe(false) | ||
| expect(isPrefixExceptDefaultStrategy('prefix')).toBe(false) | ||
| expect(isPrefixExceptDefaultStrategy('prefix_and_default')).toBe(false) | ||
| }) | ||
| }) | ||
| describe('isPrefixAndDefaultStrategy', () => { | ||
| test('should return true for "prefix_and_default" strategy', () => { | ||
| expect(isPrefixAndDefaultStrategy('prefix_and_default')).toBe(true) | ||
| }) | ||
| test('should return false for other strategies', () => { | ||
| expect(isPrefixAndDefaultStrategy('no_prefix')).toBe(false) | ||
| expect(isPrefixAndDefaultStrategy('prefix')).toBe(false) | ||
| expect(isPrefixAndDefaultStrategy('prefix_except_default')).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
| { | ||
| "compilerOptions": { | ||
| "target": "ES2018", | ||
| "module": "esnext", | ||
| "moduleResolution": "node", | ||
| "strict": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "outDir": "./dist", | ||
| "declaration": true, | ||
| "declarationDir": "./dist", | ||
| "sourceMap": true, | ||
| "rootDir": "./src", | ||
| "baseUrl": "./", | ||
| "paths": { | ||
| "*": ["node_modules/*", "src/types/*"] | ||
| }, | ||
| "types": ["jest", "node"] | ||
| }, | ||
| "include": ["src/**/*"], | ||
| "exclude": ["node_modules", "dist", "tests"] | ||
| } |
| // @ts-nocheck | ||
| import { resolve } from 'node:path' | ||
| import { defineConfig } from 'vite' | ||
| import dts from 'vite-plugin-dts' | ||
| export default defineConfig({ | ||
| build: { | ||
| lib: { | ||
| entry: resolve(__dirname, 'src/index.ts'), | ||
| name: '@i18n-micro/core', | ||
| formats: ['cjs', 'es'], | ||
| fileName: (format) => `index.${format === 'cjs' ? 'cjs' : 'mjs'}`, | ||
| }, | ||
| rollupOptions: { | ||
| external: [], | ||
| output: { | ||
| exports: 'named', | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: [dts()], | ||
| }) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
2
-50%27744
-59.56%4
100%11
-52.17%368
-73.18%1
Infinity%+ Added
- Removed
Updated