Šķîþ ţö ḿàîñ çöñţéñţ

Šĥîþ-àŵàŕé ļàñĝüàĝé þîçķéŕ

À šĥîþ-àŵàŕé ļàñĝüàĝé þîçķéŕ öƒƒéŕš ýöüŕ üšéŕš öñļý ţĥé ļöçàļéš ţĥàţ àŕé ŕéàđý, àñđ ḿàŕķš ţĥé öñéš ţĥàţ àŕé ÀÎ ţŕàñšļàţîöñ. ķàþî ŵŕîţéš éàçĥ ļöçàļé'š šţàñđîñĝ ţö à ship.json ḿàñîƒéšţ, àñđ @neokapi/i18n-react/ship ţüŕñš ţĥàţ ḿàñîƒéšţ îñţö à þîçķéŕ ḿöđéļ.

kapi.yamlship + approval gateskapi status --shipship.jsonlocale → statelanguagePickerModelLanguage pickerAI badge

Ţĥé ĝàţéš îñ ţĥé ŕéçîþé đéçîđé éàçĥ ļöçàļé'š šţàţé, àñđ ţĥé þîçķéŕ ŕéàđš ţĥé ḿàñîƒéšţ ķàþî ŵŕîţéš.

Ţŵö ĝàţéš đéçîđé ŵĥàţ šĥîþš​

À þŕöĵéçţ'š ḿàîñţàîñéŕš đéçîđé ŵĥîçĥ ţŕàñšļàţéđ ṽéŕšîöñš ţö öƒƒéŕ îţš üšéŕš. Ţĥéý đéçļàŕé ţĥàţ đéçîšîöñ àš ţŵö ĝàţéš îñ kapi.yaml, àñđ ķàþî éṽàļüàţéš ƃöţĥ ţĥé šàḿé ŵàý àĝàîñšţ ţĥé ţàŕĝéţ šţàţüš ļàđđéŕ (draft → translated → approved):

  • Ţĥé šĥîþ ĝàţé: ţĥé ƃàŕ ţö ĝö ļîṽé. À ļöçàļé ţĥàţ çļéàŕš îţ šĥîþš translated: ţŕàñšļàţéđ ŵîţĥ îţš çĥéçķš ĝŕééñ, àñđ šàƒé ţö öƒƒéŕ. Îţ îš ţĥé ship_gate / ship_gates çöñƒîĝüŕàţîöñ.
  • Ţĥé àþþŕöṽàļ ĝàţé: ţĥé ƃàŕ ţö šĥîþ àþþŕöṽéđ, ḿéàñîñĝ à þéŕšöñ àþþŕöṽéđ ţĥé çöñţéñţ. Îţ îš ţĥé established_gate / established_gates çöñƒîĝüŕàţîöñ. Ţĥé ŕéçîþé àñđ ship.json ñàḿé ţĥé àþþŕöṽéđ ŕüñĝ àñđ šţàţé established.

À ļöçàļé ţĥàţ šĥîþš translated îš ÀÎ ŵöŕķ, àñđ ţĥé þîçķéŕ ḿàŕķš îţ. À þŕöĵéçţ ţĥàţ đéļîṽéŕš öñļý àþþŕöṽéđ çöñţéñţ šàýš šö îñ îţš šĥîþ ĝàţé (ship_gate: { established: 100 }).

Đéçļàŕîñĝ ţĥé àþþŕöṽàļ ĝàţé​

Ţĥé àþþŕöṽàļ ĝàţé üšéš ţĥé šàḿé ţĥŕéé àđđîţîṽé ƒöŕḿš àñđ ţĥé šàḿé þŕéçéđéñçé àš ţĥé šĥîþ ĝàţé, àñđ ŕéšöļṽéš à gate: ñàḿé àĝàîñšţ ţĥé šĥàŕéđ gates: ŕéĝîšţŕý:

# kapi.yaml
ship_gate: { translated: 100 } # go live once fully translated
established_gate: { established: 100 } # ship approved once a person approved every translation

À ŕüļé ļîšţ ñàŕŕöŵš ţĥé ƃàŕ þéŕ çöļļéçţîöñ öŕ ļöçàļé, ḿöšţ-šþéçîƒîç ŕüļé ŵîñš:

established_gates:
- when: { locales: [ja] }
gate: { established: 100 } # Japanese needs a person on every translation
- gate: { established: 80 }
Ŵîţĥ ñö àþþŕöṽàļ ĝàţé, ñöţĥîñĝ šĥîþš àþþŕöṽéđ

À þŕöĵéçţ ŵîţĥ ñö àþþŕöṽàļ ĝàţé ĥàš ñö ļöçàļé îñ ţĥé established šţàţé: éṽéŕý ļöçàļé ţĥàţ šĥîþš ŕéàđš translated, ḿàŕķéđ ÀÎ.

À ļöçàļé ñö šĥîþ ĝàţé ḿàţçĥéš îš ñöţ ĝàţéđ

À ļöçàļé ţĥàţ ñö ship_gate öŕ ship_gates ŕüļé ḿàţçĥéš ĥàš ñö ƃàŕ ţö çļéàŕ, šö ķàþî ŕéþöŕţš îţ àš ñöţ ĝàţéđ ŕàţĥéŕ ţĥàñ šĥîþþàƃļé. Ñöţĥîñĝ ŵîţĥĥöļđš îţ öñ çöṽéŕàĝé, šö ţĥé þîçķéŕ öƒƒéŕš îţ ƃý đéƒàüļţ. Šţàļé ŵöŕđîñĝ, à ţŕàñšļàţîöñ à ŕéṽîéŵéŕ ţüŕñéđ đöŵñ, à ƒàîļîñĝ çĥéçķ, öŕ ţéŕḿš ţĥàţ ŵéŕé ñöţ çĥéçķéđ ŵîţĥĥöļđ à ļöçàļé ŵĥéţĥéŕ öŕ ñöţ à ĝàţé ḿàţçĥéš îţ.

Éḿîţţîñĝ ţĥé ḿàñîƒéšţ​

kapi status --ship ŵŕîţéš éàçĥ ļöçàļé'š šţàñđîñĝ àš à šḿàļļ ḿàñîƒéšţ ţĥàţ à ļàñĝüàĝé þîçķéŕ ŕéàđš. Ŵŕîţé îţ îñţö ýöüŕ àþþ'š šţàţîç àššéţš àš þàŕţ öƒ ţĥé ƃüîļđ:

kapi status --ship --emit public/ship.json

Ŵîţĥöüţ --emit, ţĥé ḿàñîƒéšţ ĝöéš ţö šţđöüţ, šö à ƃüîļđ šţéþ çàñ ŕéđîŕéçţ îţ:

kapi status --ship > public/ship.json

Ţĥé ƒîļé îš ķéýéđ ƃý ļöçàļé, éàçĥ éñţŕý çàŕŕýîñĝ ŵĥéţĥéŕ ţĥé ļöçàļé šĥîþš àñđ îţš šĥîþ šţàţé:

{
"fr": { "shippable": true, "state": "established" },
"de": { "shippable": true, "state": "translated" },
"nl": { "shippable": true, "state": "translated", "not_governed": ["terms"] },
"sv": { "shippable": true, "state": "not_gated" },
"ja": { "shippable": false, "state": "withheld" }
}

Ĥéŕé Ƒŕéñçĥ šĥîþš àþþŕöṽéđ (ñö ƃàđĝé), Ĝéŕḿàñ šĥîþš ţŕàñšļàţéđ (ƒļàĝĝéđ ÀÎ), Đüţçĥ šĥîþš ţĥé šàḿé ŵàý îñ à ļàñĝüàĝé ñö ţéŕḿš ĝöṽéŕñ, Šŵéđîšĥ ĥàš ñö ĝàţé àñđ îš öƒƒéŕéđ àš ñöţ ĝàţéđ, àñđ Ĵàþàñéšé îš ŵîţĥĥéļđ.

state ţàķéš öñé öƒ ƒöüŕ ṽàļüéš:

stateḾéàñîñĝshippable
establishedÀñ àþþŕöṽàļ ĝàţé ḿàţçĥéš ţĥé ļöçàļé àñđ ţĥé ļöçàļé çļéàŕš îţ: à þéŕšöñ àþþŕöṽéđ ţĥé çöñţéñţ.true
translatedÀ šĥîþ ĝàţé ḿàţçĥéš ţĥé ļöçàļé àñđ ţĥé ļöçàļé çļéàŕš îţ, àñđ ñö àþþŕöṽàļ ĝàţé îš ḿéţ: ţĥé ļöçàļé šĥîþš àš ÀÎ ţŕàñšļàţîöñ.true
withheldŢĥé ļöçàļé đöéš ñöţ šĥîþ: îţ îš šĥöŕţ öƒ îţš ĝàţé, öŕ šţàļé, ŕéĵéçţéđ öŕ ƒàîļîñĝ çöñţéñţ, öŕ üñçĥéçķéđ ţéŕḿš, ĥöļđ îţ ƃàçķ.false
not_gatedÑö ĝàţé ḿàţçĥéš ţĥé ļöçàļé, àñđ ñöţĥîñĝ ŵîţĥĥöļđš îţ.true

À ļöçàļé šþŕéàđ öṽéŕ šéṽéŕàļ çöļļéçţîöñš ţàķéš ţĥé ŵéàķéšţ šţàţé àḿöñĝ ţĥéḿ, îñ ţĥé öŕđéŕ ŵîţĥĥéļđ, ñöţ_ĝàţéđ, ţŕàñšļàţéđ, éšţàƃļîšĥéđ. shippable îš true ƒöŕ éṽéŕý šţàţé ƃüţ withheld, šö à þîçķéŕ ţĥàţ ŕéàđš öñļý shippable öƒƒéŕš à ñöţ-ĝàţéđ ļöçàļé. state ţéļļš à çļéàŕéđ ĝàţé ƒŕöḿ ñö ĝàţé.

Àñ éñţŕý çàŕŕîéš not_governed ŵĥéñ à đîḿéñšîöñ ĝöṽéŕñš ñöţĥîñĝ îñ ţĥàţ ļàñĝüàĝé. terms ḿéàñš ţĥàţ ñöñé öƒ ţĥé ţéŕḿš ƃöüñđ ŵĥéŕé ţĥé ļàñĝüàĝé'š çöñţéñţ šîţš, üñđéŕ ţĥé þŕöĵéçţ đéƒàüļţš öŕ öñ à þŕöƒîļé, ĥàš à ţéŕḿ ƒöŕ îţ. Ţĥé þîçķéŕ îĝñöŕéš ţĥé ƒîéļđ; îţ ţéļļš à ƃüîļđ šţéþ öŕ à þéŕšöñ ţĥàţ ñö ţéŕḿš ĝöṽéŕñ ţĥé ļàñĝüàĝé.

Ƒöŕ đàšĥƃöàŕđš, ţĥé kapi status --json ŕéþöŕţ çàŕŕîéš shippable àñđ ţĥé šţàţé, àš shipState, þéŕ çöļļéçţîöñ àñđ ļöçàļé, ŵîţĥ ţĥé ƒüļļ çöṽéŕàĝé þéŕçéñţàĝéš.

À ĥöšţéđ ƒééđ îñšţéàđ öƒ à ƃüîļţ ƒîļé​

Ţĥé ļöàđéŕ ţàķéš à ÜŔĻ, šö îţ çàñ ŕéàđ ţĥé ḿàñîƒéšţ ƒŕöḿ à ĥöšţéđ ƒééđ àš ŵéļļ àš ƒŕöḿ à ƃüîļţ ƒîļé. À ĥöšţéđ ƒééđ îš à šéŕṽéŕ ţĥàţ šéŕṽéš ţĥé þéŕ-ļöçàļé ḿàñîƒéšţ ļîṽé àţ à þüƃļîç ÜŔĻ. Ýöü đŕöþ ţĥé ƃüîļđ šţéþ, àñđ ţĥé þîçķéŕ ŕéàđš ţĥé çüŕŕéñţ šţàñđîñĝ öñ éàçĥ ļöàđ.

Ţĥé ƒééđ ŕéţüŕñš ţĥé ƒîļé'š šĥàþé: àñ öƃĵéçţ ķéýéđ ƃý ļöçàļé, éàçĥ ṽàļüé { shippable, state }, ŵîţĥ not_governed ŵĥéŕé îţ àþþļîéš. À ĥöšţéđ ƒééđ îš ŕéàđ-öñļý àñđ ñééđš ñö àüţĥ, šîñçé à þüƃļîç þîçķéŕ ƒéţçĥéš îţ đîŕéçţļý. Îţ šĥöüļđ šéñđ àñ ETag àñđ à šĥöŕţ Cache-Control: public, max-age=…, šö à þîçķéŕ öŕ à ÇĐÑ çàñ ŕéṽàļîđàţé çĥéàþļý ŵîţĥ à 304.

Þöîñţ ţĥé ļöàđéŕ àţ ţĥé ÜŔĻ; ñöţĥîñĝ éļšé çĥàñĝéš:

const status = await loadShipStatus("https://example.com/ship.json");
const model = languagePickerModel(status, ["en", "fr", "de", "ja"]);

Ţĥé šàḿé languagePickerModel ţŕàñšƒöŕḿ àñđ ţĥé šàḿé useShipStatus ĥööķ (ŵĥöšé šéçöñđ àŕĝüḿéñţ îš ţĥé ḿàñîƒéšţ ÜŔĻ) đŕîṽé ţĥé þîçķéŕ ƒöŕ à ƃüîļţ ƒîļé àñđ à ĥöšţéđ ƒééđ àļîķé.

Đŕîṽîñĝ ţĥé þîçķéŕ​

@neokapi/i18n-react/ship þŕöṽîđéš à đéþéñđéñçý-ƒŕéé ļöàđéŕ àñđ à ĥéàđļéšš ţŕàñšƒöŕḿ. ΃ ţĥé ḿàñîƒéšţ îš ḿîššîñĝ öŕ ḿàļƒöŕḿéđ, ţĥé ļöàđéŕ ŕéšöļṽéš ţö àñ éḿþţý öƃĵéçţ, àñđ ţĥé þîçķéŕ šĥöŵš éṽéŕý ļöçàļé ŵîţĥöüţ à ƃàđĝé.

Þàšš ļöçàļé çöđéš. Ţĥé ļàƃéļ ƒöŕ éàçĥ îš ţĥé ļöçàļé'š éñđöñýḿ (ţĥé ļàñĝüàĝé ñàḿéđ îñ îţš öŵñ ļàñĝüàĝé), ƒŕöḿ Intl.DisplayNames, šö ýöü ķééþ ñö þéŕ-ļöçàļé ļàƃéļ ţàƃļé:

import { loadShipStatus, languagePickerModel } from "@neokapi/i18n-react/ship";

const status = await loadShipStatus(); // defaults to /ship.json
const model = languagePickerModel(status, ["en", "fr", "de", "ja"]);
// → [{ locale: "fr", label: "Français", shippable: true, badge: 'ai' | null, state: "translated" }, …]

Ţĥé ļàƃéļ îš ŕéšöļṽéđ îñ ţĥîš öŕđéŕ: àñ éẋþļîçîţ label öñ à LocaleInput, ţĥéñ àñ éñţŕý îñ ţĥé labels öṽéŕŕîđé ḿàþ, ţĥéñ ţĥé Intl.DisplayNames éñđöñýḿ, ţĥéñ ţĥé ŕàŵ çöđé. Àñ éẋþļîçîţ ļàƃéļ ŕéþļàçéš ţĥé đéŕîṽéđ öñé ŵĥéŕé ýöü ŵàñţ à đéŕéñţ ƒöŕḿ, àñđ ţĥé öṽéŕŕîđé ḿàþ ñàḿéš ļöçàļéš ţĥàţ Intl çàññöţ. À þšéüđö-ļöçàļé šüçĥ àš qps ĥàš ñö šţàñđàŕđ ñàḿé, šö ĝîṽé îţ öñé:

const model = languagePickerModel(status, ["fr", "de", "qps"], {
labels: { qps: "Pseudo English" },
});
// fr → "Français" (derived), de → "Deutsch" (derived), qps → "Pseudo English" (override)

Ţĥé šîĝñàţüŕé îš languagePickerModel(status, locales, options?), ŵĥéŕé options îš { labels?: Record<string, string>; includeNotGated?: boolean }.

Ţĥé ƒîŕšţ ļéţţéŕ öƒ à đéŕîṽéđ éñđöñýḿ îš çàþîţàļîžéđ ƒöŕ à ḿéñü-šţýļé ļàƃéļ (français → Français), šö ļöŵéŕçàšé éñđöñýḿš ŕéàđ çöñšîšţéñţļý àļöñĝšîđé ţĥé öñéš ţĥàţ àŕé àļŕéàđý çàþîţàļîžéđ (Deutsch); šçŕîþţš ŵîţĥöüţ çàšé (日本語) àŕé ļéƒţ üñçĥàñĝéđ. ΃ Intl.DisplayNames îš üñàṽàîļàƃļé öŕ ĥàš ñö ñàḿé ƒöŕ à çöđé, ţĥé ļàƃéļ ƒàļļš ƃàçķ ţö ţĥé öṽéŕŕîđé ḿàþ àñđ ţĥéñ ţö ţĥé ŕàŵ çöđé; îţ ñéṽéŕ ţĥŕöŵš.

languagePickerModel ŕéţüŕñš ţĥé ļöçàļéš ŵĥöšé éñţŕý îš šĥîþþàƃļé, ŵĥîçĥ îñçļüđéš à ñöţ-ĝàţéđ ļöçàļé. Þàšš includeNotGated: false ţö öƒƒéŕ öñļý ţĥé ļöçàļéš ţĥàţ çļéàŕ à ĝàţé. Éàçĥ éñţŕý çàŕŕîéš ţĥé ḿàñîƒéšţ'š state àñđ à badge: 'ai' ŵĥéñ ţĥé ļöçàļé šĥîþš ƃüţ îš ñöţ established, àñđ null ŵĥéñ îţ îš. 'ai' îš ţĥé öñļý ƃàđĝé ţĥîš ļàýéŕ éḿîţš; àñ àþþŕöṽéđ ļöçàļé ĥàš ñö ƃàđĝé. À Ŕéàçţ ƃîñđîñĝ ŵŕàþš ţĥé šàḿé ţŵö ƒüñçţîöñš àñđ ţàķéš ţĥé šàḿé öþţîöñš àš à ţĥîŕđ àŕĝüḿéñţ:

import { useShipStatus } from "@neokapi/i18n-react/ship/react";

function LanguagePicker({ locales }) {
// loads /ship.json, derives labels, returns the model
const options = useShipStatus(locales, undefined, { labels: { qps: "Pseudo English" } });
return (
<ul>
{options.map(({ locale, label, badge }) => (
<li key={locale}>
{label}
{badge === "ai" && <span className="badge-ai">AI</span>}
</li>
))}
</ul>
);
}

Ýöü ŕéñđéŕ ţĥé þîçķéŕ ýöüŕšéļƒ: šţýļé ţĥé éñţŕîéš àñđ ţĥé AI ƃàđĝé ţö ḿàţçĥ ýöüŕ đéšîĝñ. Đéṽéļöþḿéñţ-öñļý ļöçàļéš (þšéüđö-ţŕàñšļàţîöñ šüçĥ àš qps) àŕé ñöţ þàŕţ öƒ ţĥé šĥîþ ḿàñîƒéšţ; ñàḿé ţĥéḿ ţĥŕöüĝĥ ţĥé labels öṽéŕŕîđé ḿàþ îƒ ýöü ļîšţ ţĥéḿ îñ ţĥé þîçķéŕ, öŕ šüŕƒàçé ţĥéḿ šéþàŕàţéļý îñ ýöüŕ öŵñ đéṽ ţööļîñĝ.

Çöḿþàţîƃîļîţý​

Ţĥé àþþŕöṽàļ ĝàţé îš àđđîţîṽé. À ŕéçîþé ţĥàţ đéçļàŕéš ñöñé ĥàš éṽéŕý šĥîþþîñĝ ļöçàļé ŕéàđ translated. Ţĥé þîçķéŕ ĥéļþéŕ đéĝŕàđéš šàƒéļý ŵĥéñ ship.json îš àƃšéñţ, àñđ ŕéàđš àñ éñţŕý ŵîţĥ ñö state ƃý îţš shippable ƒļàĝ àļöñé, šö à þŕöĵéçţ çàñ àđöþţ ţĥé ḿàñîƒéšţ àñđ ţĥé þîçķéŕ îñđéþéñđéñţļý. À ŕéçîþé ţĥàţ šţîļļ çàŕŕîéš verified_gate ƒàîļš ţö ļöàđ àñđ ñàḿéš established_gate.

Ŕéļàţéđ​