@digitaldefiance/i18n-lib 4.0.4 → 4.0.5
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 +279 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Part of [Express Suite](https://github.com/Digital-Defiance/express-suite)
|
|
|
31
31
|
- **Fluent Builder**: I18nBuilder for clean, chainable engine configuration
|
|
32
32
|
- **Core System Strings**: Pre-built translations for common UI elements and errors
|
|
33
33
|
- **Type Safety**: Full TypeScript support with generic types
|
|
34
|
+
- **Branded Enums**: Runtime-identifiable string keys with collision detection and component routing
|
|
34
35
|
- **Error Handling**: Comprehensive error classes with translation support and ICU formatting
|
|
35
36
|
- **93.22% Test Coverage**: 1,738 tests covering all features
|
|
36
37
|
- **Security Hardened**: See [SECURITY.md](SECURITY.md) for details
|
|
@@ -671,6 +672,21 @@ LanguageCodes.UK // 'uk'
|
|
|
671
672
|
- `switchToAdmin()` - Switch to admin context
|
|
672
673
|
- `switchToUser()` - Switch to user context
|
|
673
674
|
- `validate()` - Validate all components
|
|
675
|
+
- `registerBrandedComponent(registration)` - Register component with branded string keys
|
|
676
|
+
- `getCollisionReport()` - Get map of key collisions across components
|
|
677
|
+
|
|
678
|
+
### Branded Enum Functions
|
|
679
|
+
|
|
680
|
+
- `createI18nStringKeys(componentId, keys)` - Create a branded enum for i18n keys
|
|
681
|
+
- `createI18nStringKeysFromEnum(componentId, enum)` - Convert legacy enum to branded enum
|
|
682
|
+
- `mergeI18nStringKeys(newId, ...enums)` - Merge multiple branded enums
|
|
683
|
+
- `findStringKeySources(key)` - Find components containing a key
|
|
684
|
+
- `resolveStringKeyComponent(key)` - Resolve key to single component
|
|
685
|
+
- `getStringKeysByComponentId(id)` - Get enum by component ID
|
|
686
|
+
- `getRegisteredI18nComponents()` - List all registered components
|
|
687
|
+
- `getStringKeyValues(enum)` - Get all values from enum
|
|
688
|
+
- `isValidStringKey(value, enum)` - Type guard for key validation
|
|
689
|
+
- `checkStringKeyCollisions(...enums)` - Check enums for collisions
|
|
674
690
|
|
|
675
691
|
### Core Functions
|
|
676
692
|
|
|
@@ -738,6 +754,176 @@ const registration: ComponentRegistration<MyStringKeys, MyLanguages> = {
|
|
|
738
754
|
};
|
|
739
755
|
```
|
|
740
756
|
|
|
757
|
+
## Branded Enums
|
|
758
|
+
|
|
759
|
+
Branded enums enable runtime identification of string keys and collision detection between components. Unlike traditional TypeScript enums (erased at compile time), branded enums embed metadata for runtime component routing.
|
|
760
|
+
|
|
761
|
+
### Why Branded Enums?
|
|
762
|
+
|
|
763
|
+
- **Runtime Identification**: Determine which component a string key belongs to
|
|
764
|
+
- **Collision Detection**: Detect key collisions between components automatically
|
|
765
|
+
- **Component Routing**: Route translations to the correct handler when keys overlap
|
|
766
|
+
- **Zero Overhead**: Values remain raw strings with embedded metadata
|
|
767
|
+
|
|
768
|
+
### Creating Branded Enums
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
import { createI18nStringKeys, BrandedStringKeyValue } from '@digitaldefiance/i18n-lib';
|
|
772
|
+
|
|
773
|
+
// Create a branded enum for i18n keys
|
|
774
|
+
export const UserKeys = createI18nStringKeys('user-component', {
|
|
775
|
+
Login: 'user.login',
|
|
776
|
+
Logout: 'user.logout',
|
|
777
|
+
Profile: 'user.profile',
|
|
778
|
+
} as const);
|
|
779
|
+
|
|
780
|
+
// Export the value type for type annotations
|
|
781
|
+
export type UserKeyValue = BrandedStringKeyValue<typeof UserKeys>;
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Converting from Legacy Enums
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
import { createI18nStringKeysFromEnum } from '@digitaldefiance/i18n-lib';
|
|
788
|
+
|
|
789
|
+
// Legacy enum
|
|
790
|
+
enum LegacyUserKeys {
|
|
791
|
+
Login = 'user.login',
|
|
792
|
+
Logout = 'user.logout',
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Convert to branded enum
|
|
796
|
+
const BrandedUserKeys = createI18nStringKeysFromEnum('user-component', LegacyUserKeys);
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Registering Branded Components
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
// Use registerBrandedComponent instead of registerComponent
|
|
803
|
+
engine.registerBrandedComponent({
|
|
804
|
+
component: {
|
|
805
|
+
id: 'user-component',
|
|
806
|
+
name: 'User Component',
|
|
807
|
+
brandedStringKeys: UserKeys,
|
|
808
|
+
},
|
|
809
|
+
strings: {
|
|
810
|
+
[LanguageCodes.EN_US]: {
|
|
811
|
+
[UserKeys.Login]: 'Log In',
|
|
812
|
+
[UserKeys.Logout]: 'Log Out',
|
|
813
|
+
[UserKeys.Profile]: 'My Profile',
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Collision Detection
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
import { checkStringKeyCollisions } from '@digitaldefiance/i18n-lib';
|
|
823
|
+
|
|
824
|
+
// Check specific enums for collisions
|
|
825
|
+
const result = checkStringKeyCollisions(UserKeys, AdminKeys, CommonKeys);
|
|
826
|
+
|
|
827
|
+
if (result.hasCollisions) {
|
|
828
|
+
console.warn('String key collisions detected:');
|
|
829
|
+
result.collisions.forEach(c => {
|
|
830
|
+
console.warn(` "${c.value}" in: ${c.componentIds.join(', ')}`);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Or use the engine's collision report
|
|
835
|
+
const collisions = engine.getCollisionReport();
|
|
836
|
+
for (const [key, componentIds] of collisions) {
|
|
837
|
+
console.warn(`Key "${key}" found in: ${componentIds.join(', ')}`);
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Finding Key Sources
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
import { findStringKeySources, resolveStringKeyComponent } from '@digitaldefiance/i18n-lib';
|
|
845
|
+
|
|
846
|
+
// Find all components that have a specific key
|
|
847
|
+
const sources = findStringKeySources('user.login');
|
|
848
|
+
// Returns: ['i18n:user-component']
|
|
849
|
+
|
|
850
|
+
// Resolve to a single component (null if ambiguous)
|
|
851
|
+
const componentId = resolveStringKeyComponent('user.login');
|
|
852
|
+
// Returns: 'user-component'
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Type Guards
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
import { isValidStringKey } from '@digitaldefiance/i18n-lib';
|
|
859
|
+
|
|
860
|
+
function handleKey(key: string) {
|
|
861
|
+
if (isValidStringKey(key, UserKeys)) {
|
|
862
|
+
// key is now typed as UserKeyValue
|
|
863
|
+
return translateUserKey(key);
|
|
864
|
+
}
|
|
865
|
+
if (isValidStringKey(key, AdminKeys)) {
|
|
866
|
+
// key is now typed as AdminKeyValue
|
|
867
|
+
return translateAdminKey(key);
|
|
868
|
+
}
|
|
869
|
+
return key; // Unknown key
|
|
870
|
+
}
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
### Merging Enums
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
import { mergeI18nStringKeys, getStringKeyValues } from '@digitaldefiance/i18n-lib';
|
|
877
|
+
|
|
878
|
+
// Create a combined key set for the entire app
|
|
879
|
+
const AllKeys = mergeI18nStringKeys('all-keys',
|
|
880
|
+
CoreStringKeys,
|
|
881
|
+
UserKeys,
|
|
882
|
+
AdminKeys,
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// Get all values from an enum
|
|
886
|
+
const allValues = getStringKeyValues(AllKeys);
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Best Practices
|
|
890
|
+
|
|
891
|
+
1. **Use Namespaced Key Values**: Prevent collisions with prefixed values
|
|
892
|
+
```typescript
|
|
893
|
+
// ✅ Good - namespaced values
|
|
894
|
+
const Keys = createI18nStringKeys('user', {
|
|
895
|
+
Welcome: 'user.welcome',
|
|
896
|
+
} as const);
|
|
897
|
+
|
|
898
|
+
// ❌ Bad - generic values may collide
|
|
899
|
+
const Keys = createI18nStringKeys('user', {
|
|
900
|
+
Welcome: 'welcome',
|
|
901
|
+
} as const);
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
2. **Use Consistent Component IDs**: Match IDs across enum creation and registration
|
|
905
|
+
|
|
906
|
+
3. **Always Use `as const`**: Preserve literal types
|
|
907
|
+
```typescript
|
|
908
|
+
// ✅ Correct - literal types preserved
|
|
909
|
+
const Keys = createI18nStringKeys('id', { A: 'a' } as const);
|
|
910
|
+
|
|
911
|
+
// ❌ Wrong - types widened to string
|
|
912
|
+
const Keys = createI18nStringKeys('id', { A: 'a' });
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
4. **Check for Collisions During Development**:
|
|
916
|
+
```typescript
|
|
917
|
+
if (process.env.NODE_ENV === 'development') {
|
|
918
|
+
const collisions = engine.getCollisionReport();
|
|
919
|
+
if (collisions.size > 0) {
|
|
920
|
+
console.warn('⚠️ String key collisions detected!');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
For complete migration guide, see [BRANDED_ENUM_MIGRATION.md](docs/BRANDED_ENUM_MIGRATION.md).
|
|
926
|
+
|
|
741
927
|
## Browser Support
|
|
742
928
|
|
|
743
929
|
- Chrome/Edge: Latest 2 versions
|
|
@@ -767,6 +953,99 @@ Contributions welcome! Please:
|
|
|
767
953
|
|
|
768
954
|
## ChangeLog
|
|
769
955
|
|
|
956
|
+
### Version 4.0.4
|
|
957
|
+
|
|
958
|
+
**Branded Enums for Runtime String Key Identification**
|
|
959
|
+
|
|
960
|
+
This release introduces branded enums - a powerful feature enabling runtime identification of i18n string keys, collision detection, and intelligent component routing.
|
|
961
|
+
|
|
962
|
+
**Why Branded Enums?**
|
|
963
|
+
|
|
964
|
+
Traditional TypeScript enums are erased at compile time, making it impossible to:
|
|
965
|
+
- Determine which component a string key belongs to at runtime
|
|
966
|
+
- Detect key collisions between components
|
|
967
|
+
- Route translations to the correct handler when keys overlap
|
|
968
|
+
|
|
969
|
+
Branded enums solve these problems by embedding metadata that enables runtime identification while maintaining zero overhead (values remain raw strings).
|
|
970
|
+
|
|
971
|
+
**New Features:**
|
|
972
|
+
|
|
973
|
+
- **`createI18nStringKeys()`**: Factory function to create branded enums with component metadata
|
|
974
|
+
```typescript
|
|
975
|
+
const UserKeys = createI18nStringKeys('user-component', {
|
|
976
|
+
Login: 'user.login',
|
|
977
|
+
Logout: 'user.logout',
|
|
978
|
+
} as const);
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
- **`createI18nStringKeysFromEnum()`**: Convert existing TypeScript enums to branded enums for gradual migration
|
|
982
|
+
|
|
983
|
+
- **`mergeI18nStringKeys()`**: Combine multiple branded enums into a single namespace
|
|
984
|
+
|
|
985
|
+
- **Collision Detection**: Automatically detect when multiple components use the same string key
|
|
986
|
+
```typescript
|
|
987
|
+
const result = checkStringKeyCollisions(UserKeys, AdminKeys);
|
|
988
|
+
if (result.hasCollisions) {
|
|
989
|
+
// Handle collisions
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
- **Key Source Resolution**: Find which component(s) a key belongs to
|
|
994
|
+
```typescript
|
|
995
|
+
const sources = findStringKeySources('user.login'); // ['i18n:user-component']
|
|
996
|
+
const componentId = resolveStringKeyComponent('user.login'); // 'user-component'
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
- **Type Guards**: Runtime validation with TypeScript type narrowing
|
|
1000
|
+
```typescript
|
|
1001
|
+
if (isValidStringKey(key, UserKeys)) {
|
|
1002
|
+
// key is now typed as UserKeyValue
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
- **Engine Integration**: New `registerBrandedComponent()` method and `getCollisionReport()` for engine-level collision tracking
|
|
1007
|
+
|
|
1008
|
+
**New Types:**
|
|
1009
|
+
|
|
1010
|
+
- `BrandedStringKeys<T>` - Type alias for branded enum objects
|
|
1011
|
+
- `BrandedStringKeyValue<E>` - Extract value union from branded enum
|
|
1012
|
+
- `StringKeyCollisionResult` - Result type for collision detection
|
|
1013
|
+
|
|
1014
|
+
**API Additions:**
|
|
1015
|
+
|
|
1016
|
+
| Function | Description |
|
|
1017
|
+
|----------|-------------|
|
|
1018
|
+
| `createI18nStringKeys(componentId, keys)` | Create a branded enum for i18n keys |
|
|
1019
|
+
| `createI18nStringKeysFromEnum(componentId, enum)` | Convert legacy enum to branded enum |
|
|
1020
|
+
| `mergeI18nStringKeys(newId, ...enums)` | Merge multiple branded enums |
|
|
1021
|
+
| `findStringKeySources(key)` | Find components containing a key |
|
|
1022
|
+
| `resolveStringKeyComponent(key)` | Resolve key to single component |
|
|
1023
|
+
| `getStringKeysByComponentId(id)` | Get enum by component ID |
|
|
1024
|
+
| `getRegisteredI18nComponents()` | List all registered components |
|
|
1025
|
+
| `getStringKeyValues(enum)` | Get all values from enum |
|
|
1026
|
+
| `isValidStringKey(value, enum)` | Type guard for key validation |
|
|
1027
|
+
| `checkStringKeyCollisions(...enums)` | Check enums for collisions |
|
|
1028
|
+
|
|
1029
|
+
**Documentation:**
|
|
1030
|
+
|
|
1031
|
+
- **[BRANDED_ENUM_MIGRATION.md](docs/BRANDED_ENUM_MIGRATION.md)** - Complete migration guide with examples
|
|
1032
|
+
- Added Branded Enums section to README with usage examples
|
|
1033
|
+
- API reference updated with all new functions
|
|
1034
|
+
|
|
1035
|
+
**Breaking Changes:**
|
|
1036
|
+
|
|
1037
|
+
None - This release is fully backward compatible. Traditional enums continue to work with existing APIs. Branded enums are opt-in for new code or gradual migration.
|
|
1038
|
+
|
|
1039
|
+
**Migration:**
|
|
1040
|
+
|
|
1041
|
+
No migration required for existing code. To adopt branded enums:
|
|
1042
|
+
|
|
1043
|
+
1. Convert enums using `createI18nStringKeys()` or `createI18nStringKeysFromEnum()`
|
|
1044
|
+
2. Use `registerBrandedComponent()` instead of `registerComponent()`
|
|
1045
|
+
3. Optionally enable collision detection during development
|
|
1046
|
+
|
|
1047
|
+
See [BRANDED_ENUM_MIGRATION.md](docs/BRANDED_ENUM_MIGRATION.md) for detailed migration steps.
|
|
1048
|
+
|
|
770
1049
|
### Version 3.7.5
|
|
771
1050
|
|
|
772
1051
|
**Type Safety Improvements Release**
|