Merged in feat/syncDefaultMessage (pull request #3022)

Sync defaultMessage from lokalise

* Enhance translation sync functionality and tests

- Added logging for found component files during sync.
- Introduced tests for handling complex components with replacements.
- Updated regex in syncIntlFormatMessage to support optional second arguments.
- Removed unused test files.

* feat(syncDefaultMessage): add script for syncing default message with lokalise

* feat(syncDefaultMessage): add script for syncing default message with lokalise


Approved-by: Matilda Landström
This commit is contained in:
Joakim Jäderberg
2025-10-30 08:38:50 +00:00
parent 3962ecd858
commit bf6ed7778e
48 changed files with 316 additions and 197 deletions

View File

@@ -16,7 +16,8 @@
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared/ public/_static/shared"
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared/ public/_static/shared",
"format": "prettier --write ."
},
"dependencies": {
"@formatjs/intl": "^3.1.6",

View File

@@ -1,8 +0,0 @@
module.exports = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
tabWidth: 2,
endOfLine: "lf",
}

View File

@@ -0,0 +1,14 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
tabWidth: 2,
endOfLine: "lf",
}
export default config

View File

@@ -1,27 +1,27 @@
services:
redis-api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3101:3001"
depends_on:
- redis
environment:
- REDIS_CONNECTION=redis:6379
- PRIMARY_API_KEY=
- SECONDARY_API_KEY=
- NODE_ENV=development
- SENTRY_ENABLED=false
redis-api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3101:3001"
depends_on:
- redis
environment:
- REDIS_CONNECTION=redis:6379
- PRIMARY_API_KEY=
- SECONDARY_API_KEY=
- NODE_ENV=development
- SENTRY_ENABLED=false
redis:
image: redis:6
ports:
- "6379:6379"
redis:
image: redis:6
ports:
- "6379:6379"
redisinsight:
image: redis/redisinsight:latest
ports:
- "5540:5540"
depends_on:
- redis
redisinsight:
image: redis/redisinsight:latest
ports:
- "5540:5540"
depends_on:
- redis

View File

@@ -6,7 +6,8 @@
"scripts": {
"dev": "bun --watch src/index.ts | pino-pretty -o '{if module}[{module}] {end}{msg}' -i pid,hostname",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && tsc",
"lint:fix": "eslint . --ext ts,tsx --fix --report-unused-disable-directives --max-warnings 0 && tsc"
"lint:fix": "eslint . --ext ts,tsx --fix --report-unused-disable-directives --max-warnings 0 && tsc",
"format": "prettier --write ."
},
"dependencies": {
"@elysiajs/server-timing": "^1.3.0",
@@ -28,7 +29,6 @@
"eslint": "^9",
"eslint-plugin-simple-import-sort": "^10.0.0",
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
"typescript": "^5.7.2"
},
"prettier": {

View File

@@ -1,4 +1,4 @@
import { Queue,Worker } from "bullmq";
import { Queue, Worker } from "bullmq";
import z from "zod";
import { env } from "@/env";
@@ -6,7 +6,7 @@ import { sentry } from "@/server/sentry.server.config";
import { loggerModule } from "@/utils/logger";
import { timeout } from "@/utils/timeout";
import { bullmqredis,redis } from ".";
import { bullmqredis, redis } from ".";
const DELETE_JOB = "deleteQueueJob";
const deleteQueueLogger = loggerModule("deleteQueue");

View File

@@ -8,35 +8,39 @@
*/
const maskChar = "*";
export function mask(
value: string,
options?: { visibleStart?: number; visibleEnd?: number; maxLength?: number },
value: string,
options?: {
visibleStart?: number;
visibleEnd?: number;
maxLength?: number;
},
): string {
if (!value) return "";
if (!value) return "";
const { visibleStart = 2, visibleEnd = 2, maxLength = 10 } = options ?? {};
const { visibleStart = 2, visibleEnd = 2, maxLength = 10 } = options ?? {};
if (isEmail(value)) {
return maskEmail(value);
}
if (isEmail(value)) {
return maskEmail(value);
}
const totalVisible = visibleStart + visibleEnd;
if (value.length <= totalVisible) {
return maskChar.repeat(value.length);
}
const totalVisible = visibleStart + visibleEnd;
if (value.length <= totalVisible) {
return maskChar.repeat(value.length);
}
const start = value.slice(0, visibleStart);
const middle = value.slice(visibleStart, -visibleEnd || undefined);
const end = visibleEnd ? value.slice(-visibleEnd) : "";
const start = value.slice(0, visibleStart);
const middle = value.slice(visibleStart, -visibleEnd || undefined);
const end = visibleEnd ? value.slice(-visibleEnd) : "";
const maskedLength = Math.min(middle.length, maxLength);
return start + maskChar.repeat(maskedLength) + end;
const maskedLength = Math.min(middle.length, maxLength);
return start + maskChar.repeat(maskedLength) + end;
}
function maskEmail(email: string): string {
const [local, domain] = email.split("@");
if (!domain || !local) return mask(email);
const [subDomain, tld] = domain.split(/\.(?=[^.]+$)/);
return `${mask(local)}@${mask(subDomain ?? "")}.${tld}`;
const [local, domain] = email.split("@");
if (!domain || !local) return mask(email);
const [subDomain, tld] = domain.split(/\.(?=[^.]+$)/);
return `${mask(local)}@${mask(subDomain ?? "")}.${tld}`;
}
const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);

View File

@@ -1,8 +1,8 @@
{
"extends": "@scandic-hotels/typescript-config/bun.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
"extends": "@scandic-hotels/typescript-config/bun.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
}

View File

@@ -0,0 +1 @@
netlify/functions/data/*.json

View File

@@ -16,10 +16,10 @@ https://scandichotelsab.sharepoint.com/:x:/s/921-ContentNewweb/ETGStOQAARtJhJXG9
- Open it
- Each domain/language has its own sheet
- Export each sheet into their respective language code
- File > Export > Download as CSV UTF-8
- Save as [lang].csv in `./scripts/data/csv` folder
- File > Export > Download as CSV UTF-8
- Save as [lang].csv in `./scripts/data/csv` folder
- Run the `generate` script target
- E.g. `yarn workspace @scandic-hotels/scandic-redirect generate`
- E.g. `yarn workspace @scandic-hotels/scandic-redirect generate`
- Commit and push the JSON files in `./netlify/functions/data`.
- Create a PR
- Profit!

View File

@@ -1,12 +1,12 @@
import { createReadStream } from "fs";
import { join } from "path";
import { createReadStream } from "fs"
import { join } from "path"
export default async (req: Request) => {
try {
const body = await req.json();
const body = await req.json()
if (body.lang && body.pathname) {
const filePath = join(import.meta.dirname, `./data/${body.lang}.json`);
const filePath = join(import.meta.dirname, `./data/${body.lang}.json`)
const redirectUrl = await new Promise<string | null>(
(resolve, reject) => {
@@ -14,58 +14,58 @@ export default async (req: Request) => {
emitClose: false,
encoding: "utf-8",
highWaterMark: 1024,
});
const data: string[] = [];
})
const data: string[] = []
stream.on("data", (chunk) => {
if (data.length === 3) {
data.shift();
data.shift()
}
data.push(chunk.toString());
data.push(chunk.toString())
// Since we strip trailing slash (in the trailingSlash middleware) before entering this middleware,
// we need check matching paths both including and excluding trailing slash.
const re = new RegExp(`"${body.pathname}\/?":"([^"]+)"`);
const re = new RegExp(`"${body.pathname}\/?":"([^"]+)"`)
const match = data.join("").match(re);
const match = data.join("").match(re)
if (match?.[1]) {
stream.destroy();
resolve(match[1]);
stream.destroy()
resolve(match[1])
}
});
})
stream.on("error", (err) => {
console.error("Stream error:", err);
stream.destroy();
reject(err);
});
console.error("Stream error:", err)
stream.destroy()
reject(err)
})
stream.on("end", () => {
stream.destroy();
resolve(null); // No match found
});
stream.destroy()
resolve(null) // No match found
})
}
);
)
if (redirectUrl) {
// Make sure to exclude trailing slash in the redirectUrl to avoid an extra middleware roundtrip
const redirectUrlWithoutTrailingSlash = redirectUrl.endsWith("/")
? redirectUrl.slice(0, -1)
: redirectUrl;
: redirectUrl
if (redirectUrlWithoutTrailingSlash === body.pathname) {
console.log(
`[scandic-redirect] recieved ${body.pathname}, found ${redirectUrlWithoutTrailingSlash}, no-op`
);
return new Response("Not Found", { status: 404 });
)
return new Response("Not Found", { status: 404 })
}
console.log(
`[scandic-redirect] recieved ${body.pathname}, return ${redirectUrlWithoutTrailingSlash}, success`
);
return new Response(redirectUrlWithoutTrailingSlash);
)
return new Response(redirectUrlWithoutTrailingSlash)
}
}
console.log(`[scandic-redirect] recieved ${body.pathname}, not found`);
return new Response("Not Found", { status: 404 });
console.log(`[scandic-redirect] recieved ${body.pathname}, not found`)
return new Response("Not Found", { status: 404 })
} catch (error) {
return new Response("Bad request", { status: 400 });
return new Response("Bad request", { status: 400 })
}
};
}

View File

@@ -1,27 +1,28 @@
{
"name": "@scandic-hotels/scandic-redirect",
"version": "0.1.0",
"private": true,
"packageManager": "yarn@4.6.0",
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"generate": "jiti ./scripts/generateRedirectFile/index.ts"
},
"dependencies": {
"@netlify/functions": "^3.0.0"
},
"devDependencies": {
"convert-csv-to-json": "^3.4.0",
"jiti": "^2.6.1",
"vitest": "^3.2.4"
},
"prettier": {
"semi": false,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"endOfLine": "lf"
}
"name": "@scandic-hotels/scandic-redirect",
"version": "0.1.0",
"private": true,
"packageManager": "yarn@4.6.0",
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"generate": "jiti ./scripts/generateRedirectFile/index.ts",
"format": "prettier --write ."
},
"dependencies": {
"@netlify/functions": "^3.0.0"
},
"devDependencies": {
"convert-csv-to-json": "^3.4.0",
"jiti": "^2.6.1",
"vitest": "^3.2.4"
},
"prettier": {
"semi": false,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"endOfLine": "lf"
}
}

View File

@@ -2,11 +2,14 @@ import styles from "./page.module.css"
import type { LangParams, LayoutArgs, StatusParams } from "@/types/params"
export default async function MiddlewareError(props: LayoutArgs<LangParams & StatusParams>) {
const params = await props.params;
export default async function MiddlewareError(
props: LayoutArgs<LangParams & StatusParams>
) {
const params = await props.params
return (
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
<div className={styles.layout}>Middleware error {params.lang} {params.status}
<div className={styles.layout}>
Middleware error {params.lang} {params.status}
</div>
);
)
}

View File

@@ -6,8 +6,10 @@ import { Receipt } from "@/components/HotelReservation/MyStay/Receipt"
import type { LangParams, PageArgs } from "@/types/params"
export default async function ReceiptPage(props: PageArgs<LangParams, { RefId?: string }>) {
const searchParams = await props.searchParams;
export default async function ReceiptPage(
props: PageArgs<LangParams, { RefId?: string }>
) {
const searchParams = await props.searchParams
if (!searchParams.RefId) {
notFound()
}

View File

@@ -54,7 +54,7 @@ export default async function CampaignHotelListing({
{
label: intl.formatMessage({
id: "common.tripAdvisorRating",
defaultMessage: "TripAdvisor rating",
defaultMessage: "Tripadvisor rating",
}),
value: HotelSortOption.TripAdvisorRating,
},

View File

@@ -37,7 +37,7 @@ export default function Row({ transaction }: RowProps) {
transaction.nights === 0
? intl.formatMessage({
id: "earnAndBurn.journeyTable.pointsActivity",
defaultMessage: "Points activity",
defaultMessage: "Point activity",
})
: transaction.hotelName && transaction.city
? `${transaction.hotelName}, ${transaction.city} ${nightsMsg}`

View File

@@ -85,7 +85,7 @@ export default async function DestinationCityPage({
{
label: intl.formatMessage({
id: "common.tripAdvisorRating",
defaultMessage: "TripAdvisor rating",
defaultMessage: "Tripadvisor rating",
}),
value: HotelSortOption.TripAdvisorRating,
},

View File

@@ -235,7 +235,7 @@ export default function GuestDetails({
<span>
{intl.formatMessage({
id: "myStay.guestDetails.modifyGuestDetails",
defaultMessage: "Modify guest details",
defaultMessage: "Edit guest details",
})}
</span>
</Typography>
@@ -265,7 +265,7 @@ export default function GuestDetails({
<span>
{intl.formatMessage({
id: "myStay.guestDetails.modifyGuestDetails",
defaultMessage: "Modify guest details",
defaultMessage: "Edit guest details",
})}
</span>
</Typography>
@@ -280,7 +280,7 @@ export default function GuestDetails({
<Dialog
aria-label={intl.formatMessage({
id: "myStay.guestDetails.modifyGuestDetails",
defaultMessage: "Modify guest details",
defaultMessage: "Edit guest details",
})}
>
{({ close }) => (
@@ -288,7 +288,7 @@ export default function GuestDetails({
<ModalContentWithActions
title={intl.formatMessage({
id: "myStay.guestDetails.modifyGuestDetails",
defaultMessage: "Modify guest details",
defaultMessage: "Edit guest details",
})}
onClose={() => setIsModifyGuestDetailsOpen(false)}
content={

View File

@@ -120,7 +120,7 @@ export default function Form() {
{
id: "myStay.gla.termsAndConditionsMessage",
defaultMessage:
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>. ",
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
},
{
termsAndConditionsLink: (str) => (

View File

@@ -301,7 +301,7 @@ export default function Room({ booking, roomNr, user }: RoomProps) {
<p>
{intl.formatMessage({
id: "myStay.modifyBy",
defaultMessage: "Modify By",
defaultMessage: "Modify by",
})}
</p>
</Typography>

View File

@@ -36,7 +36,7 @@ export default async function LevelProgressCard({
<h2 id="level-progress-card-title" className={styles.title}>
{intl.formatMessage({
id: "myPages.yourLevelProgress",
defaultMessage: "Your Level Progress",
defaultMessage: "Your level progress",
})}
</h2>
</Typography>

View File

@@ -293,7 +293,7 @@ export default function BookedRoomSidePeekContent({
<p>
{intl.formatMessage({
id: "myStay.modifyBy",
defaultMessage: "Modify By",
defaultMessage: "Modify by",
})}
</p>
</Typography>

View File

@@ -85,7 +85,7 @@ export function translateSeatingType(type: string, intl: IntlShape) {
case SeatingType.Theatre:
return intl.formatMessage({
id: "meetingRoomCard.theatre",
defaultMessage: "Theatre",
defaultMessage: "Theater",
})
case SeatingType.UShape:
return intl.formatMessage({

View File

@@ -20,7 +20,8 @@
"ci:build": "yarn lint && yarn test && yarn build",
"clean": "rm -rf .next",
"check-types": "tsc --noEmit",
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared public/_static/shared"
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared public/_static/shared",
"format": "prettier --write ."
},
"dependencies": {
"@contentstack/live-preview-utils": "^3.2.1",
@@ -127,7 +128,6 @@
"json-sort-cli": "^4.0.9",
"lint-staged": "^15.5.2",
"netlify-plugin-cypress": "^2.2.1",
"prettier": "^3.5.3",
"schema-dts": "^1.1.5",
"start-server-and-test": "^2.0.11",
"ts-morph": "^25.0.1",

View File

@@ -1,8 +0,0 @@
module.exports = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
tabWidth: 2,
endOfLine: "lf",
}

View File

@@ -0,0 +1,14 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
tabWidth: 2,
endOfLine: "lf",
}
export default config