Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
51 changes: 43 additions & 8 deletions packages/preview-middleware/src/base/flp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,14 +1067,49 @@ 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 entries = (req.body as Array<I18nEntry>) || [];
entries.forEach((entry) => {
if (entry.comment) {
entry.annotation = entry.comment;
}
});
const i18nConfig = this.manifest['sap.app'].i18n;
let i18nPath = 'i18n/i18n.properties';
let fallbackLocale: string | undefined;
let supportedLocales: string[] = [];

if (typeof i18nConfig === 'string') {
i18nPath = i18nConfig;
} else if (typeof i18nConfig === 'object' && i18nConfig !== null && 'bundleUrl' in i18nConfig) {
const {
bundleUrl: i18nPathFromConfig,
supportedLocales: locales = [],
fallbackLocale: fallback
} = i18nConfig as {
bundleUrl: string;
supportedLocales?: string[];
fallbackLocale?: string;
};

i18nPath = i18nPathFromConfig;
supportedLocales = locales;
fallbackLocale = fallback;
}

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>) || []).map((entry) => ({
...entry,
annotation: entry.comment ?? entry.annotation
}));
await createPropertiesI18nEntries(filePath, entries);
this.fs.commit(() => this.sendResponse(res, 'text/plain', 201, `i18n file updated.`));
} catch (error) {
Expand Down
68 changes: 65 additions & 3 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,6 +925,69 @@ 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');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});

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);

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');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});

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'));
manifest['sap.app'].i18n = {
bundleUrl: 'i18n/i18n.properties',
supportedLocales: ['de', 'es']
};
await flp.init(manifest);

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

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' }];

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);

const webappPath = await getWebappPath(path.resolve());
const expectedPath = join(webappPath, 'i18n', 'i18n.properties');

expect(response.status).toBe(201);
expect(response.text).toBe('i18n file updated.');
expect(createPropertiesI18nEntriesMock).toHaveBeenCalledWith(expectedPath, newI18nEntry);
});
});

describe('router with enableCardGenerator in CAP project', () => {
Expand Down