Skip to content

Commit cdc61b7

Browse files
authored
Merge pull request #32 from wix-incubator/proxy-version
MINOR: Introduce new proxy implementation version
2 parents 617bb83 + e7a977f commit cdc61b7

File tree

15 files changed

+755
-472
lines changed

15 files changed

+755
-472
lines changed

.eslintrc.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,14 @@
3232
"trailingComma": "es5"
3333
}
3434
]
35-
}
35+
},
36+
"overrides": [
37+
{
38+
"files": ["*.template.ts"],
39+
"rules": {
40+
"@typescript-eslint/naming-convention": "off",
41+
"@typescript-eslint/no-unused-vars": "off"
42+
}
43+
}
44+
]
3645
}

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22
All notable changes to this project will be documented in this file.
33

4+
## [2.2.0] - 4-12-2024
5+
### Changed
6+
- Reworked the way to generate file to rely on typed recursive proxy.
7+
- How it was: result file had a function which had a generated JS object which mirrored structure of locale keys. Thus locale keys hang in memory in runtime duplicating keys loaded as s JSON file
8+
- Hot it is now: result file has compact recursuve function with a proxy which is casted to generated type which mirrors the locale keys structure. Thus there is no duplication of locale keys in runtime as they are stripped out from source code on build step.
49

510
## [2.1.15] - 27-06-2024
611
### Fixed

README.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,33 @@ output:
6565
```typescript
6666
/* eslint-disable */
6767
/* tslint:disable */
68-
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
69-
return {
68+
export type ILocaleKeys = {
7069
common: {
7170
loggedIn: {
72-
message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data),
71+
message: (data: Record<'username', unknown>) => string,
7372
},
7473
},
75-
readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data),
74+
readingWarning: (data: Record<'reader' | 'writer', unknown>) => string,
7675
};
77-
}
76+
const createProxyImpl = <R extends string>(
77+
t = (...[k]: unknown[]) => k as R,
78+
prevKeys = ''
79+
): unknown =>
80+
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
81+
get: (_, key: string): unknown => {
82+
let nextKey = prevKeys;
83+
84+
if (key !== '$value') {
85+
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
86+
}
87+
88+
return createProxyImpl(t, nextKey);
89+
},
90+
});
7891

79-
export type ILocaleKeys = ReturnType<typeof LocaleKeys>;
92+
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
93+
return createProxyImpl(t) as ILocaleKeys;
94+
}
8095

8196
```
8297

@@ -86,21 +101,36 @@ output with React Hook:
86101
/* tslint:disable */
87102
import React from 'react';
88103

89-
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
90-
return {
104+
export type ILocaleKeys = {
91105
common: {
92106
loggedIn: {
93-
message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data),
107+
message: (data: Record<'username', unknown>) => string,
94108
},
95109
},
96-
readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data),
110+
readingWarning: (data: Record<'reader' | 'writer', unknown>) => string,
97111
};
98-
}
112+
const createProxyImpl = <R extends string>(
113+
t = (...[k]: unknown[]) => k as R,
114+
prevKeys = ''
115+
): unknown =>
116+
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
117+
get: (_, key: string): unknown => {
118+
let nextKey = prevKeys;
119+
120+
if (key !== '$value') {
121+
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
122+
}
123+
124+
return createProxyImpl(t, nextKey);
125+
},
126+
});
99127

100-
export type ILocaleKeys = ReturnType<typeof LocaleKeys>;
128+
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
129+
return createProxyImpl(t) as ILocaleKeys;
130+
}
101131

102132
const LocaleKeysContext = React.createContext({} as ILocaleKeys);
103-
export const LocaleKeysProvider: React.FC<{ translateFn?: (...args: unknown[]) => string; localeKeys?: ILocaleKeys }> = ({ translateFn, localeKeys, children }) => {
133+
export const LocaleKeysProvider: React.FC<{ translateFn?: (...args: unknown[]) => string; localeKeys?: ILocaleKeys; children?: React.ReactNode }> = ({ translateFn, localeKeys, children }) => {
104134
if (!translateFn && !localeKeys) { throw new Error('Either translateFn or localeKeys must be provided') }
105135
const value = (typeof translateFn === 'function' ? LocaleKeys(translateFn) : localeKeys) as ILocaleKeys
106136
return <LocaleKeysContext.Provider value={value}>{children}</LocaleKeysContext.Provider>;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"clean-generated": "rimraf tests/__generated__ tests/**/__generated__ tests/cli-configs-sandbox/*/dist",
77
"generate-for-type-tests": "ts-node tests/pregenerate.ts",
8-
"generate-snapshot": "jest -i --updateSnapshot",
8+
"generate-snapshot": "yarn test -- --updateSnapshot",
99
"lint": "eslint '**/*.{ts,tsx}'",
1010
"lint:fix": "eslint '**/*.{ts,tsx}' --fix",
1111
"typecheck": "tsc --noEmit",

scripts/README.template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Typed-Local-Keys
1+
# Typed-Locale-Keys
22

33
Generate typescript code from locale keys JSON.
44

src/BaseWriter.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import type {
1313
import { IMPORTED_TRANSLATION_FN_TYPE_NAME } from './constants';
1414
import { getTypedParams } from './icuParams';
1515
import { isSingleCurlyBraces } from './utils';
16+
import { renderProxyEngine } from './proxyEngine/renderProxyEngine';
1617

1718
export interface Options extends GeneratorOptions {
1819
project: Project;
19-
sourceFile: Promise<NestedLocaleValues>;
20+
sourceFile: Promise<Record<string, string>>;
2021
resultFile: SourceFile;
2122
typeName: string;
2223
}
@@ -66,14 +67,36 @@ export class BaseWriter {
6667

6768
const objectStr = this.writeObjectAsStr(source);
6869

69-
this.options.resultFile.addFunction(this.buildLocaleKeysFn(objectStr));
70-
71-
this.options.resultFile.addTypeAlias({
72-
kind: StructureKind.TypeAlias,
73-
name: this.options.typeName,
74-
type: `ReturnType<typeof ${this.options.functionName}>`,
75-
isExported: true,
76-
});
70+
if (this.options.translationFn) {
71+
this.options.resultFile.addTypeAlias({
72+
kind: StructureKind.TypeAlias,
73+
name: this.options.typeName,
74+
type: objectStr,
75+
isExported: true,
76+
});
77+
const proxyImplName = 'createProxyImpl';
78+
this.options.resultFile.addStatements([
79+
renderProxyEngine({
80+
creatorFnName: proxyImplName,
81+
ownValueAlias: this.rootKey,
82+
}),
83+
]);
84+
this.options.resultFile.addFunction(
85+
this.buildLocaleKeysFn(
86+
`${proxyImplName}(${
87+
this.options.translationFn ? this.translationFnName : ''
88+
}) as ${this.options.typeName}`
89+
)
90+
);
91+
} else {
92+
this.options.resultFile.addFunction(this.buildLocaleKeysFn(objectStr));
93+
this.options.resultFile.addTypeAlias({
94+
kind: StructureKind.TypeAlias,
95+
name: this.options.typeName,
96+
type: `ReturnType<typeof ${this.options.functionName}>`,
97+
isExported: true,
98+
});
99+
}
77100
}
78101

79102
private buildLocaleKeysFn(objectStr: string) {
@@ -138,47 +161,57 @@ export class BaseWriter {
138161
key === this.rootKey
139162
? keyPrefix
140163
: [keyPrefix, key].filter(Boolean).join('.');
164+
const delimiter = this.options.translationFn ? ';' : ',';
141165

166+
let valueComment = '';
167+
let keyComment = '';
142168
let valueToSet: string;
143-
let comment = '';
144169

145-
if (typeof value === 'string') {
146-
valueToSet = this.options.translationFn
147-
? this.buildFunction(localeKey, value)
148-
: `'${localeKey}'`;
170+
if (typeof value !== 'string') {
171+
valueToSet = this.writeObjectAsStr(value, localeKey);
172+
} else {
173+
if (this.options.translationFn) {
174+
valueToSet = `(${this.buildFunctionParam(value)}) => string`;
175+
keyComment = `/* ${localeKey} */`;
176+
} else {
177+
valueToSet = `'${localeKey}'`;
178+
}
149179

150180
if (this.options.showTranslations) {
151-
comment = ` /* ${value} */`;
181+
valueComment = `/* ${value} */`;
152182
}
153-
} else {
154-
valueToSet = this.writeObjectAsStr(value, localeKey);
155183
}
156184

157-
const keyToSet = /([^A-z0-9_$]|^[0-9])/.test(key) ? `'${key}'` : key;
185+
if (keyComment) {
186+
writer.writeLine(keyComment);
187+
writer.writeLine(valueComment);
188+
valueComment = '';
189+
}
158190

159-
writer.writeLine(`${keyToSet}: ${valueToSet},${comment}`);
191+
const keyToSet = /([^A-z0-9_$]|^[0-9])/.test(key) ? `'${key}'` : key;
192+
writer.writeLine(
193+
`${keyToSet}: ${valueToSet}${delimiter} ${valueComment}`
194+
);
160195
});
161196
});
162197

163198
return writer.toString();
164199
}
165200

166-
private buildFunction(key: string, value: string): string {
201+
private buildFunctionParam(value: string) {
167202
let param = '';
168-
let secondCallParam = '';
169203
const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix);
170-
171204
const interpolationKeys = icuCompatible
172205
? getTypedParams(value).map((p) => p.name)
173206
: this.getInterpolationKeys(value);
207+
174208
if (interpolationKeys.length) {
175209
param = `data: Record<${interpolationKeys
176210
.map((k) => `'${k}'`)
177211
.join(' | ')}, unknown>`;
178-
secondCallParam = ', data';
179212
}
180213

181-
return `(${param}) => ${this.translationFnName}('${key}'${secondCallParam})`;
214+
return param;
182215
}
183216

184217
private getInterpolationKeys(value: string): string[] {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const __proxyImplName__ = <R extends string>(
2+
t = (...[k]: unknown[]) => k as R,
3+
prevKeys = ''
4+
): unknown =>
5+
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
6+
get: (_, key: string): unknown => {
7+
let nextKey = prevKeys;
8+
9+
if (key !== '__ownValueAlias__') {
10+
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
11+
}
12+
13+
return __proxyImplName__(t, nextKey);
14+
},
15+
});

src/proxyEngine/renderProxyEngine.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { readFileSync } from 'fs';
2+
import { join } from 'path';
3+
4+
export function renderProxyEngine({
5+
creatorFnName,
6+
ownValueAlias,
7+
}: {
8+
creatorFnName: string;
9+
ownValueAlias: string;
10+
}) {
11+
return readFileSync(join(__dirname, 'proxyEngine.template.ts'), 'utf-8')
12+
.replace(/__proxyImplName__/g, creatorFnName)
13+
.replace(/__ownValueAlias__/g, ownValueAlias);
14+
}

0 commit comments

Comments
 (0)