-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLocalizedUnit.ts
132 lines (113 loc) · 4.42 KB
/
LocalizedUnit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
A "localized item" type, which defines the shape of a piece of text content that has been translated into multiple languages, which are defined by the type parameter `Locales`.
A "localized item" type, which defines the shape of a piece of text content that has been translated into multiple languages, which are defined by the generic type parameter `Locales`.
The expected usage of this type is that each app or library defines its own supported locales, and a base type that is "an item localized into the languages of the supported locales". Example:
```ts
// Suppose we work for ACME Corp., and our markets are USA, Germany, and Japan:
export type AcmeLocales = 'de' | 'en' | 'ja';
// Note that you can use `'en_US'` / `'en_GB'` style locales if you wish; this lib doesn't care exactly how locales are defined, so long as they are unique strings.
export type AcmeLocalizedUnit = LocalizedUnit<AcmeLocales>;
export type AcmeLocalization = Localization<AcmeLocales>;
```
These base types aren't directly used much, but they are **adhered to**, and type-checked. Normally, each localization is expected to look something like this:
```ts
// e.g. in the file `FooComponent.localization.ts`:
export const localizations = {
avatar {
robot:
de: 'Roboter',
en: 'robot',
ja: 'ロボット',
},
werewolf: {
de: 'Werwolf',
en: 'werewolf',
ja: '狼人',
},
//...
}
} as const;
```
It is possible to just have one file with all of an app's localizations in it, but having the localizations literally implemented in TypeScript enables a couple of usually-better approaches:
1. Localizations that are specific to a single component or page can be defined in a file that is specific to that component or page, and imported and used directly in the component or page.
2. Common/shared localizations can be defined in a single file, and then imported and combined in a new file that adds just the unique localizations for the current page or component. E.g.:
```ts
// `FooComponent.localization.ts`:
import { common, button, label } from './common/localizations.ts';
export const localizations = {
common,
button,
label,
avatar {
robot:
de: 'Roboter',
en: 'robot',
ja: 'ロボット',
},
werewolf: {
de: 'Werwolf',
en: 'werewolf',
ja: '狼人',
},
//...
}
} as const;
```
(How finely-grained localizations are exported, and the naming conventions used, are details left up to the developer.)
### Metadata
To facilitate integration with other localization systems (e.g. legaprise stuff, external systems managed by localization vendors, etc), there is a special reserved key `'_metadata'`, which is used to store arbitrary metadata about the item. By default this is not used, but it is available if needed. This can be used to store things like "This localization was created on 2025-01-13 by ChatGPT o1-mini version 1.0.0", or "This localization was reviewed and approved by Jane Doe on 2025-01-13", and so on.
This library doesn't itself populate or use this metadata.
*/
export type LocalizedUnit<Locales extends string, MetadataType = unknown> = '_metadata' extends Locales ? never
: Readonly<{
[K in Locales]: string;
}> & {
readonly _metadata?: MetadataType;
};
export type LocalizedUnitOff<Locales extends string> = Readonly<
Record<Locales, string>
>;
/**
Intermediate type used only for conditional type checking in `LocalizedValues<T>`
*/
export type IsLocalizedUnitType<T, Locales extends string> = T extends { [K in Locales]: string } ? true : false;
/**
Type guard to check if a value is a `LocalizedUnit`.
*/
export const isLocalizedUnit = <Locales extends string>(
value: unknown,
): value is LocalizedUnit<Locales> =>
{
if (!value)
{
return false;
}
if (typeof value !== 'object')
{
return false;
}
// We'd like to do this, but we cannot know here what the actual set of locales is:
//
// if (!('en' in value) || !('ja' in value)) {
// return false;
// }
// if (typeof value.en !== 'string' || typeof value.ja !== 'string') {
// return false;
// }
//
// So we have to do this instead:
const locales = Object.keys(value);
if (locales.length === 0)
{
return false;
}
for (const locale of locales)
{
const localeValue = (value as Record<string, unknown>)[locale];
if (typeof localeValue !== 'string')
{
return false;
}
}
return true;
};