Skip to content

Commit

Permalink
Merge pull request #32 from wix-incubator/proxy-version
Browse files Browse the repository at this point in the history
MINOR: Introduce new proxy implementation version
  • Loading branch information
tkvlnk authored Dec 5, 2024
2 parents 617bb83 + e7a977f commit cdc61b7
Show file tree
Hide file tree
Showing 15 changed files with 755 additions and 472 deletions.
11 changes: 10 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,14 @@
"trailingComma": "es5"
}
]
}
},
"overrides": [
{
"files": ["*.template.ts"],
"rules": {
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-unused-vars": "off"
}
}
]
}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.

## [2.2.0] - 4-12-2024
### Changed
- Reworked the way to generate file to rely on typed recursive proxy.
- 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
- 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.

## [2.1.15] - 27-06-2024
### Fixed
Expand Down
56 changes: 43 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,33 @@ output:
```typescript
/* eslint-disable */
/* tslint:disable */
export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
return {
export type ILocaleKeys = {
common: {
loggedIn: {
message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data),
message: (data: Record<'username', unknown>) => string,
},
},
readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data),
readingWarning: (data: Record<'reader' | 'writer', unknown>) => string,
};
}
const createProxyImpl = <R extends string>(
t = (...[k]: unknown[]) => k as R,
prevKeys = ''
): unknown =>
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
get: (_, key: string): unknown => {
let nextKey = prevKeys;

if (key !== '$value') {
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
}

return createProxyImpl(t, nextKey);
},
});

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

```

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

export function LocaleKeys<R extends string>(t: (...args: unknown[]) => R) {
return {
export type ILocaleKeys = {
common: {
loggedIn: {
message: (data: Record<'username', unknown>) => t('common.loggedIn.message', data),
message: (data: Record<'username', unknown>) => string,
},
},
readingWarning: (data: Record<'reader' | 'writer', unknown>) => t('readingWarning', data),
readingWarning: (data: Record<'reader' | 'writer', unknown>) => string,
};
}
const createProxyImpl = <R extends string>(
t = (...[k]: unknown[]) => k as R,
prevKeys = ''
): unknown =>
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
get: (_, key: string): unknown => {
let nextKey = prevKeys;

if (key !== '$value') {
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
}

return createProxyImpl(t, nextKey);
},
});

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

const LocaleKeysContext = React.createContext({} as ILocaleKeys);
export const LocaleKeysProvider: React.FC<{ translateFn?: (...args: unknown[]) => string; localeKeys?: ILocaleKeys }> = ({ translateFn, localeKeys, children }) => {
export const LocaleKeysProvider: React.FC<{ translateFn?: (...args: unknown[]) => string; localeKeys?: ILocaleKeys; children?: React.ReactNode }> = ({ translateFn, localeKeys, children }) => {
if (!translateFn && !localeKeys) { throw new Error('Either translateFn or localeKeys must be provided') }
const value = (typeof translateFn === 'function' ? LocaleKeys(translateFn) : localeKeys) as ILocaleKeys
return <LocaleKeysContext.Provider value={value}>{children}</LocaleKeysContext.Provider>;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"clean-generated": "rimraf tests/__generated__ tests/**/__generated__ tests/cli-configs-sandbox/*/dist",
"generate-for-type-tests": "ts-node tests/pregenerate.ts",
"generate-snapshot": "jest -i --updateSnapshot",
"generate-snapshot": "yarn test -- --updateSnapshot",
"lint": "eslint '**/*.{ts,tsx}'",
"lint:fix": "eslint '**/*.{ts,tsx}' --fix",
"typecheck": "tsc --noEmit",
Expand Down
2 changes: 1 addition & 1 deletion scripts/README.template.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Typed-Local-Keys
# Typed-Locale-Keys

Generate typescript code from locale keys JSON.

Expand Down
81 changes: 57 additions & 24 deletions src/BaseWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import type {
import { IMPORTED_TRANSLATION_FN_TYPE_NAME } from './constants';
import { getTypedParams } from './icuParams';
import { isSingleCurlyBraces } from './utils';
import { renderProxyEngine } from './proxyEngine/renderProxyEngine';

export interface Options extends GeneratorOptions {
project: Project;
sourceFile: Promise<NestedLocaleValues>;
sourceFile: Promise<Record<string, string>>;
resultFile: SourceFile;
typeName: string;
}
Expand Down Expand Up @@ -66,14 +67,36 @@ export class BaseWriter {

const objectStr = this.writeObjectAsStr(source);

this.options.resultFile.addFunction(this.buildLocaleKeysFn(objectStr));

this.options.resultFile.addTypeAlias({
kind: StructureKind.TypeAlias,
name: this.options.typeName,
type: `ReturnType<typeof ${this.options.functionName}>`,
isExported: true,
});
if (this.options.translationFn) {
this.options.resultFile.addTypeAlias({
kind: StructureKind.TypeAlias,
name: this.options.typeName,
type: objectStr,
isExported: true,
});
const proxyImplName = 'createProxyImpl';
this.options.resultFile.addStatements([
renderProxyEngine({
creatorFnName: proxyImplName,
ownValueAlias: this.rootKey,
}),
]);
this.options.resultFile.addFunction(
this.buildLocaleKeysFn(
`${proxyImplName}(${
this.options.translationFn ? this.translationFnName : ''
}) as ${this.options.typeName}`
)
);
} else {
this.options.resultFile.addFunction(this.buildLocaleKeysFn(objectStr));
this.options.resultFile.addTypeAlias({
kind: StructureKind.TypeAlias,
name: this.options.typeName,
type: `ReturnType<typeof ${this.options.functionName}>`,
isExported: true,
});
}
}

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

let valueComment = '';
let keyComment = '';
let valueToSet: string;
let comment = '';

if (typeof value === 'string') {
valueToSet = this.options.translationFn
? this.buildFunction(localeKey, value)
: `'${localeKey}'`;
if (typeof value !== 'string') {
valueToSet = this.writeObjectAsStr(value, localeKey);
} else {
if (this.options.translationFn) {
valueToSet = `(${this.buildFunctionParam(value)}) => string`;
keyComment = `/* ${localeKey} */`;
} else {
valueToSet = `'${localeKey}'`;
}

if (this.options.showTranslations) {
comment = ` /* ${value} */`;
valueComment = `/* ${value} */`;
}
} else {
valueToSet = this.writeObjectAsStr(value, localeKey);
}

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

writer.writeLine(`${keyToSet}: ${valueToSet},${comment}`);
const keyToSet = /([^A-z0-9_$]|^[0-9])/.test(key) ? `'${key}'` : key;
writer.writeLine(
`${keyToSet}: ${valueToSet}${delimiter} ${valueComment}`
);
});
});

return writer.toString();
}

private buildFunction(key: string, value: string): string {
private buildFunctionParam(value: string) {
let param = '';
let secondCallParam = '';
const icuCompatible = isSingleCurlyBraces(this.interpolation.prefix);

const interpolationKeys = icuCompatible
? getTypedParams(value).map((p) => p.name)
: this.getInterpolationKeys(value);

if (interpolationKeys.length) {
param = `data: Record<${interpolationKeys
.map((k) => `'${k}'`)
.join(' | ')}, unknown>`;
secondCallParam = ', data';
}

return `(${param}) => ${this.translationFnName}('${key}'${secondCallParam})`;
return param;
}

private getInterpolationKeys(value: string): string[] {
Expand Down
15 changes: 15 additions & 0 deletions src/proxyEngine/proxyEngine.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const __proxyImplName__ = <R extends string>(
t = (...[k]: unknown[]) => k as R,
prevKeys = ''
): unknown =>
new Proxy((...args: unknown[]) => t(prevKeys, ...args), {
get: (_, key: string): unknown => {
let nextKey = prevKeys;

if (key !== '__ownValueAlias__') {
nextKey = prevKeys ? [prevKeys, key].join('.') : key;
}

return __proxyImplName__(t, nextKey);
},
});
14 changes: 14 additions & 0 deletions src/proxyEngine/renderProxyEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { readFileSync } from 'fs';
import { join } from 'path';

export function renderProxyEngine({
creatorFnName,
ownValueAlias,
}: {
creatorFnName: string;
ownValueAlias: string;
}) {
return readFileSync(join(__dirname, 'proxyEngine.template.ts'), 'utf-8')
.replace(/__proxyImplName__/g, creatorFnName)
.replace(/__ownValueAlias__/g, ownValueAlias);
}
Loading

0 comments on commit cdc61b7

Please sign in to comment.