@fgv/ts-res 5.0.0-22 → 5.0.0-23
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 +665 -44
- package/dist/ts-res.d.ts +421 -19
- package/lib/packlets/common/convert.d.ts +10 -0
- package/lib/packlets/common/convert.js +11 -1
- package/lib/packlets/common/resources.d.ts +14 -0
- package/lib/packlets/common/validate/regularExpressions.d.ts +4 -0
- package/lib/packlets/common/validate/regularExpressions.js +5 -1
- package/lib/packlets/common/validate/resources.d.ts +35 -1
- package/lib/packlets/common/validate/resources.js +54 -0
- package/lib/packlets/config/convert.js +1 -1
- package/lib/packlets/config/json.d.ts +1 -1
- package/lib/packlets/qualifier-types/config/convert.d.ts +22 -1
- package/lib/packlets/qualifier-types/config/convert.js +67 -2
- package/lib/packlets/qualifier-types/config/json.d.ts +11 -2
- package/lib/packlets/qualifier-types/helpers.d.ts +1 -2
- package/lib/packlets/qualifier-types/languageQualifierType.d.ts +27 -1
- package/lib/packlets/qualifier-types/languageQualifierType.js +75 -0
- package/lib/packlets/qualifier-types/literalQualifierType.d.ts +30 -4
- package/lib/packlets/qualifier-types/literalQualifierType.js +77 -0
- package/lib/packlets/qualifier-types/literalValueHierarchy.d.ts +6 -10
- package/lib/packlets/qualifier-types/literalValueHierarchy.js +10 -0
- package/lib/packlets/qualifier-types/qualifierType.d.ts +28 -0
- package/lib/packlets/qualifier-types/territoryQualifierType.d.ts +28 -4
- package/lib/packlets/qualifier-types/territoryQualifierType.js +77 -0
- package/lib/packlets/resource-json/compiled/convert.js +2 -1
- package/lib/packlets/resource-json/compiled/json.d.ts +6 -2
- package/lib/packlets/resource-types/jsonResourceType.d.ts +4 -0
- package/lib/packlets/resource-types/jsonResourceType.js +4 -0
- package/lib/packlets/resource-types/resourceType.d.ts +4 -0
- package/lib/packlets/resources/candidateValue.d.ts +92 -0
- package/lib/packlets/resources/candidateValue.js +128 -0
- package/lib/packlets/resources/candidateValueCollector.d.ts +55 -0
- package/lib/packlets/resources/candidateValueCollector.js +96 -0
- package/lib/packlets/resources/index.d.ts +2 -0
- package/lib/packlets/resources/index.js +2 -0
- package/lib/packlets/resources/resource.js +2 -2
- package/lib/packlets/resources/resourceBuilder.d.ts +6 -0
- package/lib/packlets/resources/resourceBuilder.js +3 -1
- package/lib/packlets/resources/resourceCandidate.d.ts +10 -1
- package/lib/packlets/resources/resourceCandidate.js +11 -5
- package/lib/packlets/resources/resourceManagerBuilder.d.ts +7 -0
- package/lib/packlets/resources/resourceManagerBuilder.js +13 -6
- package/lib/packlets/runtime/compiledResourceCollection.d.ts +20 -8
- package/lib/packlets/runtime/compiledResourceCollection.js +25 -4
- package/lib/packlets/runtime/resource-tree/common.d.ts +1 -1
- package/lib/test/unit/qualifier-types/testQualifierType.d.ts +4 -0
- package/lib/test/unit/qualifier-types/testQualifierType.js +24 -0
- package/lib/test/unit/resource-types/testDerivedResourceType.d.ts +1 -0
- package/lib/test/unit/resource-types/testDerivedResourceType.js +3 -0
- package/lib/test/unit/resources/candidateValue.test.d.ts +2 -0
- package/lib/test/unit/resources/candidateValueCollector.test.d.ts +2 -0
- package/package.json +7 -7
- package/src/index.ts +0 -75
- package/src/packlets/bundle/bundleBuilder.ts +0 -159
- package/src/packlets/bundle/bundleLoader.ts +0 -157
- package/src/packlets/bundle/bundleNormalizer.ts +0 -173
- package/src/packlets/bundle/bundleUtils.ts +0 -142
- package/src/packlets/bundle/convert.ts +0 -87
- package/src/packlets/bundle/index.ts +0 -52
- package/src/packlets/bundle/model.ts +0 -138
- package/src/packlets/common/conditions.ts +0 -226
- package/src/packlets/common/convert.ts +0 -184
- package/src/packlets/common/helpers/conditions.ts +0 -84
- package/src/packlets/common/helpers/context.ts +0 -88
- package/src/packlets/common/helpers/index.ts +0 -26
- package/src/packlets/common/helpers/qualifierDefaultValues.ts +0 -91
- package/src/packlets/common/helpers/resources.ts +0 -96
- package/src/packlets/common/index.ts +0 -30
- package/src/packlets/common/resources.ts +0 -105
- package/src/packlets/common/validate/conditions.ts +0 -595
- package/src/packlets/common/validate/index.ts +0 -27
- package/src/packlets/common/validate/regularExpressions.ts +0 -92
- package/src/packlets/common/validate/resources.ts +0 -170
- package/src/packlets/conditions/condition.ts +0 -351
- package/src/packlets/conditions/conditionCollector.ts +0 -103
- package/src/packlets/conditions/conditionDecls.ts +0 -50
- package/src/packlets/conditions/conditionSet.ts +0 -318
- package/src/packlets/conditions/conditionSetCollector.ts +0 -127
- package/src/packlets/conditions/conditionSetDecls.ts +0 -41
- package/src/packlets/conditions/conditionToken.ts +0 -180
- package/src/packlets/conditions/convert/conditionSetDecls.ts +0 -72
- package/src/packlets/conditions/convert/decls.ts +0 -95
- package/src/packlets/conditions/convert/index.ts +0 -24
- package/src/packlets/conditions/index.ts +0 -33
- package/src/packlets/config/common.ts +0 -107
- package/src/packlets/config/configInitFactory.ts +0 -142
- package/src/packlets/config/convert.ts +0 -61
- package/src/packlets/config/index.ts +0 -30
- package/src/packlets/config/json.ts +0 -43
- package/src/packlets/config/predefined/default.ts +0 -150
- package/src/packlets/config/predefined/extended.ts +0 -198
- package/src/packlets/config/predefined/index.ts +0 -26
- package/src/packlets/config/systemConfiguration.ts +0 -217
- package/src/packlets/context/contextDecls.ts +0 -76
- package/src/packlets/context/contextToken.ts +0 -252
- package/src/packlets/context/convert/decls.ts +0 -126
- package/src/packlets/context/convert/index.ts +0 -23
- package/src/packlets/context/index.ts +0 -27
- package/src/packlets/decisions/abstractDecision.ts +0 -87
- package/src/packlets/decisions/abstractDecisionCollector.ts +0 -122
- package/src/packlets/decisions/candidate.ts +0 -101
- package/src/packlets/decisions/common.ts +0 -37
- package/src/packlets/decisions/concreteDecision.ts +0 -157
- package/src/packlets/decisions/decision.ts +0 -172
- package/src/packlets/decisions/index.ts +0 -28
- package/src/packlets/import/fsItem.ts +0 -212
- package/src/packlets/import/importContext.ts +0 -183
- package/src/packlets/import/importManager.ts +0 -187
- package/src/packlets/import/importable.ts +0 -107
- package/src/packlets/import/importers/collectionImporter.ts +0 -153
- package/src/packlets/import/importers/fsItemImporter.ts +0 -154
- package/src/packlets/import/importers/importer.ts +0 -58
- package/src/packlets/import/importers/index.ts +0 -27
- package/src/packlets/import/importers/jsonImporter.ts +0 -133
- package/src/packlets/import/importers/pathImporter.ts +0 -134
- package/src/packlets/import/index.ts +0 -29
- package/src/packlets/qualifier-types/config/convert.ts +0 -108
- package/src/packlets/qualifier-types/config/index.ts +0 -27
- package/src/packlets/qualifier-types/config/json.ts +0 -126
- package/src/packlets/qualifier-types/convert.ts +0 -77
- package/src/packlets/qualifier-types/helpers.ts +0 -94
- package/src/packlets/qualifier-types/index.ts +0 -34
- package/src/packlets/qualifier-types/languageQualifierType.ts +0 -143
- package/src/packlets/qualifier-types/literalQualifierType.ts +0 -244
- package/src/packlets/qualifier-types/literalValueHierarchy.ts +0 -332
- package/src/packlets/qualifier-types/qualifierType.ts +0 -360
- package/src/packlets/qualifier-types/qualifierTypeCollector.ts +0 -102
- package/src/packlets/qualifier-types/territoryQualifierType.ts +0 -255
- package/src/packlets/qualifiers/convert/decls.ts +0 -90
- package/src/packlets/qualifiers/convert/index.ts +0 -24
- package/src/packlets/qualifiers/convert/qualifier.ts +0 -55
- package/src/packlets/qualifiers/index.ts +0 -31
- package/src/packlets/qualifiers/qualifier.ts +0 -169
- package/src/packlets/qualifiers/qualifierCollector.ts +0 -194
- package/src/packlets/qualifiers/qualifierDecl.ts +0 -80
- package/src/packlets/qualifiers/qualifierDefaultValueDecls.ts +0 -57
- package/src/packlets/qualifiers/qualifierDefaultValueToken.ts +0 -230
- package/src/packlets/resource-json/compiled/common.ts +0 -35
- package/src/packlets/resource-json/compiled/convert.ts +0 -143
- package/src/packlets/resource-json/compiled/index.ts +0 -27
- package/src/packlets/resource-json/compiled/json.ts +0 -239
- package/src/packlets/resource-json/convert.ts +0 -236
- package/src/packlets/resource-json/helpers.ts +0 -284
- package/src/packlets/resource-json/index.ts +0 -33
- package/src/packlets/resource-json/json.ts +0 -295
- package/src/packlets/resource-json/normalized.ts +0 -211
- package/src/packlets/resource-json/resourceDeclCollection.ts +0 -117
- package/src/packlets/resource-json/resourceDeclContainer.ts +0 -48
- package/src/packlets/resource-json/resourceDeclTree.ts +0 -116
- package/src/packlets/resource-types/config/convert.ts +0 -38
- package/src/packlets/resource-types/config/index.ts +0 -27
- package/src/packlets/resource-types/config/json.ts +0 -33
- package/src/packlets/resource-types/helpers.ts +0 -44
- package/src/packlets/resource-types/index.ts +0 -30
- package/src/packlets/resource-types/jsonResourceType.ts +0 -102
- package/src/packlets/resource-types/resourceType.ts +0 -292
- package/src/packlets/resource-types/resourceTypeCollector.ts +0 -79
- package/src/packlets/resources/candidateReducer.ts +0 -331
- package/src/packlets/resources/common.ts +0 -114
- package/src/packlets/resources/index.ts +0 -28
- package/src/packlets/resources/resource.ts +0 -307
- package/src/packlets/resources/resourceBuilder.ts +0 -269
- package/src/packlets/resources/resourceCandidate.ts +0 -333
- package/src/packlets/resources/resourceManagerBuilder.ts +0 -1169
- package/src/packlets/runtime/cacheListener.ts +0 -113
- package/src/packlets/runtime/cacheMetrics.ts +0 -206
- package/src/packlets/runtime/compiledResourceCollection.ts +0 -607
- package/src/packlets/runtime/conditionSetResolutionResult.ts +0 -160
- package/src/packlets/runtime/context/contextQualifierProvider.ts +0 -117
- package/src/packlets/runtime/context/contextQualifierProviderValidator.ts +0 -281
- package/src/packlets/runtime/context/index.ts +0 -26
- package/src/packlets/runtime/context/simpleContextQualifierProvider.ts +0 -225
- package/src/packlets/runtime/context/validatingSimpleContextQualifierProvider.ts +0 -96
- package/src/packlets/runtime/iResourceManager.ts +0 -130
- package/src/packlets/runtime/index.ts +0 -35
- package/src/packlets/runtime/resource-tree/common.ts +0 -126
- package/src/packlets/runtime/resource-tree/index.ts +0 -26
- package/src/packlets/runtime/resource-tree/readOnlyResourceTree.ts +0 -342
- package/src/packlets/runtime/resource-tree/resourceTreeChildren.ts +0 -125
- package/src/packlets/runtime/resource-tree/resourceTreeChildrenValidator.ts +0 -187
- package/src/packlets/runtime/resourceResolver.ts +0 -712
- package/src/packlets/runtime/validate.ts +0 -63
- package/src/packlets/zip-archive/convert.ts +0 -121
- package/src/packlets/zip-archive/index.ts +0 -76
- package/src/packlets/zip-archive/json.ts +0 -91
- package/src/packlets/zip-archive/types.ts +0 -140
- package/src/packlets/zip-archive/zipArchiveCreator.ts +0 -229
- package/src/packlets/zip-archive/zipArchiveFormat.ts +0 -158
- package/src/packlets/zip-archive/zipArchiveLoader.ts +0 -370
package/README.md
CHANGED
|
@@ -5,18 +5,123 @@
|
|
|
5
5
|
|
|
6
6
|
A TypeScript library for multidimensional resource management with internationalization and localization support. `@fgv/ts-res` provides a sophisticated system for managing resources with complex conditional logic based on qualifiers like language, territory, and custom dimensions.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Understanding the Resource Model
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
### What is a Resource?
|
|
11
|
+
|
|
12
|
+
In `@fgv/ts-res`, a **resource** represents a logical entity (like a user profile form, API configuration, or UI component) that can vary based on context. Instead of simple key-value pairs, resources are composed of **candidates** - each providing a full or partial representation of that entity for various different contexts.
|
|
13
|
+
|
|
14
|
+
The context is represented as a configurable set of typed **qualifiers** with some value. Each candidate has an associated **condition set** consisting of zero or more **conditions**, each of which specifies some qualifier and a value to be matched. Qualifiers not present in a condition set are irrelevant, so the empty condition set is *unconditional* and matches any context.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Traditional i18n approach - separate keys, full duplication
|
|
18
|
+
{
|
|
19
|
+
"profile.title.en": "User Profile",
|
|
20
|
+
"profile.title.fr": "Profil Utilisateur",
|
|
21
|
+
"profile.button.en": "Save Changes",
|
|
22
|
+
"profile.button.fr": "Enregistrer les Modifications",
|
|
23
|
+
"profile.title.en-CA": "User Profile", // Duplicated!
|
|
24
|
+
"profile.button.en-CA": "Save Changes" // Duplicated!
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ts-res approach - one resource, multiple candidates
|
|
28
|
+
{
|
|
29
|
+
"id": "user.profile",
|
|
30
|
+
"candidates": [
|
|
31
|
+
{
|
|
32
|
+
"json": { "title": "User Profile", "button": "Save Changes" },
|
|
33
|
+
"conditions": { "language": "en" }
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"json": { "title": "Profil Utilisateur", "button": "Enregistrer" },
|
|
37
|
+
"conditions": { "language": "fr" }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"json": { "button": "Save Eh!" }, // Only override button text
|
|
41
|
+
"conditions": { "language": "en", "territory": "CA" }
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Partial Candidates: The Power of Composition
|
|
48
|
+
|
|
49
|
+
It is possible that several candidates for some resource will match any given context, which enables **composition** of **partial candidates** - you don't need to duplicate entire resources, just specify what changes:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Base configuration for all environments
|
|
53
|
+
{
|
|
54
|
+
"id": "api.config",
|
|
55
|
+
"candidates": [
|
|
56
|
+
{
|
|
57
|
+
"json": {
|
|
58
|
+
"timeout": 5000,
|
|
59
|
+
"retries": 3,
|
|
60
|
+
"features": { "analytics": true, "debugging": false }
|
|
61
|
+
},
|
|
62
|
+
"conditions": {} // Base case - applies everywhere
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"json": {
|
|
66
|
+
"url": "https://api.production.com",
|
|
67
|
+
"features": { "debugging": false }
|
|
68
|
+
},
|
|
69
|
+
"conditions": { "environment": "production" }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"json": {
|
|
73
|
+
"url": "https://api.dev.com",
|
|
74
|
+
"features": { "debugging": true } // Only override debugging
|
|
75
|
+
},
|
|
76
|
+
"conditions": { "environment": "development" }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Three Resolution Strategies
|
|
83
|
+
|
|
84
|
+
**1. Single Best Match** - Pick the most specific candidate only:
|
|
85
|
+
```typescript
|
|
86
|
+
// Context: { environment: "development" }
|
|
87
|
+
const bestMatch = resolver.resolveBestResourceCandidate('api.config').orThrow();
|
|
88
|
+
console.log(bestMatch.candidate.json);
|
|
89
|
+
// Returns: { "url": "api.dev.com", "features": { "debugging": true } }
|
|
90
|
+
// Note: Only the development candidate, not merged with base
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**2. All Matching Candidates** - Get everything that matches for analysis:
|
|
94
|
+
```typescript
|
|
95
|
+
const allMatches = resolver.resolveAllCandidates('api.config').orThrow();
|
|
96
|
+
// Returns array: [base candidate, development candidate]
|
|
97
|
+
// Each candidate as separate objects, not merged
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**3. Composed Resolution** - Merge all matching candidates in priority order:
|
|
101
|
+
```typescript
|
|
102
|
+
// Context: { language: "en", territory: "CA", userType: "admin" }
|
|
103
|
+
const composed = resolver.resolveComposedResourceValue('user.dashboard').orThrow();
|
|
104
|
+
console.log(composed);
|
|
105
|
+
// Returns merged result: {
|
|
106
|
+
// title: 'Dashboard', // from base English
|
|
107
|
+
// nav: { home: 'Home', settings: 'Preferences' }, // base + Canadian override
|
|
108
|
+
// actions: {
|
|
109
|
+
// save: 'Save, eh!', // from Canadian variant
|
|
110
|
+
// cancel: 'Cancel', // from base English
|
|
111
|
+
// admin: 'Admin Panel' // from admin variant
|
|
112
|
+
// }
|
|
113
|
+
// }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Why This Matters
|
|
117
|
+
|
|
118
|
+
**Eliminates Duplication**: Change base configuration once, variants inherit automatically
|
|
119
|
+
|
|
120
|
+
**Handles Complexity**: Easily manage overlapping conditions (Canadian English admin users vs regular Canadian English users)
|
|
121
|
+
|
|
122
|
+
**Maintainable**: Add new dimensions (user types, themes, devices) without restructuring existing resources
|
|
123
|
+
|
|
124
|
+
**Flexible**: Same resource system handles i18n, configuration management, feature flags, and more
|
|
20
125
|
|
|
21
126
|
## Installation
|
|
22
127
|
|
|
@@ -26,31 +131,46 @@ npm install @fgv/ts-res
|
|
|
26
131
|
|
|
27
132
|
## Quick Start
|
|
28
133
|
|
|
134
|
+
This example demonstrates all three resolution strategies using a user dashboard that varies by language, region, and user type:
|
|
135
|
+
|
|
29
136
|
```typescript
|
|
30
137
|
import * as TsRes from '@fgv/ts-res';
|
|
31
138
|
|
|
32
139
|
// 1. Create a resource manager using predefined configuration
|
|
33
140
|
const manager = TsRes.Resources.ResourceManagerBuilder.createPredefined('default').orThrow();
|
|
34
141
|
|
|
35
|
-
// 2. Add
|
|
142
|
+
// 2. Add a resource with base + partial candidates
|
|
36
143
|
manager.addResource({
|
|
37
|
-
id: '
|
|
144
|
+
id: 'user.dashboard',
|
|
38
145
|
resourceTypeName: 'json',
|
|
39
146
|
candidates: [
|
|
147
|
+
// Base English dashboard
|
|
40
148
|
{
|
|
41
|
-
json: {
|
|
149
|
+
json: {
|
|
150
|
+
title: 'Dashboard',
|
|
151
|
+
nav: { home: 'Home', settings: 'Settings' },
|
|
152
|
+
actions: { save: 'Save', cancel: 'Cancel' }
|
|
153
|
+
},
|
|
42
154
|
conditions: { language: 'en' }
|
|
43
155
|
},
|
|
156
|
+
// Canadian English - only changes some terminology
|
|
44
157
|
{
|
|
45
|
-
json: {
|
|
46
|
-
|
|
158
|
+
json: {
|
|
159
|
+
nav: { settings: 'Preferences' }, // Only override this
|
|
160
|
+
actions: { save: 'Save, eh!' } // And this
|
|
161
|
+
},
|
|
162
|
+
conditions: { language: 'en', territory: 'CA' }
|
|
47
163
|
},
|
|
164
|
+
// Admin users get additional actions
|
|
48
165
|
{
|
|
49
|
-
json: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
166
|
+
json: {
|
|
167
|
+
actions: {
|
|
168
|
+
save: 'Save',
|
|
169
|
+
cancel: 'Cancel',
|
|
170
|
+
admin: 'Admin Panel' // Additional action
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
conditions: { userType: 'admin' }
|
|
54
174
|
}
|
|
55
175
|
]
|
|
56
176
|
}).orThrow();
|
|
@@ -58,51 +178,552 @@ manager.addResource({
|
|
|
58
178
|
// 3. Build the resource manager to prepare for resolution
|
|
59
179
|
manager.build().orThrow();
|
|
60
180
|
|
|
61
|
-
// 4.
|
|
181
|
+
// 4. Create resolver and demonstrate different resolution strategies
|
|
62
182
|
const resolver = TsRes.ResourceResolver.create(manager).orThrow();
|
|
63
183
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
language: 'en
|
|
67
|
-
|
|
184
|
+
// Canadian admin user context
|
|
185
|
+
const contextualResolver = resolver.withContext({
|
|
186
|
+
language: 'en',
|
|
187
|
+
territory: 'CA',
|
|
188
|
+
userType: 'admin'
|
|
68
189
|
}).orThrow();
|
|
69
190
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
191
|
+
// COMPOSED RESOLUTION - Merges base + Canadian + admin candidates
|
|
192
|
+
const composed = contextualResolver.resolveComposedResourceValue('user.dashboard').orThrow();
|
|
193
|
+
console.log(composed);
|
|
194
|
+
// Result: {
|
|
195
|
+
// title: 'Dashboard', // from base English
|
|
196
|
+
// nav: { home: 'Home', settings: 'Preferences' }, // base + Canadian override
|
|
197
|
+
// actions: {
|
|
198
|
+
// save: 'Save, eh!', // from Canadian variant
|
|
199
|
+
// cancel: 'Cancel', // from base English
|
|
200
|
+
// admin: 'Admin Panel' // from admin variant
|
|
201
|
+
// }
|
|
202
|
+
// }
|
|
203
|
+
|
|
204
|
+
// SINGLE BEST MATCH - Get most specific candidate only
|
|
205
|
+
const bestMatch = contextualResolver.resolveBestResourceCandidate('user.dashboard').orThrow();
|
|
206
|
+
console.log(bestMatch.candidate.json);
|
|
207
|
+
// Returns just the admin candidate (most specific match)
|
|
208
|
+
|
|
209
|
+
// ALL MATCHES - Get all matching candidates for analysis
|
|
210
|
+
const allMatches = contextualResolver.resolveAllCandidates('user.dashboard').orThrow();
|
|
211
|
+
console.log(`Found ${allMatches.length} matching candidates`);
|
|
212
|
+
// Returns array: [base English, Canadian English, admin]
|
|
72
213
|
```
|
|
73
214
|
|
|
74
|
-
##
|
|
215
|
+
## Features
|
|
75
216
|
|
|
76
|
-
|
|
217
|
+
- **Composed Resource Resolution**: Merge multiple partial candidates into complete resources
|
|
218
|
+
- **Multidimensional Qualifiers**: Organize by language, territory, user types, environments, and custom dimensions
|
|
219
|
+
- **Flexible Resolution Strategies**: Single best match, all matches, or composed resolution
|
|
220
|
+
- **Type-Safe**: Full TypeScript support with comprehensive type definitions
|
|
221
|
+
- **Partial Candidate Support**: Override only what changes, inherit the rest
|
|
222
|
+
- **Priority-Based Matching**: Configurable priority levels for different qualifiers
|
|
223
|
+
- **Resource Merging**: Support for augmenting and replacing merge strategies
|
|
224
|
+
- **File System Integration**: Import resources from file systems with automatic condition inference
|
|
225
|
+
- **Context Filtering**: Generate clean environment-specific bundles
|
|
226
|
+
- **BCP47 Language Support**: Built-in support for BCP47 language tags
|
|
227
|
+
- **Extensible Architecture**: Plugin-based system for custom qualifier types
|
|
228
|
+
|
|
229
|
+
## Technical Concepts
|
|
77
230
|
|
|
78
|
-
Qualifiers
|
|
231
|
+
### Qualifiers and Conditions
|
|
79
232
|
|
|
80
|
-
|
|
81
|
-
- **Territory**: Geographic regions (e.g., 'US', 'CA', 'FR')
|
|
82
|
-
- **Custom**: Application-specific dimensions (e.g., 'theme', 'userType')
|
|
233
|
+
**Qualifiers** define the dimensions along which resources can vary:
|
|
83
234
|
|
|
84
|
-
|
|
235
|
+
```typescript
|
|
236
|
+
// Built-in qualifiers
|
|
237
|
+
- language: 'en', 'fr', 'en-CA' // BCP47 language tags
|
|
238
|
+
- territory: 'US', 'CA', 'FR' // Geographic regions
|
|
239
|
+
- currentTerritory: 'US' // User's current location
|
|
240
|
+
|
|
241
|
+
// Custom qualifiers
|
|
242
|
+
- userType: 'admin', 'user', 'guest'
|
|
243
|
+
- environment: 'dev', 'staging', 'prod'
|
|
244
|
+
- theme: 'light', 'dark'
|
|
245
|
+
- device: 'mobile', 'tablet', 'desktop'
|
|
246
|
+
```
|
|
85
247
|
|
|
86
|
-
Conditions
|
|
248
|
+
**Conditions** specify when a candidate applies using qualifier-value pairs:
|
|
87
249
|
|
|
88
250
|
```typescript
|
|
89
251
|
{
|
|
90
|
-
language: 'en-CA',
|
|
91
|
-
territory: 'CA',
|
|
92
|
-
|
|
252
|
+
language: 'en-CA', // Canadian English
|
|
253
|
+
territory: 'CA', // Canada
|
|
254
|
+
userType: 'admin' // Admin users only
|
|
93
255
|
}
|
|
94
256
|
```
|
|
95
257
|
|
|
96
|
-
###
|
|
258
|
+
### Resolution Algorithm
|
|
259
|
+
|
|
260
|
+
**Step 1: Individual Condition Scoring**
|
|
261
|
+
Each condition is evaluated against the context to produce a score between 0.0 (NoMatch) and 1.0 (PerfectMatch):
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Language hierarchy matching
|
|
265
|
+
context: { language: "en-US" }
|
|
266
|
+
condition: { language: "en-US" } → 1.0 (PerfectMatch)
|
|
267
|
+
condition: { language: "en" } → 0.8 (partial match - more general)
|
|
268
|
+
condition: { language: "fr-FR" } → 0.0 (NoMatch)
|
|
269
|
+
|
|
270
|
+
// Context list priority (earlier positions beat later)
|
|
271
|
+
context: { languages: ["en-US", "fr-FR"] }
|
|
272
|
+
condition: { language: "en" } → matches first preference (higher score)
|
|
273
|
+
condition: { language: "fr-FR" } → matches second preference (lower score)
|
|
274
|
+
|
|
275
|
+
// Territory/literal hierarchies can also produce partial matches
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Step 2: Condition Set Evaluation**
|
|
279
|
+
A candidate's condition set matches only if ALL conditions score > 0.0. If any condition yields NoMatch (0.0), the entire candidate is eliminated.
|
|
280
|
+
|
|
281
|
+
**Step 3: Priority-First Ranking**
|
|
282
|
+
Each qualifier has a default priority that its conditions inherit. Candidates are ranked using a priority-first system:
|
|
283
|
+
|
|
284
|
+
1. **Primary rule**: Candidate with the highest priority condition ALWAYS wins
|
|
285
|
+
2. **Secondary rule**: Score is only used as tie-breaker when priorities are equal
|
|
286
|
+
3. **Comparison**: Sort conditions within each candidate by (priority DESC, score DESC), then compare candidates pairwise
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Context: { language: "en-CA", territory: "CA" }
|
|
290
|
+
|
|
291
|
+
// Example: Language (priority 600) beats Territory (priority 400)
|
|
292
|
+
Candidate A: { language: "en" } → priority 600, score 0.8 (partial match with en-CA)
|
|
293
|
+
Candidate B: { territory: "CA" } → priority 400, score 1.0 (perfect match)
|
|
294
|
+
// Winner: Candidate A (higher priority wins despite lower score)
|
|
295
|
+
|
|
296
|
+
// When priorities match, score breaks ties
|
|
297
|
+
Candidate A: { language: "en-CA" } → priority 600, score 1.0 (perfect match)
|
|
298
|
+
Candidate B: { language: "en" } → priority 600, score 0.8 (partial match)
|
|
299
|
+
// Winner: Candidate A (same priority, higher score)
|
|
300
|
+
|
|
301
|
+
// Multiple conditions: ALL must match, highest priority condition determines rank
|
|
302
|
+
Candidate C: { language: "en", territory: "CA" } → both conditions match
|
|
303
|
+
// Conditions: language (priority 600, score 0.8), territory (priority 400, score 1.0)
|
|
304
|
+
// Candidate rank determined by highest: priority 600, score 0.8
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Step 4: Fallback Resolution (when no candidates match)**
|
|
308
|
+
If no candidates pass Step 2 (all are eliminated by `NoMatch` conditions), the system falls back to resolution considering defaults:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// Qualifier configured with default language = "en-US"
|
|
312
|
+
// Context: { language: "de-DE" } (German - no candidates match)
|
|
313
|
+
|
|
314
|
+
// During fallback, NoMatch conditions get promoted to their scoreAsDefault:
|
|
315
|
+
condition: { language: "en-US" } → scoreAsDefault = 1.0 (perfect match with default)
|
|
316
|
+
condition: { language: "fr-FR" } → scoreAsDefault = 0.0 (no match with default)
|
|
317
|
+
condition: { language: "en-GB" } → scoreAsDefault = 0.7 (partial match with default)
|
|
318
|
+
|
|
319
|
+
// Now English candidates become available for German users as intelligent fallbacks
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The `scoreAsDefault` is automatically calculated when creating conditions by comparing the condition's value against the qualifier's configured default value using the same 0.0-1.0 scoring logic.
|
|
323
|
+
|
|
324
|
+
**Step 5: Resolution Strategy Application**
|
|
325
|
+
|
|
326
|
+
- **Single Best Match**: Return the top-ranked candidate only
|
|
327
|
+
- **All Candidates**: Return all candidates that passed matching (Step 2 or fallback Step 4), with their rankings
|
|
328
|
+
- **Composed Resolution**: Merge all matching candidates in rank order (lowest rank merged first, highest rank applied last)
|
|
329
|
+
|
|
330
|
+
## Creating Resources
|
|
331
|
+
|
|
332
|
+
The most flexible and maintainable way to manage resources is through filesystem import, which automatically infers conditions and resource IDs from your directory structure.
|
|
333
|
+
|
|
334
|
+
### Filesystem Import (Recommended)
|
|
335
|
+
|
|
336
|
+
Organize your resources in a directory structure that reflects their conditions. Since values like "CA" or "FR" could be either language or territory, use explicit qualifiers or tokens:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
resources/
|
|
340
|
+
├── language=en/ # language: "en"
|
|
341
|
+
│ ├── dashboard.json # id: "dashboard"
|
|
342
|
+
│ ├── profile.json # id: "profile"
|
|
343
|
+
│ └── territory=CA/ # language: "en", territory: "CA"
|
|
344
|
+
│ └── dashboard.json # id: "dashboard" (Canadian override)
|
|
345
|
+
├── language=fr/ # language: "fr"
|
|
346
|
+
│ ├── dashboard.json # id: "dashboard"
|
|
347
|
+
│ └── territory=CA/ # language: "fr", territory: "CA"
|
|
348
|
+
│ └── dashboard.json # id: "dashboard" (Quebec variant)
|
|
349
|
+
└── _resources.json # Resource collection or configuration
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Or with tokens configured for brevity:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
resources/
|
|
356
|
+
├── lang=en/ # language: "en" (using token)
|
|
357
|
+
│ ├── dashboard.json
|
|
358
|
+
│ └── terr=CA/ # territory: "CA" (using token)
|
|
359
|
+
│ └── dashboard.json
|
|
360
|
+
├── lang=fr/ # language: "fr" (using token)
|
|
361
|
+
│ └── terr=CA/ # territory: "CA" (using token)
|
|
362
|
+
│ └── dashboard.json
|
|
363
|
+
└── resources-config.json # Configuration with token definitions
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Import resources with automatic context inference:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import * as TsRes from '@fgv/ts-res';
|
|
370
|
+
|
|
371
|
+
// Create configuration with tokens for cleaner folder names
|
|
372
|
+
const config = TsRes.Config.SystemConfiguration.create({
|
|
373
|
+
name: 'my-app',
|
|
374
|
+
qualifierTypes: [
|
|
375
|
+
TsRes.QualifierTypes.LanguageQualifierType.create().orThrow(),
|
|
376
|
+
TsRes.QualifierTypes.TerritoryQualifierType.create().orThrow()
|
|
377
|
+
],
|
|
378
|
+
qualifiers: [
|
|
379
|
+
{ name: 'language', typeName: 'language', defaultPriority: 600, token: 'lang' },
|
|
380
|
+
{ name: 'territory', typeName: 'territory', defaultPriority: 400, token: 'terr' }
|
|
381
|
+
],
|
|
382
|
+
resourceTypes: [
|
|
383
|
+
TsRes.ResourceTypes.JsonResourceType.create().orThrow()
|
|
384
|
+
]
|
|
385
|
+
}).orThrow();
|
|
386
|
+
|
|
387
|
+
const manager = TsRes.Resources.ResourceManagerBuilder.create(config).orThrow();
|
|
388
|
+
|
|
389
|
+
// Import from filesystem - conditions are inferred from folder names
|
|
390
|
+
const importManager = TsRes.Import.ImportManager.create({
|
|
391
|
+
filetree: fileTree,
|
|
392
|
+
resources: manager,
|
|
393
|
+
importers: TsRes.Import.ImportManager.getDefaultImporters(),
|
|
394
|
+
initialContext: TsRes.Import.ImportContext.create().orThrow()
|
|
395
|
+
}).orThrow();
|
|
396
|
+
|
|
397
|
+
const result = importManager.importFromFileSystem({
|
|
398
|
+
type: 'path',
|
|
399
|
+
path: './resources'
|
|
400
|
+
}).orThrow();
|
|
401
|
+
|
|
402
|
+
// Result: All resources loaded with appropriate conditions
|
|
403
|
+
// - resources/lang=en/dashboard.json → { language: "en" }
|
|
404
|
+
// - resources/lang=en/terr=CA/dashboard.json → { language: "en", territory: "CA" }
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Resource File Formats
|
|
408
|
+
|
|
409
|
+
Resources can be stored in two formats:
|
|
97
410
|
|
|
98
|
-
|
|
411
|
+
**1. Resource Collection Format** (`_resources.json`):
|
|
99
412
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
413
|
+
Resource collections are flexible containers that can include:
|
|
414
|
+
- **Loose resources**: Complete resources with their candidates
|
|
415
|
+
- **Loose candidates**: Additional candidates to merge with existing resources
|
|
416
|
+
- **Nested collections**: Collections within collections for organization
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"resources": [
|
|
421
|
+
{
|
|
422
|
+
"id": "dashboard",
|
|
423
|
+
"candidates": [
|
|
424
|
+
{
|
|
425
|
+
"json": { "title": "Dashboard", "welcome": "Welcome!" },
|
|
426
|
+
"conditions": { "language": "en" }
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
"candidates": [
|
|
432
|
+
{
|
|
433
|
+
"resourceId": "dashboard",
|
|
434
|
+
"json": { "welcome": "Welcome, eh!" },
|
|
435
|
+
"conditions": { "language": "en", "territory": "CA" }
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
"resourceId": "profile",
|
|
439
|
+
"json": { "title": "User Profile" },
|
|
440
|
+
"conditions": { "language": "en" }
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
"collections": [
|
|
444
|
+
{
|
|
445
|
+
"name": "legal",
|
|
446
|
+
"resources": [
|
|
447
|
+
{
|
|
448
|
+
"id": "legal.privacy",
|
|
449
|
+
"candidates": [
|
|
450
|
+
{
|
|
451
|
+
"json": { "url": "/privacy.html" },
|
|
452
|
+
"conditions": {}
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
]
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The importer merges all these elements:
|
|
463
|
+
- Loose candidates are added to their target resources
|
|
464
|
+
- Nested collections are flattened with appropriate ID prefixing
|
|
465
|
+
- Multiple candidates for the same resource are merged
|
|
466
|
+
|
|
467
|
+
**2. Resource Tree Format** (individual JSON files):
|
|
468
|
+
```json
|
|
469
|
+
// resources/en/dashboard.json
|
|
470
|
+
{
|
|
471
|
+
"title": "Dashboard",
|
|
472
|
+
"welcome": "Welcome!",
|
|
473
|
+
"nav": {
|
|
474
|
+
"home": "Home",
|
|
475
|
+
"settings": "Settings"
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// resources/en/CA/dashboard.json (partial override)
|
|
480
|
+
{
|
|
481
|
+
"welcome": "Welcome, eh!",
|
|
482
|
+
"nav": {
|
|
483
|
+
"settings": "Preferences" // Canadian terminology
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Context Collection During Import
|
|
489
|
+
|
|
490
|
+
The importer recognizes qualifier values in folder and file names using specific patterns:
|
|
491
|
+
|
|
492
|
+
**1. Default Pattern: `qualifier=value`**
|
|
493
|
+
```
|
|
494
|
+
resources/
|
|
495
|
+
├── language=en/ # { language: "en" }
|
|
496
|
+
├── language=fr-CA/ # { language: "fr-CA" }
|
|
497
|
+
├── territory=US/ # { territory: "US" }
|
|
498
|
+
└── environment=production/ # { environment: "production" }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**2. Token Shorthand (when configured)**
|
|
502
|
+
Qualifiers can define a "token" for brevity:
|
|
503
|
+
```typescript
|
|
504
|
+
// Configuration
|
|
505
|
+
{
|
|
506
|
+
qualifiers: [
|
|
507
|
+
{ name: "language", token: "lang" }, // Allow "lang=en"
|
|
508
|
+
{ name: "homeTerritory", token: "home" }, // Allow "home=US"
|
|
509
|
+
{ name: "environment", token: "env" } // Allow "env=prod"
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
```
|
|
514
|
+
resources/
|
|
515
|
+
├── lang=en-US/ # { language: "en-US" } via token
|
|
516
|
+
├── home=CA/ # { homeTerritory: "CA" } via token
|
|
517
|
+
└── env=production/ # { environment: "production" } via token
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**3. Optional Token Pattern (use with caution!)**
|
|
521
|
+
When `tokenIsOptional: true`, the qualifier name/token can be omitted:
|
|
522
|
+
```typescript
|
|
523
|
+
{
|
|
524
|
+
qualifiers: [
|
|
525
|
+
{
|
|
526
|
+
name: "homeTerritory",
|
|
527
|
+
token: "home",
|
|
528
|
+
tokenIsOptional: true // DANGER: "CA" alone will match!
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
```
|
|
534
|
+
resources/
|
|
535
|
+
├── CA/ # { homeTerritory: "CA" } - matches without prefix!
|
|
536
|
+
├── home=US/ # { homeTerritory: "US" } - explicit still works
|
|
537
|
+
└── strings/ # Regular folder (if "strings" isn't a valid territory)
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**⚠️ Warning**: Be very cautious with `tokenIsOptional`. If your qualifier accepts permissive values (like territories that could be any 2-letter code), ordinary folder names might be misinterpreted as qualifier values.
|
|
541
|
+
|
|
542
|
+
**4. Context Accumulation**
|
|
543
|
+
Nested folders inherit and combine parent conditions:
|
|
544
|
+
```
|
|
545
|
+
resources/
|
|
546
|
+
└── language=en/
|
|
547
|
+
└── territory=CA/
|
|
548
|
+
└── role=admin/ # { language: "en", territory: "CA", role: "admin" }
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**5. Resource ID Prefixing**
|
|
552
|
+
Non-qualifier folders become part of the resource ID:
|
|
553
|
+
```
|
|
554
|
+
resources/
|
|
555
|
+
├── legal/ # Not a qualifier, becomes ID prefix
|
|
556
|
+
│ ├── privacy.json # id: "legal.privacy"
|
|
557
|
+
│ └── territory=EU/
|
|
558
|
+
│ └── privacy.json # id: "legal.privacy", conditions: { territory: "EU" }
|
|
559
|
+
└── language=fr/
|
|
560
|
+
└── legal/
|
|
561
|
+
└── privacy.json # id: "legal.privacy", conditions: { language: "fr" }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Manual Resource Creation (Alternative)
|
|
565
|
+
|
|
566
|
+
For simple cases or dynamic resources, you can create them programmatically:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
manager.addResource({
|
|
570
|
+
id: 'api.config',
|
|
571
|
+
resourceTypeName: 'json',
|
|
572
|
+
candidates: [
|
|
573
|
+
{
|
|
574
|
+
json: { url: 'localhost:3000' },
|
|
575
|
+
conditions: { environment: 'development' }
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
json: { url: 'api.production.com' },
|
|
579
|
+
conditions: { environment: 'production' }
|
|
580
|
+
}
|
|
581
|
+
]
|
|
582
|
+
}).orThrow();
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
This is useful for configuration that changes based on deployment environment or resources generated at runtime.
|
|
103
586
|
|
|
104
587
|
## Advanced Usage
|
|
105
588
|
|
|
589
|
+
### Custom Priority and Default Scoring
|
|
590
|
+
|
|
591
|
+
While priority and `scoreAsDefault` are typically determined automatically by qualifier configuration, they can be overridden for advanced use cases:
|
|
592
|
+
|
|
593
|
+
**Use Case 1: Custom Priority for Territory-Based Content**
|
|
594
|
+
|
|
595
|
+
For legal documents and branding, territory often matters more than language:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// Configuration: Elevate territory priority for legal/brand content
|
|
599
|
+
const territoryPriorityConfig = TsRes.Config.SystemConfiguration.create({
|
|
600
|
+
name: 'territory-priority',
|
|
601
|
+
qualifierTypes: [
|
|
602
|
+
TsRes.QualifierTypes.LanguageQualifierType.create().orThrow(),
|
|
603
|
+
TsRes.QualifierTypes.TerritoryQualifierType.create().orThrow()
|
|
604
|
+
],
|
|
605
|
+
qualifiers: [
|
|
606
|
+
{ name: 'language', typeName: 'language', defaultPriority: 600 },
|
|
607
|
+
{ name: 'currentTerritory', typeName: 'territory', defaultPriority: 700 } // Higher than language
|
|
608
|
+
],
|
|
609
|
+
resourceTypes: [TsRes.ResourceTypes.JsonResourceType.create().orThrow()]
|
|
610
|
+
}).orThrow();
|
|
611
|
+
|
|
612
|
+
// Legal document with territory-specific versions
|
|
613
|
+
manager.addResource({
|
|
614
|
+
id: 'legal.privacy-policy',
|
|
615
|
+
candidates: [
|
|
616
|
+
{
|
|
617
|
+
json: { url: '/privacy-us.html' },
|
|
618
|
+
conditions: { currentTerritory: 'US' } // Priority 700
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
json: { url: '/privacy-ca.html' },
|
|
622
|
+
conditions: { currentTerritory: 'CA' } // Priority 700
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
json: { url: '/privacy-generic-fr.html' },
|
|
626
|
+
conditions: { language: 'fr' } // Priority 600 (lower)
|
|
627
|
+
}
|
|
628
|
+
]
|
|
629
|
+
}).orThrow();
|
|
630
|
+
|
|
631
|
+
// Context: { language: 'fr-CA', currentTerritory: 'CA' }
|
|
632
|
+
// Winner: Canadian privacy policy (territory priority 700 > language priority 600)
|
|
633
|
+
// Result: { url: '/privacy-ca.html' } - legally correct for Canada
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
**Use Case 2: Custom scoreAsDefault for "Original Language" Scenarios**
|
|
637
|
+
|
|
638
|
+
For content like quotes or historical documents, you might want to preserve the original language:
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// Configuration: Custom scoreAsDefault for originalLanguage qualifier
|
|
642
|
+
const originalLanguageConfig = TsRes.Config.SystemConfiguration.create({
|
|
643
|
+
name: 'preserve-original',
|
|
644
|
+
qualifierTypes: [
|
|
645
|
+
TsRes.QualifierTypes.LanguageQualifierType.create().orThrow()
|
|
646
|
+
],
|
|
647
|
+
qualifiers: [
|
|
648
|
+
{ name: 'language', typeName: 'language', defaultPriority: 600, defaultValue: 'en' },
|
|
649
|
+
{ name: 'originalLanguage', typeName: 'language', defaultPriority: 650, defaultValue: 'original' }
|
|
650
|
+
],
|
|
651
|
+
resourceTypes: [TsRes.ResourceTypes.JsonResourceType.create().orThrow()]
|
|
652
|
+
}).orThrow();
|
|
653
|
+
|
|
654
|
+
// Historical quote that should prefer original language
|
|
655
|
+
manager.addResource({
|
|
656
|
+
id: 'quotes.einstein',
|
|
657
|
+
candidates: [
|
|
658
|
+
{
|
|
659
|
+
json: { text: 'Gott würfelt nicht', attribution: 'Einstein, 1926' },
|
|
660
|
+
conditions: { originalLanguage: 'de' } // German original, scoreAsDefault = 1.0
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
json: { text: 'God does not play dice', attribution: 'Einstein, 1926' },
|
|
664
|
+
conditions: { language: 'en' } // English translation, scoreAsDefault = calculated from default
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
}).orThrow();
|
|
668
|
+
|
|
669
|
+
// Context: { language: 'fr' } (French user, no direct matches)
|
|
670
|
+
// Fallback resolution: originalLanguage='de' gets scoreAsDefault=1.0, language='en' gets lower score
|
|
671
|
+
// Winner: German original (higher scoreAsDefault in fallback resolution)
|
|
672
|
+
// Result: { text: 'Gott würfelt nicht', attribution: 'Einstein, 1926' }
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
These patterns ensure that content with special requirements (legal compliance, historical accuracy) gets appropriate priority treatment.
|
|
676
|
+
|
|
677
|
+
**Individual Condition Overrides for Outlier Cases**
|
|
678
|
+
|
|
679
|
+
For rare exceptions, you can override priority or scoreAsDefault directly in individual condition declarations without creating new configurations:
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// Most content uses standard language/territory priorities
|
|
683
|
+
// But this specific legal resource needs territory to take precedence
|
|
684
|
+
|
|
685
|
+
manager.addResource({
|
|
686
|
+
id: 'legal.gdpr-notice',
|
|
687
|
+
candidates: [
|
|
688
|
+
{
|
|
689
|
+
json: { text: 'GDPR applies', url: '/gdpr-eu.html' },
|
|
690
|
+
conditions: {
|
|
691
|
+
territory: {
|
|
692
|
+
qualifier: 'territory',
|
|
693
|
+
operator: 'eq',
|
|
694
|
+
value: 'EU',
|
|
695
|
+
priority: 700 // Override: higher than normal territory priority
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
json: { text: 'Generic privacy notice', url: '/privacy.html' },
|
|
701
|
+
conditions: { language: 'en' } // Uses default language priority (600)
|
|
702
|
+
}
|
|
703
|
+
]
|
|
704
|
+
}).orThrow();
|
|
705
|
+
|
|
706
|
+
// Individual scoreAsDefault override for a specific quote
|
|
707
|
+
manager.addResource({
|
|
708
|
+
id: 'quotes.shakespeare',
|
|
709
|
+
candidates: [
|
|
710
|
+
{
|
|
711
|
+
json: { text: 'To be or not to be', source: 'Hamlet' },
|
|
712
|
+
conditions: {
|
|
713
|
+
originalLanguage: {
|
|
714
|
+
qualifier: 'originalLanguage',
|
|
715
|
+
operator: 'eq',
|
|
716
|
+
value: 'en',
|
|
717
|
+
scoreAsDefault: 1.0 // Override: perfect fallback score for English original
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
}).orThrow();
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
This approach is more efficient than creating separate configurations when you only have occasional exceptions to the standard priority rules.
|
|
726
|
+
|
|
106
727
|
### Resource Building with ResourceBuilder
|
|
107
728
|
|
|
108
729
|
```typescript
|