@i18n-micro/core 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,35 +1,83 @@
1
1
  {
2
2
  "name": "@i18n-micro/core",
3
- "version": "1.3.0",
4
- "description": "",
5
- "repository": "s00d/nuxt-i18n-micro",
3
+ "version": "1.3.2",
4
+ "description": "Core utilities for translations, formatting, and locale routing in Nuxt I18n Micro.",
5
+ "keywords": [
6
+ "formatting",
7
+ "i18n",
8
+ "locale",
9
+ "nuxt",
10
+ "translations"
11
+ ],
12
+ "homepage": "https://github.com/s00d/nuxt-i18n-micro/tree/main/packages/core#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
15
+ },
6
16
  "license": "MIT",
7
- "type": "module",
8
17
  "author": {
9
18
  "name": "s00d",
10
19
  "email": "Virus191288@gmail.com",
11
20
  "url": "https://s00d.github.io/"
12
21
  },
13
- "homepage": "https://github.com/s00d/nuxt-i18n-micro",
14
- "bugs": {
15
- "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/s00d/nuxt-i18n-micro.git",
25
+ "directory": "packages/core"
16
26
  },
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "type": "module",
33
+ "sideEffects": false,
17
34
  "main": "dist/index.cjs",
18
- "module": "dist/index.mjs",
19
35
  "types": "dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "import": {
39
+ "types": "./dist/index.d.ts",
40
+ "default": "./dist/index.mjs"
41
+ },
42
+ "require": {
43
+ "types": "./dist/index.d.cts",
44
+ "default": "./dist/index.cjs"
45
+ },
46
+ "default": "./dist/index.mjs"
47
+ },
48
+ "./helpers": {
49
+ "import": {
50
+ "types": "./dist/helpers.d.ts",
51
+ "default": "./dist/helpers.mjs"
52
+ },
53
+ "require": {
54
+ "types": "./dist/helpers.d.cts",
55
+ "default": "./dist/helpers.cjs"
56
+ },
57
+ "default": "./dist/helpers.mjs"
58
+ },
59
+ "./package.json": "./package.json"
60
+ },
20
61
  "publishConfig": {
21
62
  "access": "public"
22
63
  },
23
- "keywords": [],
24
64
  "dependencies": {
25
- "@i18n-micro/types": "1.2.0"
65
+ "@i18n-micro/types": "1.2.4"
26
66
  },
27
67
  "devDependencies": {
68
+ "publint": "^0.3.17",
28
69
  "vite": "^7.3.1",
29
- "vite-plugin-dts": "^4.5.4"
70
+ "vite-plugin-dts": "^4.5.4",
71
+ "vitest": "^3.2.4"
72
+ },
73
+ "engines": {
74
+ "node": ">=18"
30
75
  },
31
76
  "scripts": {
32
77
  "build": "vite build",
33
- "test": "jest"
78
+ "check:package": "publint",
79
+ "test": "jest",
80
+ "test:perf": "jest --config jest.perf.config.cjs",
81
+ "test:dist": "vitest run --config vitest.dist.config.ts"
34
82
  }
35
83
  }
package/fix.ts DELETED
@@ -1 +0,0 @@
1
- import '@types/jest'
package/jest.config.cjs DELETED
@@ -1,7 +0,0 @@
1
- module.exports = {
2
- roots: ['<rootDir>/tests'],
3
- testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
4
- transform: {
5
- '^.+\\.ts$': 'ts-jest',
6
- },
7
- }
package/src/base.ts DELETED
@@ -1,194 +0,0 @@
1
- import type { CleanTranslation, Getter, MissingHandler, Params, PluralFunc, TranslationKey, Translations } from '@i18n-micro/types'
2
- import { FormatService } from './format-service'
3
- import { defaultPlural, interpolate } from './helpers'
4
- import { type TranslationStorage, useTranslationHelper } from './translation'
5
-
6
- export interface BaseI18nOptions {
7
- storage?: TranslationStorage
8
- plural?: PluralFunc
9
- missingWarn?: boolean
10
- missingHandler?: (locale: string, key: string, routeName: string) => void
11
- getCustomMissingHandler?: () => MissingHandler | null
12
- }
13
-
14
- /**
15
- * Abstract base class for i18n adapters
16
- *
17
- * Contains all common translation logic (t, ts, tc, tn, td, tdr, has).
18
- * Adapters must implement abstract methods to provide current state (locale, fallbackLocale, route).
19
- */
20
- export abstract class BaseI18n {
21
- // Public fields (made public to allow type export in Nuxt plugins)
22
- public helper: ReturnType<typeof useTranslationHelper>
23
- public formatter = new FormatService()
24
- public pluralFunc: PluralFunc
25
- public missingWarn: boolean
26
- public missingHandler?: (locale: string, key: string, routeName: string) => void
27
- public getCustomMissingHandler?: () => MissingHandler | null
28
-
29
- constructor(options: BaseI18nOptions = {}) {
30
- this.helper = useTranslationHelper(options.storage)
31
- this.formatter = new FormatService()
32
- this.pluralFunc = options.plural || defaultPlural
33
- this.missingWarn = options.missingWarn ?? true
34
- this.missingHandler = options.missingHandler
35
- this.getCustomMissingHandler = options.getCustomMissingHandler
36
- }
37
-
38
- // --- Abstract methods (must be implemented by subclasses) ---
39
-
40
- /**
41
- * Get current locale
42
- */
43
- public abstract getLocale(): string
44
-
45
- /**
46
- * Get fallback locale
47
- */
48
- public abstract getFallbackLocale(): string
49
-
50
- /**
51
- * Get current route name
52
- */
53
- public abstract getRoute(): string
54
-
55
- // --- Public methods (implemented in base class) ---
56
-
57
- /**
58
- * Get translation for a key
59
- * Based on logic from src/runtime/plugins/01.plugin.ts
60
- */
61
- public t(key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation {
62
- if (!key) return ''
63
-
64
- // Use abstract getters to get current state
65
- const locale = this.getLocale()
66
- const route = routeName || this.getRoute()
67
-
68
- // 1. Try to find translation in current locale
69
- let value = this.helper.getTranslation<string>(locale, route, key)
70
-
71
- // 2. Fallback to fallbackLocale if not found and different
72
- if (!value) {
73
- const fallbackLocale = this.getFallbackLocale()
74
- if (locale !== fallbackLocale) {
75
- value = this.helper.getTranslation<string>(fallbackLocale, route, key)
76
- }
77
- }
78
-
79
- // 3. Handle missing
80
- if (!value) {
81
- // Call custom handler if set (Nuxt runtime), otherwise use instance handler
82
- const customHandler = this.getCustomMissingHandler?.()
83
- if (customHandler) {
84
- customHandler(locale, key, route)
85
- } else if (this.missingHandler) {
86
- this.missingHandler(locale, key as string, route)
87
- } else if (this.missingWarn) {
88
- const isDev = process.env.NODE_ENV !== 'production'
89
- const isClient = typeof window !== 'undefined'
90
- if (isDev && isClient) {
91
- console.warn(`Not found '${key}' key in '${locale}' locale messages for route '${route}'.`)
92
- }
93
- }
94
- value = defaultValue === undefined ? key : defaultValue || key
95
- }
96
-
97
- // 4. Interpolate
98
- return typeof value === 'string' && params ? interpolate(value, params) : (value as CleanTranslation)
99
- }
100
-
101
- /**
102
- * Get translation as string
103
- */
104
- public ts(key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string {
105
- const value = this.t(key, params, defaultValue, routeName)
106
- return value?.toString() ?? defaultValue ?? key
107
- }
108
-
109
- /**
110
- * Plural translation
111
- */
112
- public tc(key: TranslationKey, count: number | Params, defaultValue?: string): string {
113
- const { count: countValue, ...params } = typeof count === 'number' ? { count } : count
114
-
115
- if (countValue === undefined) {
116
- return defaultValue ?? key
117
- }
118
-
119
- // Getter passed to plural function
120
- const getter: Getter = (k: TranslationKey, p?: Params, dv?: string) => {
121
- return this.t(k, p, dv)
122
- }
123
-
124
- const result = this.pluralFunc(key, Number.parseInt(countValue.toString(), 10), params, this.getLocale(), getter)
125
-
126
- return result ?? defaultValue ?? key
127
- }
128
-
129
- /**
130
- * Format number
131
- */
132
- public tn(value: number, options?: Intl.NumberFormatOptions): string {
133
- return this.formatter.formatNumber(value, this.getLocale(), options)
134
- }
135
-
136
- /**
137
- * Format date
138
- */
139
- public td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string {
140
- return this.formatter.formatDate(value, this.getLocale(), options)
141
- }
142
-
143
- /**
144
- * Format relative time
145
- */
146
- public tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string {
147
- return this.formatter.formatRelativeTime(value, this.getLocale(), options)
148
- }
149
-
150
- /**
151
- * Check if translation exists
152
- * Based on logic from src/runtime/plugins/01.plugin.ts
153
- */
154
- public has(key: TranslationKey, routeName?: string): boolean {
155
- const route = routeName || this.getRoute()
156
- const locale = this.getLocale()
157
-
158
- // Check only through getTranslation (as in plugin)
159
- return !!this.helper.getTranslation(locale, route, key)
160
- }
161
-
162
- /**
163
- * Clear cache
164
- */
165
- public clearCache(): void {
166
- this.helper.clearCache()
167
- }
168
-
169
- // --- Public methods (for subclasses to use) ---
170
-
171
- /**
172
- * Core translation loading logic (without reactivity)
173
- * Subclasses can override addTranslations/addRouteTranslations to add reactivity
174
- */
175
- public loadTranslationsCore(locale: string, translations: Translations, merge: boolean, routeName = 'index'): void {
176
- if (merge) {
177
- this.helper.mergeTranslation(locale, routeName, translations, true)
178
- } else {
179
- this.helper.setTranslations(locale, translations, routeName)
180
- }
181
- }
182
-
183
- /**
184
- * Core route translation loading logic (without reactivity)
185
- * Subclasses can override addRouteTranslations to add reactivity
186
- */
187
- public loadRouteTranslationsCore(locale: string, routeName: string, translations: Translations, merge: boolean): void {
188
- if (merge) {
189
- this.helper.mergeTranslation(locale, routeName, translations, true)
190
- } else {
191
- this.helper.loadPageTranslations(locale, routeName, translations)
192
- }
193
- }
194
- }
@@ -1,41 +0,0 @@
1
- export class FormatService {
2
- formatNumber(value: number, locale: string, options?: Intl.NumberFormatOptions): string {
3
- return new Intl.NumberFormat(locale, options).format(value)
4
- }
5
-
6
- formatDate(value: Date | number | string, locale: string, options?: Intl.DateTimeFormatOptions): string {
7
- const date = new Date(value)
8
- if (Number.isNaN(date.getTime())) {
9
- return 'Invalid Date'
10
- }
11
- return new Intl.DateTimeFormat(locale, options).format(date)
12
- }
13
-
14
- formatRelativeTime(value: Date | number | string, locale: string, options?: Intl.RelativeTimeFormatOptions): string {
15
- const date = new Date(value)
16
- if (Number.isNaN(date.getTime())) {
17
- // Return "0 seconds ago" for invalid dates
18
- return new Intl.RelativeTimeFormat(locale, options).format(0, 'second')
19
- }
20
- const now = new Date()
21
- const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
22
-
23
- const units: { unit: Intl.RelativeTimeFormatUnit; seconds: number }[] = [
24
- { unit: 'year', seconds: 31536000 },
25
- { unit: 'month', seconds: 2592000 },
26
- { unit: 'day', seconds: 86400 },
27
- { unit: 'hour', seconds: 3600 },
28
- { unit: 'minute', seconds: 60 },
29
- { unit: 'second', seconds: 1 },
30
- ]
31
-
32
- for (const { unit, seconds } of units) {
33
- const diff = Math.floor(diffInSeconds / seconds)
34
- if (diff >= 1) {
35
- return new Intl.RelativeTimeFormat(locale, options).format(-diff, unit)
36
- }
37
- }
38
-
39
- return new Intl.RelativeTimeFormat(locale, options).format(0, 'second')
40
- }
41
- }
package/src/helpers.ts DELETED
@@ -1,54 +0,0 @@
1
- import type { Getter, Params, PluralFunc, Strategies, TranslationKey } from '@i18n-micro/types'
2
-
3
- const RE_TOKEN = /\{(\w+)\}/g
4
-
5
- export function interpolate(template: string, params: Params): string {
6
- if (!params) return template
7
-
8
- return template.replace(RE_TOKEN, (_, key) => {
9
- const value = params[key]
10
- return value !== undefined ? String(value) : `{${key}}`
11
- })
12
- }
13
-
14
- export function withPrefixStrategy(strategy: Strategies) {
15
- return strategy === 'prefix' || strategy === 'prefix_and_default'
16
- }
17
-
18
- export function isNoPrefixStrategy(strategy: Strategies) {
19
- return strategy === 'no_prefix'
20
- }
21
-
22
- export function isPrefixStrategy(strategy: Strategies) {
23
- return strategy === 'prefix'
24
- }
25
-
26
- export function isPrefixExceptDefaultStrategy(strategy: Strategies) {
27
- return strategy === 'prefix_except_default'
28
- }
29
-
30
- export function isPrefixAndDefaultStrategy(strategy: Strategies) {
31
- return strategy === 'prefix_and_default'
32
- }
33
-
34
- /**
35
- * Default pluralization function
36
- * Splits translation by '|' and selects form based on count
37
- * @param key - Translation key
38
- * @param count - Count for pluralization
39
- * @param params - Parameters for translation
40
- * @param _locale - Current locale (unused in default implementation)
41
- * @param getTranslation - Function to get translation value
42
- * @returns Selected plural form or null if not found
43
- */
44
- export const defaultPlural: PluralFunc = (key: TranslationKey, count: number, params: Params, _locale: string, getTranslation: Getter) => {
45
- const translation = getTranslation(key, params)
46
- if (!translation) {
47
- return null
48
- }
49
- const forms = translation.toString().split('|')
50
- if (forms.length === 0) return null
51
- const selectedForm = count < forms.length ? forms[count] : forms[forms.length - 1]
52
- if (!selectedForm) return null
53
- return selectedForm.trim().replace('{count}', count.toString())
54
- }
package/src/index.ts DELETED
@@ -1,27 +0,0 @@
1
- import { BaseI18n, type BaseI18nOptions } from './base'
2
- import { FormatService } from './format-service'
3
- import {
4
- defaultPlural,
5
- interpolate,
6
- isNoPrefixStrategy,
7
- isPrefixAndDefaultStrategy,
8
- isPrefixExceptDefaultStrategy,
9
- isPrefixStrategy,
10
- withPrefixStrategy,
11
- } from './helpers'
12
- import { type TranslationStorage, useTranslationHelper } from './translation'
13
-
14
- export {
15
- useTranslationHelper,
16
- interpolate,
17
- withPrefixStrategy,
18
- isNoPrefixStrategy,
19
- isPrefixStrategy,
20
- isPrefixExceptDefaultStrategy,
21
- isPrefixAndDefaultStrategy,
22
- defaultPlural,
23
- FormatService,
24
- BaseI18n,
25
- type TranslationStorage,
26
- type BaseI18nOptions,
27
- }
@@ -1,95 +0,0 @@
1
- import type { Translations } from '@i18n-micro/types'
2
-
3
- /**
4
- * Bare Metal: Simple translation storage without Ref, useState, devalue.
5
- * Map key: `${locale}:${routeName}` (page-specific).
6
- */
7
- export interface TranslationStorage {
8
- translations: Map<string, Translations>
9
- }
10
-
11
- function findValue<T = unknown>(data: Translations | null | undefined, key: string): T | null {
12
- if (!data || typeof key !== 'string') return null
13
-
14
- if (key in data) {
15
- const value = data[key]
16
- if (typeof value === 'object' && value !== null) {
17
- return value as T
18
- }
19
- return value as T
20
- }
21
-
22
- const parts = key.split('.')
23
- let value: unknown = data
24
- for (const part of parts) {
25
- if (value && typeof value === 'object' && part in (value as object)) {
26
- value = (value as Translations)[part]
27
- } else {
28
- return null
29
- }
30
- }
31
- return (value as T) ?? null
32
- }
33
-
34
- export function useTranslationHelper(storage?: TranslationStorage) {
35
- const translations = storage?.translations ?? new Map<string, Translations>()
36
-
37
- return {
38
- hasCache(locale: string, page: string) {
39
- const p = page || 'index'
40
- return translations.has(`${locale}:${p}`)
41
- },
42
- getCache(locale: string, routeName: string) {
43
- const rn = routeName || 'index'
44
- return translations.get(`${locale}:${rn}`)
45
- },
46
- setCache(_locale: string, _routeName: string, _cache: Map<string, unknown>) {
47
- // No-op for bare metal
48
- },
49
- hasTranslation(locale: string, key: string): boolean {
50
- for (const [k, v] of translations) {
51
- if (k.startsWith(`${locale}:`) && findValue(v, key) !== null) {
52
- return true
53
- }
54
- }
55
- return false
56
- },
57
- hasPageTranslation(locale: string, routeName: string): boolean {
58
- const rn = routeName || 'index'
59
- return translations.has(`${locale}:${rn}`)
60
- },
61
- getTranslation<T = unknown>(locale: string, routeName: string, key: string): T | null {
62
- const rn = routeName || 'index'
63
- return findValue<T>(translations.get(`${locale}:${rn}`), key)
64
- },
65
- loadTranslations(locale: string, data: Translations, routeName = 'index'): void {
66
- const rn = routeName || 'index'
67
- const key = `${locale}:${rn}`
68
- const existing = translations.get(key) ?? {}
69
- translations.set(key, { ...existing, ...data })
70
- },
71
- setTranslations(locale: string, data: Translations, routeName = 'index'): void {
72
- const rn = routeName || 'index'
73
- translations.set(`${locale}:${rn}`, data)
74
- },
75
- loadPageTranslations(locale: string, routeName: string, data: Translations): void {
76
- const rn = routeName || 'index'
77
- const key = `${locale}:${rn}`
78
- const existing = translations.get(key)
79
- if (!existing || Object.keys(existing).length === 0) {
80
- translations.set(key, data)
81
- } else {
82
- translations.set(key, { ...existing, ...data })
83
- }
84
- },
85
- mergeTranslation(locale: string, routeName: string, newTranslations: Translations, _force = false): void {
86
- const rn = routeName || 'index'
87
- const key = `${locale}:${rn}`
88
- const existing = translations.get(key) ?? {}
89
- translations.set(key, { ...existing, ...newTranslations })
90
- },
91
- clearCache(): void {
92
- translations.clear()
93
- },
94
- }
95
- }