Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/blue-crews-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/preview-middleware': major
---

i18n for CAP based apps
2 changes: 1 addition & 1 deletion packages/preview-middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ When this middleware is used together with the `reload-middleware`, then the ord
| `editors.rta` | `array` | optional | `undefined` | Configuration allowing to add mount points for runtime adaptation |
| `editors.rta.layer` | `string` | optional | `(calculated)` | Property for defining the runtime adaptation layer for changes (default is `CUSTOMER_BASE` or read from the project for adaptation projects) |
| `editors.rta.endpoints` | `array` | optional | `undefined` | List of mount points for editing |
| `editors.cardGenerator` | --- | optional | `undefined` | Configuration object to enable card generation for an application (only supported for non-CAP apps). |
| `editors.cardGenerator` | --- | optional | `undefined` | Configuration object to enable card generation for an application. |
| `editors.cardGenerator.path` | `string` | optional | `test/flpGeneratorSandbox.html` | The mount point of the local SAP Fiori launchpad which will be considered for card generation. |
| `test` | `array` | optional | `undefined` | List of configurations for automated testing. |
| `debug` | `boolean` | optional | `false` | Enables the debug output |
Expand Down
46 changes: 32 additions & 14 deletions packages/preview-middleware/src/base/flp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,12 +521,7 @@ export class FlpSandbox {
res: Response | http.ServerResponse,
next: NextFunction
) => {
if (this.projectType === 'EDMXBackend') {
this.templateConfig.enableCardGenerator = !!this.cardGenerator?.path;
} else {
this.logger.warn(`The Card Generator is not available for CAP projects.`);
this.templateConfig.enableCardGenerator = false;
}
this.templateConfig.enableCardGenerator = !!this.cardGenerator?.path;
await this.flpGetHandler(req, res, next);
}
);
Expand Down Expand Up @@ -1045,9 +1040,6 @@ export class FlpSandbox {
* @returns {Promise<void>} A promise that resolves when the route is added.
*/
async addStoreCardManifestRoute(): Promise<void> {
if (this.projectType !== 'EDMXBackend') {
return;
}
this.router.use(CARD_GENERATOR_DEFAULT.cardsStore, json());
this.logger.debug(`Add route for ${CARD_GENERATOR_DEFAULT.cardsStore}`);

Expand All @@ -1067,8 +1059,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General question: What does the i18n adjustments have to do with card generation for CAP based apps?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier, the implementation only handled the default i18n.properties file and did not account for scenarios where the manifest defined i18n configurations using a bundleUrl.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that. My question was rather what the i18n adjustment has to do with general cards support for CAP apps. According to this PR because of the missing object support for the i18n property, CAP apps have not been supported. But that's not true. Card generation fro CAP apps has been disabled in #3349 because you cannot deploy the generated cards / the generated cards will be ignored by the FLP. That only works for ABAP based FLPs. So basically my questions are:

  1. Did you verify that the non-ABAP FLP(s) now also support cards? If not, why delete the restriction in this PR?
  2. Why is this i18n change related to CAP at all? The type of the i18n property has been string | object all the time. That's nothing CAP specific. So that part rather looks like a bugfix to me.

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 All @@ -1090,9 +1111,6 @@ export class FlpSandbox {
* @returns {Promise<void>} A promise that resolves when the route is added.
*/
async addStoreI18nKeysRoute(): Promise<void> {
if (this.projectType !== 'EDMXBackend') {
return;
}
this.router.use(CARD_GENERATOR_DEFAULT.i18nStore, json());
this.logger.debug(`Add route for ${CARD_GENERATOR_DEFAULT.i18nStore}`);

Expand Down
96 changes: 51 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,18 +820,18 @@ 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);

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

server = supertest(app);
return { flp, app };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return parameters seem to be not use anywhere. Can this be deleted?

};

beforeAll(async () => {
await setupMiddleware(mockConfig as MiddlewareConfig);
});
Expand Down Expand Up @@ -926,62 +926,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