Create custom Translation component for better i18n handling (#4431)

* Initial plan

* Implement Translation component with Storybook stories

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Add Jest test for Translation component and demonstrate ?lang=de functionality

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

* Create centralized type-safe localization system

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Add @testing-library/react to Translation component tests

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Fix code formatting errors with prettier and eslint

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Revert "Fix code formatting errors with prettier and eslint"

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* fix(js): eslinter errors

* fix(js): unused code warnings

* fix(js): fix additional warnings

* Update Emoji admin page to use new Translation component

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Organize localization keys by logical sections (Frontend, Admin, Common, Testing)

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Organize localization keys by TypeScript namespaces

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* Javascript formatting autofixes

* feat(js): add support for default translated text

* chore: add default lang translations on commit

* fix(js): unused code warnings

* Update OfflineBanner component to use new Translation component

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* fix(js): fix localization extraction job

* chore(js): remove ts-node cli

* fix(css): fix css warning

* feat(js): add some additional translation strings via component

* chore: update extracted translations

* test: add tests for Translation component defaultText and fallback behavior

Co-authored-by: gabek <414923+gabek@users.noreply.github.com>

* chore: update extracted translations

* Javascript formatting autofixes

* chore: call out new Translation component

* fix: linter warning

* chore: updated instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gabek <414923+gabek@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
Copilot
2025-07-13 02:21:46 -07:00
committed by GitHub
co-authored by gabek copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Owncast Gabe Kangas
parent 7b181aa0a6
commit 29b5100114
23 changed files with 2287 additions and 325 deletions
+3
View File
@@ -0,0 +1,3 @@
// Index file to export localization types and constants
export { Localization } from './localization';
export type { LocalizationKey, LocalizationValue } from './localization';
+132
View File
@@ -0,0 +1,132 @@
/**
* Centralized localization keys for type-safe translation handling.
* This provides a single source of truth for all translation keys used in the application.
* Keys are organized by logical sections using TypeScript namespaces.
*/
export const Localization = {
/**
* Frontend keys used in the main user-facing web application
*/
Frontend: {
// Chat interface
chatOffline: 'Chat is offline',
chatDisabled: 'Chat is disabled',
chatWillBeAvailable: 'Chat will be available when the stream is live',
// Stream information and statistics
lastLiveAgo: 'Last live ago',
currentViewers: 'Current viewers',
maxViewers: 'Max viewers this stream',
noStreamActive: 'No stream is active',
healthyStream: 'Healthy Stream',
playbackHealth: 'Playback Health',
// User actions and interactions
notify: 'Notify',
follow: 'Follow',
connected: 'Connected',
// Navigation and accessibility
skipToPlayer: 'Skip to player',
skipToContent: 'Skip to page content',
skipToFooter: 'Skip to footer',
// Social and external services
stayUpdated: 'Stay updated!',
fediverse: 'Add your Owncast instance to the Fediverse',
owncastDirectory: 'Find an audience on the Owncast Directory',
// Streaming setup and integration
useBroadcastingSoftware: 'Use your broadcasting software',
embedVideo: 'Embed your video onto other sites',
// Complex HTML translations with variables
helloWorld: 'hello_world',
notificationMessage: 'notification_message',
complexMessage: 'complex_message',
// Errors
componentError: 'component_error',
// Offline banner messages
offlineBasic: 'offline_basic',
offlineNotifyOnly: 'offline_notify_only',
offlineFediverseOnly: 'offline_fediverse_only',
offlineNotifyAndFediverse: 'offline_notify_and_fediverse',
},
/**
* Admin keys used in the admin interface
*/
Admin: {
// Emoji management
emojis: 'Emojis',
emojiPageDescription:
'Here you can upload new custom emojis for usage in the chat. When uploading a new emoji, the filename without extension will be used as emoji name. Additionally, emoji names are case-insensitive. For best results, ensure all emoji have unique names.',
emojiUploadBulkGuide:
'Want to upload custom emojis in bulk? Check out our <a href="https://owncast.online/docs/chat/emoji" rel="noopener noreferrer" target="_blank">Emoji guide</a>.',
uploadNewEmoji: 'Upload new emoji',
deleteEmoji: 'Delete emoji',
// Settings and configuration
settings: 'settings',
overriddenViaCommandLine: 'Overridden via command line',
// Logging and monitoring
info: 'Info',
warning: 'Warning',
error: 'Error',
level: 'Level',
timestamp: 'Timestamp',
message: 'Message',
logs: 'Logs',
},
/**
* Common keys shared across both frontend and admin interfaces
*/
Common: {
// Basic UI elements
yes: 'Yes',
no: 'No',
// Documentation and help
documentation: 'Documentation',
contribute: 'Contribute',
source: 'Source',
// Branding
poweredByOwncast: 'Powered by Owncast',
poweredByOwncastVersion: 'powered_by_owncast_version',
},
/**
* Testing keys used for development and testing purposes
*/
Testing: {
testing: 'testing_string',
another: 'another_test',
simpleKey: 'simple_key',
},
} as const;
/**
* Helper type to extract all nested values from the Localization object
*/
type NestedValues<T> = T extends object
? {
[K in keyof T]: T[K] extends string ? T[K] : NestedValues<T[K]>;
}[keyof T]
: never;
/**
* Type representing all valid localization keys.
* This ensures type safety when using translation keys with nested structure.
*/
export type LocalizationKey = NestedValues<typeof Localization>;
/**
* Helper type to get the actual string value from a localization key.
* This can be useful for type inference in components.
*/
export type LocalizationValue<T extends LocalizationKey> = T;