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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- The `--lang` whitelist now covers the full set of STQRY-supported content languages, generated from the server's accepted set rather than a hand-maintained subset, so any language the platform accepts is no longer rejected client-side.
- `stqry quizzes` — full CRUD for the Quizzes Public API across the quiz → questions → answers hierarchy, with matching MCP tools, shell completion, and reference/coverage docs. Translated fields respect `--lang`, `--question-type` is validated client-side, answers carry a `--correct` flag, and `delete`/`remove` accept `--lang` for per-locale translation deletes.

### Fixed
Expand Down
97 changes: 97 additions & 0 deletions internal/api/languages_gen.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// Code generated; DO NOT EDIT.
//
// Source of truth: the full set of content languages the server accepts. The
// server defines a translation accessor for every such code, so the CLI mirrors
// the full set rather than a narrower subset. Regenerate when the platform adds
// a language.

package api

Expand All @@ -11,26 +16,61 @@ type Language struct {
// SupportedLanguages lists every language code that STQRY content
// endpoints accept.
var SupportedLanguages = []Language{
{Code: "af", Name: "Afrikaans"},
{Code: "ak", Name: "Akan"},
{Code: "alq", Name: "Algonquin"},
{Code: "am", Name: "Amharic"},
{Code: "ar", Name: "Arabic"},
{Code: "as", Name: "Azerbaijani"},
{Code: "ay", Name: "Aymara"},
{Code: "az", Name: "Azerbaijani"},
{Code: "be", Name: "Belarusian"},
{Code: "bg", Name: "Bulgarian"},
{Code: "bho", Name: "Bhojpuri"},
{Code: "bm", Name: "Bambara"},
{Code: "bn", Name: "Bengali"},
{Code: "bs", Name: "Bosnian"},
{Code: "buc", Name: "Kibushi"},
{Code: "ca", Name: "Catalan"},
{Code: "ceb", Name: "Cebuano"},
{Code: "ckb", Name: "Central Kurdish"},
{Code: "co", Name: "Cornish"},
{Code: "cs", Name: "Czech"},
{Code: "cy", Name: "Welsh"},
{Code: "da", Name: "Danish"},
{Code: "de", Name: "German"},
{Code: "doi", Name: "Dogri"},
{Code: "dv", Name: "Maldivian"},
{Code: "ee", Name: "Ewe"},
{Code: "el", Name: "Greek"},
{Code: "en", Name: "English (US)"},
{Code: "en-AU", Name: "English (Australian)"},
{Code: "en-CA", Name: "English (Canadian)"},
{Code: "en-GB", Name: "English (United Kingdom)"},
{Code: "en-IN", Name: "English (Indian)"},
{Code: "en-SG", Name: "English (Singapore)"},
{Code: "eo", Name: "Esperanto"},
{Code: "es", Name: "Spanish"},
{Code: "es-419", Name: "Spanish (Latin America)"},
{Code: "es-MX", Name: "Spanish (Mexico)"},
{Code: "es-US", Name: "Spanish (United States)"},
{Code: "et", Name: "Estonian"},
{Code: "eu", Name: "Basque"},
{Code: "fa", Name: "Persian"},
{Code: "fi", Name: "Finnish"},
{Code: "fil", Name: "Filipino"},
{Code: "fj", Name: "Fijian"},
{Code: "fo", Name: "Faroese"},
{Code: "fr", Name: "French"},
{Code: "fr-CA", Name: "French (Canada)"},
{Code: "fy", Name: "Western Frisian"},
{Code: "ga", Name: "Irish"},
{Code: "gd", Name: "Gaelic; Scottish Gaelic"},
{Code: "gl", Name: "Galician"},
{Code: "gn", Name: "Guarani"},
{Code: "gom", Name: "Goan Konkani gom"},
{Code: "gu", Name: "Gujarati"},
{Code: "ha", Name: "Hausa"},
{Code: "haw", Name: "Hawaiian"},
{Code: "he", Name: "Hebrew"},
{Code: "hi", Name: "Hindi"},
Expand All @@ -40,47 +80,104 @@ var SupportedLanguages = []Language{
{Code: "hu", Name: "Hungarian"},
{Code: "hy", Name: "Armenian"},
{Code: "id", Name: "Indonesian"},
{Code: "ig", Name: "Igbo"},
{Code: "ilo", Name: "Iloko"},
{Code: "is", Name: "Icelandic"},
{Code: "it", Name: "Italian"},
{Code: "iu", Name: "Inuktitut"},
{Code: "iw", Name: "Hebrew"},
{Code: "ja", Name: "Japanese"},
{Code: "jv", Name: "Javanese"},
{Code: "jw", Name: ""},
{Code: "ka", Name: "Georgian"},
{Code: "kk", Name: "Kazakh"},
{Code: "km", Name: "Khmer"},
{Code: "kn", Name: "Kannada"},
{Code: "ko", Name: "Korean (South Korea)"},
{Code: "kri", Name: "Krio"},
{Code: "ku", Name: "Kurdish"},
{Code: "ky", Name: "Kyrgyz"},
{Code: "la", Name: "Latin"},
{Code: "lb", Name: "Luxembourgish; Letzeburgesch"},
{Code: "lg", Name: "Ganda"},
{Code: "lkt", Name: "Lakota"},
{Code: "lld", Name: "Ladin"},
{Code: "ln", Name: "Lingala"},
{Code: "lo", Name: "Lao"},
{Code: "lt", Name: "Lithuanian"},
{Code: "lus", Name: "Lushai"},
{Code: "lv", Name: "Latvian"},
{Code: "mai", Name: "Maithili"},
{Code: "mg", Name: "Malagasy"},
{Code: "mi", Name: "Māori"},
{Code: "mk", Name: "Macedonian"},
{Code: "ml", Name: "Malayalam"},
{Code: "mn", Name: "Mongolian"},
{Code: "mni-Mtei", Name: "Meitei"},
{Code: "mr", Name: "Marathi"},
{Code: "ms", Name: "Malay"},
{Code: "mt", Name: "Maltese"},
{Code: "my", Name: "Burmese"},
{Code: "ne", Name: "Nepali"},
{Code: "nl", Name: "Dutch"},
{Code: "no", Name: "Norwegian"},
{Code: "nso", Name: "Northern Sotho"},
{Code: "ny", Name: "Chichewa; Chewa; Nyanja"},
{Code: "oji", Name: "Ojibwe"},
{Code: "om", Name: "Oromo"},
{Code: "or", Name: "Oriya"},
{Code: "pa", Name: "Panjabi; Punjabi"},
{Code: "pap", Name: "Papiamentu"},
{Code: "pl", Name: "Polish"},
{Code: "ps", Name: "Pushto; Pashto"},
{Code: "pt", Name: "Portuguese (Portugal)"},
{Code: "pt-BR", Name: "Portuguese (Brazil)"},
{Code: "qu", Name: "Quechua"},
{Code: "rm", Name: "Romansh"},
{Code: "ro", Name: "Romanian"},
{Code: "ru", Name: "Russian"},
{Code: "rw", Name: "Kinyarwanda"},
{Code: "sa", Name: "Sanskrit"},
{Code: "sd", Name: "Sindhi"},
{Code: "see", Name: "Seneca"},
{Code: "si", Name: "Sinhala"},
{Code: "sk", Name: "Slovak"},
{Code: "sl", Name: "Slovenian"},
{Code: "sm", Name: "Samoan"},
{Code: "sn", Name: "Shona"},
{Code: "so", Name: "Somali"},
{Code: "sq", Name: "Albanian"},
{Code: "sr", Name: "Serbian"},
{Code: "st", Name: "Sotho, Southern"},
{Code: "su", Name: "Sundanese"},
{Code: "sv", Name: "Swedish"},
{Code: "sw", Name: "Swahili"},
{Code: "swb", Name: "Shimaore"},
{Code: "ta", Name: "Tamil"},
{Code: "te", Name: "Telugu"},
{Code: "tg", Name: "Tajik"},
{Code: "th", Name: "Thai"},
{Code: "ti", Name: "Tigrinya"},
{Code: "tk", Name: "Turkmen"},
{Code: "tl", Name: "Tagalog"},
{Code: "tn", Name: "Tswana"},
{Code: "to", Name: "Tongan"},
{Code: "tr", Name: "Turkish"},
{Code: "ts", Name: "Tsonga"},
{Code: "tt", Name: "Tatar"},
{Code: "ug", Name: "Uighur; Uyghur"},
{Code: "uk", Name: "Ukrainian"},
{Code: "ur", Name: "Urdu"},
{Code: "uz", Name: "Uzbek"},
{Code: "vi", Name: "Vietnamese"},
{Code: "xh", Name: "Xhosa"},
{Code: "yi", Name: "Yiddish"},
{Code: "yo", Name: "Yoruba"},
{Code: "zh", Name: "Chinese"},
{Code: "zh-CN", Name: "Chinese (Simplified)"},
{Code: "zh-Hans", Name: "Chinese (Simplified)"},
{Code: "zh-Hant", Name: "Chinese (Traditional)"},
{Code: "zh-HK", Name: "Chinese (Hong Kong)"},
{Code: "zh-TW", Name: "Chinese (Traditional)"},
{Code: "zu", Name: "Zulu"},
}
20 changes: 6 additions & 14 deletions internal/api/languages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,18 @@ func TestValidateLanguage_Empty(t *testing.T) {
}

func TestValidateLanguage_Supported(t *testing.T) {
for _, code := range []string{"en", "fr", "zh-Hans", "zh-Hant", "pt-BR", "en-GB"} {
// The whitelist mirrors the server's full set of accepted content
// languages, not a narrower subset — the server defines a translation
// accessor for every such code, so it accepts them all. This previously
// rejected codes like `hr`, `sk`, `sl`, `sr`, `bs`, `zh` that the
// platform happily writes.
for _, code := range []string{"en", "fr", "zh", "zh-Hans", "zh-Hant", "pt-BR", "en-GB", "hr", "sk", "sl", "sr", "bs"} {
if err := ValidateLanguage(code); err != nil {
t.Errorf("%q should be supported: %v", code, err)
}
}
}

func TestValidateLanguage_RejectsBareZh(t *testing.T) {
// The reason this whole feature exists: 'zh' alone is not a STQRY content
// language and silently writing to it created orphan rows.
err := ValidateLanguage("zh")
if err == nil {
t.Fatal("'zh' should be rejected")
}
msg := err.Error()
if !strings.Contains(msg, "zh-Hans") && !strings.Contains(msg, "zh-Hant") {
t.Errorf("expected suggestion of zh-Hans or zh-Hant, got: %s", msg)
}
}

func TestValidateLanguage_Suggestion(t *testing.T) {
cases := map[string]string{
"En": "en", // case mismatch on a known code
Expand Down
Loading