i18n: scaffold panel.* and ui.* locale catalogs, wire panelCatalogs into officerdev's barrel

This commit is contained in:
2026-08-23 03:42:53 +00:00
parent dcd11a3b17
commit 6ac9187c52
14 changed files with 76 additions and 2 deletions
+2
View File
@@ -210,6 +210,7 @@
"dotenv": "^16.4.7",
"embla-carousel-react": "^8.5.1",
"hooks": "*",
"i18n": "*",
"input-otp": "^1.4.1",
"lucide-react": "^0.468.0",
"next-themes": "^0.4.4",
@@ -271,6 +272,7 @@
"version": "0.0.1",
"dependencies": {
"hooks": "workspace:*",
"i18n": "workspace:*",
"state": "workspace:*",
"widgets": "workspace:*",
},
@@ -0,0 +1 @@
{}
@@ -0,0 +1,8 @@
import type { TranslationMap } from 'i18n';
import enGB from './en-GB.json';
import ptPT from './pt-PT.json';
/** Shared components' own catalog, per locale — merged into the app-wide map by `I18nBridge` under `ui.*`. */
export const uiCatalogs: TranslationMap = { 'en-GB': enGB, 'pt-PT': ptPT };
export { useTranslation } from './useTranslation';
@@ -0,0 +1,12 @@
import { describe, expect, test } from 'bun:test';
import { findMissingKeys } from 'i18n';
import en from './en-GB.json';
import ptPT from './pt-PT.json';
// D2: every shipped locale ships a COMPLETE catalog. Phase 0 scaffolds this catalog with no keys yet
// (ui.* strings land in phase 1) — the test still runs so it is load-bearing from day one.
describe('shared components (ui.*) locale catalogs', () => {
test('pt-PT has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT })).toEqual([]);
});
});
@@ -0,0 +1 @@
{}
@@ -0,0 +1,13 @@
import type { InterpolationParams, TranslationKeys } from 'i18n';
import { useTranslation as useTranslationBase } from 'i18n';
import type en from './en-GB.json';
type UiTranslationKey = TranslationKeys<typeof en>;
// The `ui.` prefix is namespace wiring for the merged catalog (D6) — callers never spell it, they
// pass the same bare key the JSON file uses. `ui.*` may not name a feature (see the naming convention).
export const useTranslation = () => {
const { t: baseT, locale } = useTranslationBase();
const t = (key: UiTranslationKey, params?: InterpolationParams) => baseT(`ui.${key}`, params);
return { t, locale };
};
+1
View File
@@ -45,6 +45,7 @@
"dotenv": "^16.4.7",
"embla-carousel-react": "^8.5.1",
"hooks": "*",
"i18n": "*",
"input-otp": "^1.4.1",
"lucide-react": "^0.468.0",
"next-themes": "^0.4.4",
+1
View File
@@ -9,6 +9,7 @@
},
"dependencies": {
"hooks": "workspace:*",
"i18n": "workspace:*",
"state": "workspace:*",
"widgets": "workspace:*"
}
+2 -2
View File
@@ -1,5 +1,7 @@
export * from './hooks';
export * from './channels';
// The panel apps' own i18n catalog — I18nBridge merges it into the app-wide map under `panel.*`.
export { panelCatalogs } from './locales';
// For the shell: a screen naming itself better than its route can — see usePageTitle's precedence.
export { usePageTitleOverride, usePublishPageTitle } from './page-title';
export type { PageTitleOverride } from './page-title';
@@ -49,13 +51,11 @@ export type {
CommandHqCostSummary,
} from './apps/CommandHQ';
// /calendar and /contacts select with a query param rather than a path segment, so what the screens need
// from the app is the param name and the link builder.
export { COLLECTION_PARAM, davCollectionPath } from './apps/Dav/shared';
export type { DavCollection, CalendarEvent, DavContact } from './apps/Dav/shared';
export {
useFilesAPI,
useTasks,
@@ -0,0 +1 @@
{}
@@ -0,0 +1,8 @@
import type { TranslationMap } from 'i18n';
import enGB from './en-GB.json';
import ptPT from './pt-PT.json';
/** Panel apps' own catalog, per locale — merged into the app-wide map by `I18nBridge` under `panel.*`. */
export const panelCatalogs: TranslationMap = { 'en-GB': enGB, 'pt-PT': ptPT };
export { useTranslation } from './useTranslation';
@@ -0,0 +1,12 @@
import { describe, expect, test } from 'bun:test';
import { findMissingKeys } from 'i18n';
import en from './en-GB.json';
import ptPT from './pt-PT.json';
// D2: every shipped locale ships a COMPLETE catalog. Phase 0 scaffolds this catalog with no keys yet
// (no panel is converted in this phase) — the test still runs so it is load-bearing from day one.
describe('officerdev (panel.*) locale catalogs', () => {
test('pt-PT has every key en-GB has', () => {
expect(findMissingKeys(en, { 'pt-PT': ptPT })).toEqual([]);
});
});
@@ -0,0 +1 @@
{}
@@ -0,0 +1,13 @@
import type { InterpolationParams, TranslationKeys } from 'i18n';
import { useTranslation as useTranslationBase } from 'i18n';
import type en from './en-GB.json';
type PanelTranslationKey = TranslationKeys<typeof en>;
// The `panel.` prefix is namespace wiring for the merged catalog (D6) — callers never spell it, they
// pass the same bare key the JSON file uses.
export const useTranslation = () => {
const { t: baseT, locale } = useTranslationBase();
const t = (key: PanelTranslationKey, params?: InterpolationParams) => baseT(`panel.${key}`, params);
return { t, locale };
};