From 8e4872d2e7094a31e9f5d25d24f01bbeb8d0781f Mon Sep 17 00:00:00 2001 From: Mathias Date: Sun, 4 Oct 2026 22:25:51 +0200 Subject: [PATCH] feat(domain): Language, a closed set carried to the engines on the context (#61) sv and auto; an empty form value is sv, anything else is refused. LanguageFrom defaults to sv so a caller that sets nothing behaves as before. Co-Authored-By: Claude Sonnet 5.5 --- internal/domain/language.go | 47 ++++++++++++++++++++++++++++++++ internal/domain/language_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 internal/domain/language.go create mode 100644 internal/domain/language_test.go diff --git a/internal/domain/language.go b/internal/domain/language.go new file mode 100644 index 0000000..def4f07 --- /dev/null +++ b/internal/domain/language.go @@ -0,0 +1,47 @@ +package domain + +import "context" + +// Language is the spoken language an upload is transcribed as. It is chosen by the user on +// the upload form and reaches the engines as one request field, so it is a closed set, +// never free text (tapir#61). +type Language string + +const ( + // LangSwedish sends language=sv: the Swedish engines' own language, and what every + // upload was transcribed as before the choice existed. + LangSwedish Language = "sv" + // LangAuto sends no language, so the engine detects it. It is what "Other" on the form + // means: the Swedish-only engines are not used for it. + LangAuto Language = "auto" +) + +// ParseLanguage reads a form value. An empty value is Swedish (a client that predates the +// field); anything that is not exactly a known value is refused rather than normalised. +func ParseLanguage(s string) (Language, bool) { + switch Language(s) { + case "", LangSwedish: + return LangSwedish, true + case LangAuto: + return LangAuto, true + } + return "", false +} + +type languageKey struct{} + +// WithLanguage returns a context that carries the language of the job it serves. The +// Transcriber interface does not change: engines read it with LanguageFrom, the same way +// they read the progress Reporter (ADR-035). +func WithLanguage(ctx context.Context, l Language) context.Context { + return context.WithValue(ctx, languageKey{}, l) +} + +// LanguageFrom returns the language carried by ctx, or Swedish when it carries none, so a +// caller that sets nothing behaves as it always did. +func LanguageFrom(ctx context.Context) Language { + if l, ok := ctx.Value(languageKey{}).(Language); ok && l != "" { + return l + } + return LangSwedish +} diff --git a/internal/domain/language_test.go b/internal/domain/language_test.go new file mode 100644 index 0000000..ba5db04 --- /dev/null +++ b/internal/domain/language_test.go @@ -0,0 +1,41 @@ +package domain_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "git.d-ma.be/mathias/tapir/internal/domain" +) + +func TestParseLanguage_KnownValuesAndEmptyAreAcceptedEverythingElseIsNot(t *testing.T) { + tests := []struct { + in string + want domain.Language + wantOK bool + }{ + {"sv", domain.LangSwedish, true}, + {"auto", domain.LangAuto, true}, + {"", domain.LangSwedish, true}, // a client that sends no field keeps today's behaviour + {"SV", "", false}, // exact match: the value reaches a request field + {" sv", "", false}, + {"sv\n", "", false}, + {"en", "", false}, // a specific other language is not offered yet + {"sv,en", "", false}, + {"../sv", "", false}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + got, ok := domain.ParseLanguage(tt.in) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestLanguageFrom_IsSwedishWhenTheContextCarriesNoneAndOtherwiseWhatWasSet(t *testing.T) { + assert.Equal(t, domain.LangSwedish, domain.LanguageFrom(context.Background())) + ctx := domain.WithLanguage(context.Background(), domain.LangAuto) + assert.Equal(t, domain.LangAuto, domain.LanguageFrom(ctx)) +}