mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-07-14 18:15:56 -04:00
refactor(i18n): replace compiled .ts catalogs with direct .po imports
- Remove `lingui compile` / `i18n:compile` script — Vite and Metro LingUI plugins now compile `.po` files on the fly at dev/build time; no manual compile step needed
- Import `.po` files directly in `@sofa/i18n` (`./po/en.po` etc.) and add `po.d.ts` ambient module declaration so TypeScript accepts them
- Add `fallbackLocales: { default: "en" }` and `lineNumbers: false` to `lingui.config.ts`; prefix catalog paths with `<rootDir>` for correctness
- Derive `SupportedLocale` and `SUPPORTED_LOCALES` from `LOCALE_INFO` in `locales.ts` to eliminate the circular import between `index.ts` and `locales.ts`
- Update Crowdin config to use `project_id_env` instead of a hardcoded ID, drop `languages_mapping` (no longer needed), and add `preserve_hierarchy: true`
- Add a daily cron trigger (`0 6 * * *`) to the Crowdin GitHub Actions workflow and drop the now-redundant compile step from it
- Update docs and `AGENTS.md` to reflect that no compile step is required after `i18n:extract`
This commit is contained in:
@@ -6,6 +6,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "packages/i18n/src/po/en.po"
|
- "packages/i18n/src/po/en.po"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * *"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -56,13 +58,10 @@ jobs:
|
|||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Compile catalogs
|
- name: Commit updated catalogs
|
||||||
run: bun run i18n:compile
|
|
||||||
|
|
||||||
- name: Commit compiled catalogs
|
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git add packages/i18n/src/po/
|
git add packages/i18n/src/po/
|
||||||
git diff --cached --quiet || git commit -m "chore(i18n): compile updated translation catalogs"
|
git diff --cached --quiet || git commit -m "chore(i18n): update translation catalogs"
|
||||||
git push
|
git push
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ bun run check-types # TypeScript type check
|
|||||||
bun run test # Run tests
|
bun run test # Run tests
|
||||||
bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes)
|
bun run generate:openapi # Regenerate OpenAPI spec + docs API pages (run after contract/schema changes)
|
||||||
bun run i18n:extract # Run LingUI's string extraction
|
bun run i18n:extract # Run LingUI's string extraction
|
||||||
bun run i18n:compile # Run LingUI's typescript compilation
|
|
||||||
bun run i18n:claude # Prompt Claude Code to fill in untranslated strings
|
bun run i18n:claude # Prompt Claude Code to fill in untranslated strings
|
||||||
|
|
||||||
# Database commands (run from packages/db/)
|
# Database commands (run from packages/db/)
|
||||||
@@ -125,7 +124,7 @@ Cross-package imports:
|
|||||||
- **Date/number formatting** → use `formatDate`, `formatRelativeTime`, `formatNumber`, `formatBytes` from `@sofa/i18n/format` (Intl-based, locale-aware). Never use `date-fns` in app code.
|
- **Date/number formatting** → use `formatDate`, `formatRelativeTime`, `formatNumber`, `formatBytes` from `@sofa/i18n/format` (Intl-based, locale-aware). Never use `date-fns` in app code.
|
||||||
- **Native Intl polyfills** → `@formatjs/intl-*` polyfills loaded in `apps/native/src/lib/intl-polyfills.ts` (strict dependency order, with locale data for all 6 languages).
|
- **Native Intl polyfills** → `@formatjs/intl-*` polyfills loaded in `apps/native/src/lib/intl-polyfills.ts` (strict dependency order, with locale data for all 6 languages).
|
||||||
- **Error messages** → Server throws `ORPCError` with `data: { code: AppErrorCode.XXX }`. Clients map codes to localized strings via per-app `error-messages.ts`. Never display `error.message` to users.
|
- **Error messages** → Server throws `ORPCError` with `data: { code: AppErrorCode.XXX }`. Clients map codes to localized strings via per-app `error-messages.ts`. Never display `error.message` to users.
|
||||||
- After adding/changing strings, run `bun run i18n:extract` then `bun run i18n:compile`.
|
- After adding/changing strings, run `bun run i18n:extract`. The Vite and Metro plugins compile `.po` catalogs on the fly — no manual compile step needed.
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -1,10 +1,7 @@
|
|||||||
project_id: "881052"
|
project_id_env: CROWDIN_PROJECT_ID
|
||||||
api_token_env: CROWDIN_PERSONAL_TOKEN
|
api_token_env: CROWDIN_PERSONAL_TOKEN
|
||||||
|
preserve_hierarchy: true
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- source: /packages/i18n/src/po/en.po
|
- source: /packages/i18n/src/po/en.po
|
||||||
translation: /packages/i18n/src/po/%two_letters_code%.po
|
translation: /packages/i18n/src/po/%two_letters_code%.po
|
||||||
languages_mapping:
|
|
||||||
two_letters_code:
|
|
||||||
es-ES: es
|
|
||||||
pt-PT: pt
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ The translation pipeline:
|
|||||||
1. React (web) and React Native (mobile) apps wrap UI strings in `<Trans>` components or `` msg`...` `` macros
|
1. React (web) and React Native (mobile) apps wrap UI strings in `<Trans>` components or `` msg`...` `` macros
|
||||||
2. `bun run i18n:extract` scans the codebase and updates `en.po` with new strings
|
2. `bun run i18n:extract` scans the codebase and updates `en.po` with new strings
|
||||||
3. Translations are added to each locale's `.po` file (e.g. `fr.po`, `de.po`)
|
3. Translations are added to each locale's `.po` file (e.g. `fr.po`, `de.po`)
|
||||||
4. `bun run i18n:compile` compiles `.po` files into TypeScript for use at runtime
|
4. The Vite and Metro plugins compile `.po` files on the fly at dev/build time
|
||||||
5. Non-English locales are lazy-loaded on demand
|
5. Non-English locales are lazy-loaded on demand
|
||||||
|
|
||||||
## Contributing via Crowdin
|
## Contributing via Crowdin
|
||||||
@@ -51,7 +51,7 @@ bun packages/i18n/scripts/claude.ts all --dry-run
|
|||||||
bun packages/i18n/scripts/claude.ts all --model opus
|
bun packages/i18n/scripts/claude.ts all --model opus
|
||||||
```
|
```
|
||||||
|
|
||||||
The script only translates strings with empty `msgstr` — existing translations are never overwritten. It reads the locale list from [`lingui.config.ts`](https://github.com/jakejarvis/sofa/blob/main/lingui.config.ts) automatically and compiles catalogs after translating.
|
The script only translates strings with empty `msgstr` — existing translations are never overwritten. It reads the locale list from [`lingui.config.ts`](https://github.com/jakejarvis/sofa/blob/main/lingui.config.ts) automatically.
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Using this script really only makes sense if you have a [Claude Max
|
Using this script really only makes sense if you have a [Claude Max
|
||||||
@@ -64,7 +64,6 @@ The script only translates strings with empty `msgstr` — existing translations
|
|||||||
2. Run `bun run i18n:extract` — this creates a new `.po` file for the locale
|
2. Run `bun run i18n:extract` — this creates a new `.po` file for the locale
|
||||||
3. Translate the strings (via Crowdin, manually, or with `bun packages/i18n/scripts/claude.ts <locale>`)
|
3. Translate the strings (via Crowdin, manually, or with `bun packages/i18n/scripts/claude.ts <locale>`)
|
||||||
4. Add the locale to the `LOCALE_INFO` array in [`packages/i18n/src/locales.ts`](https://github.com/jakejarvis/sofa/blob/main/packages/i18n/src/locales.ts) with its native name
|
4. Add the locale to the `LOCALE_INFO` array in [`packages/i18n/src/locales.ts`](https://github.com/jakejarvis/sofa/blob/main/packages/i18n/src/locales.ts) with its native name
|
||||||
5. Run `bun run i18n:compile`
|
|
||||||
|
|
||||||
## Translation guidelines
|
## Translation guidelines
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -6,10 +6,14 @@ export default defineConfig({
|
|||||||
sourceLocale: "en",
|
sourceLocale: "en",
|
||||||
catalogs: [
|
catalogs: [
|
||||||
{
|
{
|
||||||
path: "packages/i18n/src/po/{locale}",
|
path: "<rootDir>/packages/i18n/src/po/{locale}",
|
||||||
include: ["apps/web/src", "apps/native/src"],
|
include: ["<rootDir>/apps/web/src", "<rootDir>/apps/native/src"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
format: formatter(),
|
format: formatter({
|
||||||
compileNamespace: "ts",
|
lineNumbers: false,
|
||||||
|
}),
|
||||||
|
fallbackLocales: {
|
||||||
|
default: "en",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
|
"generate:openapi": "bun scripts/generate-openapi-spec.ts && cd docs && bun run generate:api-docs",
|
||||||
"i18n:extract": "lingui extract",
|
"i18n:extract": "lingui extract",
|
||||||
"i18n:compile": "lingui compile --typescript",
|
|
||||||
"i18n:claude": "bun packages/i18n/scripts/claude.ts"
|
"i18n:claude": "bun packages/i18n/scripts/claude.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -530,16 +530,6 @@ if (!dryRun) {
|
|||||||
console.error("Extraction failed");
|
console.error("Extraction failed");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Compiling catalogs...");
|
|
||||||
const compile = Bun.spawn(["bun", "run", "i18n:compile"], {
|
|
||||||
stdout: "inherit",
|
|
||||||
stderr: "inherit",
|
|
||||||
});
|
|
||||||
if ((await compile.exited) !== 0) {
|
|
||||||
console.error("Catalog compilation failed");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Done!");
|
console.log("Done!");
|
||||||
|
|||||||
+10
-15
@@ -1,20 +1,20 @@
|
|||||||
import { i18n, type Messages } from "@lingui/core";
|
import { i18n, type Messages } from "@lingui/core";
|
||||||
|
|
||||||
import { messages as enMessages } from "./po/en";
|
import type { SupportedLocale } from "./locales";
|
||||||
|
import { messages as enMessages } from "./po/en.po";
|
||||||
|
|
||||||
export const SUPPORTED_LOCALES = ["en", "fr", "de", "es", "it", "pt"] as const;
|
export { SUPPORTED_LOCALES, type SupportedLocale } from "./locales";
|
||||||
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
|
||||||
|
|
||||||
// English always bundled — zero async on default locale
|
// English always bundled — zero async on default locale
|
||||||
i18n.load("en", enMessages);
|
i18n.load("en", enMessages);
|
||||||
i18n.activate("en");
|
i18n.activate("en");
|
||||||
|
|
||||||
const loaders: Record<string, () => Promise<{ messages: Messages }>> = {
|
const loaders: Record<Exclude<SupportedLocale, "en">, () => Promise<{ messages: Messages }>> = {
|
||||||
fr: () => import("./po/fr"),
|
fr: () => import("./po/fr.po"),
|
||||||
de: () => import("./po/de"),
|
de: () => import("./po/de.po"),
|
||||||
es: () => import("./po/es"),
|
es: () => import("./po/es.po"),
|
||||||
it: () => import("./po/it"),
|
it: () => import("./po/it.po"),
|
||||||
pt: () => import("./po/pt"),
|
pt: () => import("./po/pt.po"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function activateLocale(locale: SupportedLocale): Promise<void> {
|
export async function activateLocale(locale: SupportedLocale): Promise<void> {
|
||||||
@@ -22,12 +22,7 @@ export async function activateLocale(locale: SupportedLocale): Promise<void> {
|
|||||||
i18n.activate("en");
|
i18n.activate("en");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const loader = loaders[locale];
|
const { messages } = await loaders[locale]();
|
||||||
if (!loader) {
|
|
||||||
i18n.activate("en");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { messages } = await loader();
|
|
||||||
i18n.load(locale, messages);
|
i18n.load(locale, messages);
|
||||||
i18n.activate(locale);
|
i18n.activate(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import type { SupportedLocale } from "./index";
|
|
||||||
|
|
||||||
export interface LocaleInfo {
|
export interface LocaleInfo {
|
||||||
code: SupportedLocale;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
nativeName: string;
|
nativeName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LOCALE_INFO: LocaleInfo[] = [
|
export const LOCALE_INFO = [
|
||||||
{ code: "en", name: "English", nativeName: "English" },
|
{ code: "en", name: "English", nativeName: "English" },
|
||||||
{ code: "fr", name: "French", nativeName: "Fran\u00e7ais" },
|
{ code: "fr", name: "French", nativeName: "Fran\u00e7ais" },
|
||||||
{ code: "de", name: "German", nativeName: "Deutsch" },
|
{ code: "de", name: "German", nativeName: "Deutsch" },
|
||||||
{ code: "es", name: "Spanish", nativeName: "Espa\u00f1ol" },
|
{ code: "es", name: "Spanish", nativeName: "Espa\u00f1ol" },
|
||||||
{ code: "it", name: "Italian", nativeName: "Italiano" },
|
{ code: "it", name: "Italian", nativeName: "Italiano" },
|
||||||
{ code: "pt", name: "Portuguese", nativeName: "Portugu\u00eas" },
|
{ code: "pt", name: "Portuguese", nativeName: "Portugu\u00eas" },
|
||||||
];
|
] as const satisfies readonly LocaleInfo[];
|
||||||
|
|
||||||
|
export type SupportedLocale = (typeof LOCALE_INFO)[number]["code"];
|
||||||
|
|
||||||
|
export const SUPPORTED_LOCALES = LOCALE_INFO.map((l) => l.code) as SupportedLocale[];
|
||||||
|
|||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.po" {
|
||||||
|
import type { Messages } from "@lingui/core";
|
||||||
|
export const messages: Messages;
|
||||||
|
}
|
||||||
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+817
-817
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@ import { i18n } from "@lingui/core";
|
|||||||
import { I18nProvider } from "@lingui/react";
|
import { I18nProvider } from "@lingui/react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { messages } from "./po/en";
|
import { messages } from "./po/en.po";
|
||||||
|
|
||||||
i18n.load("en", messages);
|
i18n.load("en", messages);
|
||||||
i18n.activate("en");
|
i18n.activate("en");
|
||||||
|
|||||||
Reference in New Issue
Block a user