Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/orange-elephants-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sap-ux/preview-middleware": patch
---

fix(preview-middleware): i18n handling for cards generator
33 changes: 31 additions & 2 deletions packages/preview-middleware/src/base/flp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,8 +1067,37 @@ export class FlpSandbox {
try {
this.fs = this.fs ?? create(createStorage());
const webappPath = await getWebappPath(path.resolve(), this.fs);
const i18nPath = this.manifest['sap.app'].i18n as string;
const filePath = i18nPath ? join(webappPath, i18nPath) : join(webappPath, 'i18n', 'i18n.properties');
const i18nConfig = this.manifest['sap.app'].i18n;
let i18nPath: string;
let supportedLocales: unknown[] = [];
let fallbackLocale: string | undefined;

if (typeof i18nConfig === 'string') {
i18nPath = i18nConfig;
} else if (typeof i18nConfig === 'object' && i18nConfig !== null && 'bundleUrl' in i18nConfig) {
const { bundleUrl } = i18nConfig;
i18nPath = bundleUrl;
supportedLocales = i18nConfig.supportedLocales ?? [];
fallbackLocale = i18nConfig.fallbackLocale;
} else {
i18nPath = 'i18n/i18n.properties';
}

const requestedLocale = (req.query.locale as string) || fallbackLocale || '';
const baseFilePath = join(webappPath, i18nPath);
const filePath = requestedLocale
? baseFilePath.replace('.properties', `_${requestedLocale}.properties`)
: baseFilePath;

if (requestedLocale && supportedLocales.length > 0 && !supportedLocales.includes(requestedLocale)) {
this.sendResponse(
res,
'text/plain',
400,
`Locale "${requestedLocale}" is not supported. Supported: ${supportedLocales.join(', ')}`
);
return;
}
const entries = (req.body as Array<I18nEntry>) || [];
entries.forEach((entry) => {
if (entry.comment) {
Expand Down
95 changes: 50 additions & 45 deletions packages/preview-middleware/test/unit/base/flp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,9 +820,9 @@ describe('FlpSandbox', () => {
afterEach(() => {
fetchMock.mockRestore();
});

let flp: FlpSandbox;
const setupMiddleware = async (mockConfig: Partial<MiddlewareConfig>) => {
const flp = new FlpSandbox(mockConfig, mockProject, mockUtils, logger);
flp = new FlpSandbox(mockConfig, mockProject, mockUtils, logger);
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
await flp.init(manifest);

Expand All @@ -831,7 +831,6 @@ describe('FlpSandbox', () => {

server = supertest(app);
};

beforeAll(async () => {
await setupMiddleware(mockConfig as MiddlewareConfig);
});
Expand Down Expand Up @@ -926,62 +925,68 @@ describe('FlpSandbox', () => {
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledTimes(1);
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(filePath, newI18nEntry);
});
});
test('should handle string i18n path', async () => {
const newI18nEntry = [{ key: 'HELLO', value: 'Hello World' }];
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
manifest['sap.app'].i18n = 'i18n/custom.properties';
await flp.init(manifest);
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}?locale=de`).send(newI18nEntry);
const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'custom_de.properties');

describe('router with enableCardGenerator in CAP project', () => {
let server!: SuperTest<Test>;
const mockConfig = {
editors: {
cardGenerator: {
path: 'test/flpCardGeneratorSandbox.html'
}
}
};
expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});

let mockFsPromisesWriteFile: jest.Mock;
test('should handle bundleUrl with supported and fallback locales', async () => {
const newI18nEntry = [{ key: 'GREETING', value: 'Hallo Welt' }];
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
manifest['sap.app'].i18n = {
bundleUrl: 'i18n/i18n.properties',
supportedLocales: ['de', 'es'],
fallbackLocale: 'de'
};
await flp.init(manifest);

beforeEach(() => {
mockFsPromisesWriteFile = jest.fn();
promises.writeFile = mockFsPromisesWriteFile;
});
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}?locale=de`).send(newI18nEntry);
const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'i18n_de.properties');

afterEach(() => {
fetchMock.mockRestore();
expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});

const setupMiddleware = async (mockConfig: Partial<MiddlewareConfig>) => {
const flp = new FlpSandbox(mockConfig, mockProject, mockUtils, logger);
test('should reject unsupported locale', async () => {
const newI18nEntry = [{ key: 'GREETING', value: 'Bonjour' }];

const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
jest.spyOn(projectAccess, 'getProjectType').mockImplementationOnce(() => Promise.resolve('CAPNodejs'));
manifest['sap.app'].i18n = {
bundleUrl: 'i18n/i18n.properties',
supportedLocales: ['de', 'es']
};
await flp.init(manifest);

const app = express();
app.use(flp.router);

server = supertest(app);
};
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}?locale=fr`).send(newI18nEntry);

beforeAll(async () => {
await setupMiddleware(mockConfig as MiddlewareConfig);
expect(response.status).toBe(400);
expect(response.text).toContain('Locale "fr" is not supported');
});
test('should fallback to default i18n/i18n.properties if no i18n defined', async () => {
const newI18nEntry = [{ key: 'HELLO', value: 'Hello World' }];

test('GET /test/flpCardGeneratorSandbox.html', async () => {
const response = await server.get(
`${CARD_GENERATOR_DEFAULT.previewGeneratorSandbox}?sap-ui-xx-viewCache=false`
);
expect(response.status).toBe(200);
expect(response.type).toBe('text/html');
expect(logger.warn).toHaveBeenCalledWith('The Card Generator is not available for CAP projects.');
});
const manifest = JSON.parse(readFileSync(join(fixtures, 'simple-app/webapp/manifest.json'), 'utf-8'));
delete manifest['sap.app'].i18n;
await flp.init(manifest);
const response = await server.post(`${CARD_GENERATOR_DEFAULT.i18nStore}`).send(newI18nEntry);

test('POST /cards/store with payload', async () => {
const response = await server.post(CARD_GENERATOR_DEFAULT.cardsStore).send('hello');
expect(response.status).toBe(404);
});
const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'i18n.properties');

test('POST /editor/i18n with payload', async () => {
const response = await server.post(CARD_GENERATOR_DEFAULT.i18nStore).send('hello');
expect(response.status).toBe(404);
expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});
});

Expand Down
Loading