Skip to content

Commit 1ddc71f

Browse files
committed
feat(translation): connect with ifrc translation service
1 parent d36918a commit 1ddc71f

File tree

15 files changed

+283
-92
lines changed

15 files changed

+283
-92
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ env:
1313
APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }}
1414
APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }}
1515
APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }}
16+
APP_TRANSLATION_API_ENDPOINT: ${{ vars.APP_TRANSLATION_API_ENDPOINT }}
1617
APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }}
1718
APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }}
1819
APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }}
1920
APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }}
2021
APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }}
22+
APP_TRANSLATION_API_KEY: ${{ vars.APP_TRANSLATION_API_KEY }}
2123
APP_TITLE: ${{ vars.APP_TITLE }}
2224
GITHUB_WORKFLOW: true
2325

app/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export default defineConfig({
1616
return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER');
1717
},
1818
APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
19+
20+
APP_TRANSLATION_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
21+
APP_TRANSLATION_API_KEY: Schema.string(),
22+
1923
APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
2024
APP_MAPBOX_ACCESS_TOKEN: Schema.string(),
2125
APP_TINY_API_KEY: Schema.string(),

app/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
"translatte": "tsx scripts/translatte/main.ts",
1414
"translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
1515
"translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
16-
"initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api",
16+
"initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api && pnpm initialize:type:translations",
1717
"initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts",
1818
"initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts",
19-
"generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api",
19+
"initialize:type:translations": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/translationTypes.ts",
20+
"generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api && pnpm generate:type:translations",
2021
"generate:type:go-api": "GO_API_HASH=$(git rev-parse HEAD:go-api); dotenv -- cross-var openapi-typescript https://raw.githubusercontent.com/IFRCGo/go-api-artifacts/refs/heads/main/generated/$GO_API_HASH/openapi-schema.yaml -o ./generated/types.ts --alphabetize",
2122
"generate:type:risk-api": "dotenv -- cross-var openapi-typescript ../go-risk-module-api/openapi-schema.yaml -o ./generated/riskTypes.ts --alphabetize",
23+
"generate:type:translations": "dotenv -- cross-var openapi-typescript \"%APP_TRANSLATION_API_ENDPOINT%swagger/v1/swagger.json/\" -o ./generated/translationTypes.ts --alphabetize",
24+
"postgenerate:type": "tsx scripts/fix-generated.ts",
2225
"prestart": "pnpm initialize:type",
2326
"start": "pnpm -F @ifrc-go/ui build && vite",
2427
"prebuild": "pnpm initialize:type",

app/scripts/fix-generated.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { readFileSync, writeFileSync } from 'fs';
2+
3+
const path = 'generated/translationTypes.ts';
4+
5+
const content = readFileSync(path, 'utf-8');
6+
7+
// If already added, skip
8+
writeFileSync(path, `// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-nocheck\n${content}`);
9+
console.log('✔ Added // @ts-nocheck to translationTypes.ts');
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { readFileSync } from "fs";
2+
3+
// FIXME: get this from params
4+
const applicationId = 18;
5+
6+
function resolveUrl(from: string, to: string) {
7+
const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
8+
if (resolvedUrl.protocol === 'resolve:') {
9+
const { pathname, search, hash } = resolvedUrl;
10+
return pathname + search + hash;
11+
}
12+
return resolvedUrl.toString();
13+
}
14+
15+
/*
16+
async function fetchTranslations(ifrcApiUrl: string, ifrcApiKey: string) {
17+
const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/`);
18+
19+
const headers: RequestInit['headers'] = {
20+
'Accept': 'application/json',
21+
'X-API-KEY': ifrcApiKey,
22+
}
23+
24+
const promise = fetch(
25+
endpoint,
26+
{
27+
method: 'GET',
28+
headers,
29+
}
30+
);
31+
32+
return promise;
33+
}
34+
35+
async function postTranslation(ifrcApiUrl: string, ifrcApiKey: string) {
36+
const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation`);
37+
38+
const headers: RequestInit['headers'] = {
39+
// 'Accept': 'application/json',
40+
'X-API-KEY': ifrcApiKey,
41+
'Content-Type': 'application/json',
42+
}
43+
44+
const promise = fetch(
45+
endpoint,
46+
{
47+
method: 'POST',
48+
headers,
49+
body: JSON.stringify({
50+
page: 'home',
51+
keyName: 'pageTitle',
52+
value: 'IFRC GO | Home',
53+
languageCode: 'en',
54+
}),
55+
}
56+
);
57+
58+
return promise;
59+
}
60+
*/
61+
62+
async function fullAppImport(importFilePath: string, ifrcApiUrl: string, ifrcApiKey: string) {
63+
const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/fullappimport`);
64+
const translationFile = readFileSync(importFilePath);
65+
const uint8FileData = new Uint8Array(translationFile);
66+
const blob = new Blob([uint8FileData], {
67+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
68+
});
69+
70+
const formData = new FormData();
71+
formData.append('files', blob, 'translations.xlsx');
72+
73+
const headers: RequestInit['headers'] = {
74+
'Accept': 'application/json',
75+
'X-API-KEY': ifrcApiKey,
76+
}
77+
78+
const promise = fetch(
79+
endpoint,
80+
{
81+
method: 'POST',
82+
headers,
83+
body: formData,
84+
}
85+
);
86+
87+
return promise;
88+
}
89+
90+
async function pushStringsFromExcelToIfrc(importFilePath: string, apiUrl: string, apiKey: string) {
91+
const response = await fullAppImport(importFilePath, apiUrl, apiKey);
92+
93+
try {
94+
const responseJson = await response.json();
95+
console.info(responseJson);
96+
} catch(e) {
97+
console.info(e);
98+
const responseText = await response.text();
99+
console.info(responseText);
100+
}
101+
}
102+
103+
export default pushStringsFromExcelToIfrc;

app/scripts/translatte/main.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import pushStringsFromExcel from './commands/pushStringsFromExcel';
1515
import exportServerStringsToExcel from './commands/exportServerStringsToExcel';
1616
import clearServerStrings from './commands/clearServerStrings';
1717
import pushStringsDref from './commands/pushStringsDref';
18+
import pushStringsFromExcelToIfrc from './commands/pushStringsFromExcelToIfrc';
1819

1920
const currentDir = cwd();
2021

@@ -255,6 +256,37 @@ yargs(hideBin(process.argv))
255256
);
256257
},
257258
)
259+
.command(
260+
'push-strings-from-excel-to-ifrc <IMPORT_FILE_PATH>',
261+
'Import migration from excel file and push it to server',
262+
(yargs) => {
263+
yargs.positional('IMPORT_FILE_PATH', {
264+
type: 'string',
265+
describe: 'Find the import file on IMPORT_FILE_PATH',
266+
});
267+
yargs.options({
268+
'api-key': {
269+
type: 'string',
270+
describe: 'API key to access the API server',
271+
require: true,
272+
},
273+
'api-url': {
274+
type: 'string',
275+
describe: 'URL for the API server',
276+
require: true,
277+
}
278+
});
279+
},
280+
async (argv) => {
281+
const importFilePath = (argv.IMPORT_FILE_PATH as string);
282+
283+
await pushStringsFromExcelToIfrc(
284+
importFilePath,
285+
argv.apiUrl as string,
286+
argv.apiKey as string,
287+
);
288+
},
289+
)
258290
.command(
259291
'push-strings-dref <IMPORT_FILE_PATH>',
260292
'IMPORTANT!!! Temporary command, do not use!',
@@ -282,7 +314,7 @@ yargs(hideBin(process.argv))
282314
await pushStringsDref(
283315
importFilePath,
284316
argv.apiUrl as string,
285-
argv.authToken as string,
317+
argv.apiKey as string,
286318
);
287319
},
288320
)

app/src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const {
66
APP_MAPBOX_ACCESS_TOKEN,
77
APP_TINY_API_KEY,
88
APP_RISK_API_ENDPOINT,
9+
APP_TRANSLATION_API_ENDPOINT,
10+
APP_TRANSLATION_API_KEY,
911
APP_SDT_URL,
1012
APP_POWER_BI_REPORT_ID_1,
1113
APP_SENTRY_DSN,
@@ -30,9 +32,11 @@ export const api = APP_API_ENDPOINT;
3032
export const adminUrl = APP_ADMIN_URL ?? `${api}admin/`;
3133
export const mbtoken = APP_MAPBOX_ACCESS_TOKEN;
3234
export const riskApi = APP_RISK_API_ENDPOINT;
35+
export const translationApi = APP_TRANSLATION_API_ENDPOINT;
3336
export const sdtUrl = APP_SDT_URL;
3437
export const powerBiReportId1 = APP_POWER_BI_REPORT_ID_1;
3538

39+
export const translationApiKey = APP_TRANSLATION_API_KEY;
3640
export const tinyApiKey = APP_TINY_API_KEY;
3741
export const sentryAppDsn = APP_SENTRY_DSN;
3842
export const sentryTracesSampleRate = APP_SENTRY_TRACES_SAMPLE_RATE;

app/src/utils/restRequest/go.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { type ContextInterface } from '@togglecorp/toggle-request';
99
import {
1010
api,
1111
riskApi,
12+
translationApi,
13+
translationApiKey,
1214
} from '#config';
1315
import { type UserAuth } from '#contexts/user';
1416
import {
@@ -39,8 +41,10 @@ export interface TransformedError {
3941
debugMessage: string;
4042
}
4143

44+
type ApiType = 'go' | 'risk' | 'translation';
45+
4246
export interface AdditionalOptions {
43-
apiType?: 'go' | 'risk';
47+
apiType?: ApiType;
4448
formData?: boolean;
4549
isCsvRequest?: boolean;
4650
enforceEnglishForQuery?: boolean;
@@ -111,6 +115,18 @@ type GoContextInterface = ContextInterface<
111115
AdditionalOptions
112116
>;
113117

118+
function getEndPoint(apiType: ApiType | undefined) {
119+
if (apiType === 'risk') {
120+
return riskApi;
121+
}
122+
123+
if (apiType === 'translation') {
124+
return translationApi;
125+
}
126+
127+
return api;
128+
}
129+
114130
export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additionalOptions) => {
115131
if (isFalsyString(url)) {
116132
return '';
@@ -124,7 +140,7 @@ export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additi
124140
const { apiType } = additionalOptions;
125141

126142
return resolveUrl(
127-
apiType === 'risk' ? riskApi : api,
143+
getEndPoint(apiType),
128144
url,
129145
);
130146
};
@@ -164,6 +180,7 @@ export const processGoOptions: GoContextInterface['transformOptions'] = (
164180
} = requestOptions;
165181

166182
const {
183+
apiType,
167184
formData,
168185
isCsvRequest,
169186
isExcelRequest,
@@ -176,10 +193,15 @@ export const processGoOptions: GoContextInterface['transformOptions'] = (
176193
const user = getFromStorage<UserAuth | undefined>(KEY_USER_STORAGE);
177194
const token = user?.token;
178195

196+
// FIXME: only inject on go apis
179197
const defaultHeaders: HeadersInit = {
180198
Authorization: token ? `Token ${token}` : '',
181199
};
182200

201+
if (apiType === 'translation') {
202+
defaultHeaders['x-api-key'] = translationApiKey;
203+
}
204+
183205
if (method === 'GET') {
184206
// Query
185207
defaultHeaders['Accept-Language'] = enforceEnglishForQuery ? 'en' : currentLanguage;
@@ -239,7 +261,17 @@ const isSuccessfulStatus = (status: number): boolean => status >= 200 && status
239261

240262
const isContentTypeExcel = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_EXCEL;
241263

242-
const isContentTypeJson = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_JSON;
264+
const isContentTypeJson = (res: Response): boolean => {
265+
const contentTypeHeaders = res.headers.get('content-type');
266+
267+
if (isNotDefined(contentTypeHeaders)) {
268+
return false;
269+
}
270+
271+
const mediaTypes = contentTypeHeaders.split('; ');
272+
273+
return mediaTypes[0]?.toLowerCase() === CONTENT_TYPE_JSON;
274+
};
243275

244276
const isLoginRedirect = (url: string): boolean => new URL(url).pathname.includes('login');
245277

app/src/utils/restRequest/index.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from '@togglecorp/toggle-request';
66

77
import type { paths as riskApiPaths } from '#generated/riskTypes';
8+
import type { paths as translationApiPaths } from '#generated/translationTypes';
89
import type { paths as goApiPaths } from '#generated/types';
910

1011
import type {
@@ -23,19 +24,31 @@ export type GoApiUrlQuery<URL extends keyof goApiPaths, METHOD extends 'GET' | '
2324
export type GoApiBody<URL extends keyof goApiPaths, METHOD extends 'POST' | 'PUT' | 'PATCH'> = ApiBody<goApiPaths, URL, METHOD>
2425

2526
export type RiskApiResponse<URL extends keyof riskApiPaths, METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' = 'GET'> = ApiResponse<riskApiPaths, URL, METHOD>;
26-
// type RiskApiUrlQuery<
27-
// URL extends keyof riskApiPaths,
28-
// METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET'
29-
// > = ApiUrlQuery<riskApiPaths, URL, METHOD>
30-
// type RiskApiBody<
31-
// URL extends keyof riskApiPaths,
32-
// METHOD extends 'POST' | 'PUT' | 'PATCH'
33-
// > = ApiBody<riskApiPaths, URL, METHOD>
3427

3528
export type ListResponseItem<RESPONSE extends {
3629
results?: Array<unknown>
3730
} | undefined> = NonNullable<NonNullable<RESPONSE>['results']>[number];
3831

32+
/*
33+
const useTranslationRequest = useRequest as <
34+
PATH extends keyof translationApiPaths,
35+
METHOD extends VALID_METHOD | undefined = 'GET',
36+
>(
37+
requestOptions: CustomRequestOptions<translationApiPaths, PATH, METHOD> & {
38+
apiType: 'translation'
39+
}
40+
) => CustomRequestReturn<translationApiPaths, PATH, METHOD>;
41+
*/
42+
43+
// FIXME: identify a way to do this without a cast
44+
const useTranslationLazyRequest = useLazyRequest as <
45+
PATH extends keyof translationApiPaths,
46+
CONTEXT = unknown,
47+
METHOD extends VALID_METHOD | undefined = 'GET',
48+
>(
49+
requestOptions: CustomLazyRequestOptions<translationApiPaths, PATH, METHOD, CONTEXT> & { apiType: 'translation' }
50+
) => CustomLazyRequestReturn<translationApiPaths, PATH, METHOD, CONTEXT>;
51+
3952
// FIXME: identify a way to do this without a cast
4053
const useGoRequest = useRequest as <
4154
PATH extends keyof goApiPaths,
@@ -76,4 +89,6 @@ export {
7689
useGoRequest as useRequest,
7790
useRiskLazyRequest,
7891
useRiskRequest,
92+
useTranslationLazyRequest,
93+
// useTranslationRequest,
7994
};

0 commit comments

Comments
 (0)