Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/cli/src/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ const withWillErrorInNextVersion = (message: string): string =>

// Static function related errors
const withStaticError = (message: string): string =>
`<Static> rules violation: ${message}`;
`<Derive> rules violation: ${message}`;

const withDeclareStaticError = (message: string): string =>
`declareStatic() rules violation: ${message}`;
`derive() rules violation: ${message}`;
// Synchronous wrappers for backward compatibility
export const warnApiKeyInConfigSync = (optionsFilepath: string): string =>
`${colorizeFilepath(
Expand Down Expand Up @@ -59,7 +59,7 @@ export const warnMissingReturnSync = (
): string =>
withLocation(
file,
`Function ${colorizeFunctionName(functionName)} is wrapped in ${colorizeComponent('<Static>')} tags but does have an explicit return statement. Static functions must have an explicit return statment.`,
`Function ${colorizeFunctionName(functionName)} is wrapped in ${colorizeComponent('<Derive>')} (or ${colorizeComponent('<Static>')}) tags but does not have an explicit return statement. Static functions must have an explicit return statement.`,
location
);

Expand Down Expand Up @@ -257,7 +257,7 @@ export const warnDeclareStaticNotWrappedSync = (
withLocation(
file,
withDeclareStaticError(
`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. This call is not wrapped in declareStatic(). Ensure the function is properly wrapped with declareStatic() and does not have circular import dependencies.`
`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. This call is not wrapped in derive() (or declareStatic()). Ensure the function is properly wrapped with derive() and does not have circular import dependencies.`
),
location
);
Expand All @@ -270,7 +270,7 @@ export const warnDeclareStaticNoResultsSync = (
withLocation(
file,
withDeclareStaticError(
`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. DeclareStatic can only receive function invocations and cannot use undefined values or looped calls to construct its result.`
`Could not resolve ${colorizeFunctionName(formatCodeClamp(functionName))}. derive() can only receive function invocations and cannot use undefined values or looped calls to construct its result.`
),
location
);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/generated/version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// This file is auto-generated. Do not edit manually.
export const PACKAGE_VERSION = '2.6.25';
export const PACKAGE_VERSION = '2.6.27';
5 changes: 5 additions & 0 deletions packages/cli/src/react/jsx/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export const DECLARE_VAR_FUNCTION = 'declareVar';
export const DECLARE_STATIC_FUNCTION = 'declareStatic';
export const DERIVE_FUNCTION = 'derive';
export const MSG_REGISTRATION_FUNCTION = 'msg';
export const INLINE_TRANSLATION_HOOK = 'useGT';
export const INLINE_TRANSLATION_HOOK_ASYNC = 'getGT';
export const INLINE_MESSAGE_HOOK = 'useMessages';
export const INLINE_MESSAGE_HOOK_ASYNC = 'getMessages';
export const TRANSLATION_COMPONENT = 'T';
export const STATIC_COMPONENT = 'Static';
export const DERIVE_COMPONENT = 'Derive';

// GT translation functions
export const GT_TRANSLATION_FUNCS = [
Expand All @@ -17,8 +19,10 @@ export const GT_TRANSLATION_FUNCS = [
MSG_REGISTRATION_FUNCTION,
DECLARE_VAR_FUNCTION,
DECLARE_STATIC_FUNCTION,
DERIVE_FUNCTION,
TRANSLATION_COMPONENT,
STATIC_COMPONENT,
DERIVE_COMPONENT,
'Var',
'DateTime',
'Currency',
Expand All @@ -33,6 +37,7 @@ export const VARIABLE_COMPONENTS = [
'Currency',
'Num',
STATIC_COMPONENT,
DERIVE_COMPONENT,
];

export const GT_ATTRIBUTES_WITH_SUGAR = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,147 @@ describe('parseTranslationComponent with cross-file resolution', () => {
);
});

it('should resolve functions across multiple files with re-exports using Derive', () => {
// Same as the first test but uses Derive instead of Static
const pageFile = `
import { T, Derive } from "gt-next";
import { utils1 } from "./libs/utils1";

function getStatic() {
return 1 ? "static" : "dynamic";
}

export default function Page() {
return (
<>
<T>test <Derive>{utils1()}</Derive></T>
</>
);
}
`;

const utils1File = `
import { utils3 } from "./utils2";

export function utils1() {
if (Math.random() > 0.5) {
return utils3();
}
return 1 ? "utils1-a" : "utils1-b";
}
`;

const utils2File = `
export * from "./utils3";
`;

const utils3File = `
import { utils1 } from "./utils1";
export function utils3() {
if (Math.random() > 0.5) {
}
return 1 ? "utils3-a" : "utils3-b";
}
`;

// Set up file system mocks
mockFs.readFileSync.mockImplementation((path: fs.PathOrFileDescriptor) => {
switch (path) {
case '/test/derive/libs/utils1.ts':
return utils1File;
case '/test/derive/libs/utils2.ts':
return utils2File;
case '/test/derive/libs/utils3.ts':
return utils3File;
default:
throw new Error(`File not found: ${path}`);
}
});

mockResolveImportPath.mockImplementation(
(_currentFile: string, importPath: string) => {
if (importPath === './libs/utils1')
return '/test/derive/libs/utils1.ts';
if (importPath === './utils2') return '/test/derive/libs/utils2.ts';
if (importPath === './utils3') return '/test/derive/libs/utils3.ts';
if (importPath === './utils1') return '/test/derive/libs/utils1.ts';
return null;
}
);

const ast = parse(pageFile, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});

let tLocalName = '';
const importAliases: Record<string, string> = {};

traverse(ast, {
ImportDeclaration(path) {
if (path.node.source.value === 'gt-next') {
path.node.specifiers.forEach((spec) => {
if (
t.isImportSpecifier(spec) &&
t.isIdentifier(spec.imported) &&
spec.imported.name === 'T'
) {
tLocalName = spec.local.name;
importAliases[tLocalName] = 'T';
}
if (
t.isImportSpecifier(spec) &&
t.isIdentifier(spec.imported) &&
spec.imported.name === 'Derive'
) {
importAliases[spec.local.name] = 'Derive';
}
});
}
},
});

traverse(ast, {
Program(programPath) {
const tBinding = programPath.scope.getBinding(tLocalName);
if (tBinding) {
parseTranslationComponent({
originalName: 'T',
localName: tLocalName,
path: tBinding.path,
updates,
config: {
importAliases,
parsingOptions,
pkgs: [Libraries.GT_NEXT],
file: '/test/derive/page.tsx',
},
output: {
errors,
warnings,
unwrappedExpressions: [],
},
});
}
},
});

// Derive is normalized to Static internally, so output is the same
expect(errors).toHaveLength(0);
expect(updates).toHaveLength(4);

const staticContents = updates.map((u) => (u.source[1] as { c: string }).c);
expect(staticContents).toContain('utils3-a');
expect(staticContents).toContain('utils3-b');
expect(staticContents).toContain('utils1-a');
expect(staticContents).toContain('utils1-b');

// Derive normalizes to Static type internally
updates.forEach((update) => {
expect((update.source[1] as any).t).toBe('Static');
});
});

it('should handle declareStatic imported with alias', () => {
// Test for declareStatic import aliasing - used within Static component
const pageFile = `
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/react/jsx/utils/jsxParsing/parseJsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { isAcceptedPluralForm, JsxChildren } from 'generaltranslation/internal';
import { isStaticExpression } from '../../evaluateJsx.js';
import {
STATIC_COMPONENT,
DERIVE_COMPONENT,
TRANSLATION_COMPONENT,
VARIABLE_COMPONENTS,
} from '../constants.js';
Expand Down Expand Up @@ -360,7 +361,10 @@ function buildJSXTree({
});

if (elementIsVariable) {
if (componentType === STATIC_COMPONENT) {
if (
componentType === STATIC_COMPONENT ||
componentType === DERIVE_COMPONENT
) {
const helperElement = helperPath.get('children');
const results = {
nodeType: 'element' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
warnDeclareStaticNoResultsSync,
warnDeclareStaticNotWrappedSync,
} from '../../../console/index.js';
import { DECLARE_STATIC_FUNCTION } from './constants.js';
import {
DECLARE_STATIC_FUNCTION,
DERIVE_FUNCTION,
} from './constants.js';

import traverseModule from '@babel/traverse';
import generateModule from '@babel/generator';
Expand All @@ -39,7 +42,7 @@ const resolveImportPathCache = new Map<string, string | null>();
const processFunctionCache = new Map<string, StringNode | null>();

/**
* Checks if an expression is static or uses declareStatic
* Checks if an expression is static or uses derive()
* Returns a Node representing the parsed expression
* @param expr - The expression to check
* @param tPath - NodePath for scope resolution
Expand Down Expand Up @@ -68,7 +71,7 @@ export function handleStaticExpression(
errors
);
if (variants) {
// We found declareStatic -> return as ChoiceNode
// We found derive() -> return as ChoiceNode
return {
type: 'choice',
nodes: variants.map((v) => ({ type: 'text', text: v })),
Expand Down Expand Up @@ -210,12 +213,12 @@ export function handleStaticExpression(
}

/**
* Given a CallExpression, if it is declareStatic(<call>) or declareStatic(await <call>),
* Given a CallExpression, if it is derive(<call>) or derive(await <call>),
* return all possible string outcomes of that argument call as an array of strings.
*
* Examples:
* declareStatic(time()) -> ["day", "night"]
* declareStatic(await time()) -> ["day", "night"]
* derive(time()) -> ["day", "night"]
* derive(await time()) -> ["day", "night"]
*
* Returns null if it can't be resolved.
*/
Expand All @@ -227,7 +230,7 @@ function getDeclareStaticVariants(
errors: string[]
): string[] | null {
// --- Validate Callee --- //
// Must be declareStatic(...) or an alias of it
// Must be derive(...) or an alias of it
if (!t.isIdentifier(call.callee)) {
const code =
call.arguments.length > 0
Expand All @@ -243,11 +246,11 @@ function getDeclareStaticVariants(
return null;
}

// Check if this is declareStatic by name or by checking the import
// Check if this is derive by name or by checking the import
const calleeName = call.callee.name;
const calleeBinding = tPath.scope.getBinding(calleeName);

// If it's not literally named 'declareStatic', check if it's imported from GT
// If it's not literally named 'derive', check if it's imported from GT
if (!calleeBinding) {
return null;
}
Expand All @@ -259,12 +262,15 @@ function getDeclareStaticVariants(
? imported.name
: imported.value;

// Only proceed if the original name is 'declareStatic'
if (originalName !== DECLARE_STATIC_FUNCTION) {
// Only proceed if the original name is 'derive' (or the deprecated 'declareStatic')
if (
originalName !== DECLARE_STATIC_FUNCTION &&
originalName !== DERIVE_FUNCTION
) {
return null;
}
} else {
// Not an import specifier, so it's not declareStatic
// Not an import specifier, so it's not derive
errors.push(
warnDeclareStaticNotWrappedSync(
file,
Expand All @@ -282,7 +288,7 @@ function getDeclareStaticVariants(
const arg = call.arguments[0];
if (!t.isExpression(arg)) return null;

// Handle await expression: declareStatic(await time())
// Handle await expression: derive(await time())
if (t.isAwaitExpression(arg)) {
// Resolve the inner call's possible string outcomes
return resolveCallStringVariants(
Expand Down Expand Up @@ -331,7 +337,7 @@ function resolveCallStringVariants(
// return results.size ? [...results] : null;
// }

// Handle function identifier calls: declareStatic(time())
// Handle function identifier calls: derive(time())
if (t.isCallExpression(expression) && t.isIdentifier(expression.callee)) {
const functionName = expression.callee.name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import { ParsingConfig } from '../types.js';
import { ParsingOutput } from '../types.js';
import { handleStaticExpression } from '../../parseDeclareStatic.js';
import { handleStaticExpression } from '../../parseDerive.js';
import { nodeToStrings } from '../../parseString.js';
import { indexVars } from 'generaltranslation/internal';
import { isValidIcu } from '../../../evaluateJsx.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/react/jsx/utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ===== Tree Construction ===== //
// Used for parseDeclareStatic.ts
// Used for parseDerive.ts
type StringNode = StringTextNode | StringSequenceNode | StringChoiceNode;

type StringTextNode = {
Expand Down
Loading