Skip to content

Commit a90895e

Browse files
[Website] locale-segment routing and shared Lingui factory (#20079)
**1. Shared Lingui factory in `twenty-shared`** - Extracted `createI18nInstanceFactory` into `packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts` so every package gets the same per-render Lingui bootstrap with a per-locale singleton cache and a `SOURCE_LOCALE` fallback. - `twenty-emails/src/utils/i18n.utils.ts` now consumes the shared factory. **2. `twenty-website-new` Lingui bootstrap + Crowdin wiring** - `lingui.config.ts`, `src/lib/i18n/*`, `nx run twenty-website-new:lingui:{extract,compile}`. - 31 locale PO files generated; minified compiled output kept out of Prettier and Oxlint. - `i18n-{push,pull}.yaml` workflows updated to include `twenty-website-new` in Crowdin sync. **3. `app/[locale]/...` segment routing with English at the root** - All marketing routes moved under `src/app/[locale]/`; static generation preserved (15 routes × 31 locales = 465 prerendered URLs). - Middleware behavior: - `/{en}/...` → 301 redirect to unprefixed canonical. - `/{non-en}/...` → pass through, set `NEXT_LOCALE` cookie. ### What this PR explicitly does not do (deferred) - Lingui-wrapping the actual marketing copy. Keys, build pipeline, and runtime are wired; copy migration is a separate, reviewer-friendlier PR.
1 parent 9b7f7d0 commit a90895e

177 files changed

Lines changed: 1295 additions & 238 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/i18n-pull.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
npx nx run twenty-server:lingui:compile --strict
5959
npx nx run twenty-emails:lingui:compile --strict
6060
npx nx run twenty-front:lingui:compile --strict
61+
npx nx run twenty-website-new:lingui:compile --strict
6162
continue-on-error: true
6263

6364
- name: Stash any changes before pulling translations
@@ -115,6 +116,7 @@ jobs:
115116
npx nx run twenty-server:lingui:compile
116117
npx nx run twenty-emails:lingui:compile
117118
npx nx run twenty-front:lingui:compile
119+
npx nx run twenty-website-new:lingui:compile
118120
git status
119121
git add .
120122
if ! git diff --staged --quiet --exit-code; then

.github/workflows/i18n-push.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
npx nx run twenty-server:lingui:extract
4242
npx nx run twenty-emails:lingui:extract
4343
npx nx run twenty-front:lingui:extract
44+
npx nx run twenty-website-new:lingui:extract
4445
4546
- name: Check and commit extracted files
4647
id: check_extract_changes
@@ -60,6 +61,7 @@ jobs:
6061
npx nx run twenty-server:lingui:compile
6162
npx nx run twenty-emails:lingui:compile
6263
npx nx run twenty-front:lingui:compile
64+
npx nx run twenty-website-new:lingui:compile
6365
6466
- name: Check and commit compiled files
6567
id: check_compile_changes

packages/twenty-emails/src/utils/i18n.utils.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { setupI18n, type I18n, type Messages } from '@lingui/core';
1+
import { type Messages } from '@lingui/core';
2+
import { createI18nInstanceFactory } from 'twenty-shared/i18n';
23
import { type APP_LOCALES } from 'twenty-shared/translations';
3-
import { isDefined } from 'twenty-shared/utils';
44
import { messages as afMessages } from '@/locales/generated/af-ZA';
55
import { messages as arMessages } from '@/locales/generated/ar-SA';
66
import { messages as caMessages } from '@/locales/generated/ca-ES';
@@ -67,20 +67,4 @@ const messages: Record<keyof typeof APP_LOCALES, Messages> = {
6767
'zh-TW': zhHantMessages,
6868
};
6969

70-
const i18nInstancesMap: Partial<Record<keyof typeof APP_LOCALES, I18n>> = {};
71-
72-
export const createI18nInstance = (locale: keyof typeof APP_LOCALES): I18n => {
73-
if (isDefined(i18nInstancesMap[locale])) {
74-
return i18nInstancesMap[locale];
75-
}
76-
77-
const i18nInstance = setupI18n();
78-
const localeMessages = messages[locale] ?? messages.en;
79-
80-
i18nInstance.load(locale, localeMessages);
81-
i18nInstance.activate(locale);
82-
83-
i18nInstancesMap[locale] = i18nInstance;
84-
85-
return i18nInstance;
86-
};
70+
export const createI18nInstance = createI18nInstanceFactory(messages);

packages/twenty-shared/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
"import": "./dist/database-events.mjs",
6767
"require": "./dist/database-events.cjs"
6868
},
69+
"./i18n": {
70+
"types": "./dist/i18n/index.d.ts",
71+
"import": "./dist/i18n.mjs",
72+
"require": "./dist/i18n.cjs"
73+
},
6974
"./logic-function": {
7075
"types": "./dist/logic-function/index.d.ts",
7176
"import": "./dist/logic-function.mjs",
@@ -118,6 +123,7 @@
118123
"application",
119124
"constants",
120125
"database-events",
126+
"i18n",
121127
"logic-function",
122128
"metadata",
123129
"testing",
@@ -142,6 +148,9 @@
142148
"database-events": [
143149
"dist/database-events/index.d.ts"
144150
],
151+
"i18n": [
152+
"dist/i18n/index.d.ts"
153+
],
145154
"logic-function": [
146155
"dist/logic-function/index.d.ts"
147156
],

packages/twenty-shared/project.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"{projectRoot}/constants/dist",
2121
"{projectRoot}/database-events/package.json",
2222
"{projectRoot}/database-events/dist",
23+
"{projectRoot}/i18n/package.json",
24+
"{projectRoot}/i18n/dist",
2325
"{projectRoot}/logic-function/package.json",
2426
"{projectRoot}/logic-function/dist",
2527
"{projectRoot}/metadata/package.json",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { setupI18n, type I18n, type Messages } from '@lingui/core';
2+
3+
import { type AppLocale } from '@/translations/constants/AppLocales';
4+
import { SOURCE_LOCALE } from '@/translations/constants/SourceLocale';
5+
6+
export type LocaleMessagesMap = Partial<Record<AppLocale, Messages>>;
7+
8+
export const createI18nInstanceFactory = (
9+
messagesByLocale: LocaleMessagesMap,
10+
) => {
11+
const cache: Partial<Record<AppLocale, I18n>> = {};
12+
13+
return (locale: AppLocale): I18n => {
14+
const cached = cache[locale];
15+
if (cached !== undefined) {
16+
return cached;
17+
}
18+
19+
const fallbackMessages = messagesByLocale[SOURCE_LOCALE] ?? {};
20+
const localeMessages = messagesByLocale[locale] ?? fallbackMessages;
21+
22+
const i18n = setupI18n();
23+
i18n.load(locale, localeMessages);
24+
i18n.activate(locale);
25+
26+
cache[locale] = i18n;
27+
return i18n;
28+
};
29+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* _____ _
3+
*|_ _|_ _____ _ __ | |_ _ _
4+
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
5+
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
6+
* |_| \_/\_/ \___|_| |_|\__|\__, |
7+
* |___/
8+
*/
9+
10+
export type { LocaleMessagesMap } from './create-i18n-instance-factory';
11+
export { createI18nInstanceFactory } from './create-i18n-instance-factory';

packages/twenty-website-new/.oxlintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"categories": {
55
"correctness": "error"
66
},
7-
"ignorePatterns": ["node_modules"],
7+
"ignorePatterns": ["node_modules", "src/locales/generated"],
88
"rules": {
99
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
1010
"no-console": "off",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/locales/generated
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { defineConfig } from '@lingui/conf';
2+
import { formatter } from '@lingui/format-po';
3+
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
4+
5+
export default defineConfig({
6+
sourceLocale: SOURCE_LOCALE,
7+
locales: Object.values(APP_LOCALES),
8+
pseudoLocale: 'pseudo-en',
9+
fallbackLocales: {
10+
'pseudo-en': 'en',
11+
default: SOURCE_LOCALE,
12+
},
13+
catalogs: [
14+
{
15+
path: '<rootDir>/src/locales/{locale}',
16+
include: ['src'],
17+
},
18+
],
19+
catalogsMergePath: '<rootDir>/src/locales/generated/{locale}',
20+
compileNamespace: 'ts',
21+
format: formatter({ lineNumbers: false, printLinguiId: true }),
22+
});

0 commit comments

Comments
 (0)