Skip to content

Commit 08cdafe

Browse files
authored
Merge pull request #807 from nextcloud-libraries/fix/typed-var-placeholder-keys
fix(types): type translation variable keys
2 parents 985fe30 + 888f9fc commit 08cdafe

File tree

1 file changed

+22
-12
lines changed

1 file changed

+22
-12
lines changed

lib/translation.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,17 @@ interface TranslationVariableReplacementObject<T> {
3131
escape: boolean
3232
}
3333

34+
/**
35+
* Extracts variables from a translation key
36+
* @notExported
37+
*/
38+
type ExtractedVariables<T extends string> =
39+
T extends `${string}{${infer Variable}}${infer Rest}`
40+
? Variable | ExtractedVariables<Rest>
41+
: never
42+
3443
/** @notExported */
35-
type TranslationVariables = Record<string, string | number | TranslationVariableReplacementObject<string | number>>
44+
type TranslationVariables<K extends string> = Record<ExtractedVariables<K>, string | number | TranslationVariableReplacementObject<string | number>>
3645

3746
/**
3847
* Translate a string
@@ -47,10 +56,10 @@ type TranslationVariables = Record<string, string | number | TranslationVariable
4756
*
4857
* @return {string}
4958
*/
50-
export function translate(
59+
export function translate<T extends string>(
5160
app: string,
52-
text: string,
53-
vars?: TranslationVariables,
61+
text: T,
62+
vars?: TranslationVariables<T>,
5463
number?: number,
5564
options?: TranslationOptions,
5665
): string {
@@ -71,8 +80,8 @@ export function translate(
7180
// TODO: cache this function to avoid inline recreation
7281
// of the same function over and over again in case
7382
// translate() is used in a loop
74-
const _build = (text: string, vars?: TranslationVariables, number?: number) => {
75-
return text.replace(/%n/g, '' + number).replace(/{([^{}]*)}/g, (match, key) => {
83+
const _build = (text: string, vars?: TranslationVariables<T>, number?: number) => {
84+
return text.replace(/%n/g, '' + number).replace(/{([^{}]*)}/g, (match, key: ExtractedVariables<T>) => {
7685
if (vars === undefined || !(key in vars)) {
7786
return optEscape(match)
7887
}
@@ -119,12 +128,12 @@ export function translate(
119128
* @param {object} vars of placeholder key to value
120129
* @param {object} options options object
121130
*/
122-
export function translatePlural(
131+
export function translatePlural<T extends string, K extends string, >(
123132
app: string,
124-
textSingular: string,
125-
textPlural: string,
133+
textSingular: T,
134+
textPlural: K,
126135
number: number,
127-
vars?: Record<string, string | number>,
136+
vars?: TranslationVariables<T> & TranslationVariables<K>,
128137
options?: TranslationOptions,
129138
): string {
130139
const identifier = '_' + textSingular + '_::_' + textPlural + '_'
@@ -139,10 +148,11 @@ export function translatePlural(
139148
}
140149
}
141150

151+
// vars type is casted to allow extra keys without runtime filtering (they are harmless), and without allowing wrong keys in translate
142152
if (number === 1) {
143-
return translate(app, textSingular, vars, number, options)
153+
return translate(app, textSingular, vars as TranslationVariables<T>, number, options)
144154
} else {
145-
return translate(app, textPlural, vars, number, options)
155+
return translate(app, textPlural, vars as TranslationVariables<K>, number, options)
146156
}
147157
}
148158

0 commit comments

Comments
 (0)