imba-localization 0.4.12 → 0.5.0

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/README.md CHANGED
@@ -8,6 +8,7 @@ A lightweight Imba module for loading and handling JSON-based localization files
8
8
  - 🔍 **Automatic language detection** - Uses the user's browser language settings
9
9
  - 💾 **Persistence**: Stores user choice in local storage across sessions
10
10
  - 🔄 **Smart fallback system** - Falls back to a default language when needed
11
+ - 📦 **Optional split loading** - Loads only the selected language file and caches languages on demand
11
12
  - 🧠 **Intuitive access** - Proxy-based access to translation strings
12
13
  - 📡 **Event handling** - Support for `onready`, `onchange`, and `onerror` events
13
14
  - 🧾 **Type declarations** - Includes package-level TypeScript declarations for editor and language-server imports
@@ -30,6 +31,13 @@ npm install imba-localization
30
31
  bun add imba-localization
31
32
  ```
32
33
 
34
+ For local module development:
35
+
36
+ ```bash
37
+ bun install
38
+ bun run test
39
+ ```
40
+
33
41
  ## 🚀 Quick Start
34
42
 
35
43
  ### 1️⃣ Preload the localization file
@@ -71,9 +79,9 @@ loc.onready = do
71
79
 
72
80
  loc.onerror = do(error, details)
73
81
  # The Localization object can return following types of errors:
74
- # 'no_localization_file' - if there were a problem when downloading JSON file
75
- # 'no_default_localization' - if there is no localization in the file for the default language
76
- # 'no_localization_key' - if there is no requiered (from the interface) key in the file
82
+ # 'localization-no-file' - if there was a problem downloading a JSON file
83
+ # 'localization-no-default' - if there is no default localization
84
+ # 'localization-no-key' - if a requested interface key is missing
77
85
  console.error "Localization error:", error, details
78
86
 
79
87
  loc.onchange = do(lang_key)
@@ -94,7 +102,9 @@ console.log loc.languages[loc.active]
94
102
 
95
103
  ```
96
104
 
97
- ## 📄 JSON Structure
105
+ ## 📄 Bundle JSON Structure
106
+
107
+ The original single-file format remains supported and is the default mode:
98
108
 
99
109
  Your localization file should follow this format:
100
110
 
@@ -119,22 +129,80 @@ Your localization file should follow this format:
119
129
  }
120
130
  ```
121
131
 
132
+ ## 📦 Split Language Files
133
+
134
+ Split mode loads a small manifest first and then downloads only the selected language. Previously loaded languages are kept in memory and are not requested again.
135
+
136
+ ```imba
137
+ const loc = new Localization('/localization/languages.json', 'en', {split: true})
138
+
139
+ # Optional when application startup needs to wait explicitly.
140
+ await loc.pending
141
+
142
+ # Assignment still works and starts an asynchronous language switch.
143
+ loc.active = 'fr'
144
+
145
+ # Use `use` when the caller needs to wait for the new dictionary.
146
+ await loc.use('fr')
147
+ ```
148
+
149
+ Recommended file structure:
150
+
151
+ ```text
152
+ public/localization/
153
+ ├── languages.json
154
+ ├── en.json
155
+ ├── fr.json
156
+ └── ru.json
157
+ ```
158
+
159
+ `languages.json` contains only language metadata and file locations:
160
+
161
+ ```json
162
+ {
163
+ "default": "en",
164
+ "languages": {
165
+ "en": { "name": "English", "flag": "us", "src": "en.json" },
166
+ "fr": { "name": "Français", "flag": "fr", "src": "fr.json" },
167
+ "ru": { "name": "Русский", "flag": "ru", "src": "ru.json" }
168
+ }
169
+ }
170
+ ```
171
+
172
+ Each language file contains the dictionary directly:
173
+
174
+ ```json
175
+ {
176
+ "welcome": "Welcome",
177
+ "goodbye": "Goodbye",
178
+ "user": {
179
+ "profile": "Profile",
180
+ "settings": "Settings"
181
+ }
182
+ }
183
+ ```
184
+
185
+ If `src` is omitted, the language code is used as the filename (`ru` → `ru.json`). The manifest `default` overrides the constructor fallback when present. A failed switch keeps the current language active and reports `localization-no-file` through `onerror`.
186
+
122
187
  ## 🛠️ API Reference
123
188
 
124
189
  ### Constructor
125
190
 
126
191
  ```imba
127
- new Localization(url, default = 'en')
192
+ new Localization(url, default = 'en', options = {})
128
193
  ```
129
194
 
130
195
  - `url`: Path to your JSON localization file
131
196
  - `default`: Fallback language code (defaults to 'en')
197
+ - `options.split`: Treat `url` as a language manifest and load dictionaries on demand
132
198
 
133
199
  ### Properties
134
200
 
135
201
  - `active`: Get or set the code of the active language
136
- - `languages`: Object containing all loaded language data
202
+ - `languages`: Available languages; unloaded split entries contain only `$` metadata
137
203
  - `preferred`: Detected browser language (first 2 characters of `navigator.language`)
204
+ - `loaded`: Map of language codes whose dictionaries are already in memory
205
+ - `pending`: Promise settled after initial localization loading finishes
138
206
 
139
207
  ### Methods
140
208
 
@@ -142,6 +210,8 @@ new Localization(url, default = 'en')
142
210
  - `text(path, fallback = '', data = null)`: Reads a localized string and replaces `{key}` placeholders from `data`
143
211
  - `table(path)`: Reads a nested object table, returning `{}` when missing
144
212
  - `render(value, data = null)`: Replaces `{key}` placeholders in any string-like value
213
+ - `use(language)`: Loads and activates a language, returning a Promise
214
+ - `load(language)`: Loads a split dictionary without activating it
145
215
 
146
216
  ### Events
147
217
 
package/index.d.ts CHANGED
@@ -1,13 +1,26 @@
1
1
  export type LocalizationLanguage = Record<string, any>;
2
2
  export type LocalizationLanguages = Record<string, LocalizationLanguage>;
3
3
 
4
+ export interface LocalizationOptions {
5
+ split?: boolean;
6
+ }
7
+
8
+ export interface LocalizationManifestLanguage {
9
+ name?: string;
10
+ flag?: string;
11
+ src?: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export type LocalizationManifest = Record<string, string | LocalizationManifestLanguage>;
16
+
4
17
  export interface LocalizationErrorState {
5
18
  cache: Record<string, boolean>;
6
19
  throw(code: string, details?: any): void;
7
20
  }
8
21
 
9
22
  export class Localization {
10
- constructor(url: string, fallback?: string);
23
+ constructor(url: string, fallback?: string, options?: LocalizationOptions);
11
24
 
12
25
  onready?: () => void;
13
26
  onerror?: (error: string, details?: any) => void;
@@ -17,6 +30,11 @@ export class Localization {
17
30
  preferred: string;
18
31
  default: string;
19
32
  ready: boolean;
33
+ mode: "bundle" | "split";
34
+ source: string;
35
+ manifest: LocalizationManifest;
36
+ loaded: Record<string, boolean>;
37
+ pending: Promise<void>;
20
38
  err: LocalizationErrorState;
21
39
 
22
40
  active: string;
@@ -25,6 +43,8 @@ export class Localization {
25
43
  lookup(path: string | string[], fallback?: unknown): unknown;
26
44
  text(path: string | string[], fallback?: string, data?: Record<string, unknown> | null): string;
27
45
  table(path: string | string[]): Record<string, any>;
46
+ use(language: string): Promise<string>;
47
+ load(language: string): Promise<boolean>;
28
48
 
29
49
  [key: string]: any;
30
50
  }
package/localization.imba CHANGED
@@ -1,4 +1,6 @@
1
1
  # Emoji flags mapping - comprehensive list by region
2
+ export {Localization} from './state.imba'
3
+
2
4
  export const flags = {
3
5
  # Europe
4
6
  'gb': '🇬🇧', 'us': '🇺🇸', 'ru': '🇷🇺', 'de': '🇩🇪', 'fr': '🇫🇷',
@@ -62,97 +64,6 @@ export const flags = {
62
64
  'dg': '🇩🇬', 'ea': '🇪🇦', 'ic': '🇮🇨', 'xk': '🇽🇰'
63
65
  }
64
66
 
65
- export class Localization
66
- onready
67
- onerror
68
- onchange
69
- languages = {}
70
- preferred = (window..navigator..language || 'en-US').slice(0, 2)
71
- default
72
- ready = false
73
- err = {
74
- cache: {}
75
- throw: do(code, details)
76
- return if err.cache[code]
77
- if onerror isa Function and ready
78
- onerror(code, details)
79
- else
80
- console.log "Localization error:", code, details
81
- err.cache[code] = true
82
- }
83
-
84
- def constructor url, fallback = 'en'
85
- default = fallback
86
- window.fetch(url)
87
- .then(do(response) response.json!)
88
- .then(do(data) _finalize(data, undefined))
89
- .catch(do(error) _finalize(undefined, error))
90
-
91
- return new Proxy self, {
92
- get: do(target, p, receiver)
93
- if Reflect.has(target, p)
94
- return Reflect.get(target, p, receiver)
95
- if !target.ready
96
- target.err.throw("Request before localization is ready:", p)
97
- return
98
- return if target.err.cache[p]
99
- return target.languages[p] if target.languages[p]
100
- return target.languages[target.active][p] if target.languages[target.active] and target.languages[target.active][p]
101
- target.err.throw('localization-no-key', p)
102
- return ''
103
- }
104
-
105
- def render value, data = null
106
- let text = String(value or '')
107
- return text unless data and typeof data == 'object'
108
- text.replace /\{([^}]+)\}/g, do(_match, key)
109
- const value = data[key]
110
- if value == undefined or value == null then '' else String(value)
111
-
112
- def lookup path, fallback = ''
113
- const keys = if Array.isArray(path) then path else String(path or '').split('.').filter(do(part) part)
114
- let value = languages..[active] or languages..[default] or {}
115
- for key in keys
116
- if value and typeof value == 'object' and value[key] != undefined
117
- value = value[key]
118
- else
119
- return fallback
120
- value
121
-
122
- def text path, fallback = '', data = null
123
- const value = lookup(path, fallback)
124
- return render(value, data) if typeof value == 'string' or typeof value == 'number'
125
- render(fallback, data)
126
-
127
- def table path
128
- const value = lookup(path, {})
129
- if value and typeof value == 'object' then value else {}
130
-
131
- def _finalize data, error
132
- if error or !data
133
- err.throw('localization-no-file',error)
134
- elif !data[default]
135
- err.throw('localization-no-default', default)
136
- else
137
- languages = data
138
- ready = true
139
- err.cache = {}
140
- onready! if onready isa Function
141
- onchange(active) if onchange isa Function
142
-
143
- get active
144
- const saved = window.localStorage.getItem('imba-localization')
145
- return saved if saved and languages[saved]
146
- return preferred if languages[preferred]
147
- return default
148
-
149
- set active name
150
- name = name and languages[name] ? name : active
151
- if window.localStorage.getItem('imba-localization') != name
152
- window.localStorage.setItem('imba-localization', name)
153
- onchange(name) if onchange isa Function
154
- err.cache = {}
155
-
156
67
  export const path-arrow-down = <path d="M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z">
157
68
 
158
69
  tag language-selector
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imba-localization",
3
- "version": "0.4.12",
3
+ "version": "0.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/imba-localization.git"
@@ -25,8 +25,17 @@
25
25
  "keywords": ["imba", "theme", "localization"],
26
26
  "license": "MIT",
27
27
  "type": "module",
28
+ "scripts": {
29
+ "test": "bimba state.imba --outdir .cache/test --external imba --external imba/runtime && bun test"
30
+ },
31
+ "devDependencies": {
32
+ "bimba-cli": "latest",
33
+ "imba": "latest",
34
+ "typescript": "^5.9.3"
35
+ },
28
36
  "files": [
29
37
  "localization.imba",
38
+ "state.imba",
30
39
  "index.d.ts"
31
40
  ]
32
41
  }
package/state.imba ADDED
@@ -0,0 +1,195 @@
1
+ export class Localization
2
+ onready
3
+ onerror
4
+ onchange
5
+ languages = {}
6
+ preferred = (window..navigator..language || 'en-US').slice(0, 2)
7
+ default
8
+ ready = false
9
+ mode = 'bundle'
10
+ source = ''
11
+ manifest = {}
12
+ loaded = {}
13
+ loading = {}
14
+ pending
15
+ #active = null
16
+ #request = 0
17
+ err = {
18
+ cache: {}
19
+ throw: do(code, details)
20
+ return if err.cache[code]
21
+ if onerror isa Function
22
+ onerror(code, details)
23
+ else
24
+ console.log "Localization error:", code, details
25
+ err.cache[code] = true
26
+ }
27
+
28
+ def constructor url, fallback = 'en', options = {}
29
+ default = fallback
30
+ source = url
31
+ mode = options..split ? 'split' : 'bundle'
32
+ pending = if mode == 'split' then _manifest! else _bundle!
33
+
34
+ return new Proxy self, {
35
+ get: do(target, p, receiver)
36
+ if Reflect.has(target, p)
37
+ return Reflect.get(target, p, receiver)
38
+ if !target.ready
39
+ target.err.throw("Request before localization is ready:", p)
40
+ return
41
+ return if target.err.cache[p]
42
+ return target.languages[p] if target.languages[p]
43
+ return target.languages[target.active][p] if target.languages[target.active] and target.languages[target.active][p]
44
+ target.err.throw('localization-no-key', p)
45
+ return ''
46
+ }
47
+
48
+ def render value, data = null
49
+ let text = String(value or '')
50
+ return text unless data and typeof data == 'object'
51
+ text.replace /\{([^}]+)\}/g, do(_match, key)
52
+ const value = data[key]
53
+ if value == undefined or value == null then '' else String(value)
54
+
55
+ def lookup path, fallback = ''
56
+ const keys = if Array.isArray(path) then path else String(path or '').split('.').filter(do(part) part)
57
+ let value = languages..[active] or languages..[default] or {}
58
+ for key in keys
59
+ if value and typeof value == 'object' and value[key] != undefined
60
+ value = value[key]
61
+ else
62
+ return fallback
63
+ value
64
+
65
+ def text path, fallback = '', data = null
66
+ const value = lookup(path, fallback)
67
+ return render(value, data) if typeof value == 'string' or typeof value == 'number'
68
+ render(fallback, data)
69
+
70
+ def table path
71
+ const value = lookup(path, {})
72
+ if value and typeof value == 'object' then value else {}
73
+
74
+ def use name
75
+ name = name and languages[name] ? name : active
76
+ return active unless name
77
+ const request = ++#request
78
+ if mode == 'split' and !loaded[name]
79
+ unless await load(name)
80
+ imba.commit!
81
+ return active
82
+ return active if request != #request
83
+ _activate(name)
84
+
85
+ def load name
86
+ return false unless languages[name]
87
+ return true if loaded[name]
88
+ return await loading[name] if loading[name]
89
+ loading[name] = _load(name)
90
+ const success = await loading[name]
91
+ delete loading[name]
92
+ success
93
+
94
+ def _load name
95
+ try
96
+ const data = await _read(_resolve(name))
97
+ const content = if data..[name] and data[name]..$ then data[name] else data
98
+ unless content and typeof content == 'object' and !Array.isArray(content)
99
+ throw new Error("Localization file for {name} is not an object")
100
+ const settings = _settings(name)
101
+ const language = Object.assign({}, content)
102
+ language.$ = Object.assign({}, content..$, settings)
103
+ languages[name] = language
104
+ loaded[name] = true
105
+ return true
106
+ catch error
107
+ err.throw('localization-no-file', {language: name, url: _resolve(name), error})
108
+ return false
109
+
110
+ def _bundle
111
+ try
112
+ _finalize(await _read(source), undefined)
113
+ catch error
114
+ _finalize(undefined, error)
115
+
116
+ def _manifest
117
+ try
118
+ const data = await _read(source)
119
+ const catalog = data..languages
120
+ default = data.default if data..default isa 'string'
121
+ unless catalog and typeof catalog == 'object' and !Array.isArray(catalog) and Object.keys(catalog).length
122
+ throw new Error('Localization manifest has no languages')
123
+ manifest = catalog
124
+ for own name of manifest
125
+ languages[name] = {$: _settings(name)}
126
+ unless languages[default]
127
+ err.throw('localization-no-default', default)
128
+ return
129
+ let name = _select!
130
+ let success = await load(name)
131
+ if !success and name != default
132
+ name = default
133
+ success = await load(name)
134
+ return unless success
135
+ _finish(name)
136
+ catch error
137
+ err.throw('localization-no-file', {url: source, error})
138
+
139
+ def _read url
140
+ const response = await window.fetch(url)
141
+ unless response.ok
142
+ throw new Error("Localization request failed with status {response.status}")
143
+ await response.json!
144
+
145
+ def _resolve name
146
+ const settings = _settings(name)
147
+ const file = settings.src or "{name}.json"
148
+ new window.URL(file, new window.URL(source, window.location.href)).toString!
149
+
150
+ def _settings name
151
+ const settings = manifest[name]
152
+ return {src: settings} if settings isa 'string'
153
+ settings or {}
154
+
155
+ def _select
156
+ const saved = window.localStorage.getItem('imba-localization')
157
+ return saved if saved and languages[saved]
158
+ return preferred if languages[preferred]
159
+ default
160
+
161
+ def _activate name
162
+ return active unless languages[name] and loaded[name]
163
+ const changed = #active != name
164
+ #active = name
165
+ if window.localStorage.getItem('imba-localization') != name
166
+ window.localStorage.setItem('imba-localization', name)
167
+ err.cache = {}
168
+ onchange(name) if changed and onchange isa Function
169
+ imba.commit! if ready
170
+ #active
171
+
172
+ def _finish name
173
+ #active = name
174
+ ready = true
175
+ err.cache = {}
176
+ onready! if onready isa Function
177
+ onchange(name) if onchange isa Function
178
+ imba.commit!
179
+
180
+ def _finalize data, error
181
+ if error or !data
182
+ err.throw('localization-no-file', error)
183
+ elif !data[default]
184
+ err.throw('localization-no-default', default)
185
+ else
186
+ languages = data
187
+ loaded = {}
188
+ loaded[name] = true for own name of languages
189
+ _finish(_select!)
190
+
191
+ get active
192
+ #active or _select!
193
+
194
+ set active name
195
+ use(name)