diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19e9c4d306..66f327b67b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,13 @@ env: APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }} APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }} APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }} + APP_TRANSLATION_API_ENDPOINT: ${{ vars.APP_TRANSLATION_API_ENDPOINT }} APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }} APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }} APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }} APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }} APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }} + APP_TRANSLATION_API_KEY: ${{ vars.APP_TRANSLATION_API_KEY }} APP_TITLE: ${{ vars.APP_TITLE }} GITHUB_WORKFLOW: true diff --git a/app/env.ts b/app/env.ts index d0e7ea635c..8e3b64f5e4 100644 --- a/app/env.ts +++ b/app/env.ts @@ -16,6 +16,10 @@ export default defineConfig({ return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER'); }, APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), + + APP_TRANSLATION_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }), + APP_TRANSLATION_API_KEY: Schema.string(), + APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }), APP_MAPBOX_ACCESS_TOKEN: Schema.string(), APP_TINY_API_KEY: Schema.string(), diff --git a/app/package.json b/app/package.json index 25f33196fa..f3a6cfb38a 100644 --- a/app/package.json +++ b/app/package.json @@ -13,12 +13,15 @@ "translatte": "tsx scripts/translatte/main.ts", "translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json", "translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json", - "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api", + "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api && pnpm initialize:type:translations", "initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts", "initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts", + "initialize:type:translations": "test -f ./generated/translationTypes.ts && true || cp types.stub.ts ./generated/translationTypes.ts", "generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api", "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", "generate:type:risk-api": "dotenv -- cross-var openapi-typescript ../go-risk-module-api/openapi-schema.yaml -o ./generated/riskTypes.ts --alphabetize", + "generate:type:translations": "dotenv -- cross-var openapi-typescript \"%APP_TRANSLATION_API_ENDPOINT%swagger/v1/swagger.json/\" -o ./generated/translationTypes.ts --alphabetize", + "postgenerate:type:translations": "tsx scripts/fix-generated.ts", "prestart": "pnpm initialize:type", "start": "pnpm -F @ifrc-go/ui build && vite", "prebuild": "pnpm initialize:type", diff --git a/app/scripts/fix-generated.ts b/app/scripts/fix-generated.ts new file mode 100644 index 0000000000..90a4c61722 --- /dev/null +++ b/app/scripts/fix-generated.ts @@ -0,0 +1,9 @@ +import { readFileSync, writeFileSync } from 'fs'; + +const path = 'generated/translationTypes.ts'; + +const content = readFileSync(path, 'utf-8'); + +// If already added, skip +writeFileSync(path, `// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-nocheck\n${content}`); +console.log('✔ Added // @ts-nocheck to translationTypes.ts'); diff --git a/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts b/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts new file mode 100644 index 0000000000..e3b20ae103 --- /dev/null +++ b/app/scripts/translatte/commands/pushStringsFromExcelToIfrc.ts @@ -0,0 +1,103 @@ +import { readFileSync } from "fs"; + +// FIXME: get this from params +const applicationId = 18; + +function resolveUrl(from: string, to: string) { + const resolvedUrl = new URL(to, new URL(from, 'resolve://')); + if (resolvedUrl.protocol === 'resolve:') { + const { pathname, search, hash } = resolvedUrl; + return pathname + search + hash; + } + return resolvedUrl.toString(); +} + +/* +async function fetchTranslations(ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/`); + + const headers: RequestInit['headers'] = { + 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + } + + const promise = fetch( + endpoint, + { + method: 'GET', + headers, + } + ); + + return promise; +} + +async function postTranslation(ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation`); + + const headers: RequestInit['headers'] = { + // 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + 'Content-Type': 'application/json', + } + + const promise = fetch( + endpoint, + { + method: 'POST', + headers, + body: JSON.stringify({ + page: 'home', + keyName: 'pageTitle', + value: 'IFRC GO | Home', + languageCode: 'en', + }), + } + ); + + return promise; +} +*/ + +async function fullAppImport(importFilePath: string, ifrcApiUrl: string, ifrcApiKey: string) { + const endpoint = resolveUrl(ifrcApiUrl, `Application/${applicationId}/Translation/fullappimport`); + const translationFile = readFileSync(importFilePath); + const uint8FileData = new Uint8Array(translationFile); + const blob = new Blob([uint8FileData], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + + const formData = new FormData(); + formData.append('files', blob, 'translations.xlsx'); + + const headers: RequestInit['headers'] = { + 'Accept': 'application/json', + 'X-API-KEY': ifrcApiKey, + } + + const promise = fetch( + endpoint, + { + method: 'POST', + headers, + body: formData, + } + ); + + return promise; +} + +async function pushStringsFromExcelToIfrc(importFilePath: string, apiUrl: string, apiKey: string) { + const response = await fullAppImport(importFilePath, apiUrl, apiKey); + + try { + const responseJson = await response.json(); + console.info(responseJson); + } catch(e) { + console.info(e); + const responseText = await response.text(); + console.info(responseText); + } +} + +export default pushStringsFromExcelToIfrc; diff --git a/app/scripts/translatte/commands/syncEnStrings.ts b/app/scripts/translatte/commands/syncEnStrings.ts index 3b3b7982ef..023abae742 100644 --- a/app/scripts/translatte/commands/syncEnStrings.ts +++ b/app/scripts/translatte/commands/syncEnStrings.ts @@ -1,7 +1,7 @@ import { isDefined, isFalsyString } from "@togglecorp/fujs"; import { fetchServerState, postLanguageStrings, writeFilePromisify } from "../utils"; -async function syncEnStrings(sourceApiUrl: string, desinationApiUrl: string, authToken: string) { +async function syncEnStrings(sourceApiUrl: string, destinationApiUrl: string, authToken: string) { const serverStrings = await fetchServerState(sourceApiUrl, authToken); const enStrings = serverStrings.filter((string) => string.language === 'en'); @@ -23,7 +23,7 @@ async function syncEnStrings(sourceApiUrl: string, desinationApiUrl: string, aut const result = await postLanguageStrings( 'en', actions, - desinationApiUrl, + destinationApiUrl, authToken, ) diff --git a/app/scripts/translatte/main.ts b/app/scripts/translatte/main.ts index 9a2f34c02f..74152ba5c1 100644 --- a/app/scripts/translatte/main.ts +++ b/app/scripts/translatte/main.ts @@ -16,6 +16,7 @@ import exportServerStringsToExcel from './commands/exportServerStringsToExcel'; import clearServerStrings from './commands/clearServerStrings'; import pushStringsDref from './commands/pushStringsDref'; import syncEnStrings from './commands/syncEnStrings'; +import pushStringsFromExcelToIfrc from './commands/pushStringsFromExcelToIfrc'; const currentDir = cwd(); @@ -256,6 +257,37 @@ yargs(hideBin(process.argv)) ); }, ) + .command( + 'push-strings-from-excel-to-ifrc ', + 'Import migration from excel file and push it to server', + (yargs) => { + yargs.positional('IMPORT_FILE_PATH', { + type: 'string', + describe: 'Find the import file on IMPORT_FILE_PATH', + }); + yargs.options({ + 'api-key': { + type: 'string', + describe: 'API key to access the API server', + require: true, + }, + 'api-url': { + type: 'string', + describe: 'URL for the API server', + require: true, + } + }); + }, + async (argv) => { + const importFilePath = (argv.IMPORT_FILE_PATH as string); + + await pushStringsFromExcelToIfrc( + importFilePath, + argv.apiUrl as string, + argv.apiKey as string, + ); + }, + ) .command( 'push-strings-dref ', 'IMPORTANT!!! Temporary command, do not use!', @@ -283,7 +315,7 @@ yargs(hideBin(process.argv)) await pushStringsDref( importFilePath, argv.apiUrl as string, - argv.authToken as string, + argv.apiKey as string, ); }, ) diff --git a/app/src/config.ts b/app/src/config.ts index ddc85d2e47..92cc521d03 100644 --- a/app/src/config.ts +++ b/app/src/config.ts @@ -6,6 +6,8 @@ const { APP_MAPBOX_ACCESS_TOKEN, APP_TINY_API_KEY, APP_RISK_API_ENDPOINT, + APP_TRANSLATION_API_ENDPOINT, + APP_TRANSLATION_API_KEY, APP_SDT_URL, APP_POWER_BI_REPORT_ID_1, APP_SENTRY_DSN, @@ -30,9 +32,11 @@ export const api = APP_API_ENDPOINT; export const adminUrl = APP_ADMIN_URL ?? `${api}admin/`; export const mbtoken = APP_MAPBOX_ACCESS_TOKEN; export const riskApi = APP_RISK_API_ENDPOINT; +export const translationApi = APP_TRANSLATION_API_ENDPOINT; export const sdtUrl = APP_SDT_URL; export const powerBiReportId1 = APP_POWER_BI_REPORT_ID_1; +export const translationApiKey = APP_TRANSLATION_API_KEY; export const tinyApiKey = APP_TINY_API_KEY; export const sentryAppDsn = APP_SENTRY_DSN; export const sentryTracesSampleRate = APP_SENTRY_TRACES_SAMPLE_RATE; diff --git a/app/src/utils/resolveUrl.ts b/app/src/utils/resolveUrl.ts index 8c6880a4af..5151ce9f12 100644 --- a/app/src/utils/resolveUrl.ts +++ b/app/src/utils/resolveUrl.ts @@ -1,9 +1,9 @@ // eslint-disable-next-line import/prefer-default-export -export function resolveUrl(from: string, to: string) { - const resolvedUrl = new URL(to, new URL(from, 'resolve://')); - if (resolvedUrl.protocol === 'resolve:') { - const { pathname, search, hash } = resolvedUrl; - return pathname + search + hash; - } +export function resolveUrl(base: string, endpoint: string) { + const baseSafe = base.endsWith('/') ? base : `${base}/`; + const endpointSafe = endpoint.startsWith('.') ? endpoint : `.${endpoint}`; + + const resolvedUrl = new URL(endpointSafe, baseSafe); + return resolvedUrl.toString(); } diff --git a/app/src/utils/restRequest/go.ts b/app/src/utils/restRequest/go.ts index 477822b687..d31af707fd 100644 --- a/app/src/utils/restRequest/go.ts +++ b/app/src/utils/restRequest/go.ts @@ -9,6 +9,8 @@ import { type ContextInterface } from '@togglecorp/toggle-request'; import { api, riskApi, + translationApi, + translationApiKey, } from '#config'; import { type UserAuth } from '#contexts/user'; import { @@ -39,8 +41,10 @@ export interface TransformedError { debugMessage: string; } +type ApiType = 'go' | 'risk' | 'translation'; + export interface AdditionalOptions { - apiType?: 'go' | 'risk'; + apiType?: ApiType; formData?: boolean; isCsvRequest?: boolean; enforceEnglishForQuery?: boolean; @@ -111,6 +115,18 @@ type GoContextInterface = ContextInterface< AdditionalOptions >; +function getEndPoint(apiType: ApiType | undefined) { + if (apiType === 'risk') { + return riskApi; + } + + if (apiType === 'translation') { + return translationApi; + } + + return api; +} + export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additionalOptions) => { if (isFalsyString(url)) { return ''; @@ -123,10 +139,12 @@ export const processGoUrls: GoContextInterface['transformUrl'] = (url, _, additi const { apiType } = additionalOptions; - return resolveUrl( - apiType === 'risk' ? riskApi : api, - url, + const resolvedUrl = resolveUrl( + getEndPoint(apiType), + `.${url}`, ); + + return resolvedUrl; }; type Literal = string | number | boolean | File; @@ -164,6 +182,7 @@ export const processGoOptions: GoContextInterface['transformOptions'] = ( } = requestOptions; const { + apiType, formData, isCsvRequest, isExcelRequest, @@ -176,10 +195,15 @@ export const processGoOptions: GoContextInterface['transformOptions'] = ( const user = getFromStorage(KEY_USER_STORAGE); const token = user?.token; + // FIXME: only inject on go apis const defaultHeaders: HeadersInit = { Authorization: token ? `Token ${token}` : '', }; + if (apiType === 'translation') { + defaultHeaders['x-api-key'] = translationApiKey; + } + if (method === 'GET') { // Query defaultHeaders['Accept-Language'] = enforceEnglishForQuery ? 'en' : currentLanguage; @@ -239,7 +263,17 @@ const isSuccessfulStatus = (status: number): boolean => status >= 200 && status const isContentTypeExcel = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_EXCEL; -const isContentTypeJson = (res: Response): boolean => res.headers.get('content-type') === CONTENT_TYPE_JSON; +const isContentTypeJson = (res: Response): boolean => { + const contentTypeHeaders = res.headers.get('content-type'); + + if (isNotDefined(contentTypeHeaders)) { + return false; + } + + const mediaTypes = contentTypeHeaders.split('; '); + + return mediaTypes[0]?.toLowerCase() === CONTENT_TYPE_JSON; +}; const isLoginRedirect = (url: string): boolean => new URL(url).pathname.includes('login'); diff --git a/app/src/utils/restRequest/index.ts b/app/src/utils/restRequest/index.ts index ad31e30028..95bd556773 100644 --- a/app/src/utils/restRequest/index.ts +++ b/app/src/utils/restRequest/index.ts @@ -5,7 +5,9 @@ import { } from '@togglecorp/toggle-request'; import type { paths as riskApiPaths } from '#generated/riskTypes'; +// import type { paths as translationApiPaths } from '#generated/translationTypes'; import type { paths as goApiPaths } from '#generated/types'; +import type { paths as translationApiPaths } from '#translationTypes'; import type { ApiBody, @@ -23,19 +25,31 @@ export type GoApiUrlQuery = ApiBody export type RiskApiResponse = ApiResponse; -// type RiskApiUrlQuery< -// URL extends keyof riskApiPaths, -// METHOD extends 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET' -// > = ApiUrlQuery -// type RiskApiBody< -// URL extends keyof riskApiPaths, -// METHOD extends 'POST' | 'PUT' | 'PATCH' -// > = ApiBody export type ListResponseItem } | undefined> = NonNullable['results']>[number]; +/* +const useTranslationRequest = useRequest as < + PATH extends keyof translationApiPaths, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomRequestOptions & { + apiType: 'translation' + } +) => CustomRequestReturn; +*/ + +// FIXME: identify a way to do this without a cast +const useTranslationLazyRequest = useLazyRequest as < + PATH extends keyof translationApiPaths, + CONTEXT = unknown, + METHOD extends VALID_METHOD | undefined = 'GET', +>( + requestOptions: CustomLazyRequestOptions & { apiType: 'translation' } +) => CustomLazyRequestReturn; + // FIXME: identify a way to do this without a cast const useGoRequest = useRequest as < PATH extends keyof goApiPaths, @@ -76,4 +90,6 @@ export { useGoRequest as useRequest, useRiskLazyRequest, useRiskRequest, + useTranslationLazyRequest, + // useTranslationRequest, }; diff --git a/app/src/views/RootLayout/index.tsx b/app/src/views/RootLayout/index.tsx index 2a2ee6b52a..512d047428 100644 --- a/app/src/views/RootLayout/index.tsx +++ b/app/src/views/RootLayout/index.tsx @@ -28,10 +28,9 @@ import { _cs, isDefined, isFalsyString, - listToGroupList, + isNotDefined, listToMap, mapToList, - mapToMap, } from '@togglecorp/fujs'; import GlobalFooter from '#components/GlobalFooter'; @@ -46,8 +45,8 @@ import UserContext from '#contexts/user'; import useAuth from '#hooks/domain/useAuth'; import useDebouncedValue from '#hooks/useDebouncedValue'; import { - useLazyRequest, useRequest, + useTranslationLazyRequest, } from '#utils/restRequest'; import i18n from './i18n.json'; @@ -99,66 +98,57 @@ export function Component() { const { trigger: fetchLanguage, - } = useLazyRequest<'/api/v2/language/{id}/', { pages: Array }>({ - url: '/api/v2/language/{id}/', + } = useTranslationLazyRequest<'/api/Application/{applicationId}/Translation', { page: string }>({ + apiType: 'translation', + url: '/api/Application/{applicationId}/Translation', // FIXME: fix typing in server (medium priority) - query: ({ pages }) => ({ page_name: pages }) as never, - pathVariables: () => ({ id: currentLanguage }), - onSuccess: (response, { pages }) => { - const stringMap = mapToMap( - listToGroupList( - response.strings?.map(({ value, page_name, ...otherArgs }) => { - // NOTE: removing empty translations or translations without pages - if (isFalsyString(value) || isFalsyString(page_name)) { - return undefined; - } - return { - value, - page_name, - ...otherArgs, - }; - }).filter(isDefined), - ({ page_name }) => page_name ?? 'common', - ), - (key) => key, - (values) => ( - listToMap( - values, - ({ key }) => key, - ({ value }) => value, - ) - ), - ); - + query: ({ page }) => ({ + Offset: 0, + Limit: 1000, + KeyPage: page, + LanguageCode: currentLanguage, + }), + pathVariables: () => ({ applicationId: 18 }), + onSuccess: (response, { page }) => { setStrings( - (prevValue) => { - const namespaces = Object.keys(prevValue); - - return { - ...listToMap( - namespaces, - (namespace) => namespace, - (namespace) => ({ - ...prevValue[namespace], - ...stringMap?.[namespace], - }), - ), + (prevStrings) => { + const newStrings = { + ...prevStrings, + [page]: { + ...prevStrings[page], + ...listToMap( + (response.values ?? []).map((item) => { + const { keyName, value } = item; + + if (isNotDefined(keyName) || isNotDefined(value)) { + return undefined; + } + + return { + keyName, + value, + }; + }).filter(isDefined), + ({ keyName }) => keyName, + ({ value }) => value, + ), + }, }; + + return newStrings; }, ); + setLanguageNamespaceStatus( (prevValue) => ({ ...prevValue, - ...listToMap( - pages, - (key) => key, - () => 'fetched', - ), + [page]: 'fetched', }), ); + setLanguagePending(false); }, - onFailure: (err, { pages }) => { + onFailure: (err, { page }) => { // eslint-disable-next-line no-console console.error(err); @@ -166,62 +156,49 @@ export function Component() { setLanguageNamespaceStatus( (prevValue) => ({ ...prevValue, - ...listToMap( - pages, - (key) => key, - () => 'failed', - ), + [page]: 'failed', }), ); setLanguagePending(false); }, }); - const queuedLanguages = useMemo( - () => { - const languages = mapToList( - languageNamespaceStatus, - (item, key) => ({ key, status: item }), - ); - return languages - .filter((item) => item.status === 'queued') - .map((item) => item.key) - .sort() - .join(','); - }, + const queuedNamespaces = useMemo( + () => mapToList( + languageNamespaceStatus, + (item, key) => ({ key, status: item }), + ).filter( + (item) => item.status === 'queued', + ).map((item) => item.key).sort(), [languageNamespaceStatus], ); + const queuedNamespace = queuedNamespaces[0]; + useEffect( () => { if ( languagePending || currentLanguage === 'en' - || isFalsyString(queuedLanguages) + || isFalsyString(queuedNamespace) ) { return undefined; } languageRequestTimeoutRef.current = window.setTimeout( () => { - const keys = queuedLanguages.split(','); - unstable_batchedUpdates(() => { // FIXME: check if the component is still mounted setLanguageNamespaceStatus( (prevState) => ({ ...prevState, - ...listToMap( - keys, - (key) => key, - () => 'pending', - ), + [queuedNamespace]: 'pending', }), ); setLanguagePending(true); }); - fetchLanguage({ pages: keys }); + fetchLanguage({ page: queuedNamespace }); }, // FIXME: use constant 200, @@ -232,7 +209,7 @@ export function Component() { }; }, [ - queuedLanguages, + queuedNamespace, languagePending, currentLanguage, fetchLanguage, diff --git a/app/translationTypes.ts b/app/translationTypes.ts new file mode 100644 index 0000000000..52d328f9da --- /dev/null +++ b/app/translationTypes.ts @@ -0,0 +1,586 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-nocheck +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + + +export interface paths { + "/api/Application": { + get: operations["Application_List"]; + post: operations["Application_Register"]; + }; + "/api/Application/{applicationId}": { + get: operations["Application_Get"]; + put: operations["Application_Update"]; + }; + "/api/Application/{applicationId}/Key": { + get: operations["Key_List"]; + }; + "/api/Application/{applicationId}/Page": { + get: operations["Page_List"]; + }; + "/api/Application/{applicationId}/Translation": { + get: operations["Translation_ListTranslations"]; + post: operations["Translation_CreateTranslation"]; + }; + "/api/Application/{applicationId}/Translation/{translationId}": { + get: operations["Translation_GetTranslation"]; + put: operations["Translation_UpdateTranslation"]; + delete: operations["Translation_DeleteTranslation"]; + }; + "/api/Application/{applicationId}/Translation/checkimport": { + post: operations["Translation_CheckImport"]; + }; + "/api/Application/{applicationId}/Translation/export": { + get: operations["Translation_ExportTranslations"]; + }; + "/api/Application/{applicationId}/Translation/fullappimport": { + post: operations["Translation_FullAppImport"]; + }; + "/api/Application/{applicationId}/Translation/import": { + post: operations["Translation_ImportTranslations"]; + }; + "/api/Application/{applicationId}/TranslationKey": { + get: operations["TranslationKey_List"]; + }; + "/api/Application/{applicationId}/TranslationKey/{translationKeyId}": { + get: operations["TranslationKey_Get"]; + }; + "/api/AutomaticTranslation": { + post: operations["AutomaticTranslation_StartAutomaticTranslation"]; + }; + "/api/Detect": { + post: operations["Home_Detect"]; + }; + "/api/Language": { + get: operations["Language_List"]; + }; + "/api/Translate": { + post: operations["Home_Translate"]; + }; +} + +export type webhooks = Record; + +export interface components { + schemas: { + ApplicationRegistrationViewModel: { + /** Format: int32 */ + applicationId: number; + automaticTranslationEnabled?: boolean; + languageCodes?: string[]; + name: string; + }; + ApplicationUpdateViewModel: { + automaticTranslationEnabled?: boolean; + languageCodes?: string[]; + name: string; + }; + ApplicationViewModel: { + automaticTranslationEnabled?: boolean; + /** Format: int32 */ + id?: number; + languages: components["schemas"]["LanguageViewModel"][]; + name: string; + }; + DetectionRequestModel: { + text?: string; + }; + DetectionResult: components["schemas"]["DetectionResultBase"] & { + alternatives?: components["schemas"]["DetectionResultBase"][]; + }; + DetectionResultBase: { + isTranslationSupported?: boolean; + isTransliterationSupported?: boolean; + language?: string; + /** Format: float */ + score?: number; + }; + LanguageViewModel: { + code?: string; + isActive?: boolean; + localeName?: string | null; + name?: string; + }; + ListResultViewModelOfTranslationKeyViewModel: { + /** Format: int64 */ + count?: number; + page?: components["schemas"]["PagingViewModel"]; + values?: components["schemas"]["TranslationKeyViewModel"][]; + }; + ListResultViewModelOfTranslationViewModel: { + /** Format: int64 */ + count?: number; + page?: components["schemas"]["PagingViewModel"]; + values?: components["schemas"]["TranslationViewModel"][]; + }; + PagingViewModel: { + /** Format: int32 */ + limit?: number; + /** Format: int32 */ + offset?: number; + }; + ProblemDetails: { + detail?: string | null; + instance?: string | null; + /** Format: int32 */ + status?: number | null; + title?: string | null; + type?: string | null; + [key: string]: (Record | null) | undefined; + }; + TranslationConfig: { + allowedAddresses?: string | null; + apikey?: string; + disabled?: boolean; + endPoint?: string; + /** Format: int32 */ + id?: number; + key1?: string; + key2?: string; + region?: string; + }; + TranslationCreateViewModel: { + keyName: string; + languageCode: string; + page: string; + value: string; + }; + TranslationImportAction: { + isSuccess?: boolean; + key?: string; + languageCode?: string | null; + message?: string; + newValue?: string | null; + originalValue?: string | null; + page?: string; + }; + TranslationImportResult: { + isSuccess?: boolean; + message?: string; + translationActions?: components["schemas"]["TranslationImportAction"][]; + }; + TranslationKeyDetailsTranslationViewModel: { + /** Format: int32 */ + id?: number; + isAutomaticTranslation?: boolean; + languageCode?: string; + value?: string; + }; + TranslationKeyDetailsViewModel: { + /** Format: int32 */ + applicationId?: number; + applicationName?: string; + /** Format: int32 */ + id?: number; + name?: string; + page?: string; + values?: components["schemas"]["TranslationKeyDetailsTranslationViewModel"][]; + }; + TranslationKeyViewModel: { + /** Format: int32 */ + applicationId?: number; + /** Format: int32 */ + id?: number; + name?: string; + page?: string; + translatedTo?: string[]; + }; + TranslationRequestModel: { + from?: string | null; + text?: string; + textType?: string | null; + to?: string; + }; + TranslationUpdateViewModel: { + value: string; + }; + TranslationViewModel: { + /** Format: int32 */ + applicationId?: number; + applicationName?: string; + /** Format: int32 */ + id?: number; + isAutomaticTranslation?: boolean; + /** Format: int32 */ + keyId?: number; + keyName?: string; + keyPage?: string; + languageCode?: string; + languageName?: string; + value?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} + +export type $defs = Record; + +export type external = Record; + +export interface operations { + + Application_List: { + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ApplicationViewModel"][]; + }; + }; + }; + }; + Application_Register: { + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["ApplicationRegistrationViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": number; + }; + }; + }; + }; + Application_Get: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ApplicationViewModel"]; + }; + }; + }; + }; + Application_Update: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["ApplicationUpdateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Key_List: { + parameters: { + query?: { + page?: string | null; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": string[]; + }; + }; + }; + }; + Page_List: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": string[]; + }; + }; + }; + }; + Translation_ListTranslations: { + parameters: { + query?: { + KeyName?: string | null; + KeyPage?: string | null; + LanguageCode?: string | null; + TranslationId?: number | null; + Offset?: number; + Limit?: number | null; + Value?: string | null; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ListResultViewModelOfTranslationViewModel"]; + }; + }; + }; + }; + Translation_CreateTranslation: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationCreateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_GetTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationViewModel"]; + }; + }; + 404: { + content: { + "application/json": components<'read'>["schemas"]["ProblemDetails"]; + }; + }; + }; + }; + Translation_UpdateTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationUpdateViewModel"]; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_DeleteTranslation: { + parameters: { + path: { + applicationId: number; + translationId: number; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_CheckImport: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + Translation_ExportTranslations: { + parameters: { + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Translation_FullAppImport: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + Translation_ImportTranslations: { + parameters: { + path: { + applicationId: number; + }; + }; + requestBody?: { + content: { + "multipart/form-data": { + files?: string[] | null; + }; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationImportResult"]; + }; + }; + }; + }; + TranslationKey_List: { + parameters: { + query?: { + Name?: string | null; + Page?: string | null; + Limit?: number | null; + Offset?: number; + }; + path: { + applicationId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["ListResultViewModelOfTranslationKeyViewModel"]; + }; + }; + }; + }; + TranslationKey_Get: { + parameters: { + path: { + applicationId: number; + translationKeyId: number; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationKeyDetailsViewModel"]; + }; + }; + }; + }; + AutomaticTranslation_StartAutomaticTranslation: { + responses: { + 200: { + content: { + "application/octet-stream": string; + }; + }; + }; + }; + Home_Detect: { + parameters: { + query?: { + apiKey?: string; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["DetectionRequestModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["DetectionResult"]; + }; + }; + }; + }; + Language_List: { + parameters: { + query?: { + IsActive?: boolean | null; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["LanguageViewModel"][]; + }; + }; + }; + }; + Home_Translate: { + parameters: { + header?: { + "x-api-key"?: string; + }; + }; + requestBody: { + content: { + "application/json": components<'write'>["schemas"]["TranslationRequestModel"]; + }; + }; + responses: { + 200: { + content: { + "application/json": components<'read'>["schemas"]["TranslationConfig"]; + }; + }; + }; + }; +} diff --git a/app/tsconfig.json b/app/tsconfig.json index 752b896862..b76e64fe2b 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -15,7 +15,8 @@ "#strings/*": ["./src/strings/*"], "#utils/*": ["./src/utils/*"], "#views/*": ["./src/views/*"], - "#routes": ["./src/App/routes"] + "#routes": ["./src/App/routes"], + "#translationTypes": ["./translationTypes.ts"] }, "target": "ESNext", diff --git a/nginx-serve/Dockerfile b/nginx-serve/Dockerfile index 8c095eaaf7..5d924cae04 100644 --- a/nginx-serve/Dockerfile +++ b/nginx-serve/Dockerfile @@ -27,6 +27,8 @@ ENV APP_ENVIRONMENT=APP_ENVIRONMENT_PLACEHOLDER ENV APP_MAPBOX_ACCESS_TOKEN=APP_MAPBOX_ACCESS_TOKEN_PLACEHOLDER ENV APP_TINY_API_KEY=APP_TINY_API_KEY_PLACEHOLDER ENV APP_API_ENDPOINT=https://APP-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_TRANSLATION_API_ENDPOINT=https://APP-TRANSLATION-API-ENDPOINT-PLACEHOLDER.COM/ +ENV APP_TRANSLATION_API_KEY=APP_TRANSLATION_API_KEY_PLACEHOLDER ENV APP_RISK_API_ENDPOINT=https://APP-RISK-API-ENDPOINT-PLACEHOLDER.COM/ ENV APP_SDT_URL=https://APP-SDT-URL-PLACEHOLDER.COM/ ENV APP_SENTRY_DSN=https://APP-SENTRY-DSN-PLACEHOLDER.COM/ diff --git a/nginx-serve/apply-config.sh b/nginx-serve/apply-config.sh index 1361d3390e..b6c010f3ef 100755 --- a/nginx-serve/apply-config.sh +++ b/nginx-serve/apply-config.sh @@ -28,8 +28,10 @@ find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\| find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_ENVIRONMENT|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_MAPBOX_ACCESS_TOKEN|g" {} + find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_TINY_API_KEY|g" {} + +find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\|$APP_TRANSLATION_API_KEY|g" {} + # NOTE: We don't need a word boundary at end as we already have a trailing slash find "$DESTINATION_DIRECTORY" -type f -exec sed -i "s|\