Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,9 @@ Repo-specific design rules:
- CI or deployment work is not done when GitHub Actions is merely green; the deployed GitHub Pages app MUST be opened and verified to boot without shell errors.
- GitHub Pages deployment for `prompter.managed-code.com` MUST serve from the custom-domain root with `<base href="/">`; repo-name path prefixes are forbidden.
- Version text shown in the app must come from automated build or release metadata, never from manually edited About copy.
- The runtime must negotiate browser language from supported cultures and default to English.
- Supported runtime cultures are English, Ukrainian, French, Spanish, Portuguese, and Italian.
- The runtime must negotiate the initial language from the browser's supported cultures, fall back to English when there is no supported match, and persist the user's explicit language choice in user settings for later sessions.
- UI localization in Blazor must use resource-based localization with shared catalogs and named culture constants instead of screen-local hardcoded strings.
- Supported runtime cultures are English, Ukrainian, French, Spanish, Portuguese, Italian, and German.
- Russian must never be added as a supported runtime culture.

### Critical
Expand Down
7 changes: 4 additions & 3 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ flowchart LR
| `Storage` | Browser persistence and cloud transfer orchestration | Keeps scripts and settings local-first while exposing provider-backed import/export | `src/PrompterOne.Shared/Storage`, `src/PrompterOne.Shared/Library/Services/Storage` | browser `IStorage` and VFS registration, authoritative browser repositories for scripts/folders, provider credential persistence, scripts/settings snapshot transfer | routed page layout, teleprompter rendering, video-stream upload workflows |
| `Cross-Tab Messaging` | Same-origin browser runtime coordination | Lets separate WASM tabs coordinate browser-owned state without a backend | `src/PrompterOne.Shared/AppShell/Services`, `src/PrompterOne.Shared/Settings/Services`, `src/PrompterOne.Shared/wwwroot/app` | `BroadcastChannel` bridge, typed envelopes, settings fan-out, same-origin tab sync | server state, cross-origin transport, collaborative editor conflict resolution |
| `Diagnostics` | Error and operation feedback layer | Makes recoverable and fatal issues visible in the shell | `src/PrompterOne.Shared/Diagnostics` | banners, error boundary reporting, operation status wiring | owning business logic of the failing feature |
| `Localization` | Culture and UI text contract | Keeps supported runtime languages consistent and browser-driven | `src/PrompterOne.Shared/Localization`, `src/PrompterOne.Core/Localization` | text catalogs, culture bootstrap, supported culture rules | feature behavior or screen-specific layout ownership |
| `Localization` | Culture and UI text contract | Keeps supported runtime languages consistent, browser-driven, and user-overridable through persisted settings | `src/PrompterOne.Shared/Localization`, `src/PrompterOne.Core/Localization` | shared resource catalogs, startup culture bootstrap, browser-language negotiation, persisted user language override, supported culture rules | feature behavior or screen-specific layout ownership |
| `Workspace` | Active script/session state model | Gives editor, learn, read, and go-live one shared script context | `src/PrompterOne.Core/Workspace` | loaded script state, previews, estimated duration, active session metadata | feature-specific rendering details |
| `Media` | Browser media and scene domain | Models cameras, microphones, transforms, and audio bus state | `src/PrompterOne.Core/Media`, `src/PrompterOne.Shared/Media` | media device models, scene state, browser media interop | routed screen layout ownership |
Comment on lines 100 to 105
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title references removing OBS Go Live modules, but the changes here are primarily localization/culture (new German support, persisted language preference, localized settings/diagnostics UI). Consider renaming the PR (or updating the PR description) so reviewers and release notes accurately reflect the scope.

Copilot uses AI. Check for mistakes.
| `Streaming` | Program capture, transport, and target routing domain | Defines how one composed program feed is described, which source/output modules can attach to it, and which external targets are genuinely reachable without a PrompterOne backend | `src/PrompterOne.Core/Streaming` | program-capture profiles, source/output module contracts, transport connection profiles, downstream target descriptors, routing normalization, standalone transport constraints | Razor UI or page layout concerns |
Expand Down Expand Up @@ -397,6 +397,7 @@ If a native embedded browser host returns later, media access must not rely on s
- standalone Blazor WebAssembly host
- serves the app shell and static asset references
- applies browser-language culture selection before the WASM runtime starts rendering routed UI
- falls back to the persisted user language override from typed settings before browser negotiation, with English as the final fallback
- must stay free of server-only runtime dependencies

### `src/PrompterOne.Shared`
Expand All @@ -407,7 +408,7 @@ If a native embedded browser host returns later, media access must not rely on s
- exact design shell and imported `design` assets
- shared UI localization catalog for supported browser cultures
- browser interop and app DI wiring
- browser-backed `IUserSettingsStore` wiring for persisted reader, theme, scene, and studio preferences
- browser-backed `IUserSettingsStore` wiring for persisted reader, theme, language, scene, and studio preferences
- dynamic library folder components and folder/document browser storage adapters
- UI diagnostics banner and global error boundary
- debounced editor autosave and body-only TPS source authoring
Expand Down Expand Up @@ -547,7 +548,7 @@ flowchart LR
- The runtime must remain backend-free.
- `Go Live` may operate multiple concurrent browser publish transports when the operator explicitly arms them, but every active transport must still consume the same canonical browser-owned program feed.
- `Go Live` must not require any PrompterOne-owned backend, relay, ingest layer, or media server; only third-party browser-facing transport infrastructure is allowed.
- Browser-language localization must default to English and support `en`, `uk`, `fr`, `es`, `pt`, and `it`.
- Browser-language localization must default to English, must allow a persisted user override, and must support `en`, `uk`, `fr`, `es`, `it`, `de`, and `pt`.
- Russian is intentionally unsupported and must fall back to English.
- Visual fidelity should prefer copying the exact design classes and structure over inventing replacements.
- Browser tests require Playwright Chromium to be installed locally.
Expand Down
4 changes: 2 additions & 2 deletions src/PrompterOne.App/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using PrompterOne.App;
using PrompterOne.App.Services;
using PrompterOne.Shared.Localization;
using PrompterOne.Shared.Services;
using PrompterOne.Shared.Settings.Services;

Expand All @@ -22,5 +22,5 @@
builder.Services.AddPrompterOneShared();

var host = builder.Build();
await BrowserCultureRuntime.ApplyPreferredCultureAsync(host.Services.GetRequiredService<IJSRuntime>());
await host.Services.GetRequiredService<AppCulturePreferenceService>().InitializeAsync();
await host.RunAsync();
49 changes: 0 additions & 49 deletions src/PrompterOne.App/Services/BrowserCultureRuntime.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/PrompterOne.App/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&family=Playfair+Display:ital,wght@0,700;0,800;0,900;1,700;1,800&display=swap" rel="stylesheet" />
<link rel="icon" type="image/png" href="_content/PrompterOne.Shared/favicon.png" />
<script src="_content/PrompterOne.Shared/localization/browser-culture.js"></script>
<script src="_content/PrompterOne.Shared/theme/browser-theme.js"></script>
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/tokens.css" />
<link rel="stylesheet" href="_content/PrompterOne.Shared/design/components.css" />
Expand Down
22 changes: 22 additions & 0 deletions src/PrompterOne.Core/Localization/AppCultureCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,45 @@ public static class AppCultureCatalog
public const string UkrainianCultureName = "uk";
public const string FrenchCultureName = "fr";
public const string SpanishCultureName = "es";
public const string GermanCultureName = "de";
public const string PortugueseCultureName = "pt";
public const string ItalianCultureName = "it";

private const char CultureSeparator = '-';
private const char AlternateCultureSeparator = '_';
private const string EnglishDisplayName = "English";
private const string FrenchDisplayName = "Français";
private const string GermanDisplayName = "Deutsch";
private const string ItalianDisplayName = "Italiano";
private const string PortugueseDisplayName = "Português";
private const string RussianLanguageName = "ru";
private const string SpanishDisplayName = "Español";
private const string UkrainianDisplayName = "Українська";

private static readonly IReadOnlyList<AppCultureDefinition> SupportedCultureDefinitions =
[
new(EnglishCultureName, EnglishDisplayName),
new(UkrainianCultureName, UkrainianDisplayName),
new(FrenchCultureName, FrenchDisplayName),
new(SpanishCultureName, SpanishDisplayName),
new(ItalianCultureName, ItalianDisplayName),
new(GermanCultureName, GermanDisplayName),
new(PortugueseCultureName, PortugueseDisplayName)
];

private static readonly HashSet<string> SupportedCultures = new(StringComparer.OrdinalIgnoreCase)
{
EnglishCultureName,
UkrainianCultureName,
FrenchCultureName,
SpanishCultureName,
GermanCultureName,
PortugueseCultureName,
ItalianCultureName
};

public static IReadOnlyList<AppCultureDefinition> SupportedCultureDefinitionsInDisplayOrder => SupportedCultureDefinitions;

public static IReadOnlyCollection<string> SupportedCultureNames => SupportedCultures;

public static string ResolvePreferredCulture(IEnumerable<string?> requestedCultures)
Expand Down
3 changes: 3 additions & 0 deletions src/PrompterOne.Core/Localization/AppCultureDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace PrompterOne.Core.Localization;

public sealed record AppCultureDefinition(string CultureName, string DisplayName);
15 changes: 6 additions & 9 deletions src/PrompterOne.Shared/AppShell/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ namespace PrompterOne.Shared.Layout;
public partial class MainLayout : LayoutComponentBase, IDisposable
{
private const string GoLiveWidgetIdleElapsed = "00:00:00";
private const string GoLiveWidgetLiveStateLabel = "Live";
private const string GoLiveWidgetRecordingStateLabel = "Rec";
private const string GoLiveWidgetStreamingRecordingStateLabel = "Live + Rec";
private const string RouteChangedLogTemplate = "Route changed to {Location}.";

[Inject] private AppBootstrapper Bootstrapper { get; set; } = null!;
Expand Down Expand Up @@ -64,9 +61,9 @@ public partial class MainLayout : LayoutComponentBase, IDisposable

private string GoLiveIndicatorCopy => GoLiveIndicatorState switch
{
RecordingStateValue => "Recording active",
StreamingStateValue => "Stream active",
_ => "Ready"
RecordingStateValue => Text(UiTextKey.HeaderGoLiveIndicatorRecording),
StreamingStateValue => Text(UiTextKey.HeaderGoLiveIndicatorStreaming),
_ => Text(UiTextKey.HeaderGoLiveIndicatorReady)
};

private string GoLiveIndicatorState => GoLiveSessionState.IsRecordingActive
Expand All @@ -87,9 +84,9 @@ public partial class MainLayout : LayoutComponentBase, IDisposable

private string GoLiveWidgetStateLabel => (GoLiveSessionState.IsStreamActive, GoLiveSessionState.IsRecordingActive) switch
{
(true, true) => GoLiveWidgetStreamingRecordingStateLabel,
(true, false) => GoLiveWidgetLiveStateLabel,
(false, true) => GoLiveWidgetRecordingStateLabel,
(true, true) => Text(UiTextKey.HeaderGoLiveWidgetLiveRecording),
(true, false) => Text(UiTextKey.HeaderGoLiveWidgetLive),
(false, true) => Text(UiTextKey.HeaderGoLiveWidgetRecording),
_ => GoLiveIndicatorCopy
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using PrompterOne.Core.Services.Rsvp;
using PrompterOne.Core.Services.Streaming;
using PrompterOne.Core.Services.Workspace;
using PrompterOne.Shared.Localization;
using PrompterOne.Shared.Services.Diagnostics;
using PrompterOne.Shared.Services.Editor;
using PrompterOne.Shared.Settings.Services;
Expand Down Expand Up @@ -64,6 +65,7 @@ public static IServiceCollection AddPrompterOneShared(this IServiceCollection se
services.AddScoped<BrowserCloudStorageStore>();
services.AddScoped<BrowserFileStorageStore>();
services.AddScoped<BrowserThemeService>();
services.AddScoped<AppCulturePreferenceService>();
services.AddScoped<CloudStorageProviderFactory>();
services.AddScoped<CloudStorageTransferService>();
services.AddScoped<StudioSettingsStore>();
Expand Down
2 changes: 2 additions & 0 deletions src/PrompterOne.Shared/Contracts/UiTestIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ public static class Settings
public const string AboutVersion = "settings-about-version";
public const string AppearancePanel = "settings-appearance-panel";
public const string AiPanel = "settings-ai-panel";
public const string LanguageSelect = "settings-language-select";
public const string CameraFrameRate = "settings-camera-frame-rate";
public const string CameraPreviewCard = "settings-camera-preview-card";
public const string CameraPreviewEmpty = "settings-camera-preview-empty";
Expand Down Expand Up @@ -256,6 +257,7 @@ public static class Settings
public const string NoiseSuppression = "settings-noise-suppression";
public const string Page = "settings-page";
public const string PrimaryMic = "settings-primary-mic";
public const string Title = "settings-title";
public const string RecordingAudioBitrate = "settings-recording-audio-bitrate";
public const string RecordingAutoRecord = "settings-recording-auto-record";
public const string RecordingPanel = "settings-recording-panel";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
@implements IDisposable
@namespace PrompterOne.Shared.Components.Diagnostics
@using Microsoft.Extensions.Localization
@using PrompterOne.Shared.Localization
@inject NavigationManager Navigation
@inject PrompterOne.Shared.Services.Diagnostics.BrowserConnectivityService Connectivity
@inject IStringLocalizer<SharedResource> Localizer

@if (Connectivity.IsVisible)
{
Expand All @@ -12,20 +15,20 @@
data-state="@Connectivity.State"
data-testid="@UiTestIds.Diagnostics.Connectivity">
<div class="app-shell-card app-shell-card-status">
<span class="app-shell-eyebrow">Connection</span>
<span class="app-shell-eyebrow">@Text(UiTextKey.DiagnosticsConnectivityEyebrow)</span>
<h2 id="@UiDomIds.Diagnostics.ConnectivityTitle">@Connectivity.Title</h2>
<p id="@UiDomIds.Diagnostics.ConnectivityMessage">@Connectivity.Message</p>
<div class="app-shell-actions">
<button type="button"
id="@UiDomIds.Diagnostics.ConnectivityRetry"
class="app-shell-btn app-shell-btn-primary"
@onclick="HandleRetry"
data-testid="@UiTestIds.Diagnostics.ConnectivityRetry">Retry Now</button>
data-testid="@UiTestIds.Diagnostics.ConnectivityRetry">@Text(UiTextKey.DiagnosticsConnectivityRetryNow)</button>
<button type="button"
id="@UiDomIds.Diagnostics.ConnectivityDismiss"
class="app-shell-btn"
@onclick="HandleDismiss"
data-testid="@UiTestIds.Diagnostics.ConnectivityDismiss">Dismiss</button>
data-testid="@UiTestIds.Diagnostics.ConnectivityDismiss">@Text(UiTextKey.DiagnosticsDismiss)</button>
</div>
</div>
</section>
Expand All @@ -48,6 +51,8 @@
private void HandleConnectivityChanged(object? sender, EventArgs args) =>
InvokeAsync(StateHasChanged);

private string Text(UiTextKey key) => Localizer[key.ToString()];

public void Dispose()
{
Connectivity.Changed -= HandleConnectivityChanged;
Expand Down
Loading