Skip to content

Commit e2a2bb4

Browse files
committed
feat(translation): connect with ifrc translation service
1 parent 1ce897c commit e2a2bb4

File tree

18 files changed

+881
-100
lines changed

18 files changed

+881
-100
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: 4 additions & 1 deletion
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+
"initialize:type:translations": "test -f ./generated/translationTypes.ts && true || cp types.stub.ts ./generated/translationTypes.ts",
1920
"generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api",
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:translations": "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
@@ -16,6 +16,7 @@ import exportServerStringsToExcel from './commands/exportServerStringsToExcel';
1616
import clearServerStrings from './commands/clearServerStrings';
1717
import pushStringsDref from './commands/pushStringsDref';
1818
import syncEnStrings from './commands/syncEnStrings';
19+
import pushStringsFromExcelToIfrc from './commands/pushStringsFromExcelToIfrc';
1920

2021
const currentDir = cwd();
2122

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

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/resolveUrl.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// eslint-disable-next-line import/prefer-default-export
2-
export function resolveUrl(from: string, to: string) {
3-
const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
4-
if (resolvedUrl.protocol === 'resolve:') {
5-
const { pathname, search, hash } = resolvedUrl;
6-
return pathname + search + hash;
7-
}
2+
export function resolveUrl(base: string, endpoint: string) {
3+
const baseSafe = base.endsWith('/') ? base : `${base}/`;
4+
const endpointSafe = endpoint.startsWith('.') ? endpoint : `.${endpoint}`;
5+
6+
const resolvedUrl = new URL(endpointSafe, baseSafe);
7+
88
return resolvedUrl.toString();
99
}

app/src/utils/restRequest/go.ts

Lines changed: 39 additions & 5 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 '';
@@ -123,10 +139,12 @@ export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additi
123139

124140
const { apiType } = additionalOptions;
125141

126-
return resolveUrl(
127-
apiType === 'risk' ? riskApi : api,
128-
url,
142+
const resolvedUrl = resolveUrl(
143+
getEndPoint(apiType),
144+
`.${url}`,
129145
);
146+
147+
return resolvedUrl;
130148
};
131149

132150
type Literal = string | number | boolean | File;
@@ -164,6 +182,7 @@ export const processGoOptions: GoContextInterface['transformOptions'] = (
164182
} = requestOptions;
165183

166184
const {
185+
apiType,
167186
formData,
168187
isCsvRequest,
169188
isExcelRequest,
@@ -176,10 +195,15 @@ export const processGoOptions: GoContextInterface['transformOptions'] = (
176195
const user = getFromStorage<UserAuth | undefined>(KEY_USER_STORAGE);
177196
const token = user?.token;
178197

198+
// FIXME: only inject on go apis
179199
const defaultHeaders: HeadersInit = {
180200
Authorization: token ? `Token ${token}` : '',
181201
};
182202

203+
if (apiType === 'translation') {
204+
defaultHeaders['x-api-key'] = translationApiKey;
205+
}
206+
183207
if (method === 'GET') {
184208
// Query
185209
defaultHeaders['Accept-Language'] = enforceEnglishForQuery ? 'en' : currentLanguage;
@@ -239,7 +263,17 @@ const isSuccessfulStatus = (status: number): boolean => status >= 200 && status
239263

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

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

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

0 commit comments

Comments
 (0)